@gcforms/editor 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.5] - 2025-06-23
9
+
10
+ ### Changed
11
+
12
+ - Update Yarn to version 4.9.2
13
+ - Update Lexical to version 0.32.0
14
+
15
+ ## [1.0.4] - 2025-06-02
16
+
17
+ ### Changed
18
+
19
+ - Bumped Lexical version to 0.31.2
20
+ - Refactored URL validation and sanitization
21
+ - Allow mailto: links
22
+ - Wrote unit tests for URL utils
23
+
8
24
  ## [1.0.3] - 2025-05-13
9
25
 
10
26
  ### Changed
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@gcforms/editor",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "author": "Canadian Digital Service",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
9
- "packageManager": "yarn@4.6.0",
9
+ "packageManager": "yarn@4.9.2",
10
10
  "dependencies": {
11
- "@lexical/react": "^0.31.1",
12
- "lexical": "^0.31.1"
11
+ "@lexical/react": "^0.32.0",
12
+ "lexical": "^0.32.0"
13
13
  },
14
14
  "peerDependencies": {
15
15
  "react": "^19.1.0",
@@ -0,0 +1,103 @@
1
+ import { sanitizeUrl, isValidUrl } from "./url";
2
+
3
+ describe("sanitizeUrl", () => {
4
+ it("returns safe http/https URLs unchanged", () => {
5
+ expect(sanitizeUrl("http://example.com")).toBe("http://example.com");
6
+ expect(sanitizeUrl("https://example.com")).toBe("https://example.com");
7
+ });
8
+
9
+ it("returns safe mailto", () => {
10
+ expect(sanitizeUrl("mailto:test@example.com")).toBe("mailto:test@example.com");
11
+ });
12
+
13
+ it("returns relative URLs unchanged", () => {
14
+ expect(sanitizeUrl("/relative/path")).toBe("/relative/path");
15
+ expect(sanitizeUrl("foo/bar")).toBe("foo/bar");
16
+ expect(sanitizeUrl("index.html")).toBe("index.html");
17
+ });
18
+
19
+ it("trims whitespace from URLs", () => {
20
+ expect(sanitizeUrl(" http://example.com ")).toBe("http://example.com");
21
+ expect(sanitizeUrl(" /foo/bar ")).toBe("/foo/bar");
22
+ });
23
+
24
+ it("returns about:blank for unsafe URLs (does not sanitize)", () => {
25
+ expect(sanitizeUrl("javascript:alert(1)")).toBe("about:blank");
26
+ expect(sanitizeUrl("data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==")).toBe(
27
+ "about:blank"
28
+ );
29
+ });
30
+
31
+ it("returns trimmed input for non-URL strings", () => {
32
+ expect(sanitizeUrl("not a url")).toBe("not a url");
33
+ expect(sanitizeUrl(" not a url ")).toBe("not a url");
34
+ });
35
+
36
+ it("returns about:blank for unsupported protocols", () => {
37
+ expect(sanitizeUrl("ftp://example.com")).toBe("about:blank");
38
+ });
39
+
40
+ it("handles empty string", () => {
41
+ expect(sanitizeUrl("")).toBe("");
42
+ });
43
+ });
44
+
45
+ describe("isValidUrl", () => {
46
+ it("returns true for valid http/https URLs", () => {
47
+ expect(isValidUrl("http://example.com")).toBe(true);
48
+ expect(isValidUrl("https://sub.domain.com/path?query=1")).toBe(true);
49
+ });
50
+
51
+ it("returns true for valid mailto URLs", () => {
52
+ expect(isValidUrl("mailto:test@example.com")).toBe(true);
53
+ });
54
+
55
+ it("returns true for valid tel URLs", () => {
56
+ expect(isValidUrl("tel:+1234567890")).toBe(true);
57
+ expect(isValidUrl("tel:123-456-7890")).toBe(true);
58
+ expect(isValidUrl("tel:(123)456-7890")).toBe(true);
59
+ });
60
+
61
+ it("returns true for valid sms URLs", () => {
62
+ expect(isValidUrl("sms:+1234567890")).toBe(true);
63
+ expect(isValidUrl("sms:123-456-7890")).toBe(true);
64
+ expect(isValidUrl("sms:(123)456-7890")).toBe(true);
65
+ });
66
+
67
+ it("returns false for invalid URLs", () => {
68
+ expect(isValidUrl("notaurl")).toBe(false);
69
+ expect(isValidUrl("http:/bad")).toBe(false);
70
+ expect(isValidUrl("ftp://example.com")).toBe(false); // ftp is not in regexp
71
+ expect(isValidUrl("javascript:alert(1)")).toBe(false);
72
+ expect(isValidUrl("data:image/png;base64,abc")).toBe(false);
73
+ expect(isValidUrl("tel:")).toBe(false);
74
+ expect(isValidUrl("sms:")).toBe(false);
75
+ expect(isValidUrl("tel:abc-def-ghij")).toBe(false);
76
+ expect(isValidUrl("sms:abc-def-ghij")).toBe(false);
77
+ });
78
+
79
+ it("returns false for empty string", () => {
80
+ expect(isValidUrl("")).toBe(false);
81
+ });
82
+
83
+ it("returns true for mailto with query params", () => {
84
+ expect(isValidUrl("mailto:user@example.com?subject=Hello")).toBe(true);
85
+ });
86
+
87
+ it("returns false for malformed mailto addresses", () => {
88
+ expect(isValidUrl("mailto:user@")).toBe(false);
89
+ expect(isValidUrl("mailto:@example.com")).toBe(false);
90
+ expect(isValidUrl("mailto:")).toBe(false);
91
+ });
92
+
93
+ it("returns false for URLs with internal whitespace", () => {
94
+ expect(isValidUrl("http://exa mple.com")).toBe(false);
95
+ expect(isValidUrl("mailto:test@ example.com")).toBe(false);
96
+ expect(isValidUrl("tel:123 456 7890")).toBe(false);
97
+ expect(isValidUrl("sms:123 456 7890")).toBe(false);
98
+ });
99
+
100
+ it("returns false for internationalized domain names (unicode)", () => {
101
+ expect(isValidUrl("http://exämple.com")).toBe(false);
102
+ });
103
+ });
package/src/utils/url.ts CHANGED
@@ -6,25 +6,29 @@
6
6
  *
7
7
  */
8
8
 
9
- export function sanitizeUrl(url: string): string {
10
- /** A pattern that matches safe URLs. */
11
- const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^&:/?#]*(?:[/?#]|$))/gi;
12
-
13
- /** A pattern that matches safe data URLs. */
14
- const DATA_URL_PATTERN =
15
- /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;
16
-
17
- url = String(url).trim();
9
+ const SUPPORTED_URL_PROTOCOLS = new Set(["http:", "https:", "mailto:", "tel:", "sms:"]);
18
10
 
19
- if (url.match(SAFE_URL_PATTERN) || url.match(DATA_URL_PATTERN)) return url;
11
+ export function sanitizeUrl(url: string): string {
12
+ try {
13
+ const parsedUrl = new URL(url);
20
14
 
21
- return url;
15
+ if (!SUPPORTED_URL_PROTOCOLS.has(parsedUrl.protocol)) {
16
+ return "about:blank";
17
+ }
18
+ } catch {
19
+ return url.trim();
20
+ }
21
+ return url.trim();
22
22
  }
23
23
 
24
24
  /**
25
- * A pattern that matches a valid URL.
25
+ * Validates if a string is a well-formed URL.
26
+ * Supports http, https, mailto, tel, and sms protocols.
26
27
  */
27
- const urlRegExp = new RegExp(/^(https?:\/\/)?([a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,}(\/[^\s]*)?$/);
28
+ const urlRegExp = new RegExp(
29
+ /^(https?:\/\/([a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,}(\/[^\s]*)?|mailto:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\?[^\s]*)?|tel:\+?[0-9\-().]{3,}|sms:\+?[0-9\-().]{3,})$/
30
+ );
31
+
28
32
  export function isValidUrl(url: string): boolean {
29
33
  return urlRegExp.test(url);
30
34
  }