Browse Source

Merge pull request #3475 from abpframework/feat/insertion

Created DomInsertionService
pull/3488/head
Mehmet Erim 6 years ago
committed by GitHub
parent
commit
6136ec41b4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts
  2. 1
      npm/ng-packs/packages/core/src/lib/services/index.ts
  3. 4
      npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts
  4. 6
      npm/ng-packs/packages/core/src/lib/strategies/content-security.strategy.ts
  5. 52
      npm/ng-packs/packages/core/src/lib/strategies/content.strategy.ts
  6. 1
      npm/ng-packs/packages/core/src/lib/strategies/index.ts
  7. 12
      npm/ng-packs/packages/core/src/lib/tests/content-security.strategy.spec.ts
  8. 82
      npm/ng-packs/packages/core/src/lib/tests/content.strategy.spec.ts
  9. 15
      npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts
  10. 10
      npm/ng-packs/packages/core/src/lib/tests/generator-utils.spec.ts
  11. 17
      npm/ng-packs/packages/core/src/lib/utils/generator-utils.ts
  12. 9
      npm/ng-packs/packages/theme-basic/src/lib/services/initial.service.ts
  13. 29
      npm/ng-packs/packages/theme-shared/src/lib/tests/append-content.token.spec.ts
  14. 12
      npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts
  15. 15
      npm/ng-packs/packages/theme-shared/src/lib/tokens/append-content.token.ts

17
npm/ng-packs/packages/core/src/lib/services/dom-insertion.service.ts

@ -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);
}
}

1
npm/ng-packs/packages/core/src/lib/services/index.ts

@ -1,6 +1,7 @@
export * from './application-configuration.service';
export * from './auth.service';
export * from './config-state.service';
export * from './dom-insertion.service';
export * from './lazy-load.service';
export * from './localization.service';
export * from './profile-state.service';

4
npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts

@ -13,6 +13,10 @@ export class LazyLoadService {
loadedLibraries: { [url: string]: ReplaySubject<void> } = {};
load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event>;
/**
*
* @deprecated Use other overload that requires a strategy as first param
*/
load(
urlOrUrls: string | string[],
type: 'script' | 'style',

6
npm/ng-packs/packages/core/src/lib/strategies/content-security.strategy.ts

@ -14,7 +14,7 @@ export class LooseContentSecurityStrategy extends ContentSecurityStrategy {
}
}
export class StrictContentSecurityStrategy extends ContentSecurityStrategy {
export class NoContentSecurityStrategy extends ContentSecurityStrategy {
constructor() {
super();
}
@ -26,7 +26,7 @@ export const CONTENT_SECURITY_STRATEGY = {
Loose(nonce: string) {
return new LooseContentSecurityStrategy(nonce);
},
Strict() {
return new StrictContentSecurityStrategy();
None() {
return new NoContentSecurityStrategy();
},
};

52
npm/ng-packs/packages/core/src/lib/strategies/content.strategy.ts

@ -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
npm/ng-packs/packages/core/src/lib/strategies/index.ts

@ -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';

12
npm/ng-packs/packages/core/src/lib/tests/content-security.strategy.spec.ts

@ -1,7 +1,7 @@
import {
CONTENT_SECURITY_STRATEGY,
LooseContentSecurityStrategy,
StrictContentSecurityStrategy,
NoContentSecurityStrategy,
} from '../strategies';
import { uuid } from '../utils';
@ -18,10 +18,10 @@ describe('LooseContentSecurityStrategy', () => {
});
});
describe('StrictContentSecurityStrategy', () => {
describe('NoContentSecurityStrategy', () => {
describe('#applyCSP', () => {
it('should not set nonce attribute', () => {
const strategy = new StrictContentSecurityStrategy();
const strategy = new NoContentSecurityStrategy();
const element = document.createElement('link');
strategy.applyCSP(element);
@ -32,9 +32,9 @@ describe('StrictContentSecurityStrategy', () => {
describe('CONTENT_SECURITY_STRATEGY', () => {
test.each`
name | Strategy | nonce
${'Loose'} | ${LooseContentSecurityStrategy} | ${uuid()}
${'Strict'} | ${StrictContentSecurityStrategy} | ${undefined}
name | Strategy | nonce
${'Loose'} | ${LooseContentSecurityStrategy} | ${uuid()}
${'None'} | ${NoContentSecurityStrategy} | ${undefined}
`('should successfully map $name to $Strategy.name', ({ name, Strategy, nonce }) => {
expect(CONTENT_SECURITY_STRATEGY[name](nonce)).toEqual(new Strategy(nonce));
});

82
npm/ng-packs/packages/core/src/lib/tests/content.strategy.spec.ts

@ -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]()));
},
);
});

15
npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts

@ -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);
});
});

10
npm/ng-packs/packages/core/src/lib/tests/generator-utils.spec.ts

@ -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);
});
});
});

17
npm/ng-packs/packages/core/src/lib/utils/generator-utils.ts

@ -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;
}

9
npm/ng-packs/packages/theme-basic/src/lib/services/initial.service.ts

@ -1,4 +1,4 @@
import { LazyLoadService, AddReplaceableComponent } from '@abp/ng.core';
import { DomInsertionService, AddReplaceableComponent, CONTENT_STRATEGY } from '@abp/ng.core';
import { Injectable } from '@angular/core';
import { Store } from '@ngxs/store';
import styles from '../constants/styles';
@ -8,8 +8,9 @@ import { EmptyLayoutComponent } from '../components/empty-layout/empty-layout.co
@Injectable({ providedIn: 'root' })
export class InitialService {
constructor(private lazyLoadService: LazyLoadService, private store: Store) {
this.appendStyle().subscribe();
constructor(private domInsertion: DomInsertionService, private store: Store) {
this.appendStyle();
this.store.dispatch([
new AddReplaceableComponent({
key: 'Theme.ApplicationLayoutComponent',
@ -27,6 +28,6 @@ export class InitialService {
}
appendStyle() {
return this.lazyLoadService.load(null, 'style', styles, 'head', 'beforeend');
this.domInsertion.insertContent(CONTENT_STRATEGY.AppendStyleToHead(styles));
}
}

29
npm/ng-packs/packages/theme-shared/src/lib/tests/append-content.token.spec.ts

@ -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);
});
});

12
npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts

@ -1,4 +1,4 @@
import { CoreModule, LazyLoadService } from '@abp/ng.core';
import { CoreModule, noop, LazyLoadService } from '@abp/ng.core';
import { DatePipe } from '@angular/common';
import { APP_INITIALIZER, Injector, ModuleWithProviders, NgModule } from '@angular/core';
import { NgbDateParserFormatter, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
@ -25,7 +25,13 @@ import { chartJsLoaded$ } from './utils/widget-utils';
import { PaginationComponent } from './components/pagination/pagination.component';
import { LoadingComponent } from './components/loading/loading.component';
import { LoadingDirective } from './directives/loading.directive';
import { THEME_SHARED_APPEND_CONTENT } from './tokens/append-content.token';
/**
*
* @deprecated To be deleted in v2.6
*
*/
export function appendScript(injector: Injector) {
const fn = () => {
import('chart.js').then(() => chartJsLoaded$.next(true));
@ -87,8 +93,8 @@ export class ThemeSharedModule {
{
provide: APP_INITIALIZER,
multi: true,
deps: [Injector],
useFactory: appendScript,
deps: [THEME_SHARED_APPEND_CONTENT],
useFactory: noop,
},
{ provide: HTTP_ERROR_CONFIG, useValue: options.httpErrorConfig },
{

15
npm/ng-packs/packages/theme-shared/src/lib/tokens/append-content.token.ts

@ -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…
Cancel
Save