Skip to content

Commit

Permalink
fix(href-sanitization): Accept markupSanitizer option, downcase tagNa…
Browse files Browse the repository at this point in the history
…me and attributeName (#50)

Add npm 'start' script, note in readme how to run tests in browser.
Refactor tests to be more DRY (use mobiledoc creation helpers).
Refactor renderers to share more helper functions.
Refactor renderer shape to remove some conditionals (adds
`#sectionElementRendererFor` and `#markupElementRendererFor`).

fixes #49
fixes #48
  • Loading branch information
bantic authored Mar 6, 2017
1 parent a3d94c8 commit aa1aedc
Show file tree
Hide file tree
Showing 11 changed files with 494 additions and 405 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,39 @@ var renderer = new MobiledocDOMRenderer({
var rendered = renderer.render(mobiledoc);
```

#### markupSanitizer

Use this renderer option to customize how markup attribute values are sanitized.
The renderer's default markupSanitizer only sanitizes `href` values, prefixing
unsafe values with the string `"unsafe:"`. All other attribute values are
passed through unchanged.

To change this behavior, pass your own markupSanitizer function when
instantiating the renderer. If your markupSanitizer function returns a string,
that value will be used when rendering. If it returns a falsy value, the
renderer's default markupSanitizer will be used.

```
var renderer = new MobiledocDOMRenderer({
markupSanitizer: function({tagName, attributeName, attributeValue}) {
// This function will be called for every attribute on every markup.
// Return a sanitized attributeValue or undefined (in which case the
// default sanitizer will be used)
}
});
```

The default sanitization of href values uses an environment-appropriate url
parser if it can find one. It's unlikely, but if the renderer is in an
environment where it cannot determine a url parser it will throw. (This can
happen when running the renderer in a VM Sandbox, like ember-cli-fastboot
does.) In this case you must supply a custom markupSanitizer that can handle
`href` sanitization.

### Tests

* `npm test`
* To run tests via testem: `npm test`
* To run tests in the browser: `npm start` and open http://localhost:4200/tests

### Releasing

Expand Down
19 changes: 9 additions & 10 deletions lib/renderer-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,18 @@

export default class RendererFactory {
constructor({
cards,
atoms,
cardOptions,
cards=[],
atoms=[],
cardOptions={},
unknownCardHandler,
unknownAtomHandler,
markupElementRenderer,
sectionElementRenderer,
dom
markupElementRenderer={},
sectionElementRenderer={},
dom,
markupSanitizer=null
}={}) {
cards = cards || [];
validateCards(cards);
atoms = atoms || [];
validateAtoms(atoms);
cardOptions = cardOptions || {};

if (!dom) {
if (typeof window === 'undefined') {
Expand All @@ -77,7 +75,8 @@
unknownAtomHandler,
markupElementRenderer,
sectionElementRenderer,
dom
dom,
markupSanitizer
};
}

Expand Down
112 changes: 61 additions & 51 deletions lib/renderers/0-2.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,21 @@ import {
} from '../utils/section-types';
import {
isValidSectionTagName,
isMarkupSectionElementName,
isValidMarkerType
} from '../utils/tag-names';
import {
reduceAndSanitizeAttributes,
sanitizeAttributeValue
reduceAttributes
} from '../utils/sanitization-utils';
import {
createMarkupSanitizerWithFallback,
defaultSectionElementRenderer,
defaultMarkupElementRenderer
} from '../utils/render-utils';

export const MOBILEDOC_VERSION = '0.2.0';

const IMAGE_SECTION_TAG_NAME = 'img';

function createElementFromMarkerType(dom, [tagName, attributes]=['', []]){
let element = dom.createElement(tagName);
attributes = attributes || [];

for (let i=0,l=attributes.length; i<l; i=i+2) {
let propName = attributes[i],
propValue = attributes[i+1];
element.setAttribute(propName, sanitizeAttributeValue(propName, propValue, tagName));
}
return element;
}

function validateVersion(version) {
if (version !== MOBILEDOC_VERSION) {
throw new Error(`Unexpected Mobiledoc version "${version}"`);
Expand All @@ -47,7 +38,8 @@ export default class Renderer {
unknownCardHandler,
markupElementRenderer,
sectionElementRenderer,
dom
dom,
markupSanitizer
} = options;
let {
version,
Expand All @@ -64,24 +56,21 @@ export default class Renderer {
this.cards = cards;
this.cardOptions = cardOptions;
this.unknownCardHandler = unknownCardHandler || this._defaultUnknownCardHandler;
this.markupSanitizer = createMarkupSanitizerWithFallback(markupSanitizer);

this.sectionElementRenderer = {};
if (sectionElementRenderer) {
for (let key in sectionElementRenderer) {
if (sectionElementRenderer.hasOwnProperty(key)) {
this.sectionElementRenderer[key.toLowerCase()] = sectionElementRenderer[key];
}
}
}
this.sectionElementRenderer = {
'__default__': defaultSectionElementRenderer
};
Object.keys(sectionElementRenderer).forEach(key => {
this.sectionElementRenderer[key.toLowerCase()] = sectionElementRenderer[key];
});

this.markupElementRenderer = {};
if (markupElementRenderer) {
for (let key in markupElementRenderer) {
if (markupElementRenderer.hasOwnProperty(key)) {
this.markupElementRenderer[key.toLowerCase()] = markupElementRenderer[key];
}
}
}
this.markupElementRenderer = {
'__default__': defaultMarkupElementRenderer
};
Object.keys(markupElementRenderer).forEach(key => {
this.markupElementRenderer[key.toLowerCase()] = markupElementRenderer[key];
});

this._renderCallbacks = [];
this._teardownCallbacks = [];
Expand Down Expand Up @@ -160,15 +149,7 @@ export default class Renderer {
let markerType = this.markerTypes[openTypes[j]];
let [tagName, attrs=[]] = markerType;
if (isValidMarkerType(tagName)) {
let lowerCaseTagName = tagName.toLowerCase();
if (this.markupElementRenderer[lowerCaseTagName]) {
let attrObj = reduceAndSanitizeAttributes(attrs, lowerCaseTagName);
let openedElement = this.markupElementRenderer[lowerCaseTagName](tagName, this.dom, attrObj);
pushElement(openedElement);
} else {
let openedElement = createElementFromMarkerType(this.dom, markerType);
pushElement(openedElement);
}
pushElement(this.renderMarkupElement(tagName, attrs));
} else {
closeCount--;
}
Expand All @@ -183,6 +164,37 @@ export default class Renderer {
}
}

/**
* @param attrs Array
*/
renderMarkupElement(tagName, attrs) {
tagName = tagName.toLowerCase();
attrs = this.sanitizeAttributes(tagName, reduceAttributes(attrs));

let renderer = this.markupElementRendererFor(tagName);
return renderer(tagName, this.dom, attrs);
}

markupElementRendererFor(tagName) {
return this.markupElementRenderer[tagName] ||
this.markupElementRenderer.__default__;
}

sanitizeAttributes(tagName, attrsObj) {
let sanitized = {};

Object.keys(attrsObj).forEach(attributeName => {
let attributeValue = attrsObj[attributeName];
sanitized[attributeName] = this.sanitizeAttribute({tagName, attributeName, attributeValue});
});

return sanitized;
}

sanitizeAttribute({tagName, attributeName, attributeValue}) {
return this.markupSanitizer({tagName, attributeName, attributeValue});
}

renderListItem(markers) {
const element = this.dom.createElement('li');
this.renderMarkersOnElement(element, markers);
Expand Down Expand Up @@ -270,23 +282,21 @@ export default class Renderer {
}

renderMarkupSection([type, tagName, markers]) {
tagName = tagName.toLowerCase();
if (!isValidSectionTagName(tagName, MARKUP_SECTION_TYPE)) {
return;
}

let element;
let lowerCaseTagName = tagName.toLowerCase();
if (this.sectionElementRenderer[lowerCaseTagName]) {
element = this.sectionElementRenderer[lowerCaseTagName](tagName, this.dom);
} else if (isMarkupSectionElementName(tagName)) {
element = this.dom.createElement(tagName);
} else {
element = this.dom.createElement('div');
element.setAttribute('class', tagName);
}
let renderer = this.sectionElementRendererFor(tagName);
let element = renderer(tagName, this.dom);

this.renderMarkersOnElement(element, markers);
return element;
}

sectionElementRendererFor(tagName) {
return this.sectionElementRenderer[tagName] ||
this.sectionElementRenderer.__default__;
}
}

Loading

0 comments on commit aa1aedc

Please sign in to comment.