Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make LinkView FastBoot™-compatible #10316

Merged
merged 5 commits into from
Jan 31, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});