Skip to content

Commit

Permalink
Merge pull request #746 from jelhan/fix-content-security-policy
Browse files Browse the repository at this point in the history
CSP: refactor to not require `style-src 'unsafe-inline` and add test coverage
  • Loading branch information
simonihmig committed Jan 17, 2019
2 parents 9c7fc44 + ab3955f commit da34ea2
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 57 deletions.
45 changes: 19 additions & 26 deletions addon/components/base/bs-collapse.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { not, alias, and } from '@ember/object/computed';
import Component from '@ember/component';
import { isPresent, isEmpty } from '@ember/utils';
import { observer, computed } from '@ember/object';
import { isPresent } from '@ember/utils';
import { observer } from '@ember/object';
import { next } from '@ember/runloop';
import { htmlSafe, camelize } from '@ember/string';
import { camelize } from '@ember/string';
import transitionEnd from 'ember-bootstrap/utils/transition-end';
import { assert } from '@ember/debug';

/**
An Ember component that mimics the behaviour of [Bootstrap's collapse.js plugin](http://getbootstrap.com/javascript/#collapse)
Expand All @@ -28,7 +29,6 @@ import transitionEnd from 'ember-bootstrap/utils/transition-end';
export default Component.extend({

classNameBindings: ['collapse', 'collapsing'],
attributeBindings: ['style'],

/**
* Collapsed/expanded state
Expand Down Expand Up @@ -61,13 +61,6 @@ export default Component.extend({
*/
transitioning: false,

/**
* @property collapseSize
* @type number
* @private
*/
collapseSize: null,

/**
* The size of the element when collapsed. Defaults to 0.
*
Expand Down Expand Up @@ -120,14 +113,14 @@ export default Component.extend({
*/
transitionDuration: 350,

style: computed('collapseSize', function() {
let size = this.get('collapseSize');
let dimension = this.get('collapseDimension');
if (isEmpty(size)) {
return htmlSafe('');
}
return htmlSafe(`${dimension}: ${size}px`);
}),
setCollapseSize() {
let { collapseSize: size, collapseDimension: dimension } = this.getProperties('collapseSize', 'collapseDimension');

assert(`collapseDimension must be either "width" or "height". ${dimension} given.`, ["width", "height"].includes(dimension));

this.element.style.width = dimension === 'width' && size ? `${size}px` : '';
this.element.style.height = dimension === 'height' && size ? `${size}px` : '';
},

/**
* The action to be sent when the element is about to be hidden.
Expand Down Expand Up @@ -182,14 +175,14 @@ export default Component.extend({
}
this.set('transitioning', false);
if (this.get('resetSizeWhenNotCollapsing')) {
this.set('collapseSize', null);
this.setCollapseSize(null);
}
this.get('onShown')();
});

next(this, function() {
if (!this.get('isDestroyed')) {
this.set('collapseSize', this.getExpandedSize('show'));
this.setCollapseSize(this.getExpandedSize('show'));
}
});
},
Expand Down Expand Up @@ -225,24 +218,24 @@ export default Component.extend({

this.setProperties({
transitioning: true,
collapseSize: this.getExpandedSize('hide'),
active: false
});
this.setCollapseSize(this.getExpandedSize('hide'));

transitionEnd(this.get('element'), this.get('transitionDuration')).then(() => {
if (this.get('isDestroyed')) {
return;
}
this.set('transitioning', false);
if (this.get('resetSizeWhenNotCollapsing')) {
this.set('collapseSize', null);
this.setCollapseSize(null);
}
this.get('onHidden')();
});

next(this, function() {
if (!this.get('isDestroyed')) {
this.set('collapseSize', this.get('collapsedSize'));
this.setCollapseSize(this.get('collapsedSize'));
}
});
},
Expand All @@ -267,13 +260,13 @@ export default Component.extend({

_updateCollapsedSize: observer('collapsedSize', function() {
if (!this.get('resetSizeWhenNotCollapsing') && this.get('collapsed') && !this.get('collapsing')) {
this.set('collapseSize', this.get('collapsedSize'));
this.setCollapseSize(this.get('collapsedSize'));
}
}),

_updateExpandedSize: observer('expandedSize', function() {
if (!this.get('resetSizeWhenNotCollapsing') && !this.get('collapsed') && !this.get('collapsing')) {
this.set('collapseSize', this.get('expandedSize'));
this.setCollapseSize(this.get('expandedSize'));
}
})
});
24 changes: 14 additions & 10 deletions addon/components/base/bs-progress/bar.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { readOnly } from '@ember/object/computed';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { htmlSafe } from '@ember/string';
import layout from 'ember-bootstrap/templates/components/bs-progress/bar';
import TypeClass from 'ember-bootstrap/mixins/type-class';

Expand All @@ -20,7 +19,7 @@ export default Component.extend(TypeClass, {
classNames: ['progress-bar'],
classNameBindings: ['progressBarStriped'],

attributeBindings: ['style', 'ariaValuenow', 'ariaValuemin', 'ariaValuemax'],
attributeBindings: ['ariaValuenow', 'ariaValuemin', 'ariaValuemax'],

/**
* The lower limit of the value range
Expand Down Expand Up @@ -139,14 +138,19 @@ export default Component.extend(TypeClass, {
}).readOnly(),

/**
* @property style
* @type string
* @method updateStyles
* @return void
* @private
* @readonly
*/
style: computed('percent', function() {
let percent = this.get('percent');
return htmlSafe(`width: ${percent}%`);
})

updateStyles() {
let percent = Number.parseFloat(this.get('percent'));
this.element.style.width = !Number.isNaN(percent) ? `${percent}%` : '';
},

didInsertElement() {
this.updateStyles();
},
didUpdateAttrs() {
this.updateStyles();
}
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"ember-cli": "~3.5.0",
"ember-cli-app-version": "^3.0.0",
"ember-cli-blueprint-test-helpers": "^0.19.1",
"ember-cli-content-security-policy": "^1.0.0",
"ember-cli-dependency-checker": "^3.0.0",
"ember-cli-eslint": "^5.0.0",
"ember-cli-fastboot": "^2.0.0",
Expand Down
26 changes: 26 additions & 0 deletions tests/dummy/config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,28 @@ module.exports = function(environment) {
// when it is created
},

contentSecurityPolicy: {
'default-src': ["'none'"],
'script-src': [
"'self'",
// test file loaded assertion injected as <script> tag by ember-cli
"'sha256-37u63EBe1EibDZ3vZNr6mxLepqlY1CQw+4N89HrzP9s='",
],
'font-src': ["'self'"],
'connect-src': ["'self'"],
'img-src': [
"'self'",
// Bootstrap 4 uses data URL for some SVG images in CSS
"data:",
],
'style-src': ["'self'"],
'media-src': ["'self'"],
'frame-src': [
// iframe used in application template of dummy app
"https://ghbtns.com/",
],
},

fastboot: {
hostWhitelist: [/^localhost:\d+$/]
},
Expand All @@ -44,6 +66,10 @@ module.exports = function(environment) {

ENV.APP.rootElement = '#ember-testing';
ENV.APP.autoboot = false;

// testem requires frame-src 'self' to run
// https://github.com/rwjblue/ember-cli-content-security-policy/blob/v1.0.0/index.js#L85-L88
ENV.contentSecurityPolicy['frame-src'].push('self');
}

if (environment === 'production') {
Expand Down
21 changes: 21 additions & 0 deletions tests/helpers/setup-stylesheet-support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export default function({ beforeEach, afterEach }) {
let styleSheet = [...document.styleSheets].find((sheet) => sheet.href.includes('test-support.css'));
let numberOfInsertedRules;

beforeEach(function() {
numberOfInsertedRules = 0;

this.insertCSSRule = function(rule) {
styleSheet.insertRule(rule, styleSheet.length);
numberOfInsertedRules++;
};
});

afterEach(function() {
// since we insert the rules at the end of the stylesheet, we could safely
// remove the same amount of rules from the end as we have inserted
for (let i = 1; i <= numberOfInsertedRules; i++) {
styleSheet.deleteRule(styleSheet.length);
}
});
}
33 changes: 28 additions & 5 deletions tests/integration/components/bs-popover-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import {
setupForPositioning,
assertPositioning
} from '../../helpers/contextual-help';
import setupStylesheetSupport from '../../helpers/setup-stylesheet-support';

module('Integration | Component | bs-popover', function(hooks) {
setupRenderingTest(hooks);
setupStylesheetSupport(hooks);

test('it has correct markup', async function(assert) {
// Template block usage:
Expand All @@ -30,15 +32,18 @@ module('Integration | Component | bs-popover', function(hooks) {
});

skip('it supports different placements', async function(assert) {
this.insertCSSRule('#wrapper { margin: 200px }');

let placements = ['top', 'left', 'bottom', 'right'];
this.set('placement', placements[0]);
await render(hbs`
<div style="margin: 200px">
<div id="wrapper">
{{#bs-popover/element id="popover-element" placement=placement title="dummy title"}}
template block text
{{/bs-popover/element}}
</div>
`);

for (let placement of placements) {
this.set('placement', placement);
let placementClass = popoverPositionClass(placement);
Expand All @@ -47,8 +52,17 @@ module('Integration | Component | bs-popover', function(hooks) {
});

test('should place popover on top of element', async function(assert) {
await render(
hbs`<div id="ember-bootstrap-wormhole"></div><div id="wrapper"><p style="margin-top: 200px"><button class="btn" id="target">Click me{{#bs-popover placement="top" title="very very very very very very very long popover" fade=false}}very very very very very very very long popover{{/bs-popover}}</button></p></div>`
this.insertCSSRule('#wrapper p { margin-top: 200px }');

await render(hbs`
<div id="ember-bootstrap-wormhole"></div>
<div id="wrapper">
<p>
<button class="btn" id="target">
Click me{{#bs-popover placement="top" title="very very very very very very very long popover" fade=false}}very very very very very very very long popover{{/bs-popover}}
</button>
</p>
</div>`
);

setupForPositioning();
Expand All @@ -58,9 +72,18 @@ module('Integration | Component | bs-popover', function(hooks) {
});

test('should adjust popover arrow', async function(assert) {
this.insertCSSRule('#wrapper p { margin-top: 200px }');

let expectedArrowPosition = versionDependent(225, 219);
await render(
hbs`<div id="ember-bootstrap-wormhole"></div><div id="wrapper"><p style="margin-top: 200px"><button class="btn" id="target">Click me{{#bs-popover placement="top" autoPlacement=true viewportSelector="#wrapper" title="very very very very very very very long popover" fade=false}}very very very very very very very long popover{{/bs-popover}}</button></p></div>`
await render(hbs`
<div id="ember-bootstrap-wormhole"></div>
<div id="wrapper">
<p>
<button class="btn" id="target">
Click me{{#bs-popover placement="top" autoPlacement=true viewportSelector="#wrapper" title="very very very very very very very long popover" fade=false}}very very very very very very very long popover{{/bs-popover}}
</button>
</p>
</div>`
);

setupForPositioning('right');
Expand Down
9 changes: 6 additions & 3 deletions tests/integration/components/bs-progress-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { test, testBS3, testBS4 } from '../../helpers/bootstrap-test';
import hbs from 'htmlbars-inline-precompile';
import setupStylesheetSupport from '../../helpers/setup-stylesheet-support';

module('Integration | Component | bs-progress', function(hooks) {
setupRenderingTest(hooks);
setupStylesheetSupport(hooks);

test('bs-progress has correct markup', async function(assert) {
// Template block usage:
Expand Down Expand Up @@ -41,9 +43,11 @@ module('Integration | Component | bs-progress', function(hooks) {
}
];

this.insertCSSRule('.progress-bar { transition: none; }');
this.insertCSSRule('.width-500 { width: 500px }');

await render(hbs`
<style type="text/css">.progress-bar { transition: none; }</style>
<div style="width: 500px">
<div class="width-500">
{{#bs-progress as |p|}}
{{p.bar value=value minValue=minValue maxValue=maxValue}}
{{/bs-progress}}
Expand All @@ -60,7 +64,6 @@ module('Integration | Component | bs-progress', function(hooks) {

assert.equal(this.element.querySelector('.progress-bar').offsetWidth, expectedWidth, 'Progress bar has expected width.');
});

});

test('progress bar has invisible label for screen readers', async function(assert) {
Expand Down

0 comments on commit da34ea2

Please sign in to comment.