Skip to content

Commit

Permalink
Merge pull request #10316 from emberjs/more-node-tests
Browse files Browse the repository at this point in the history
Make LinkView FastBoot™-compatible
  • Loading branch information
tomdale committed Jan 31, 2015
2 parents e0f340f + 60ff02d commit 04463cf
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 32 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"emberjs-build": "0.0.22",
"express": "^4.5.0",
"glob": "~4.3.2",
"htmlbars": "0.8.3",
"htmlbars": "0.8.4",
"qunit-extras": "^1.3.0",
"qunitjs": "^1.16.0",
"route-recognizer": "0.1.5",
Expand Down
2 changes: 1 addition & 1 deletion packages/ember-htmlbars/lib/hooks/attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function attribute(env, morph, element, attrName, attrValue) {
if (isStream(attrValue)) {
throw new EmberError('Bound attributes are not yet supported in Ember.js');
} else {
var sanitizedValue = sanitizeAttributeValue(element, attrName, attrValue);
var sanitizedValue = sanitizeAttributeValue(env.dom, element, attrName, attrValue);
env.dom.setProperty(element, attrName, sanitizedValue);
}
}
Expand Down
17 changes: 13 additions & 4 deletions packages/ember-metal-views/lib/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import environment from "ember-metal/environment";

var domHelper = environment.hasDOM ? new DOMHelper() : null;

function Renderer(_helper) {
function Renderer(_helper, _destinedForDOM) {
this._uuid = 0;

// These sizes and values are somewhat arbitrary (but sensible)
Expand All @@ -14,6 +14,7 @@ function Renderer(_helper) {
this._elements = new Array(17);
this._inserts = {};
this._dom = _helper || domHelper;
this._destinedForDOM = _destinedForDOM === undefined ? true : _destinedForDOM;
}

function Renderer_renderTree(_view, _parentView, _insertAt) {
Expand Down Expand Up @@ -63,9 +64,17 @@ function Renderer_renderTree(_view, _parentView, _insertAt) {
contextualElement = parent._childViewsMorph.contextualElement;
}
if (!contextualElement && view._didCreateElementWithoutMorph) {
// This code path is only used by createElement and rerender when createElement
// was previously called on a view.
contextualElement = document.body;
// This code path is used by view.createElement(), which has two purposes:
//
// 1. Legacy usage of `createElement()`. Nobody really knows what the point
// of that is. This usage may be removed in Ember 2.0.
// 2. FastBoot, which creates an element and has no DOM to insert it into.
//
// For FastBoot purposes, rendering the DOM without a contextual element
// should work fine, because it essentially re-emits the original markup
// as a String, which will then be parsed again by the browser, which will
// apply the appropriate parsing rules.
contextualElement = typeof document !== 'undefined' ? document.body : null;
}
element = this.createElement(view, contextualElement);

Expand Down
2 changes: 1 addition & 1 deletion packages/ember-views/lib/system/render_buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ RenderBuffer.prototype = {
},

outerContextualElement: function() {
if (!this._outerContextualElement) {
if (this._outerContextualElement === undefined) {
Ember.deprecate("The render buffer expects an outer contextualElement to exist." +
" This ensures DOM that requires context is correctly generated (tr, SVG tags)." +
" Defaulting to document.body, but this will be removed in the future");
Expand Down
21 changes: 14 additions & 7 deletions packages/ember-views/lib/system/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
subscribers
} from "ember-metal/instrumentation";

function EmberRenderer(domHelper) {
this._super$constructor(domHelper);
function EmberRenderer(domHelper, _destinedForDOM) {
this._super$constructor(domHelper, _destinedForDOM);
this.buffer = new RenderBuffer(domHelper);
}

Expand Down Expand Up @@ -110,21 +110,28 @@ Renderer.prototype.didCreateElement = function (view) {
}
}; // hasElement
Renderer.prototype.willInsertElement = function (view) {
if (view.trigger) { view.trigger('willInsertElement'); }
if (this._destinedForDOM) {
if (view.trigger) { view.trigger('willInsertElement'); }
}
}; // will place into DOM
Renderer.prototype.didInsertElement = function (view) {
if (view._transitionTo) {
view._transitionTo('inDOM');
}
if (view.trigger) { view.trigger('didInsertElement'); }

if (this._destinedForDOM) {
if (view.trigger) { view.trigger('didInsertElement'); }
}
}; // inDOM // placed into DOM

Renderer.prototype.willRemoveElement = function (view) {};

Renderer.prototype.willDestroyElement = function (view) {
if (view.trigger) {
view.trigger('willDestroyElement');
view.trigger('willClearRender');
if (this._destinedForDOM) {
if (view.trigger) {
view.trigger('willDestroyElement');
view.trigger('willClearRender');
}
}
};

Expand Down
24 changes: 15 additions & 9 deletions packages/ember-views/lib/system/sanitize_attribute_value.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* jshint scripturl:true */

var parsingNode;
var badProtocols = {
'javascript:': true,
'vbscript:': true
Expand All @@ -20,13 +19,9 @@ export var badAttributes = {
'background': true
};

export default function sanitizeAttributeValue(element, attribute, value) {
export default function sanitizeAttributeValue(dom, element, attribute, value) {
var tagName;

if (!parsingNode) {
parsingNode = document.createElement('a');
}

if (!element) {
tagName = null;
} else {
Expand All @@ -38,9 +33,20 @@ export default function sanitizeAttributeValue(element, attribute, value) {
}

if ((tagName === null || badTags[tagName]) && badAttributes[attribute]) {
parsingNode.href = value;

if (badProtocols[parsingNode.protocol] === true) {
// Previously, we relied on creating a new `<a>` element and setting
// its `href` in order to get the DOM to parse and extract its protocol.
// Naive approaches to URL parsing are susceptible to all sorts of XSS
// attacks.
//
// However, this approach does not work in environments without a DOM,
// such as Node & FastBoot. We have extracted the logic for parsing to
// the DOM helper, so that in locations without DOM, we can substitute
// our own robust URL parsing.
//
// This will also allow us to use the new `URL` API in browsers that
// support it, and skip the process of creating an element entirely.
var protocol = dom.protocolForURL(value);
if (badProtocols[protocol] === true) {
return 'unsafe:' + value;
}
}
Expand Down
8 changes: 4 additions & 4 deletions packages/ember-views/lib/views/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -1232,7 +1232,7 @@ var View = CoreView.extend({
// Determine the current value and add it to the render buffer
// if necessary.
attributeValue = get(this, property);
View.applyAttributeBindings(buffer, attributeName, attributeValue);
View.applyAttributeBindings(this.renderer._dom, buffer, attributeName, attributeValue);
} else {
unspecifiedAttributeBindings[property] = attributeName;
}
Expand All @@ -1252,7 +1252,7 @@ var View = CoreView.extend({

attributeValue = get(this, property);

View.applyAttributeBindings(elem, attributeName, attributeValue);
View.applyAttributeBindings(this.renderer._dom, elem, attributeName, attributeValue);
};

this.registerObserver(this, property, observer);
Expand Down Expand Up @@ -2176,8 +2176,8 @@ View.views = {};
View.childViewsProperty = childViewsProperty;

// Used by Handlebars helpers, view element attributes
View.applyAttributeBindings = function(elem, name, initialValue) {
var value = sanitizeAttributeValue(elem[0], name, initialValue);
View.applyAttributeBindings = function(dom, elem, name, initialValue) {
var value = sanitizeAttributeValue(dom, elem[0], name, initialValue);
var type = typeOf(value);

// if this changes, also change the logic in ember-handlebars/lib/helpers/binding.js
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import sanitizeAttributeValue from "ember-views/system/sanitize_attribute_value";
import { SafeString } from "ember-htmlbars/utils/string";
import { DOMHelper } from "morph";

QUnit.module('ember-views: sanitizeAttributeValue(null, "href")');

var goodProtocols = ['https', 'http', 'ftp', 'tel', 'file'];
var dom = new DOMHelper();

for (var i = 0, l = goodProtocols.length; i < l; i++) {
buildProtocolTest(goodProtocols[i]);
Expand All @@ -14,7 +16,7 @@ function buildProtocolTest(protocol) {
expect(1);

var expected = protocol + '://foo.com';
var actual = sanitizeAttributeValue(null, 'href', expected);
var actual = sanitizeAttributeValue(dom, null, 'href', expected);

equal(actual, expected, 'protocol not escaped');
});
Expand All @@ -26,7 +28,7 @@ test('blocks javascript: protocol', function() {
expect(1);

var expected = 'javascript:alert("foo")';
var actual = sanitizeAttributeValue(null, 'href', expected);
var actual = sanitizeAttributeValue(dom, null, 'href', expected);

equal(actual, 'unsafe:' + expected, 'protocol escaped');
});
Expand All @@ -37,7 +39,7 @@ test('blocks blacklisted protocols', function() {
expect(1);

var expected = 'javascript:alert("foo")';
var actual = sanitizeAttributeValue(null, 'href', expected);
var actual = sanitizeAttributeValue(dom, null, 'href', expected);

equal(actual, 'unsafe:' + expected, 'protocol escaped');
});
Expand All @@ -48,7 +50,7 @@ test('does not block SafeStrings', function() {
expect(1);

var expected = 'javascript:alert("foo")';
var actual = sanitizeAttributeValue(null, 'href', new SafeString(expected));
var actual = sanitizeAttributeValue(dom, null, 'href', new SafeString(expected));

equal(actual, expected, 'protocol unescaped');
});
81 changes: 80 additions & 1 deletion tests/node/app-boot-test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
/*globals __dirname*/
/*globals global,__dirname*/

var path = require('path');
var distPath = path.join(__dirname, '../../dist');

/*jshint -W079 */
global.EmberENV = {
FEATURES: {
'ember-application-instance-initializers': true,
'ember-application-visit': true
}
};

var Ember = require(path.join(distPath, 'ember.debug.cjs'));
var compile = require(path.join(distPath, 'ember-template-compiler')).compile;
Ember.testing = true;
Expand Down Expand Up @@ -117,3 +124,75 @@ QUnit.test("It is possible to render a view with a nested {{view}} helper in Nod
var serializer = new SimpleDOM.HTMLSerializer(SimpleDOM.voidMap);
ok(serializer.serialize(morph.element).match(/<h1>Hello World<\/h1> <div><div id="(.*)" class="ember-view"><p>The files are \*inside\* the computer\?\!<\/p><\/div><\/div>/));
});

function createApplication() {
var App = Ember.Application.extend().create({
autoboot: false
});

App.Router = Ember.Router.extend({
location: 'none'
});

return App;
}

QUnit.test("It is possible to render a view with {{link-to}} in Node", function() {
QUnit.stop();

var run = Ember.run;
var app;
var URL = require('url');

var domHelper = new DOMHelper(new SimpleDOM.Document());
domHelper.protocolForURL = function(url) {
var protocol = URL.parse(url).protocol;
return (protocol == null) ? ':' : protocol;
};

run(function() {
app = createApplication();

app.Router.map(function() {
this.route('photos');
});

app.instanceInitializer({
name: 'register-application-template',
initialize: function(app) {
app.registry.register('renderer:-dom', {
create: function() {
return new Ember.View._Renderer(domHelper);
}
});
app.registry.register('template:application', compile("<h1>{{#link-to 'photos'}}Go to photos{{/link-to}}</h1>"));
}
});
});

app.visit('/').then(function(instance) {
QUnit.start();

var morph = {
contextualElement: {},
setContent: function(element) {
this.element = element;
}
};

var view = instance.view;

view._morph = morph;

var renderer = view.renderer;

run(function() {
renderer.renderTree(view);
});

var serializer = new SimpleDOM.HTMLSerializer(SimpleDOM.voidMap);
var serialized = serializer.serialize(morph.element);
ok(serialized.match(/href="\/photos"/), "Rendered output contains /photos: " + serialized);
});
});

0 comments on commit 04463cf

Please sign in to comment.