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-security.strategy'; |
||||
|
export * from './content.strategy'; |
||||
export * from './cross-origin.strategy'; |
export * from './cross-origin.strategy'; |
||||
export * from './dom.strategy'; |
export * from './dom.strategy'; |
||||
export * from './loading.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 { |
export function uuid(a?: any): string { |
||||
return a |
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); |
: ('' + 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