prevent context consumption of route-context from a modal that is not routable

This commit is contained in:
Niels Lyngsø
2024-11-06 21:42:25 +01:00
parent d8517bda79
commit 715bf1c58f
6 changed files with 121 additions and 9 deletions

View File

@@ -0,0 +1,40 @@
import type { UmbContextToken } from '../token/index.js';
import { UmbContextBoundary } from './context-boundary.js';
import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api';
export class UmbContextBoundaryController extends UmbContextBoundary implements UmbController {
#host: UmbControllerHost;
#controllerAlias: string;
public get controllerAlias(): string {
return this.#controllerAlias;
}
constructor(host: UmbControllerHost, contextAlias: string | UmbContextToken<any>) {
super(host.getHostElement(), contextAlias);
this.#host = host;
// Makes the controllerAlias unique for this instance, this enables multiple Contexts to be provided under the same name. (This only makes sense cause of Context Token Discriminators)
// This does mean that if someone provides a context with the same name, but with a different instance, it will not override the previous instance. But its good since it enables extensions to provide contexts at the same scope of other contexts.
this.#controllerAlias = 'umbContextBoundary_' + contextAlias.toString();
// If this API is already provided with this alias? Then we do not want to register this controller:
const existingControllers = host.getUmbControllers((x) => x.controllerAlias === this.controllerAlias);
if (existingControllers.length > 0) {
// This just an additional awareness feature to make devs Aware, the alternative would be adding it anyway, but that would destroy existing controller of this alias.
// Back out, this instance is already provided, by another controller.
throw new Error(
`Context API: The context boundary of '${this.controllerAlias}' is already provided by another Context Provider Controller.`,
);
} else {
host.addUmbController(this);
}
}
public override destroy(): void {
if (this.#host) {
this.#host.removeUmbController(this);
(this.#host as unknown) = undefined;
}
super.destroy();
}
}

View File

@@ -0,0 +1,60 @@
import type { UmbContextRequestEvent } from '../consume/context-request.event.js';
import type { UmbContextToken } from '../token/index.js';
import { UMB_CONTENT_REQUEST_EVENT_TYPE } from '../consume/context-request.event.js';
import { UmbContextProvideEventImplementation } from './context-provide.event.js';
/**
* @class UmbContextBoundary
*/
export class UmbContextBoundary {
#eventTarget: EventTarget;
#contextAlias: string;
/**
* Creates an instance of UmbContextBoundary.
* @param {EventTarget} eventTarget - the host element for this context provider
* @param {string | UmbContextToken} contextIdentifier - a string or token to identify the context
* @param {*} instance - the instance to provide
* @memberof UmbContextBoundary
*/
constructor(eventTarget: EventTarget, contextIdentifier: string | UmbContextToken<any>) {
this.#eventTarget = eventTarget;
const idSplit = contextIdentifier.toString().split('#');
this.#contextAlias = idSplit[0];
this.#eventTarget.addEventListener(UMB_CONTENT_REQUEST_EVENT_TYPE, this.#handleContextRequest);
}
/**
* @private
* @param {UmbContextRequestEvent} event - the event to handle
* @memberof UmbContextBoundary
*/
#handleContextRequest = ((event: UmbContextRequestEvent): void => {
if (event.contextAlias !== this.#contextAlias) return;
if (event.stopAtContextMatch) {
// Since the alias matches, we will stop it from bubbling further up. But we still allow it to ask the other Contexts of the element. Hence not calling `event.stopImmediatePropagation();`
event.stopPropagation();
}
}) as EventListener;
/**
* @memberof UmbContextBoundary
*/
public hostConnected(): void {
//this.hostElement.addEventListener(UMB_CONTENT_REQUEST_EVENT_TYPE, this.#handleContextRequest);
this.#eventTarget.dispatchEvent(new UmbContextProvideEventImplementation(this.#contextAlias));
}
/**
* @memberof UmbContextBoundary
*/
public hostDisconnected(): void {}
destroy(): void {
(this.#eventTarget as unknown) = undefined;
}
}

View File

@@ -46,7 +46,7 @@ export class UmbContextProvider<BaseType = unknown, ResultType extends BaseType
/**
* @private
* @param {UmbContextRequestEvent} event
* @param {UmbContextRequestEvent} event - the event to handle
* @memberof UmbContextProvider
*/
#handleContextRequest = ((event: UmbContextRequestEvent): void => {

View File

@@ -1,3 +1,5 @@
export * from './context-boundary.js';
export * from './context-boundary.controller.js';
export * from './context-provide.event.js';
export * from './context-provider.controller.js';
export * from './context-provider.js';
export * from './context-provide.event.js';

View File

@@ -13,10 +13,14 @@ import {
type UUIModalDialogElement,
type UUIModalSidebarElement,
} from '@umbraco-cms/backoffice/external/uui';
import type { UmbRouterSlotElement } from '@umbraco-cms/backoffice/router';
import { UMB_ROUTE_CONTEXT, type UmbRouterSlotElement } from '@umbraco-cms/backoffice/router';
import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api';
import type { UmbContextRequestEvent } from '@umbraco-cms/backoffice/context-api';
import { UMB_CONTENT_REQUEST_EVENT_TYPE, UmbContextProvider } from '@umbraco-cms/backoffice/context-api';
import {
UMB_CONTENT_REQUEST_EVENT_TYPE,
UmbContextBoundary,
UmbContextProvider,
} from '@umbraco-cms/backoffice/context-api';
@customElement('umb-modal')
export class UmbModalElement extends UmbLitElement {
@@ -41,7 +45,7 @@ export class UmbModalElement extends UmbLitElement {
#innerElement = new UmbBasicState<HTMLElement | undefined>(undefined);
#modalExtensionObserver?: UmbObserverController<ManifestModal | undefined>;
#modalRouterElement: UmbRouterSlotElement = document.createElement('umb-router-slot');
#modalRouterElement?: HTMLDivElement | UmbRouterSlotElement;
#onClose = () => {
this.element?.removeEventListener(UUIModalCloseEvent, this.#onClose);
@@ -85,6 +89,7 @@ export class UmbModalElement extends UmbLitElement {
*
*/
if (this.#modalContext.router) {
this.#modalRouterElement = document.createElement('umb-router-slot');
this.#modalRouterElement.routes = [
{
path: '',
@@ -92,9 +97,13 @@ export class UmbModalElement extends UmbLitElement {
},
];
this.#modalRouterElement.parent = this.#modalContext.router;
} else {
this.#modalRouterElement = document.createElement('div');
new UmbContextBoundary(this.#modalRouterElement, UMB_ROUTE_CONTEXT).hostConnected();
}
this.element.appendChild(this.#modalRouterElement);
this.#observeModal(this.#modalContext.alias.toString());
const provider = new UmbContextProvider(this.element, UMB_MODAL_CONTEXT, this.#modalContext);
@@ -151,14 +160,14 @@ export class UmbModalElement extends UmbLitElement {
}
#appendInnerElement(element: HTMLElement) {
this.#modalRouterElement.appendChild(element);
this.#modalRouterElement!.appendChild(element);
this.#innerElement.setValue(element);
}
#removeInnerElement() {
const innerElement = this.#innerElement.getValue();
if (innerElement) {
this.#modalRouterElement.removeChild(innerElement);
this.#modalRouterElement!.removeChild(innerElement);
this.#innerElement.setValue(undefined);
}
}

View File

@@ -259,9 +259,9 @@ export class UmbModalRouteRegistrationController<
}
public open(params: { [key: string]: string | number }, prepend?: string) {
if (this.active) return;
if (this.active || !this.#routeBuilder) return;
window.history.pushState({}, '', this.#routeBuilder?.(params) + (prepend ? `${prepend}` : ''));
window.history.pushState({}, '', this.#routeBuilder(params) + (prepend ? `${prepend}` : ''));
}
/**
@@ -277,6 +277,7 @@ export class UmbModalRouteRegistrationController<
return this;
}
public _internal_setRouteBuilder(urlBuilder: UmbModalRouteBuilder) {
if (!this.#routeContext) return;
this.#routeBuilder = urlBuilder;
this.#urlBuilderCallback?.(urlBuilder);
}