mirror of https://github.com/abpframework/abp.git
committed by
GitHub
15 changed files with 264 additions and 18 deletions
@ -0,0 +1,17 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { ContentStrategy } from '../strategies/content.strategy'; |
|||
import { generateHash } from '../utils'; |
|||
|
|||
@Injectable({ providedIn: 'root' }) |
|||
export class DomInsertionService { |
|||
readonly inserted = new Set(); |
|||
|
|||
insertContent(contentStrategy: ContentStrategy) { |
|||
const hash = generateHash(contentStrategy.content); |
|||
|
|||
if (this.inserted.has(hash)) return; |
|||
|
|||
contentStrategy.insertElement(); |
|||
this.inserted.add(hash); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
import { ContentSecurityStrategy, CONTENT_SECURITY_STRATEGY } from './content-security.strategy'; |
|||
import { DomStrategy, DOM_STRATEGY } from './dom.strategy'; |
|||
|
|||
export abstract class ContentStrategy<T extends HTMLScriptElement | HTMLStyleElement = any> { |
|||
constructor( |
|||
public content: string, |
|||
protected domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(), |
|||
protected contentSecurityStrategy: ContentSecurityStrategy = CONTENT_SECURITY_STRATEGY.None(), |
|||
) {} |
|||
|
|||
abstract createElement(): T; |
|||
|
|||
insertElement() { |
|||
const element = this.createElement(); |
|||
|
|||
this.contentSecurityStrategy.applyCSP(element); |
|||
this.domStrategy.insertElement(element); |
|||
} |
|||
} |
|||
|
|||
export class StyleContentStrategy extends ContentStrategy<HTMLStyleElement> { |
|||
createElement(): HTMLStyleElement { |
|||
const element = document.createElement('style'); |
|||
element.textContent = this.content; |
|||
|
|||
return element; |
|||
} |
|||
} |
|||
|
|||
export class ScriptContentStrategy extends ContentStrategy<HTMLScriptElement> { |
|||
createElement(): HTMLScriptElement { |
|||
const element = document.createElement('script'); |
|||
element.textContent = this.content; |
|||
|
|||
return element; |
|||
} |
|||
} |
|||
|
|||
export const CONTENT_STRATEGY = { |
|||
AppendScriptToBody(content: string) { |
|||
return new ScriptContentStrategy(content, DOM_STRATEGY.AppendToBody()); |
|||
}, |
|||
AppendScriptToHead(content: string) { |
|||
return new ScriptContentStrategy(content, DOM_STRATEGY.AppendToHead()); |
|||
}, |
|||
AppendStyleToHead(content: string) { |
|||
return new StyleContentStrategy(content, DOM_STRATEGY.AppendToHead()); |
|||
}, |
|||
PrependStyleToHead(content: string) { |
|||
return new StyleContentStrategy(content, DOM_STRATEGY.PrependToHead()); |
|||
}, |
|||
}; |
|||
@ -1,4 +1,5 @@ |
|||
export * from './content-security.strategy'; |
|||
export * from './content.strategy'; |
|||
export * from './cross-origin.strategy'; |
|||
export * from './dom.strategy'; |
|||
export * from './loading.strategy'; |
|||
|
|||
@ -0,0 +1,82 @@ |
|||
import { |
|||
CONTENT_STRATEGY, |
|||
StyleContentStrategy, |
|||
ScriptContentStrategy, |
|||
DOM_STRATEGY, |
|||
CONTENT_SECURITY_STRATEGY, |
|||
} from '../strategies'; |
|||
import { uuid } from '../utils'; |
|||
|
|||
describe('StyleContentStrategy', () => { |
|||
describe('#createElement', () => { |
|||
it('should create a style element', () => { |
|||
const strategy = new StyleContentStrategy(''); |
|||
const element = strategy.createElement(); |
|||
|
|||
expect(element.tagName).toBe('STYLE'); |
|||
}); |
|||
}); |
|||
|
|||
describe('#insertElement', () => { |
|||
it('should use given dom and content security strategies', () => { |
|||
const domStrategy = DOM_STRATEGY.PrependToHead(); |
|||
const contentSecurityStrategy = CONTENT_SECURITY_STRATEGY.None(); |
|||
|
|||
contentSecurityStrategy.applyCSP = jest.fn((el: HTMLScriptElement) => {}); |
|||
domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => {}) as any; |
|||
|
|||
const strategy = new StyleContentStrategy('', domStrategy, contentSecurityStrategy); |
|||
const element = strategy.createElement(); |
|||
strategy.insertElement(); |
|||
|
|||
expect(contentSecurityStrategy.applyCSP).toHaveBeenCalledWith(element); |
|||
expect(domStrategy.insertElement).toHaveBeenCalledWith(element); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('ScriptContentStrategy', () => { |
|||
describe('#createElement', () => { |
|||
it('should create a style element', () => { |
|||
const nonce = uuid(); |
|||
const strategy = new ScriptContentStrategy(''); |
|||
const element = strategy.createElement(); |
|||
|
|||
expect(element.tagName).toBe('SCRIPT'); |
|||
}); |
|||
}); |
|||
|
|||
describe('#insertElement', () => { |
|||
it('should use given dom and content security strategies', () => { |
|||
const nonce = uuid(); |
|||
|
|||
const domStrategy = DOM_STRATEGY.PrependToHead(); |
|||
const contentSecurityStrategy = CONTENT_SECURITY_STRATEGY.Loose(nonce); |
|||
|
|||
contentSecurityStrategy.applyCSP = jest.fn((el: HTMLScriptElement) => {}); |
|||
domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => {}) as any; |
|||
|
|||
const strategy = new ScriptContentStrategy('', domStrategy, contentSecurityStrategy); |
|||
const element = strategy.createElement(); |
|||
strategy.insertElement(); |
|||
|
|||
expect(contentSecurityStrategy.applyCSP).toHaveBeenCalledWith(element); |
|||
expect(domStrategy.insertElement).toHaveBeenCalledWith(element); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('CONTENT_STRATEGY', () => { |
|||
test.each` |
|||
name | Strategy | domStrategy |
|||
${'AppendScriptToBody'} | ${ScriptContentStrategy} | ${'AppendToBody'} |
|||
${'AppendScriptToHead'} | ${ScriptContentStrategy} | ${'AppendToHead'} |
|||
${'AppendStyleToHead'} | ${StyleContentStrategy} | ${'AppendToHead'} |
|||
${'PrependStyleToHead'} | ${StyleContentStrategy} | ${'PrependToHead'} |
|||
`(
|
|||
'should successfully map $name to $Strategy.name with $domStrategy dom strategy', |
|||
({ name, Strategy, domStrategy }) => { |
|||
expect(CONTENT_STRATEGY[name]('')).toEqual(new Strategy('', DOM_STRATEGY[domStrategy]())); |
|||
}, |
|||
); |
|||
}); |
|||
@ -0,0 +1,15 @@ |
|||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; |
|||
import { DomInsertionService } from '../services'; |
|||
import { CONTENT_STRATEGY } from '../strategies'; |
|||
|
|||
describe('DomInsertionService', () => { |
|||
let spectator: SpectatorService<DomInsertionService>; |
|||
const createService = createServiceFactory(DomInsertionService); |
|||
|
|||
beforeEach(() => (spectator = createService())); |
|||
|
|||
it('should be insert an element', () => { |
|||
spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}')); |
|||
expect(spectator.service.inserted.has(1437348290)).toBe(true); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,10 @@ |
|||
import { generateHash } from '../utils'; |
|||
|
|||
describe('GeneratorUtils', () => { |
|||
describe('#generateHash', () => { |
|||
test('should generate a hash', async () => { |
|||
const hash = generateHash('some content \n with second line'); |
|||
expect(hash).toBe(1112440527); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -1,6 +1,19 @@ |
|||
// tslint:disable: no-bitwise
|
|||
|
|||
export function uuid(a?: any): string { |
|||
return a |
|||
? // tslint:disable-next-line: no-bitwise
|
|||
(a ^ ((Math.random() * 16) >> (a / 4))).toString(16) |
|||
? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16) |
|||
: ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid); |
|||
} |
|||
|
|||
export function generateHash(value: string): number { |
|||
let hashed = 0; |
|||
let charCode: number; |
|||
|
|||
for (let i = 0; i < value.length; i++) { |
|||
charCode = value.charCodeAt(i); |
|||
hashed = (hashed << 5) - hashed + charCode; |
|||
hashed |= 0; |
|||
} |
|||
return hashed; |
|||
} |
|||
|
|||
@ -0,0 +1,29 @@ |
|||
import { Component } from '@angular/core'; |
|||
import { createComponentFactory, Spectator } from '@ngneat/spectator'; |
|||
import { THEME_SHARED_APPEND_CONTENT } from '../tokens/append-content.token'; |
|||
import { DomInsertionService } from '@abp/ng.core'; |
|||
import { chartJsLoaded$ } from '../utils'; |
|||
|
|||
@Component({ selector: 'abp-dummy', template: '' }) |
|||
class DummyComponent {} |
|||
|
|||
describe('AppendContentToken', () => { |
|||
let spectator: Spectator<DummyComponent>; |
|||
const createComponent = createComponentFactory(DummyComponent); |
|||
|
|||
beforeEach(() => (spectator = createComponent())); |
|||
|
|||
it('should insert a style element to the DOM', () => { |
|||
spectator.get(THEME_SHARED_APPEND_CONTENT); |
|||
expect(spectator.get(DomInsertionService).inserted.size).toBe(1); |
|||
}); |
|||
|
|||
it('should be loaded the chart.js', done => { |
|||
chartJsLoaded$.subscribe(loaded => { |
|||
expect(loaded).toBe(true); |
|||
done(); |
|||
}); |
|||
|
|||
spectator.get(THEME_SHARED_APPEND_CONTENT); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,15 @@ |
|||
import { CONTENT_STRATEGY, DomInsertionService } from '@abp/ng.core'; |
|||
import { inject, InjectionToken } from '@angular/core'; |
|||
import styles from '../constants/styles'; |
|||
import { chartJsLoaded$ } from '../utils/widget-utils'; |
|||
|
|||
export const THEME_SHARED_APPEND_CONTENT = new InjectionToken<void>('THEME_SHARED_APPEND_CONTENT', { |
|||
providedIn: 'root', |
|||
factory: () => { |
|||
const domInsertion: DomInsertionService = inject(DomInsertionService); |
|||
|
|||
domInsertion.insertContent(CONTENT_STRATEGY.AppendStyleToHead(styles)); |
|||
|
|||
import('chart.js').then(() => chartJsLoaded$.next(true)); |
|||
}, |
|||
}); |
|||
Loading…
Reference in new issue