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

CSP: refactor to not require style-src 'unsafe-inline and add test coverage #746

Merged
merged 11 commits into from
Jan 17, 2019
Merged
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