From 1857e6b293c10a6740ebc07cc0d2bb73f16a3638 Mon Sep 17 00:00:00 2001 From: Anatoly Ostrovsky Date: Thu, 13 Nov 2025 01:14:21 +0200 Subject: [PATCH 1/8] Sanitize ngAttrSrcset images --- src/core/compile/compile.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/core/compile/compile.js b/src/core/compile/compile.js index 1264cce70..bf2ac2846 100644 --- a/src/core/compile/compile.js +++ b/src/core/compile/compile.js @@ -2767,14 +2767,23 @@ export class CompileProvider { attr.$$element.classList.value, ); } else { - attr.$set(name, newValue); + attr.$set( + name, + name === "srcset" + ? $sce.getTrustedMediaUrl(newValue) + : newValue, + ); } }); }); if (interpolateFn.expressions.length == 0) { - // if there is nothing to watch, its a constant - attr.$set(name, newValue); + attr.$set( + name, + name === "srcset" + ? $sce.getTrustedMediaUrl(newValue) + : newValue, + ); } }, }; From 912c3b4a20ec085a6fd148e0d3b666dbc8ef5fb3 Mon Sep 17 00:00:00 2001 From: Anatoly Ostrovsky Date: Thu, 13 Nov 2025 21:10:16 +0200 Subject: [PATCH 2/8] Fix for impure binary expression. Types and test imrovements --- @types/core/scope/interface.d.ts | 4 +- @types/core/scope/scope.d.ts | 4 +- @types/namespace.d.ts | 8 +- @types/shared/utils.d.ts | 13 +- src/animations/animate.js | 17 +-- src/animations/animate.spec.js | 6 +- src/animations/runner/animate-runner.js | 2 +- src/core/interpolate/interpolate.spec.js | 8 + src/core/scope/interface.ts | 4 +- src/core/scope/scope.js | 37 ++++- src/core/scope/scope.spec.js | 2 +- src/directive/http/http.js | 1 + src/namespace.ts | 8 +- src/services/sse/sse.js | 2 +- src/shared/noderef.js | 21 +-- src/shared/noderef.spec.js | 187 +++++++++++++++++++++++ src/shared/shared.html | 1 + src/shared/utils.js | 35 ++++- src/shared/utils.spec.js | 70 ++++++++- 19 files changed, 379 insertions(+), 51 deletions(-) create mode 100644 src/shared/noderef.spec.js diff --git a/@types/core/scope/interface.d.ts b/@types/core/scope/interface.d.ts index b46815bf4..56f7e6695 100644 --- a/@types/core/scope/interface.d.ts +++ b/@types/core/scope/interface.d.ts @@ -5,10 +5,10 @@ export interface AsyncQueueTask { fn: (...args: any[]) => any; locals: Record; } -export type ListenerFunction = (newValue: any, originalTarget: object) => void; +export type ListenerFn = (newValue?: any, originalTarget?: object) => void; export interface Listener { originalTarget: object; - listenerFn: ListenerFunction; + listenerFn: ListenerFn; watchFn: CompiledExpression; id: number; scopeId: number; diff --git a/@types/core/scope/scope.d.ts b/@types/core/scope/scope.d.ts index 47d3feb5a..1f80bd38d 100644 --- a/@types/core/scope/scope.d.ts +++ b/@types/core/scope/scope.d.ts @@ -128,12 +128,12 @@ export class Scope { * function is invoked when changes to that property are detected. * * @param {string} watchProp - An expression to be watched in the context of this model. - * @param {import('./interface.ts').ListenerFunction} [listenerFn] - A function to execute when changes are detected on watched context. + * @param {ng.ListenerFn} [listenerFn] - A function to execute when changes are detected on watched context. * @param {boolean} [lazy] - A flag to indicate if the listener should be invoked immediately. Defaults to false. */ $watch( watchProp: string, - listenerFn?: import("./interface.ts").ListenerFunction, + listenerFn?: ng.ListenerFn, lazy?: boolean, ): () => void; $new(childInstance: any): any; diff --git a/@types/namespace.d.ts b/@types/namespace.d.ts index a4d0e21e4..1c26c3434 100644 --- a/@types/namespace.d.ts +++ b/@types/namespace.d.ts @@ -2,6 +2,10 @@ export { angular } from "./index.js"; import { Angular as TAngular } from "./angular.js"; import { Attributes as TAttributes } from "./core/compile/attributes.js"; import { Scope as TScope } from "./core/scope/scope.js"; +import { + ListenerFn as TListenerFn, + Listener as TListener, +} from "./core/scope/interface.ts"; import { NgModule as TNgModule } from "./core/di/ng-module.js"; import { InjectorService as TInjectorService } from "./core/di/internal-injector.js"; import { @@ -107,8 +111,10 @@ declare global { type TemplateCacheService = Map; type TemplateRequestService = TTemplateRequestService; type ErrorHandlingConfig = TErrorHandlingConfig; - type WindowService = Window; + type ListenerFn = TListenerFn; + type Listener = TListener; type DocumentService = Document; + type WindowService = Window; type WorkerConfig = TWorkerConfig; type WorkerConnection = TWorkerConnection; } diff --git a/@types/shared/utils.d.ts b/@types/shared/utils.d.ts index 3764dcee5..a93aa7b95 100644 --- a/@types/shared/utils.d.ts +++ b/@types/shared/utils.d.ts @@ -488,7 +488,18 @@ export function toDebugString(obj: any): any; * The resulting string key is in 'type:hashKey' format. */ export function hashKey(obj: any): string; -export function mergeClasses(a: any, b: any): any; +/** + * Merges two class name values into a single space-separated string. + * Accepts strings, arrays of strings, or null/undefined values. + * + * @param {string | string[] | null | undefined} a - The first class name(s). + * @param {string | string[] | null | undefined} b - The second class name(s). + * @returns {string} A single string containing all class names separated by spaces. + */ +export function mergeClasses( + a: string | string[] | null | undefined, + b: string | string[] | null | undefined, +): string; /** * Converts all accepted directives format into proper directive name. * @param {string} name Name to normalize diff --git a/src/animations/animate.js b/src/animations/animate.js index b9b37b53d..981c71358 100644 --- a/src/animations/animate.js +++ b/src/animations/animate.js @@ -1,4 +1,10 @@ -import { isFunction, isObject, minErr, extend } from "../shared/utils.js"; +import { + isFunction, + isObject, + minErr, + extend, + mergeClasses, +} from "../shared/utils.js"; import { removeElement, animatedomInsert } from "../shared/dom.js"; import { NG_ANIMATE_CLASSNAME } from "./shared.js"; @@ -14,15 +20,6 @@ import { NG_ANIMATE_CLASSNAME } from "./shared.js"; const $animateMinErr = minErr("$animate"); -function mergeClasses(a, b) { - if (!a && !b) return ""; - if (!a) return b; - if (!b) return a; - if (Array.isArray(a)) a = a.join(" "); - if (Array.isArray(b)) b = b.join(" "); - return `${a} ${b}`; -} - // if any other type of options value besides an Object value is // passed into the $animate.method() animation then this helper code // will be run which will ignore it. While this patch is not the diff --git a/src/animations/animate.spec.js b/src/animations/animate.spec.js index cb2b3e592..fb3306409 100644 --- a/src/animations/animate.spec.js +++ b/src/animations/animate.spec.js @@ -1,6 +1,6 @@ import { createElementFromHTML, dealoc } from "../shared/dom.js"; import { Angular } from "../angular.js"; -import { isObject } from "../shared/utils.js"; +import { isObject, mergeClasses } from "../shared/utils.js"; import { isFunction, wait } from "../shared/utils.js"; import { createInjector } from "../core/di/injector.js"; @@ -496,4 +496,8 @@ describe("$animate", () => { }); }); }); + + describe("mergeClasses", () => { + expect(mergeClasses); + }); }); diff --git a/src/animations/runner/animate-runner.js b/src/animations/runner/animate-runner.js index 0ec701def..673bd41ce 100644 --- a/src/animations/runner/animate-runner.js +++ b/src/animations/runner/animate-runner.js @@ -2,7 +2,7 @@ * @fileoverview * Frame-synchronized animation runner and scheduler. * Provides async batching of animation callbacks using requestAnimationFrame. - * In AngularJS, this user to be implemented as `$$AnimateRunner` + * In AngularJS, this used to be implemented as `$$AnimateRunner` */ /** diff --git a/src/core/interpolate/interpolate.spec.js b/src/core/interpolate/interpolate.spec.js index 51b2c2cc1..95bdec688 100644 --- a/src/core/interpolate/interpolate.spec.js +++ b/src/core/interpolate/interpolate.spec.js @@ -29,6 +29,14 @@ describe("$interpolate", () => { expect(interp({})).toEqual("why u no }}work{{"); }); + it("evaluates binary expressions", function () { + let interp = $interpolate("{{a + b}}"); + expect(interp({ a: 11, b: 22 })).toEqual("33"); + + interp = $interpolate("{{a + b + c}}"); + expect(interp({ a: 11, b: 22, c: 33 })).toEqual("66"); + }); + it("evaluates many expressions", function () { const interp = $interpolate("First {{anAttr}}, then {{anotherAttr}}!"); expect(interp({ anAttr: "42", anotherAttr: "43" })).toEqual( diff --git a/src/core/scope/interface.ts b/src/core/scope/interface.ts index 360eaeeab..6626df9b8 100644 --- a/src/core/scope/interface.ts +++ b/src/core/scope/interface.ts @@ -7,11 +7,11 @@ export interface AsyncQueueTask { locals: Record; } -export type ListenerFunction = (newValue: any, originalTarget: object) => void; +export type ListenerFn = (newValue?: any, originalTarget?: object) => void; export interface Listener { originalTarget: object; - listenerFn: ListenerFunction; + listenerFn: ListenerFn; watchFn: CompiledExpression; id: number; // Deregistration id scopeId: number; // The scope id that created the Listener diff --git a/src/core/scope/scope.js b/src/core/scope/scope.js index c6c1215e4..562b04de1 100644 --- a/src/core/scope/scope.js +++ b/src/core/scope/scope.js @@ -616,7 +616,7 @@ export class Scope { * function is invoked when changes to that property are detected. * * @param {string} watchProp - An expression to be watched in the context of this model. - * @param {import('./interface.ts').ListenerFunction} [listenerFn] - A function to execute when changes are detected on watched context. + * @param {ng.ListenerFn} [listenerFn] - A function to execute when changes are detected on watched context. * @param {boolean} [lazy] - A flag to indicate if the listener should be invoked immediately. Defaults to false. */ $watch(watchProp, listenerFn, lazy = false) { @@ -638,7 +638,7 @@ export class Scope { return () => {}; } - /** @type {import('./interface.ts').Listener} */ + /** @type {ng.Listener} */ const listener = { originalTarget: this.$target, listenerFn: listenerFn, @@ -696,13 +696,34 @@ export class Scope { } // 6 case ASTType.BinaryExpression: { - let expr = get.decoratedNode.body[0].expression.toWatch[0]; - key = expr.property ? expr.property.name : expr.name; - if (!key) { - throw new Error("Unable to determine key"); + if (get.decoratedNode.body[0].expression.isPure) { + let expr = get.decoratedNode.body[0].expression.toWatch[0]; + key = expr.property ? expr.property.name : expr.name; + if (!key) { + throw new Error("Unable to determine key"); + } + listener.property.push(key); + break; + } else { + let keys = []; + get.decoratedNode.body[0].expression.toWatch.forEach((x) => { + const k = x.property ? x.property.name : x.name; + if (!k) { + throw new Error("Unable to determine key"); + } + keys.push(k); + }); + keys.forEach((key) => { + this.#registerKey(key, listener); + this.#scheduleListener([listener]); + }); + + return () => { + keys.forEach((key) => { + this.#deregisterKey(key, listener.id); + }); + }; } - listener.property.push(key); - break; } // 7 case ASTType.UnaryExpression: { diff --git a/src/core/scope/scope.spec.js b/src/core/scope/scope.spec.js index 3aa9ff7f1..cc9663b52 100644 --- a/src/core/scope/scope.spec.js +++ b/src/core/scope/scope.spec.js @@ -107,7 +107,7 @@ describe("Scope", () => { }); describe("$nonscope", () => { - it("should ignore objects with $nonscope property", () => { + it("should ignore objects with $nonscope propercoty", () => { const res = createScope({ $nonscope: true }); expect(res.$id).toBeUndefined(); }); diff --git a/src/directive/http/http.js b/src/directive/http/http.js index d5ccbea40..7ca44b570 100644 --- a/src/directive/http/http.js +++ b/src/directive/http/http.js @@ -77,6 +77,7 @@ export function createHttpDirective(method, attrName) { * @param {ng.ParseService} $parse * @param {ng.StateService} $state * @param {ng.SseService} $sse + * @param {*} $animate * @returns {ng.Directive} */ return function ($http, $compile, $log, $parse, $state, $sse, $animate) { diff --git a/src/namespace.ts b/src/namespace.ts index 3e8a5b3b4..dd4f76e00 100644 --- a/src/namespace.ts +++ b/src/namespace.ts @@ -5,6 +5,10 @@ export { angular } from "./index.js"; import { Angular as TAngular } from "./angular.js"; import { Attributes as TAttributes } from "./core/compile/attributes.js"; import { Scope as TScope } from "./core/scope/scope.js"; +import { + ListenerFn as TListenerFn, + Listener as TListener, +} from "./core/scope/interface.ts"; import { NgModule as TNgModule } from "./core/di/ng-module.js"; import { InjectorService as TInjectorService } from "./core/di/internal-injector.js"; @@ -130,8 +134,10 @@ declare global { // Support types export type ErrorHandlingConfig = TErrorHandlingConfig; - export type WindowService = Window; + export type ListenerFn = TListenerFn; + export type Listener = TListener; export type DocumentService = Document; + export type WindowService = Window; export type WorkerConfig = TWorkerConfig; export type WorkerConnection = TWorkerConnection; } diff --git a/src/services/sse/sse.js b/src/services/sse/sse.js index 9c22c631a..c9ca0520b 100644 --- a/src/services/sse/sse.js +++ b/src/services/sse/sse.js @@ -134,7 +134,7 @@ export class SseProvider { es.close(); }, connect() { - if (closed == false) { + if (closed === false) { close(); } connect(); diff --git a/src/shared/noderef.js b/src/shared/noderef.js index 06de2e36e..ac21e1734 100644 --- a/src/shared/noderef.js +++ b/src/shared/noderef.js @@ -67,8 +67,8 @@ export class NodeRef { } // Handle array of elements - else if (element instanceof Array) { - if (element.length == 1) { + else if (Array.isArray(element)) { + if (element.length === 1) { this.initial = element[0].cloneNode(true); this.node = element[0]; } else { @@ -129,19 +129,12 @@ export class NodeRef { /** @returns {NodeList|Node[]} */ get nodelist() { - assertArg(this.isList, "nodes"); - if (this._nodes.length === 0) { - return this._nodes; - } - if (this._nodes[0].parentElement) { + if (this._nodes.length === 0) return []; + if (this._nodes[0].parentElement) return this._nodes[0].parentElement.childNodes; - } else { - const fragment = document.createDocumentFragment(); - this._nodes.forEach((el) => { - fragment.appendChild(el); - }); - return fragment.childNodes; - } + const fragment = document.createDocumentFragment(); + this._nodes.forEach((el) => fragment.appendChild(el)); + return fragment.childNodes; } /** @returns {Element | Node | ChildNode | NodeList | Node[]} */ diff --git a/src/shared/noderef.spec.js b/src/shared/noderef.spec.js new file mode 100644 index 000000000..25745faf6 --- /dev/null +++ b/src/shared/noderef.spec.js @@ -0,0 +1,187 @@ +import { NodeRef } from "./noderef.js"; + +describe("NodeRef", () => { + let div; + let span; + let textNode; + + beforeEach(() => { + div = document.createElement("div"); + div.textContent = "hello"; + span = document.createElement("span"); + span.textContent = "world"; + textNode = document.createTextNode("plain text"); + }); + + describe("constructor", () => { + it("wraps a single Element", () => { + const ref = new NodeRef(div); + expect(ref.node).toBe(div); + expect(ref.element).toBe(div); + expect(ref.size).toBe(1); + expect(ref.isList).toBeFalse(); + expect(ref.isElement()).toBeTrue(); + }); + + it("wraps a single Node", () => { + const ref = new NodeRef(textNode); + expect(ref.node).toBe(textNode); + expect(ref.isElement()).toBeFalse(); + }); + + it("wraps an array of Nodes", () => { + const ref = new NodeRef([div, span]); + expect(ref.nodes).toEqual([div, span]); + expect(ref.isList).toBeTrue(); + expect(ref.size).toBe(2); + }); + + it("wraps an array with one Node", () => { + const ref = new NodeRef([div]); + expect(ref.node).toBe(div); + expect(ref.isList).toBeFalse(); + expect(ref.size).toBe(1); + }); + + it("wraps a NodeList", () => { + const parent = document.createElement("div"); + parent.append(div, span); + const ref = new NodeRef(parent.childNodes); + expect(ref.nodes).toEqual([div, span]); + expect(ref.isList).toBeTrue(); + }); + + it("wraps an HTML string", () => { + const html = "

hi

"; + const ref = new NodeRef(html); + expect(ref.element.tagName.toLowerCase()).toBe("p"); + expect(ref.element.textContent).toBe("hi"); + }); + + it("handles HTML string with multiple nodes", () => { + const html = "
A
B"; + const ref = new NodeRef(html); + expect(ref.getAny().outerHTML).toContain("div"); + }); + + it("throws on invalid input", () => { + expect(() => new NodeRef(123)).toThrowError( + "Invalid element passed to NodeRef", + ); + expect(() => new NodeRef(null)).toThrow(); + expect(() => new NodeRef(undefined)).toThrow(); + }); + }); + + describe("getters and setters", () => { + it("sets and gets node", () => { + const ref = new NodeRef(div); + const newDiv = document.createElement("div"); + ref.node = newDiv; + expect(ref.node).toBe(newDiv); + expect(ref.element).toBe(newDiv); + }); + + it("sets and gets nodes array", () => { + const ref = new NodeRef(div); + ref.nodes = [div, span]; + expect(ref.nodes).toEqual([div, span]); + expect(ref.isList).toBeTrue(); + }); + + it("returns correct element and node", () => { + const ref = new NodeRef(div); + expect(ref.element).toBe(div); + expect(ref.node).toBe(div); + }); + + it("returns size correctly", () => { + const ref1 = new NodeRef(div); + const ref2 = new NodeRef([div, span]); + expect(ref1.size).toBe(1); + expect(ref2.size).toBe(2); + }); + }); + + describe("list and collection methods", () => { + it("getAny() returns first node of list", () => { + const ref = new NodeRef([div, span]); + expect(ref.getAny()).toBe(div); + }); + + it("getAll() returns all nodes", () => { + const ref = new NodeRef([div, span]); + expect(ref.getAll()).toEqual([div, span]); + }); + + it("collection() always returns array", () => { + const ref1 = new NodeRef(div); + const ref2 = new NodeRef([div, span]); + expect(ref1.collection()).toEqual([div]); + expect(ref2.collection()).toEqual([div, span]); + }); + + it("getIndex() and setIndex() work properly", () => { + const ref = new NodeRef([div, span]); + expect(ref.getIndex(1)).toBe(span); + const newNode = document.createElement("p"); + ref.setIndex(1, newNode); + expect(ref.getIndex(1)).toBe(newNode); + }); + }); + + describe("nodelist and clone", () => { + it("returns live nodelist if attached to DOM", () => { + const parent = document.createElement("div"); + parent.append(div, span); + const ref = new NodeRef([div, span]); + const list = ref.nodelist; + expect(list[0]).toBe(div); + expect(list[1]).toBe(span); + }); + + it("returns fragment-based nodelist if detached", () => { + const ref = new NodeRef([div, span]); + const list = ref.nodelist; + expect(list.length).toBe(2); + expect(list[0].isEqualNode(div)).toBeTrue(); + }); + + it("clone() creates deep copy", () => { + const ref = new NodeRef([div, span]); + const clone = ref.clone(); + expect(clone.nodes[0].isEqualNode(div)).toBeTrue(); + expect(clone.nodes[0]).not.toBe(div); + }); + + it("clone() works on single element", () => { + const ref = new NodeRef(div); + const clone = ref.clone(); + expect(clone.node.isEqualNode(div)).toBeTrue(); + expect(clone.node).not.toBe(div); + }); + }); + + describe("edge cases", () => { + it("handles text node only", () => { + const ref = new NodeRef(textNode); + expect(ref.node.textContent).toBe("plain text"); + expect(ref.isElement()).toBeFalse(); + }); + + it("handles DocumentFragment", () => { + const frag = document.createDocumentFragment(); + frag.append(div, span); + const ref = new NodeRef(frag.childNodes); + expect(ref.isList).toBeTrue(); + expect(ref.nodes.length).toBe(2); + }); + + it("handles malformed HTML string gracefully", () => { + const html = "
Missing close"; + const ref = new NodeRef(html); + expect(ref.node).toBeTruthy(); + expect(ref.getAny().nodeType).toBe(Node.ELEMENT_NODE); + }); + }); +}); diff --git a/src/shared/shared.html b/src/shared/shared.html index ca51488d5..d3be5190f 100644 --- a/src/shared/shared.html +++ b/src/shared/shared.html @@ -14,6 +14,7 @@ + diff --git a/src/shared/utils.js b/src/shared/utils.js index df2768afd..316cd08c8 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -1106,13 +1106,38 @@ export function hashKey(obj) { return `${objType}:${obj}`; } +/** + * Merges two class name values into a single space-separated string. + * Accepts strings, arrays of strings, or null/undefined values. + * + * @param {string | string[] | null | undefined} a - The first class name(s). + * @param {string | string[] | null | undefined} b - The second class name(s). + * @returns {string} A single string containing all class names separated by spaces. + */ export function mergeClasses(a, b) { if (!a && !b) return ""; - if (!a) return b; - if (!b) return a; - if (Array.isArray(a)) a = a.join(" "); - if (Array.isArray(b)) b = b.join(" "); - return a + " " + b; + if (!a) return Array.isArray(b) ? b.join(" ").trim() : b; + if (!b) return Array.isArray(a) ? a.join(" ").trim() : a; + if (Array.isArray(a)) a = normalizeStringArray(a); + if (Array.isArray(b)) b = normalizeStringArray(b); + return `${a.trim()} ${b.trim()}`.trim(); +} + +/** + * Joins an array of strings into a single string, trimming each + * element and ignoring empty strings, null, and undefined + * @param {any[]} arr + * @returns {string} + */ +function normalizeStringArray(arr) { + const cleaned = []; + for (const item of arr) { + if (item) { + const trimmed = item.trim(); + if (trimmed) cleaned.push(trimmed); + } + } + return cleaned.join(" "); } /** diff --git a/src/shared/utils.spec.js b/src/shared/utils.spec.js index 1d6c65361..4dff379c5 100644 --- a/src/shared/utils.spec.js +++ b/src/shared/utils.spec.js @@ -1,4 +1,4 @@ -import { hashKey, startsWith } from "./utils.js"; +import { hashKey, mergeClasses, startsWith } from "./utils.js"; describe("api", () => { describe("hashKey()", () => { @@ -175,4 +175,72 @@ describe("api", () => { expect(startsWith("short", "shorter")).toBe(false); }); }); + + describe("mergeClasses", () => { + it("should return empty string if both arguments are null or undefined", () => { + expect(mergeClasses(null, null)).toBe(""); + expect(mergeClasses(undefined, undefined)).toBe(""); + expect(mergeClasses(null, undefined)).toBe(""); + }); + + it("should return first argument if second is null/undefined", () => { + expect(mergeClasses("btn", null)).toBe("btn"); + expect(mergeClasses(["btn", "primary"], undefined)).toBe("btn primary"); + }); + + it("should return second argument if first is null/undefined", () => { + expect(mergeClasses(null, "active")).toBe("active"); + expect(mergeClasses(undefined, ["active", "large"])).toBe("active large"); + }); + + it("should merge two strings with a space", () => { + expect(mergeClasses("btn", "active")).toBe("btn active"); + }); + + it("should always trim merged strings", () => { + expect(mergeClasses("btn ", " active")).toBe("btn active"); + }); + + it("should merge two arrays of strings", () => { + expect(mergeClasses(["btn", "primary"], ["active", "large"])).toBe( + "btn primary active large", + ); + }); + + it("should merge string and array", () => { + expect(mergeClasses("btn", ["active", "large"])).toBe("btn active large"); + expect(mergeClasses(["btn", "primary"], "active")).toBe( + "btn primary active", + ); + }); + + it("should ignore empty strings, null, and undefined in arrays", () => { + expect( + mergeClasses(["btn", "", null, undefined], ["active", "", null]), + ).toBe("btn active"); + }); + + it("should ignore empty strings, null, and undefined in arrays if second argument is empty", () => { + expect(mergeClasses(["btn", "", null, undefined], undefined)).toBe("btn"); + }); + + it("should ignore empty strings, null, and undefined in arrays if first argument is empty", () => { + expect(mergeClasses(undefined, ["btn", "", null, undefined])).toBe("btn"); + }); + + it("should handle single argument arrays correctly", () => { + expect(mergeClasses(["btn"], ["active"])).toBe("btn active"); + }); + + it("should handle single argument arrays correctly", () => { + expect(mergeClasses(["btn ", " test"], [" active"])).toBe( + "btn test active", + ); + }); + + it("should handle one argument as string and the other as empty array", () => { + expect(mergeClasses("btn", [])).toBe("btn"); + expect(mergeClasses([], "active")).toBe("active"); + }); + }); }); From 89518acb03fffdc7667dc784c83ffa9d2842758e Mon Sep 17 00:00:00 2001 From: Anatoly Ostrovsky Date: Fri, 14 Nov 2025 18:05:17 +0200 Subject: [PATCH 3/8] Test fixes --- @types/core/sanitize/sanitize-uri.d.ts | 2 +- src/animations/animate.spec.js | 6 +---- src/core/sanitize/sanitize-uri.js | 35 +++++++++++++++----------- src/core/sanitize/sanitize-uri.spec.js | 2 +- src/core/scope/scope.spec.js | 6 ++--- src/router/state/state.spec.js | 2 +- 6 files changed, 27 insertions(+), 26 deletions(-) diff --git a/@types/core/sanitize/sanitize-uri.d.ts b/@types/core/sanitize/sanitize-uri.d.ts index 17d3229e3..5f51d5156 100644 --- a/@types/core/sanitize/sanitize-uri.d.ts +++ b/@types/core/sanitize/sanitize-uri.d.ts @@ -35,6 +35,6 @@ export class SanitizeUriProvider implements ServiceProvider { /** * @returns {import("./interface").SanitizerFn} */ - $get(): any; + $get: (string | (($window: ng.WindowService) => any))[]; } export type ServiceProvider = import("../../interface.ts").ServiceProvider; diff --git a/src/animations/animate.spec.js b/src/animations/animate.spec.js index fb3306409..cb2b3e592 100644 --- a/src/animations/animate.spec.js +++ b/src/animations/animate.spec.js @@ -1,6 +1,6 @@ import { createElementFromHTML, dealoc } from "../shared/dom.js"; import { Angular } from "../angular.js"; -import { isObject, mergeClasses } from "../shared/utils.js"; +import { isObject } from "../shared/utils.js"; import { isFunction, wait } from "../shared/utils.js"; import { createInjector } from "../core/di/injector.js"; @@ -496,8 +496,4 @@ describe("$animate", () => { }); }); }); - - describe("mergeClasses", () => { - expect(mergeClasses); - }); }); diff --git a/src/core/sanitize/sanitize-uri.js b/src/core/sanitize/sanitize-uri.js index 90a8515a3..e98541df3 100644 --- a/src/core/sanitize/sanitize-uri.js +++ b/src/core/sanitize/sanitize-uri.js @@ -1,5 +1,4 @@ import { isDefined } from "../../shared/utils.js"; -import { urlResolve } from "../../shared/url-utils/url-utils.js"; /** @typedef {import('../../interface.ts').ServiceProvider} ServiceProvider */ @@ -55,21 +54,27 @@ export class SanitizeUriProvider { /** * @returns {import("./interface").SanitizerFn} */ - $get() { - return (uri, isMediaUrl) => { - if (!uri) return uri; + $get = [ + "$window", + /** @param {ng.WindowService} $window */ + ($window) => { + return /** @type {import("./interface").SanitizerFn} */ ( + (uri, isMediaUrl) => { + if (!uri) return uri; - /** @type {RegExp} */ - const regex = isMediaUrl - ? this._imgSrcSanitizationTrustedUrlList - : this._aHrefSanitizationTrustedUrlList; + /** @type {RegExp} */ + const regex = isMediaUrl + ? this._imgSrcSanitizationTrustedUrlList + : this._aHrefSanitizationTrustedUrlList; - const normalizedVal = urlResolve(uri.trim()).href; + const normalizedVal = new URL(uri.trim(), $window.location.href).href; - if (normalizedVal !== "" && !normalizedVal.match(regex)) { - return `unsafe:${normalizedVal}`; - } - return uri; - }; - } + if (normalizedVal !== "" && !normalizedVal.match(regex)) { + return `unsafe:${normalizedVal}`; + } + return uri; + } + ); + }, + ]; } diff --git a/src/core/sanitize/sanitize-uri.spec.js b/src/core/sanitize/sanitize-uri.spec.js index 919b4dbfa..35d6c81fd 100644 --- a/src/core/sanitize/sanitize-uri.spec.js +++ b/src/core/sanitize/sanitize-uri.spec.js @@ -8,7 +8,7 @@ describe("sanitizeUri", () => { let $$sanitizeUri; beforeEach(() => { sanitizeUriProvider = new SanitizeUriProvider(); - $$sanitizeUri = sanitizeUriProvider.$get(); + $$sanitizeUri = sanitizeUriProvider.$get[1](window); sanitizeHref = function (uri) { return $$sanitizeUri(uri, false); diff --git a/src/core/scope/scope.spec.js b/src/core/scope/scope.spec.js index cc9663b52..c9af945e7 100644 --- a/src/core/scope/scope.spec.js +++ b/src/core/scope/scope.spec.js @@ -107,7 +107,7 @@ describe("Scope", () => { }); describe("$nonscope", () => { - it("should ignore objects with $nonscope propercoty", () => { + it("should ignore objects with $nonscope property", () => { const res = createScope({ $nonscope: true }); expect(res.$id).toBeUndefined(); }); @@ -592,7 +592,7 @@ describe("Scope", () => { expect(scope.counter).toBe(4); }); - it("calls only the listeners registerred at the moment the watched value changes", async () => { + it("calls only the listeners registered at the moment the watched value changes", async () => { scope.someValue = "a"; scope.counter = 0; @@ -823,7 +823,7 @@ describe("Scope", () => { expect(scope.$$watchersCount).toEqual(scope.$handler.watchers.size); }); - it("should fire upon $watch registration on initial registeration", async () => { + it("should fire upon $watch registration on initial registration", async () => { logs = ""; scope.a = 1; scope.$watch("a", () => { diff --git a/src/router/state/state.spec.js b/src/router/state/state.spec.js index 30c73eba2..ad6829076 100644 --- a/src/router/state/state.spec.js +++ b/src/router/state/state.spec.js @@ -118,7 +118,7 @@ describe("$state", () => { expect($stateProvider).toBeDefined(); }); - it("should should not allow states that are already registerred", () => { + it("should should not allow states that are already registered", () => { expect(() => { $stateProvider.state({ name: "toString", url: "/to-string" }); }).not.toThrow(); From 1671cef7bd0498dc348fba21bd042a5a845015d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:07:20 +0000 Subject: [PATCH 4/8] Bump js-yaml from 4.1.0 to 4.1.1 Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 20fcdd78e..783e27e9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,31 +10,31 @@ "license": "MIT", "devDependencies": { "@eslint/js": "^9.28.0", - "@playwright/test": "latest", - "@rollup/plugin-commonjs": "latest", - "@rollup/plugin-node-resolve": "latest", - "@rollup/plugin-terser": "latest", - "@types/jasmine": "latest", + "@playwright/test": "*", + "@rollup/plugin-commonjs": "*", + "@rollup/plugin-node-resolve": "*", + "@rollup/plugin-terser": "*", + "@types/jasmine": "*", "cssnano": "^7.1.1", "eslint": "^9.28.0", "eslint-config-prettier": "^10.1.5", - "eslint-plugin-import": "latest", + "eslint-plugin-import": "*", "eslint-plugin-prettier": "^5.4.1", - "eslint-plugin-promise": "latest", - "express": "latest", + "eslint-plugin-promise": "*", + "express": "*", "globals": "^16.2.0", "husky": "^9.1.7", - "jasmine-core": "latest", - "npm-run-all": "latest", - "playwright": "latest", + "jasmine-core": "*", + "npm-run-all": "*", + "playwright": "*", "postcss": "^8.5.6", - "prettier": "latest", - "rollup": "latest", - "rollup-plugin-version-injector": "latest", + "prettier": "*", + "rollup": "*", + "rollup-plugin-version-injector": "*", "sinon": "19.0.4", - "typedoc": "latest", - "typescript": "latest", - "vite": "latest" + "typedoc": "*", + "typescript": "*", + "vite": "*" }, "engines": { "node": ">=24.0.0" @@ -3926,9 +3926,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { From e0b847d1cb09c7f174b21c41253d51ba8426d0df Mon Sep 17 00:00:00 2001 From: Anatoly Ostrovsky Date: Fri, 21 Nov 2025 10:21:01 +0200 Subject: [PATCH 5/8] Type improvements. Ignore events for scope --- @types/animations/animate.d.ts | 74 ++-------------------- @types/core/compile/attributes.d.ts | 4 +- @types/core/sanitize/interface.d.ts | 8 +++ @types/core/sanitize/sanitize-uri.d.ts | 7 ++- @types/services/sce/sce.d.ts | 86 ++------------------------ src/animations/animate-example.html | 60 ++++++++++++++++++ src/animations/animate.js | 74 ++-------------------- src/core/compile/attributes.js | 2 +- src/core/sanitize/interface.ts | 10 ++- src/core/sanitize/sanitize-uri.js | 7 ++- src/core/scope/scope.js | 5 ++ src/core/scope/scope.spec.js | 4 ++ src/services/sce/sce.js | 85 +------------------------ 13 files changed, 112 insertions(+), 314 deletions(-) create mode 100644 @types/core/sanitize/interface.d.ts create mode 100644 src/animations/animate-example.html diff --git a/@types/animations/animate.d.ts b/@types/animations/animate.d.ts index d67a2d3b2..c49f6f1e3 100644 --- a/@types/animations/animate.d.ts +++ b/@types/animations/animate.d.ts @@ -212,74 +212,12 @@ export class AnimateProvider { */ enabled: any; /** - * Cancels the provided animation and applies the end state of the animation. - * Note that this does not cancel the underlying operation, e.g. the setting of classes or - * adding the element to the DOM. - * - * @param {import('./runner/animate-runner.js').AnimateRunner} runner An animation runner returned by an $animate function. - * - * @example - - - angular.module('animationExample', []).component('cancelExample', { - templateUrl: 'template.html', - controller: function($element, $animate) { - this.runner = null; - - this.addClass = function() { - this.runner = $animate.addClass($element.querySelectorAll('div'), 'red'); - let ctrl = this; - this.runner.finally(function() { - ctrl.runner = null; - }); - }; - - this.removeClass = function() { - this.runner = $animate.removeClass($element.querySelectorAll('div'), 'red'); - let ctrl = this; - this.runner.finally(function() { - ctrl.runner = null; - }); - }; - - this.cancel = function() { - $animate.cancel(this.runner); - }; - } - }); - - -

- - -
- -
-

CSS-Animated Text
-

-
- - - - - .red-add, .red-remove { - transition: all 4s cubic-bezier(0.250, 0.460, 0.450, 0.940); - } - - .red, - .red-add.red-add-active { - color: #FF0000; - font-size: 40px; - } - - .red-remove.red-remove-active { - font-size: 10px; - color: black; - } - - -
- */ + * Cancels the provided animation and applies the end state of the animation. + * Note that this does not cancel the underlying operation, e.g. the setting of classes or + * adding the element to the DOM. + * + * @param {import('./runner/animate-runner.js').AnimateRunner} runner An animation runner returned by an $animate function. + */ cancel( runner: import("./runner/animate-runner.js").AnimateRunner, ): void; diff --git a/@types/core/compile/attributes.d.ts b/@types/core/compile/attributes.d.ts index 899b7cb00..390584f87 100644 --- a/@types/core/compile/attributes.d.ts +++ b/@types/core/compile/attributes.d.ts @@ -1,7 +1,7 @@ export class Attributes { static $nonscope: boolean; /** - * @param {ng.Scope} $rootScope + * @param {ng.RootScopeService} $rootScope * @param {*} $animate * @param {ng.ExceptionHandlerService} $exceptionHandler * @param {*} $sce @@ -9,7 +9,7 @@ export class Attributes { * @param {Object} [attributesToCopy] */ constructor( - $rootScope: ng.Scope, + $rootScope: ng.RootScopeService, $animate: any, $exceptionHandler: ng.ExceptionHandlerService, $sce: any, diff --git a/@types/core/sanitize/interface.d.ts b/@types/core/sanitize/interface.d.ts new file mode 100644 index 000000000..0046947df --- /dev/null +++ b/@types/core/sanitize/interface.d.ts @@ -0,0 +1,8 @@ +/** + * Sanitizer function that processes a URI string and optionally + * treats it as a media URL. + */ +export type SanitizerFn = ( + uri: string | null | undefined, + isMediaUrl?: boolean, +) => string | null | undefined; diff --git a/@types/core/sanitize/sanitize-uri.d.ts b/@types/core/sanitize/sanitize-uri.d.ts index 5f51d5156..1ca5acb40 100644 --- a/@types/core/sanitize/sanitize-uri.d.ts +++ b/@types/core/sanitize/sanitize-uri.d.ts @@ -33,8 +33,11 @@ export class SanitizeUriProvider implements ServiceProvider { regexp?: RegExp | undefined, ): RegExp | SanitizeUriProvider; /** - * @returns {import("./interface").SanitizerFn} + * @returns {import("./interface.ts").SanitizerFn} */ - $get: (string | (($window: ng.WindowService) => any))[]; + $get: ( + | string + | (($window: ng.WindowService) => import("./interface.ts").SanitizerFn) + )[]; } export type ServiceProvider = import("../../interface.ts").ServiceProvider; diff --git a/@types/services/sce/sce.d.ts b/@types/services/sce/sce.d.ts index 4c3a225ce..9fb472893 100644 --- a/@types/services/sce/sce.d.ts +++ b/@types/services/sce/sce.d.ts @@ -24,85 +24,7 @@ export namespace SCE_CONTEXTS { * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict * Contextual Escaping (SCE)} services to AngularTS. * - * For an overview of this service and the functionnality it provides in AngularTS, see the main - * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how - * SCE works in their application, which shouldn't be needed in most cases. - * - *
- * AngularTS strongly relies on contextual escaping for the security of bindings: disabling or - * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners, - * changes to this service will also influence users, so be extra careful and document your changes. - *
- * - * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of - * the `$sce` service to customize the way Strict Contextual Escaping works in AngularTS. This is - * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to - * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things - * work because `$sce` delegates to `$sceDelegate` for these operations. - * - * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. - * - * The default instance of `$sceDelegate` should work out of the box with little pain. While you - * can override it completely to change the behavior of `$sce`, the common case would - * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting - * your own trusted and banned resource lists for trusting URLs used for loading AngularTS resources - * such as templates. Refer {@link ng.$sceDelegateProvider#trustedResourceUrlList - * $sceDelegateProvider.trustedResourceUrlList} and {@link - * ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList} - */ -/** - * - * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate - * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}. - * - * The `$sceDelegateProvider` allows one to get/set the `trustedResourceUrlList` and - * `bannedResourceUrlList` used to ensure that the URLs used for sourcing AngularTS templates and - * other script-running URLs are safe (all places that use the `$sce.RESOURCE_URL` context). See - * {@link ng.$sceDelegateProvider#trustedResourceUrlList - * $sceDelegateProvider.trustedResourceUrlList} and - * {@link ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList}, - * - * For the general details about this service in AngularTS, read the main page for {@link ng.$sce - * Strict Contextual Escaping (SCE)}. - * - * **Example**: Consider the following case. - * - * - your app is hosted at url `http://myapp.example.com/` - * - but some of your templates are hosted on other domains you control such as - * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. - * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. - * - * Here is what a secure configuration for this scenario might look like: - * - * ``` - * angular.module('myApp', []).config(function($sceDelegateProvider) { - * $sceDelegateProvider.trustedResourceUrlList([ - * // Allow same origin resource loads. - * 'self', - * // Allow loading from our assets domain. Notice the difference between * and **. - * 'http://srv*.assets.example.com/**' - * ]); - * - * // The banned resource URL list overrides the trusted resource URL list so the open redirect - * // here is blocked. - * $sceDelegateProvider.bannedResourceUrlList([ - * 'http://myapp.example.com/clickThru**' - * ]); - * }); - * ``` - * Note that an empty trusted resource URL list will block every resource URL from being loaded, and will require - * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates - * requested by {@link ng.$templateRequest $templateRequest} that are present in - * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism - * to populate your templates in that cache at config time, then it is a good idea to remove 'self' - * from the trusted resource URL lsit. This helps to mitigate the security impact of certain types - * of issues, like for instance attacker-controlled `ng-includes`. - */ -/** - * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict - * Contextual Escaping (SCE)} services to AngularTS. - * - * For an overview of this service and the functionnality it provides in AngularTS, see the main + * For an overview of this service and the functionality it provides in AngularTS, see the main * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how * SCE works in their application, which shouldn't be needed in most cases. * @@ -179,7 +101,7 @@ export namespace SCE_CONTEXTS { export class SceDelegateProvider { /** * - * @param {Array=} trustedResourceUrlList When provided, replaces the trustedResourceUrlList with + * @param {Array=} value When provided, replaces the trustedResourceUrlList with * the value provided. This must be an array or null. A snapshot of this array is used so * further changes to the array are ignored. * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items @@ -198,7 +120,7 @@ export class SceDelegateProvider { * its origin with other apps! It is a good idea to limit it to only your application's directory. *
*/ - trustedResourceUrlList: (value: any, ...args: any[]) => any[]; + trustedResourceUrlList: (value?: any[] | undefined, ...args: any[]) => any[]; /** * * @param {Array=} bannedResourceUrlList When provided, replaces the `bannedResourceUrlList` with @@ -226,7 +148,7 @@ export class SceDelegateProvider { | string | (( $injector: ng.InjectorService, - $$sanitizeUri: any, + $$sanitizeUri: import("../../core/sanitize/interface.ts").SanitizerFn, $exceptionHandler: ng.ExceptionHandlerService, ) => { trustAs: (type: string, trustedValue: any) => any; diff --git a/src/animations/animate-example.html b/src/animations/animate-example.html new file mode 100644 index 000000000..878706297 --- /dev/null +++ b/src/animations/animate-example.html @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/animations/animate.js b/src/animations/animate.js index 981c71358..fc9b67459 100644 --- a/src/animations/animate.js +++ b/src/animations/animate.js @@ -304,74 +304,12 @@ export function AnimateProvider($provide) { enabled: $$animateQueue.enabled, /** - * Cancels the provided animation and applies the end state of the animation. - * Note that this does not cancel the underlying operation, e.g. the setting of classes or - * adding the element to the DOM. - * - * @param {import('./runner/animate-runner.js').AnimateRunner} runner An animation runner returned by an $animate function. - * - * @example - - - angular.module('animationExample', []).component('cancelExample', { - templateUrl: 'template.html', - controller: function($element, $animate) { - this.runner = null; - - this.addClass = function() { - this.runner = $animate.addClass($element.querySelectorAll('div'), 'red'); - let ctrl = this; - this.runner.finally(function() { - ctrl.runner = null; - }); - }; - - this.removeClass = function() { - this.runner = $animate.removeClass($element.querySelectorAll('div'), 'red'); - let ctrl = this; - this.runner.finally(function() { - ctrl.runner = null; - }); - }; - - this.cancel = function() { - $animate.cancel(this.runner); - }; - } - }); - - -

- - -
- -
-

CSS-Animated Text
-

-
- - - - - .red-add, .red-remove { - transition: all 4s cubic-bezier(0.250, 0.460, 0.450, 0.940); - } - - .red, - .red-add.red-add-active { - color: #FF0000; - font-size: 40px; - } - - .red-remove.red-remove-active { - font-size: 10px; - color: black; - } - - -
- */ + * Cancels the provided animation and applies the end state of the animation. + * Note that this does not cancel the underlying operation, e.g. the setting of classes or + * adding the element to the DOM. + * + * @param {import('./runner/animate-runner.js').AnimateRunner} runner An animation runner returned by an $animate function. + */ cancel(runner) { if (runner.cancel) { runner.cancel(); diff --git a/src/core/compile/attributes.js b/src/core/compile/attributes.js index 301e624c8..79ef8571d 100644 --- a/src/core/compile/attributes.js +++ b/src/core/compile/attributes.js @@ -21,7 +21,7 @@ export class Attributes { static $nonscope = true; /** - * @param {ng.Scope} $rootScope + * @param {ng.RootScopeService} $rootScope * @param {*} $animate * @param {ng.ExceptionHandlerService} $exceptionHandler * @param {*} $sce diff --git a/src/core/sanitize/interface.ts b/src/core/sanitize/interface.ts index fca01f011..0046947df 100644 --- a/src/core/sanitize/interface.ts +++ b/src/core/sanitize/interface.ts @@ -2,9 +2,7 @@ * Sanitizer function that processes a URI string and optionally * treats it as a media URL. */ -export interface SanitizerFn { - ( - uri: string | null | undefined, - isMediaUrl?: boolean, - ): string | null | undefined; -} +export type SanitizerFn = ( + uri: string | null | undefined, + isMediaUrl?: boolean, +) => string | null | undefined; diff --git a/src/core/sanitize/sanitize-uri.js b/src/core/sanitize/sanitize-uri.js index e98541df3..080c13eda 100644 --- a/src/core/sanitize/sanitize-uri.js +++ b/src/core/sanitize/sanitize-uri.js @@ -1,4 +1,5 @@ import { isDefined } from "../../shared/utils.js"; +import { $injectTokens } from "../../injection-tokens.js"; /** @typedef {import('../../interface.ts').ServiceProvider} ServiceProvider */ @@ -52,13 +53,13 @@ export class SanitizeUriProvider { } /** - * @returns {import("./interface").SanitizerFn} + * @returns {import("./interface.ts").SanitizerFn} */ $get = [ - "$window", + $injectTokens.$window, /** @param {ng.WindowService} $window */ ($window) => { - return /** @type {import("./interface").SanitizerFn} */ ( + return /** @type {import("./interface.ts").SanitizerFn} */ ( (uri, isMediaUrl) => { if (!uri) return uri; diff --git a/src/core/scope/scope.js b/src/core/scope/scope.js index 562b04de1..81a0a0b34 100644 --- a/src/core/scope/scope.js +++ b/src/core/scope/scope.js @@ -136,6 +136,11 @@ export function isUnsafeGlobal(target) { return true; } + // Events + if (typeof Event !== "undefined" && target instanceof Event) { + return true; + } + // Cross-origin or non-enumerable window objects try { return Object.prototype.toString.call(target) === "[object Window]"; diff --git a/src/core/scope/scope.spec.js b/src/core/scope/scope.spec.js index c9af945e7..c2592b294 100644 --- a/src/core/scope/scope.spec.js +++ b/src/core/scope/scope.spec.js @@ -3058,4 +3058,8 @@ describe("isUnsafeGlobal", () => { }; expect(() => isUnsafeGlobal(fakeCrossOrigin)).not.toThrow(); }); + + it("ignores events", () => { + expect(isUnsafeGlobal(new PointerEvent("test"))).toBeTrue(); + }); }); diff --git a/src/services/sce/sce.js b/src/services/sce/sce.js index 74cd62ebe..4c6a5e9d5 100644 --- a/src/services/sce/sce.js +++ b/src/services/sce/sce.js @@ -88,7 +88,7 @@ export function adjustMatcher(matcher) { * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict * Contextual Escaping (SCE)} services to AngularTS. * - * For an overview of this service and the functionnality it provides in AngularTS, see the main + * For an overview of this service and the functionality it provides in AngularTS, see the main * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how * SCE works in their application, which shouldn't be needed in most cases. * @@ -115,85 +115,6 @@ export function adjustMatcher(matcher) { * ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList} */ -/** - * - * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate - * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}. - * - * The `$sceDelegateProvider` allows one to get/set the `trustedResourceUrlList` and - * `bannedResourceUrlList` used to ensure that the URLs used for sourcing AngularTS templates and - * other script-running URLs are safe (all places that use the `$sce.RESOURCE_URL` context). See - * {@link ng.$sceDelegateProvider#trustedResourceUrlList - * $sceDelegateProvider.trustedResourceUrlList} and - * {@link ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList}, - * - * For the general details about this service in AngularTS, read the main page for {@link ng.$sce - * Strict Contextual Escaping (SCE)}. - * - * **Example**: Consider the following case. - * - * - your app is hosted at url `http://myapp.example.com/` - * - but some of your templates are hosted on other domains you control such as - * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. - * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. - * - * Here is what a secure configuration for this scenario might look like: - * - * ``` - * angular.module('myApp', []).config(function($sceDelegateProvider) { - * $sceDelegateProvider.trustedResourceUrlList([ - * // Allow same origin resource loads. - * 'self', - * // Allow loading from our assets domain. Notice the difference between * and **. - * 'http://srv*.assets.example.com/**' - * ]); - * - * // The banned resource URL list overrides the trusted resource URL list so the open redirect - * // here is blocked. - * $sceDelegateProvider.bannedResourceUrlList([ - * 'http://myapp.example.com/clickThru**' - * ]); - * }); - * ``` - * Note that an empty trusted resource URL list will block every resource URL from being loaded, and will require - * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates - * requested by {@link ng.$templateRequest $templateRequest} that are present in - * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism - * to populate your templates in that cache at config time, then it is a good idea to remove 'self' - * from the trusted resource URL lsit. This helps to mitigate the security impact of certain types - * of issues, like for instance attacker-controlled `ng-includes`. - */ - -/** - * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict - * Contextual Escaping (SCE)} services to AngularTS. - * - * For an overview of this service and the functionnality it provides in AngularTS, see the main - * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how - * SCE works in their application, which shouldn't be needed in most cases. - * - *
- * AngularTS strongly relies on contextual escaping for the security of bindings: disabling or - * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners, - * changes to this service will also influence users, so be extra careful and document your changes. - *
- * - * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of - * the `$sce` service to customize the way Strict Contextual Escaping works in AngularTS. This is - * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to - * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things - * work because `$sce` delegates to `$sceDelegate` for these operations. - * - * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. - * - * The default instance of `$sceDelegate` should work out of the box with little pain. While you - * can override it completely to change the behavior of `$sce`, the common case would - * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting - * your own trusted and banned resource lists for trusting URLs used for loading AngularTS resources - * such as templates. Refer {@link ng.$sceDelegateProvider#trustedResourceUrlList - * $sceDelegateProvider.trustedResourceUrlList} and {@link - * ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList} - */ /** * * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate @@ -250,7 +171,7 @@ export class SceDelegateProvider { /** * - * @param {Array=} trustedResourceUrlList When provided, replaces the trustedResourceUrlList with + * @param {Array=} value When provided, replaces the trustedResourceUrlList with * the value provided. This must be an array or null. A snapshot of this array is used so * further changes to the array are ignored. * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items @@ -312,7 +233,7 @@ export class SceDelegateProvider { /** * * @param {ng.InjectorService} $injector - * @param {*} $$sanitizeUri + * @param {import("../../core/sanitize/interface.ts").SanitizerFn} $$sanitizeUri * @param {ng.ExceptionHandlerService} $exceptionHandler * @returns */ From dc4cebe3b9a755e869fbcf9ba28ab0c9f4edb3aa Mon Sep 17 00:00:00 2001 From: Anatoly Ostrovsky Date: Thu, 13 Nov 2025 01:39:30 +0200 Subject: [PATCH 6/8] Demo --- @types/directive/wasm/wasm.d.ts | 3 +++ index.html | 28 +++++----------------------- src/directive/wasm/math.ts | 8 ++++++++ src/directive/wasm/math.wasm | Bin 0 -> 109 bytes src/directive/wasm/wasm.js | 17 +++++++++++++++++ src/ng.js | 2 ++ 6 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 @types/directive/wasm/wasm.d.ts create mode 100644 src/directive/wasm/math.ts create mode 100644 src/directive/wasm/math.wasm create mode 100644 src/directive/wasm/wasm.js diff --git a/@types/directive/wasm/wasm.d.ts b/@types/directive/wasm/wasm.d.ts new file mode 100644 index 000000000..757fb4578 --- /dev/null +++ b/@types/directive/wasm/wasm.d.ts @@ -0,0 +1,3 @@ +export function ngWasmDirective(): { + link: ($scope: any, $element: any, $attrs: any) => Promise; +}; diff --git a/index.html b/index.html index 4bc4fa906..7b707829b 100644 --- a/index.html +++ b/index.html @@ -98,28 +98,10 @@ -
-

Todos

- -
    -
  • - {{ todo.task }} {{ todo.done }} - -
  • -
-
- - -
- - -
- -
Fade me in out
- - -
+ + + + + {{ x }} diff --git a/src/directive/wasm/math.ts b/src/directive/wasm/math.ts new file mode 100644 index 000000000..acaba61a1 --- /dev/null +++ b/src/directive/wasm/math.ts @@ -0,0 +1,8 @@ +// // math.ts +// export function add(a: i32, b: i32): i32 { +// return a + b; +// } + +// export function mul(a: i32, b: i32): i32 { +// return a * b; +// } diff --git a/src/directive/wasm/math.wasm b/src/directive/wasm/math.wasm new file mode 100644 index 0000000000000000000000000000000000000000..cf925020ae876bfb674a58a8a85a49abf1b2f72c GIT binary patch literal 109 zcmXBJu?mAg5Jl1VW^~;+g@uyZU)euVFimh#6bq|Kna_^F>K?cl_e=n+IEXu4Re+-8 xO0v{zg-#tQnlpVrVweA^o~q+C0h*5)4EdFp?G!u$k!Yw}iRjVu-Pnz}u^)L;53B$H literal 0 HcmV?d00001 diff --git a/src/directive/wasm/wasm.js b/src/directive/wasm/wasm.js new file mode 100644 index 000000000..5c4c536a0 --- /dev/null +++ b/src/directive/wasm/wasm.js @@ -0,0 +1,17 @@ +export function ngWasmDirective() { + return { + link: async function ($scope, $element, $attrs) { + try { + const response = await fetch($attrs.src); + const bytes = await response.arrayBuffer(); + const { instance } = await WebAssembly.instantiate(bytes); + const exports = instance.exports; + + $scope["demo"] = exports; + } catch (err) { + console.error("[wasm-loader] Failed to load:", err); + $element.addClass("wasm-error"); + } + }, + }; +} diff --git a/src/ng.js b/src/ng.js index 6f1cb05ca..1e91d999b 100644 --- a/src/ng.js +++ b/src/ng.js @@ -135,6 +135,7 @@ import { SseProvider } from "./services/sse/sse.js"; import { ngViewportDirective } from "./directive/viewport/viewport.js"; import { ngWorkerDirective } from "./directive/worker/worker.js"; import { WorkerProvider } from "./services/worker/worker.js"; +import { ngWasmDirective } from "./directive/wasm/wasm.js"; /** * Initializes core `ng` module. @@ -215,6 +216,7 @@ export function registerNgModule(angular) { ngValue: ngValueDirective, ngModelOptions: ngModelOptionsDirective, ngViewport: ngViewportDirective, + ngWasm: ngWasmDirective, ngWorker: ngWorkerDirective, }) .directive({ From cb370adf4876466fb72125c057b3d470a6a6b2e7 Mon Sep 17 00:00:00 2001 From: Anatoly Ostrovsky Date: Mon, 24 Nov 2025 20:06:10 +0200 Subject: [PATCH 7/8] Wasm provider and wasm directive --- @types/core/di/ng-module.d.ts | 7 +++++++ @types/directive/wasm/wasm.d.ts | 2 +- @types/shared/utils.d.ts | 4 ++++ index.html | 26 ++++++++++++++++++-------- src/core/di/injector.js | 12 ++++++++++++ src/core/di/ng-module.js | 23 ++++++++++++++++++++++- src/directive/wasm/wasm.js | 16 ++++------------ src/shared/utils.js | 10 ++++++++++ 8 files changed, 78 insertions(+), 22 deletions(-) diff --git a/@types/core/di/ng-module.d.ts b/@types/core/di/ng-module.d.ts index 087cb7a25..09647b445 100644 --- a/@types/core/di/ng-module.d.ts +++ b/@types/core/di/ng-module.d.ts @@ -49,6 +49,7 @@ export class NgModule { /** @type {!Array.>} */ runBlocks: Array>; services: any[]; + wasmModules: any[]; /** * @param {string} name * @param {any} object @@ -153,4 +154,10 @@ export class NgModule { name: string, ctlFn: import("../../interface.ts").Injectable, ): NgModule; + /** + * @param {string} name + * @param {string} src + * @returns {NgModule} + */ + wasm(name: string, src: string): NgModule; } diff --git a/@types/directive/wasm/wasm.d.ts b/@types/directive/wasm/wasm.d.ts index 757fb4578..5b323a182 100644 --- a/@types/directive/wasm/wasm.d.ts +++ b/@types/directive/wasm/wasm.d.ts @@ -1,3 +1,3 @@ export function ngWasmDirective(): { - link: ($scope: any, $element: any, $attrs: any) => Promise; + link: ($scope: any, _: any, $attrs: any) => Promise; }; diff --git a/@types/shared/utils.d.ts b/@types/shared/utils.d.ts index a93aa7b95..5049c9454 100644 --- a/@types/shared/utils.d.ts +++ b/@types/shared/utils.d.ts @@ -582,5 +582,9 @@ export function wait(t?: number): Promise; * // returns false */ export function startsWith(str: string, search: string): boolean; +/** + * @param {string} src + */ +export function instantiateWasm(src: string): Promise; export const isProxySymbol: unique symbol; export const ngAttrPrefixes: string[]; diff --git a/index.html b/index.html index 7b707829b..78cb45a82 100644 --- a/index.html +++ b/index.html @@ -54,16 +54,22 @@ } class Demo { - constructor($eventBus, $scope) { - $eventBus.subscribe("demo", (val) => { - $scope["$ctrl"].mailBox = val; + constructor($eventBus, $scope, adder) { + adder.then((x) => { + debugger; + this.adder = x; }); } + + test() { + this.y = this.adder.add(1, 2); + } } window.angular .module("todo", []) .controller("TodoCtrl", TodoController) - .controller("DemoCtrl", Demo); + .controller("DemoCtrl", Demo) + .wasm("adder", "src/directive/wasm/math.wasm"); }); @@ -98,10 +104,14 @@ - - - - +
+ {{ $ctrl.y }} + +
+ + + + {{ x }} diff --git a/src/core/di/injector.js b/src/core/di/injector.js index a65fa7073..0b0212cf4 100644 --- a/src/core/di/injector.js +++ b/src/core/di/injector.js @@ -39,6 +39,7 @@ export function createInjector(modulesToLoad, strictDi = false) { service: supportObject(service), value: supportObject(value), constant: supportObject(constant), + wasm: supportObject(wasm), decorator, }, }; @@ -154,6 +155,17 @@ export function createInjector(modulesToLoad, strictDi = false) { protoInstanceInjector.cache[name] = value; } + /** + * Register a wasm module (available during config). + * @param {string} name + * @param {string} value + * @returns {void} + */ + function wasm(name, value) { + providerInjector.cache[name] = value; + protoInstanceInjector.cache[name] = value; + } + /** * Register a decorator function to modify or replace an existing service. * @param name - The name of the service to decorate. diff --git a/src/core/di/ng-module.js b/src/core/di/ng-module.js index 86203cc89..f4f9bd225 100644 --- a/src/core/di/ng-module.js +++ b/src/core/di/ng-module.js @@ -1,5 +1,10 @@ import { $injectTokens as $t } from "../../injection-tokens.js"; -import { isFunction, isString, assert } from "../../shared/utils.js"; +import { + isFunction, + isString, + assert, + instantiateWasm, +} from "../../shared/utils.js"; /** @private */ export const INJECTOR_LITERAL = "$injector"; @@ -60,6 +65,8 @@ export class NgModule { } this.services = []; + + this.wasmModules = []; } /** @@ -226,4 +233,18 @@ export class NgModule { this.invokeQueue.push([CONTROLLER_LITERAL, "register", [name, ctlFn]]); return this; } + + /** + * @param {string} name + * @param {string} src + * @returns {NgModule} + */ + wasm(name, src) { + this.invokeQueue.push([ + $t.$provide, + "wasm", + [name, (async () => instantiateWasm(src))()], + ]); + return this; + } } diff --git a/src/directive/wasm/wasm.js b/src/directive/wasm/wasm.js index 5c4c536a0..de7ff8569 100644 --- a/src/directive/wasm/wasm.js +++ b/src/directive/wasm/wasm.js @@ -1,17 +1,9 @@ +import { instantiateWasm } from "../../shared/utils.js"; + export function ngWasmDirective() { return { - link: async function ($scope, $element, $attrs) { - try { - const response = await fetch($attrs.src); - const bytes = await response.arrayBuffer(); - const { instance } = await WebAssembly.instantiate(bytes); - const exports = instance.exports; - - $scope["demo"] = exports; - } catch (err) { - console.error("[wasm-loader] Failed to load:", err); - $element.addClass("wasm-error"); - } + link: async function ($scope, _, $attrs) { + $scope.$target[$attrs.as || "wasm"] = await instantiateWasm($attrs.src); }, }; } diff --git a/src/shared/utils.js b/src/shared/utils.js index 316cd08c8..3d93257bf 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -1278,3 +1278,13 @@ export function wait(t = 0) { export function startsWith(str, search) { return str.slice(0, search.length) === search; } + +/** + * @param {string} src + */ +export async function instantiateWasm(src) { + const response = await fetch(src); + const bytes = await response.arrayBuffer(); + const { instance } = await WebAssembly.instantiate(bytes); + return instance.exports; +} From 65a9becb51ca1b448ec6aaa7ccc1192558bc334c Mon Sep 17 00:00:00 2001 From: Anatoly Ostrovsky Date: Mon, 24 Nov 2025 20:36:56 +0200 Subject: [PATCH 8/8] Refactor utils --- @types/shared/utils.d.ts | 7 +++++-- index.html | 10 +++------- src/core/di/ng-module.js | 2 +- src/core/scope/scope.js | 4 ++++ src/shared/utils.js | 18 +++++++++++++----- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/@types/shared/utils.d.ts b/@types/shared/utils.d.ts index 5049c9454..62fbea116 100644 --- a/@types/shared/utils.d.ts +++ b/@types/shared/utils.d.ts @@ -583,8 +583,11 @@ export function wait(t?: number): Promise; */ export function startsWith(str: string, search: string): boolean; /** - * @param {string} src + * Loads and instantiates a WebAssembly module with proper error handling. + * + * @param {string} src - URL to the wasm file + * @returns {Promise} - Resolves to wasm exports */ -export function instantiateWasm(src: string): Promise; +export function instantiateWasm(src: string): Promise; export const isProxySymbol: unique symbol; export const ngAttrPrefixes: string[]; diff --git a/index.html b/index.html index 78cb45a82..2163fecf3 100644 --- a/index.html +++ b/index.html @@ -55,14 +55,11 @@ class Demo { constructor($eventBus, $scope, adder) { - adder.then((x) => { - debugger; - this.adder = x; - }); + this.adder = adder; } - test() { - this.y = this.adder.add(1, 2); + async test() { + this.y = (await this.adder).add(1, 2); } } window.angular @@ -108,7 +105,6 @@ {{ $ctrl.y }} - diff --git a/src/core/di/ng-module.js b/src/core/di/ng-module.js index f4f9bd225..aedf85a4f 100644 --- a/src/core/di/ng-module.js +++ b/src/core/di/ng-module.js @@ -243,7 +243,7 @@ export class NgModule { this.invokeQueue.push([ $t.$provide, "wasm", - [name, (async () => instantiateWasm(src))()], + [name, (() => instantiateWasm(src))()], ]); return this; } diff --git a/src/core/scope/scope.js b/src/core/scope/scope.js index 81a0a0b34..80c28542d 100644 --- a/src/core/scope/scope.js +++ b/src/core/scope/scope.js @@ -136,6 +136,10 @@ export function isUnsafeGlobal(target) { return true; } + if (target instanceof Promise) { + return true; + } + // Events if (typeof Event !== "undefined" && target instanceof Event) { return true; diff --git a/src/shared/utils.js b/src/shared/utils.js index 3d93257bf..1e8fcd724 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -1280,11 +1280,19 @@ export function startsWith(str, search) { } /** - * @param {string} src + * Loads and instantiates a WebAssembly module with proper error handling. + * + * @param {string} src - URL to the wasm file + * @returns {Promise} - Resolves to wasm exports */ export async function instantiateWasm(src) { - const response = await fetch(src); - const bytes = await response.arrayBuffer(); - const { instance } = await WebAssembly.instantiate(bytes); - return instance.exports; + try { + const r = await fetch(src); + if (!r.ok) throw new Error(`${r}`); + const bytes = await r.arrayBuffer(); + const { instance } = await WebAssembly.instantiate(bytes); + return instance.exports; + } catch (e) { + throw new Error(e); + } }