escape jsx strings
Mbun.lock
| @@ -24,6 +24,7 @@ | |||
|---|---|---|---|
| 24 | 24 | }, | |
| 25 | 25 | "devDependencies": { | |
| 26 | 26 | "@biomejs/biome": "^2.4.7", | |
| 27 | + | "@kitajs/ts-html-plugin": "^4.1.4", | |
| 27 | 28 | "@simplewebauthn/browser": "^13.3.0", | |
| 28 | 29 | "@types/argon2": "^0.15.4", | |
| 29 | 30 | "@types/bun": "latest", | |
| @@ -144,6 +145,8 @@ | |||
|---|---|---|---|
| 144 | 145 | ||
| 145 | 146 | "@kitajs/html": ["@kitajs/html@4.2.13", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-o+8e61EsoLDPTP7rsPkYolca1YFybHuxU2Lr5fWDZCUkYT/6uBlVkvnZUdCXMQKentJL9dxwpR8/xK2Q+U4LhA=="], | |
| 146 | 147 | ||
| 148 | + | "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.4", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.9.3" }, "bin": { "xss-scan": "dist/cli.js", "ts-html-plugin": "dist/cli.js" } }, "sha512-xK5mNrhnIy73kJFKx5yVGChJyWFRGmIaE0sjlVxVYllk5dyaEYVCrIh1N8AfnseEHka8gAqzPGW95HlkhDvnJA=="], | |
| 149 | + | ||
| 147 | 150 | "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], | |
| 148 | 151 | ||
| 149 | 152 | "@peculiar/asn1-android": ["@peculiar/asn1-android@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ=="], | |
| @@ -218,6 +221,10 @@ | |||
|---|---|---|---|
| 218 | 221 | ||
| 219 | 222 | "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], | |
| 220 | 223 | ||
| 224 | + | "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], | |
| 225 | + | ||
| 226 | + | "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], | |
| 227 | + | ||
| 221 | 228 | "argon2": ["argon2@0.44.0", "", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.0.0", "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" } }, "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig=="], | |
| 222 | 229 | ||
| 223 | 230 | "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], | |
| @@ -234,10 +241,14 @@ | |||
|---|---|---|---|
| 234 | 241 | ||
| 235 | 242 | "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], | |
| 236 | 243 | ||
| 244 | + | "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], | |
| 245 | + | ||
| 237 | 246 | "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], | |
| 238 | 247 | ||
| 239 | 248 | "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], | |
| 240 | 249 | ||
| 250 | + | "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], | |
| 251 | + | ||
| 241 | 252 | "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], | |
| 242 | 253 | ||
| 243 | 254 | "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], | |
| @@ -270,8 +281,12 @@ | |||
|---|---|---|---|
| 270 | 281 | ||
| 271 | 282 | "elysia": ["elysia@1.4.27", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-2UlmNEjPJVA/WZVPYKy+KdsrfFwwNlqSBW1lHz6i2AHc75k7gV4Rhm01kFeotH7PDiHIX2G8X3KnRPc33SGVIg=="], | |
| 272 | 283 | ||
| 284 | + | "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], | |
| 285 | + | ||
| 273 | 286 | "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], | |
| 274 | 287 | ||
| 288 | + | "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], | |
| 289 | + | ||
| 275 | 290 | "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], | |
| 276 | 291 | ||
| 277 | 292 | "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], | |
| @@ -280,6 +295,10 @@ | |||
|---|---|---|---|
| 280 | 295 | ||
| 281 | 296 | "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], | |
| 282 | 297 | ||
| 298 | + | "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], | |
| 299 | + | ||
| 300 | + | "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], | |
| 301 | + | ||
| 283 | 302 | "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], | |
| 284 | 303 | ||
| 285 | 304 | "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], | |
| @@ -416,8 +435,12 @@ | |||
|---|---|---|---|
| 416 | 435 | ||
| 417 | 436 | "ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="], | |
| 418 | 437 | ||
| 438 | + | "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], | |
| 439 | + | ||
| 419 | 440 | "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], | |
| 420 | 441 | ||
| 442 | + | "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], | |
| 443 | + | ||
| 421 | 444 | "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], | |
| 422 | 445 | ||
| 423 | 446 | "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], | |
| @@ -474,10 +497,18 @@ | |||
|---|---|---|---|
| 474 | 497 | ||
| 475 | 498 | "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], | |
| 476 | 499 | ||
| 500 | + | "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], | |
| 501 | + | ||
| 477 | 502 | "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], | |
| 478 | 503 | ||
| 479 | 504 | "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], | |
| 480 | 505 | ||
| 506 | + | "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], | |
| 507 | + | ||
| 508 | + | "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], | |
| 509 | + | ||
| 510 | + | "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], | |
| 511 | + | ||
| 481 | 512 | "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], | |
| 482 | 513 | ||
| 483 | 514 | "bun-types/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], | |
Mpackage.json
| @@ -19,6 +19,7 @@ | |||
|---|---|---|---|
| 19 | 19 | "private": true, | |
| 20 | 20 | "devDependencies": { | |
| 21 | 21 | "@biomejs/biome": "^2.4.7", | |
| 22 | + | "@kitajs/ts-html-plugin": "^4.1.4", | |
| 22 | 23 | "@simplewebauthn/browser": "^13.3.0", | |
| 23 | 24 | "@types/argon2": "^0.15.4", | |
| 24 | 25 | "@types/bun": "latest", | |
Msrc/routes/repos.tsx
| @@ -1,4 +1,4 @@ | |||
|---|---|---|---|
| 1 | - | import { readFileSync, rmSync } from "node:fs"; | |
| 1 | + | import { existsSync, readFileSync, renameSync, rmSync } from "node:fs"; | |
| 2 | 2 | import path from "node:path"; | |
| 3 | 3 | import { Elysia, t } from "elysia"; | |
| 4 | 4 | import { fileTypeFromBuffer } from "file-type"; | |
| @@ -20,7 +20,12 @@ import { db } from "../db/index.ts"; | |||
|---|---|---|---|
| 20 | 20 | import { contentDisposition } from "../lib/contentDisposition.ts"; | |
| 21 | 21 | import { redirect } from "../lib/redirect.ts"; | |
| 22 | 22 | import { requireAdmin, resolveSession } from "../middleware/session.ts"; | |
| 23 | - | import { git, repoPath, type TreeEntry } from "../services/git.ts"; | |
| 23 | + | import { | |
| 24 | + | git, | |
| 25 | + | invalidateRefCache, | |
| 26 | + | repoPath, | |
| 27 | + | type TreeEntry, | |
| 28 | + | } from "../services/git.ts"; | |
| 24 | 29 | import { | |
| 25 | 30 | hasBinaryContent, | |
| 26 | 31 | prepareDiff, | |
| @@ -1057,6 +1062,90 @@ export const repoRoutes = new Elysia() | |||
|---|---|---|---|
| 1057 | 1062 | return new Response(null, { status: 302, headers: { Location: "/" } }); | |
| 1058 | 1063 | }) | |
| 1059 | 1064 | ||
| 1065 | + | .post( | |
| 1066 | + | "/:repo/settings/rename", | |
| 1067 | + | async ({ params, body, cookie }) => { | |
| 1068 | + | const user = await resolveSession(cookie.session.value); | |
| 1069 | + | const deny = requireAdmin(user); | |
| 1070 | + | if (deny) return deny; | |
| 1071 | + | const repo = await getRepo(params.repo, true); | |
| 1072 | + | if (!repo) return new Response("Not found", { status: 404 }); | |
| 1073 | + | ||
| 1074 | + | const oldName = repo.name; | |
| 1075 | + | const newName = body.new_name?.trim() ?? ""; | |
| 1076 | + | const confirm = body.confirm_name?.trim() ?? ""; | |
| 1077 | + | const back = (msg: string) => | |
| 1078 | + | redirect( | |
| 1079 | + | `/${oldName}/settings?error=${encodeURIComponent(msg)}`, | |
| 1080 | + | ); | |
| 1081 | + | ||
| 1082 | + | if (confirm !== oldName) { | |
| 1083 | + | return back("Confirmation did not match the current name."); | |
| 1084 | + | } | |
| 1085 | + | if (newName === oldName) { | |
| 1086 | + | return back("New name is the same as the current name."); | |
| 1087 | + | } | |
| 1088 | + | if (newName.toLowerCase() === oldName.toLowerCase()) { | |
| 1089 | + | return back("Case-only renames are not supported."); | |
| 1090 | + | } | |
| 1091 | + | if (!VALID_REPO_NAME_RE.test(newName)) { | |
| 1092 | + | return back("Invalid repository name."); | |
| 1093 | + | } | |
| 1094 | + | ||
| 1095 | + | const clash = await db | |
| 1096 | + | .selectFrom("repositories") | |
| 1097 | + | .select("id") | |
| 1098 | + | .where("name", "=", newName) | |
| 1099 | + | .executeTakeFirst(); | |
| 1100 | + | if (clash) return back("Repository name already taken."); | |
| 1101 | + | ||
| 1102 | + | const fromPath = repoPath(oldName); | |
| 1103 | + | const toPath = repoPath(newName); | |
| 1104 | + | if (existsSync(toPath)) { | |
| 1105 | + | return back( | |
| 1106 | + | "A directory for that name already exists on disk.", | |
| 1107 | + | ); | |
| 1108 | + | } | |
| 1109 | + | ||
| 1110 | + | try { | |
| 1111 | + | renameSync(fromPath, toPath); | |
| 1112 | + | } catch (err) { | |
| 1113 | + | console.error( | |
| 1114 | + | `rename ${fromPath} -> ${toPath} failed`, | |
| 1115 | + | err, | |
| 1116 | + | ); | |
| 1117 | + | return back("Failed to rename repository on disk."); | |
| 1118 | + | } | |
| 1119 | + | ||
| 1120 | + | try { | |
| 1121 | + | await db | |
| 1122 | + | .updateTable("repositories") | |
| 1123 | + | .set({ name: newName }) | |
| 1124 | + | .where("id", "=", repo.id) | |
| 1125 | + | .execute(); | |
| 1126 | + | } catch (err) { | |
| 1127 | + | console.error("db rename failed; rolling back disk", err); | |
| 1128 | + | try { | |
| 1129 | + | renameSync(toPath, fromPath); | |
| 1130 | + | } catch (rb) { | |
| 1131 | + | console.error("rollback rename failed", rb); | |
| 1132 | + | } | |
| 1133 | + | return back("Failed to update repository record."); | |
| 1134 | + | } | |
| 1135 | + | ||
| 1136 | + | invalidateRefCache(oldName); | |
| 1137 | + | return redirect( | |
| 1138 | + | `/${newName}/settings?success=${encodeURIComponent("Repository renamed.")}`, | |
| 1139 | + | ); | |
| 1140 | + | }, | |
| 1141 | + | { | |
| 1142 | + | body: t.Object({ | |
| 1143 | + | new_name: t.String(), | |
| 1144 | + | confirm_name: t.String(), | |
| 1145 | + | }), | |
| 1146 | + | }, | |
| 1147 | + | ) | |
| 1148 | + | ||
| 1060 | 1149 | .post( | |
| 1061 | 1150 | "/:repo/settings/labels", | |
| 1062 | 1151 | async ({ params, body, cookie }) => { | |
Msrc/views/CommentThread.tsx
| @@ -45,7 +45,9 @@ export function CommentThread({ | |||
|---|---|---|---|
| 45 | 45 | version={comment.author_avatar_version} | |
| 46 | 46 | size={24} | |
| 47 | 47 | /> | |
| 48 | - | <strong>{displayName(comment.author_username)}</strong> | |
| 48 | + | <strong safe> | |
| 49 | + | {displayName(comment.author_username)} | |
| 50 | + | </strong> | |
| 49 | 51 | <div class="timeline-author-right"> | |
| 50 | 52 | {canEditComment(comment) && ( | |
| 51 | 53 | <details class="inline-edit-details"> | |
| @@ -76,6 +78,7 @@ export function CommentThread({ | |||
|---|---|---|---|
| 76 | 78 | name="edit_body" | |
| 77 | 79 | rows="6" | |
| 78 | 80 | maxlength={config.MAX_TEXT_BODY_BYTES} | |
| 81 | + | safe | |
| 79 | 82 | > | |
| 80 | 83 | {comment.body} | |
| 81 | 84 | </textarea> | |
| @@ -92,7 +95,7 @@ export function CommentThread({ | |||
|---|---|---|---|
| 92 | 95 | </div> | |
| 93 | 96 | )} | |
| 94 | 97 | <div class="timeline-body markdown-body"> | |
| 95 | - | {comment.bodyHtml} | |
| 98 | + | {comment.bodyHtml as "safe"} | |
| 96 | 99 | </div> | |
| 97 | 100 | <ReactionBar | |
| 98 | 101 | reactions={commentReactions.get(comment.id) ?? []} | |
| @@ -103,7 +106,7 @@ export function CommentThread({ | |||
|---|---|---|---|
| 103 | 106 | </div> | |
| 104 | 107 | ))} | |
| 105 | 108 | ||
| 106 | - | {user && ( | |
| 109 | + | {!!user && ( | |
| 107 | 110 | <div class="timeline-item timeline-item-new"> | |
| 108 | 111 | <h3 class="section-title">Add a comment</h3> | |
| 109 | 112 | <form method="POST" action={`${baseUrl}/comments`}> | |
Msrc/views/DateWithEdited.tsx
| @@ -8,12 +8,18 @@ export function DateWithEdited({ | |||
|---|---|---|---|
| 8 | 8 | editedAt: string | null; | |
| 9 | 9 | }) { | |
| 10 | 10 | const label = formatDate(date); | |
| 11 | - | if (!editedAt) return <time datetime={date}>{label}</time>; | |
| 11 | + | if (!editedAt) | |
| 12 | + | return ( | |
| 13 | + | <time datetime={date} safe> | |
| 14 | + | {label} | |
| 15 | + | </time> | |
| 16 | + | ); | |
| 12 | 17 | return ( | |
| 13 | 18 | <time | |
| 14 | 19 | datetime={date} | |
| 15 | 20 | class="edited-indicator" | |
| 16 | 21 | title={`Edited ${formatDateTime(editedAt)}`} | |
| 22 | + | safe | |
| 17 | 23 | > | |
| 18 | 24 | *{label} | |
| 19 | 25 | </time> | |
Msrc/views/DiffView.tsx
| @@ -57,7 +57,7 @@ export function renderFileTree(tree: Map<string, FileTreeNode>): JSX.Element { | |||
|---|---|---|---|
| 57 | 57 | <details open> | |
| 58 | 58 | <summary class="file-nav-dir-toggle"> | |
| 59 | 59 | <span class="file-nav-toggle-icon">▾</span> | |
| 60 | - | {name}/ | |
| 60 | + | <span safe>{name}/</span> | |
| 61 | 61 | </summary> | |
| 62 | 62 | {renderFileTree(node.children)} | |
| 63 | 63 | </details> | |
| @@ -71,10 +71,13 @@ export function renderFileTree(tree: Map<string, FileTreeNode>): JSX.Element { | |||
|---|---|---|---|
| 71 | 71 | > | |
| 72 | 72 | <span | |
| 73 | 73 | class={`file-nav-status file-status-${node.file.status}`} | |
| 74 | + | safe | |
| 74 | 75 | > | |
| 75 | 76 | {statusLetter(node.file.status)} | |
| 76 | 77 | </span> | |
| 77 | - | <span class="file-nav-name">{name}</span> | |
| 78 | + | <span class="file-nav-name" safe> | |
| 79 | + | {name} | |
| 80 | + | </span> | |
| 78 | 81 | <span class="file-nav-stat"> | |
| 79 | 82 | {node.file.isBinary ? ( | |
| 80 | 83 | <span class="nav-binary"> | |
| @@ -148,7 +151,7 @@ export function DiffView({ files, repo, sha }: DiffViewProps) { | |||
|---|---|---|---|
| 148 | 151 | <> | |
| 149 | 152 | {files.length > 0 && ( | |
| 150 | 153 | <div class="commit-stats-bar"> | |
| 151 | - | <span class="commit-stats-text">{changedStr}</span> | |
| 154 | + | <span class="commit-stats-text" safe>{changedStr}</span> | |
| 152 | 155 | </div> | |
| 153 | 156 | )} | |
| 154 | 157 | ||
| @@ -185,15 +188,16 @@ export function DiffView({ files, repo, sha }: DiffViewProps) { | |||
|---|---|---|---|
| 185 | 188 | </span> | |
| 186 | 189 | <span | |
| 187 | 190 | class={`diff-status-badge diff-status-${f.status}`} | |
| 191 | + | safe | |
| 188 | 192 | > | |
| 189 | 193 | {sl} | |
| 190 | 194 | </span> | |
| 191 | - | <span class="diff-file-path mono"> | |
| 195 | + | <span class="diff-file-path mono" safe> | |
| 192 | 196 | {displayPath} | |
| 193 | 197 | </span> | |
| 194 | 198 | {f.status === "renamed" && | |
| 195 | 199 | f.oldPath !== f.newPath && ( | |
| 196 | - | <span class="diff-rename-arrow"> | |
| 200 | + | <span class="diff-rename-arrow" safe> | |
| 197 | 201 | ← {f.oldPath} | |
| 198 | 202 | </span> | |
| 199 | 203 | )} | |
| @@ -239,11 +243,12 @@ export function DiffView({ files, repo, sha }: DiffViewProps) { | |||
|---|---|---|---|
| 239 | 243 | )} | |
| 240 | 244 | {f.status !== "deleted" && ( | |
| 241 | 245 | <> | |
| 242 | - | {sha && ( | |
| 246 | + | {!!sha && ( | |
| 243 | 247 | <a | |
| 244 | 248 | href={`/${repo.name}/blob/${sha}/${displayPath}`} | |
| 245 | 249 | class="btn btn-xs btn-secondary" | |
| 246 | 250 | title={`View file at ${sha.slice(0, 7)}`} | |
| 251 | + | safe | |
| 247 | 252 | > | |
| 248 | 253 | @ {sha.slice(0, 7)} | |
| 249 | 254 | </a> | |
| @@ -252,6 +257,7 @@ export function DiffView({ files, repo, sha }: DiffViewProps) { | |||
|---|---|---|---|
| 252 | 257 | href={`/${repo.name}/blob/${repo.default_branch}/${displayPath}`} | |
| 253 | 258 | class="btn btn-xs btn-secondary" | |
| 254 | 259 | title={`View file at ${repo.default_branch}`} | |
| 260 | + | safe | |
| 255 | 261 | > | |
| 256 | 262 | @ {repo.default_branch} | |
| 257 | 263 | </a> | |
| @@ -278,6 +284,7 @@ export function DiffView({ files, repo, sha }: DiffViewProps) { | |||
|---|---|---|---|
| 278 | 284 | <th | |
| 279 | 285 | colspan="4" | |
| 280 | 286 | class="diff-hunk-header" | |
| 287 | + | safe | |
| 281 | 288 | > | |
| 282 | 289 | { | |
| 283 | 290 | hunk.header | |
| @@ -310,7 +317,7 @@ export function DiffView({ files, repo, sha }: DiffViewProps) { | |||
|---|---|---|---|
| 310 | 317 | </td> | |
| 311 | 318 | <td class="diff-code"> | |
| 312 | 319 | { | |
| 313 | - | row.html | |
| 320 | + | row.html as "safe" | |
| 314 | 321 | } | |
| 315 | 322 | </td> | |
| 316 | 323 | </tr> | |
Msrc/views/EditableTitle.tsx
| @@ -25,7 +25,9 @@ export function EditableTitle({ | |||
|---|---|---|---|
| 25 | 25 | /> | |
| 26 | 26 | )} | |
| 27 | 27 | <div class="title-with-edit"> | |
| 28 | - | <h2 class="issue-detail-title">{title}</h2> | |
| 28 | + | <h2 class="issue-detail-title" safe> | |
| 29 | + | {title} | |
| 30 | + | </h2> | |
| 29 | 31 | {canEdit && ( | |
| 30 | 32 | <> | |
| 31 | 33 | <div class="title-edit-form-area"> | |
Msrc/views/LabelBadges.tsx
| @@ -25,6 +25,7 @@ export function LabelBadges({ | |||
|---|---|---|---|
| 25 | 25 | <span | |
| 26 | 26 | class="label-badge" | |
| 27 | 27 | style={`background:${label.color};color:${labelTextColor(label.color)}`} | |
| 28 | + | safe | |
| 28 | 29 | > | |
| 29 | 30 | {label.name} | |
| 30 | 31 | </span> | |
| @@ -58,7 +59,7 @@ export function LabelBadges({ | |||
|---|---|---|---|
| 58 | 59 | > | |
| 59 | 60 | <select name="label_id" class="label-select"> | |
| 60 | 61 | {available.map((label) => ( | |
| 61 | - | <option value={String(label.id)}> | |
| 62 | + | <option value={String(label.id)} safe> | |
| 62 | 63 | {label.name} | |
| 63 | 64 | </option> | |
| 64 | 65 | ))} | |
Msrc/views/ReactionBar.tsx
| @@ -25,26 +25,30 @@ export function ReactionBar({ | |||
|---|---|---|---|
| 25 | 25 | const all = [...ALLOWED_REACTIONS]; | |
| 26 | 26 | return ( | |
| 27 | 27 | <div class="reaction-bar"> | |
| 28 | - | {reactions.map((r) => ( | |
| 29 | - | <form method="POST" action={postUrl} class="reaction-form"> | |
| 30 | - | {commentId != null && ( | |
| 31 | - | <input | |
| 32 | - | type="hidden" | |
| 33 | - | name="comment_id" | |
| 34 | - | value={String(commentId)} | |
| 35 | - | /> | |
| 36 | - | )} | |
| 37 | - | <input type="hidden" name="emoji" value={r.emoji} /> | |
| 38 | - | <button | |
| 39 | - | type="submit" | |
| 40 | - | class={`reaction-btn${r.userReacted ? " reacted" : ""}`} | |
| 41 | - | disabled={!user} | |
| 42 | - | > | |
| 43 | - | {r.emoji} {r.count} | |
| 44 | - | </button> | |
| 45 | - | </form> | |
| 46 | - | ))} | |
| 47 | - | {user && ( | |
| 28 | + | {reactions.map((r) => { | |
| 29 | + | const unsafeEmoji = r.emoji; | |
| 30 | + | return ( | |
| 31 | + | <form method="POST" action={postUrl} class="reaction-form"> | |
| 32 | + | {commentId != null && ( | |
| 33 | + | <input | |
| 34 | + | type="hidden" | |
| 35 | + | name="comment_id" | |
| 36 | + | value={String(commentId)} | |
| 37 | + | /> | |
| 38 | + | )} | |
| 39 | + | <input type="hidden" name="emoji" value={r.emoji} /> | |
| 40 | + | <button | |
| 41 | + | type="submit" | |
| 42 | + | class={`reaction-btn${r.userReacted ? " reacted" : ""}`} | |
| 43 | + | disabled={!user} | |
| 44 | + | safe | |
| 45 | + | > | |
| 46 | + | {unsafeEmoji + " " + r.count} | |
| 47 | + | </button> | |
| 48 | + | </form> | |
| 49 | + | ); | |
| 50 | + | })} | |
| 51 | + | {!!user && ( | |
| 48 | 52 | <details class="reaction-picker"> | |
| 49 | 53 | <summary class="reaction-add-btn">+</summary> | |
| 50 | 54 | <div class="reaction-picker-dropdown"> | |
| @@ -71,6 +75,7 @@ export function ReactionBar({ | |||
|---|---|---|---|
| 71 | 75 | <button | |
| 72 | 76 | type="submit" | |
| 73 | 77 | class="reaction-picker-btn" | |
| 78 | + | safe | |
| 74 | 79 | > | |
| 75 | 80 | {e} | |
| 76 | 81 | </button> | |
Msrc/views/Settings.tsx
| @@ -57,8 +57,16 @@ export function Settings({ | |||
|---|---|---|---|
| 57 | 57 | <div class="container container-narrow"> | |
| 58 | 58 | <h1 class="page-title">Settings</h1> | |
| 59 | 59 | ||
| 60 | - | {successMsg && <p class="form-success">{successMsg}</p>} | |
| 61 | - | {error && <p class="form-error">{error}</p>} | |
| 60 | + | {!!successMsg && ( | |
| 61 | + | <p class="form-success" safe> | |
| 62 | + | {successMsg} | |
| 63 | + | </p> | |
| 64 | + | )} | |
| 65 | + | {!!error && ( | |
| 66 | + | <p class="form-error" safe> | |
| 67 | + | {error} | |
| 68 | + | </p> | |
| 69 | + | )} | |
| 62 | 70 | ||
| 63 | 71 | {/* Avatar */} | |
| 64 | 72 | <div class="form-card"> | |
| @@ -240,7 +248,7 @@ export function Settings({ | |||
|---|---|---|---|
| 240 | 248 | <div class="passkey-list"> | |
| 241 | 249 | {passkeys.map((pk) => ( | |
| 242 | 250 | <div class="passkey-item"> | |
| 243 | - | <span class="passkey-date"> | |
| 251 | + | <span class="passkey-date" safe> | |
| 244 | 252 | Added {formatDate(pk.created_at)} | |
| 245 | 253 | </span> | |
| 246 | 254 | <form | |
| @@ -300,13 +308,13 @@ export function Settings({ | |||
|---|---|---|---|
| 300 | 308 | {sshKeys.map((key) => ( | |
| 301 | 309 | <div class="passkey-item"> | |
| 302 | 310 | <div class="ssh-key-info"> | |
| 303 | - | <span class="ssh-key-name"> | |
| 311 | + | <span class="ssh-key-name" safe> | |
| 304 | 312 | {key.name} | |
| 305 | 313 | </span> | |
| 306 | - | <span class="passkey-date ssh-key-fingerprint"> | |
| 314 | + | <span class="passkey-date ssh-key-fingerprint" safe> | |
| 307 | 315 | {key.fingerprint} | |
| 308 | 316 | </span> | |
| 309 | - | <span class="passkey-date"> | |
| 317 | + | <span class="passkey-date" safe> | |
| 310 | 318 | Added {formatDate(key.created_at)} | |
| 311 | 319 | </span> | |
| 312 | 320 | </div> | |
| @@ -409,17 +417,17 @@ export function Settings({ | |||
|---|---|---|---|
| 409 | 417 | <li class="queue-item"> | |
| 410 | 418 | <div class="queue-item-meta"> | |
| 411 | 419 | <div class="queue-item-header"> | |
| 412 | - | <strong> | |
| 420 | + | <strong safe> | |
| 413 | 421 | {u.username} | |
| 414 | 422 | </strong> | |
| 415 | - | <span class="queue-item-date"> | |
| 423 | + | <span class="queue-item-date" safe> | |
| 416 | 424 | {formatDateTime( | |
| 417 | 425 | u.created_at, | |
| 418 | 426 | )} | |
| 419 | 427 | </span> | |
| 420 | 428 | </div> | |
| 421 | - | {u.register_application && ( | |
| 422 | - | <p class="queue-item-answer"> | |
| 429 | + | {!!u.register_application && ( | |
| 430 | + | <p class="queue-item-answer" safe> | |
| 423 | 431 | {u.register_application} | |
| 424 | 432 | </p> | |
| 425 | 433 | )} | |
Msrc/views/auth/Login.tsx
| @@ -9,7 +9,11 @@ export function Login({ error }: LoginProps) { | |||
|---|---|---|---|
| 9 | 9 | <Layout user={null} title="Sign in"> | |
| 10 | 10 | <div class="auth-container"> | |
| 11 | 11 | <h1 class="page-title">Sign in</h1> | |
| 12 | - | {error && <p class="form-error">{error}</p>} | |
| 12 | + | {!!error && ( | |
| 13 | + | <p class="form-error" safe> | |
| 14 | + | {error} | |
| 15 | + | </p> | |
| 16 | + | )} | |
| 13 | 17 | <form method="POST" action="/login" class="auth-form"> | |
| 14 | 18 | <div class="form-group"> | |
| 15 | 19 | <label for="username">Username</label> | |
Msrc/views/auth/Register.tsx
| @@ -29,7 +29,11 @@ export function Register({ error, question, pending }: RegisterProps) { | |||
|---|---|---|---|
| 29 | 29 | <Layout user={null} title="Register"> | |
| 30 | 30 | <div class="auth-container"> | |
| 31 | 31 | <h1 class="page-title">Create account</h1> | |
| 32 | - | {error && <p class="form-error">{error}</p>} | |
| 32 | + | {!!error && ( | |
| 33 | + | <p class="form-error" safe> | |
| 34 | + | {error} | |
| 35 | + | </p> | |
| 36 | + | )} | |
| 33 | 37 | <form method="POST" action="/register" class="auth-form"> | |
| 34 | 38 | <div class="form-group"> | |
| 35 | 39 | <label for="username">Username</label> | |
| @@ -44,9 +48,11 @@ export function Register({ error, question, pending }: RegisterProps) { | |||
|---|---|---|---|
| 44 | 48 | title="Letters, numbers, hyphens and underscores only" | |
| 45 | 49 | /> | |
| 46 | 50 | </div> | |
| 47 | - | {question && ( | |
| 51 | + | {!!question && ( | |
| 48 | 52 | <div class="form-group"> | |
| 49 | - | <label for="application">{question}</label> | |
| 53 | + | <label for="application" safe> | |
| 54 | + | {question} | |
| 55 | + | </label> | |
| 50 | 56 | <textarea | |
| 51 | 57 | id="application" | |
| 52 | 58 | name="application" | |
Msrc/views/ci/CiHistory.tsx
| @@ -90,9 +90,9 @@ function CiHelp({ repo }: { repo: RepositoryRow }) { | |||
|---|---|---|---|
| 90 | 90 | {CI_VARIABLES.map(([name, desc]) => ( | |
| 91 | 91 | <> | |
| 92 | 92 | <dt> | |
| 93 | - | <code>{name}</code> | |
| 93 | + | <code safe>{name}</code> | |
| 94 | 94 | </dt> | |
| 95 | - | <dd>{desc}</dd> | |
| 95 | + | <dd safe>{desc}</dd> | |
| 96 | 96 | </> | |
| 97 | 97 | ))} | |
| 98 | 98 | </dl> | |
| @@ -100,7 +100,7 @@ function CiHelp({ repo }: { repo: RepositoryRow }) { | |||
|---|---|---|---|
| 100 | 100 | <div class="ci-help-section"> | |
| 101 | 101 | <h4 class="ci-help-section-title">Status badge</h4> | |
| 102 | 102 | <p class="ci-help-badge-desc">Embed in your README:</p> | |
| 103 | - | <code class="ci-help-badge-code">{``}</code> | |
| 103 | + | <code class="ci-help-badge-code" safe>{``}</code> | |
| 104 | 104 | <h4 | |
| 105 | 105 | class="ci-help-section-title" | |
| 106 | 106 | style="margin-top: var(--space-4)" | |
| @@ -117,9 +117,9 @@ function CiHelp({ repo }: { repo: RepositoryRow }) { | |||
|---|---|---|---|
| 117 | 117 | ].map(([k, v]) => ( | |
| 118 | 118 | <> | |
| 119 | 119 | <dt> | |
| 120 | - | <code>{k}</code> | |
| 120 | + | <code safe>{k}</code> | |
| 121 | 121 | </dt> | |
| 122 | - | <dd>{v}</dd> | |
| 122 | + | <dd safe>{v}</dd> | |
| 123 | 123 | </> | |
| 124 | 124 | ))} | |
| 125 | 125 | </dl> | |
| @@ -145,13 +145,9 @@ export function CiHistory({ | |||
|---|---|---|---|
| 145 | 145 | ); | |
| 146 | 146 | return ( | |
| 147 | 147 | <Layout user={user} title={`Pipelines — ${repo.name}`}> | |
| 148 | - | { | |
| 149 | - | (isRunning ? ( | |
| 150 | - | <meta http-equiv="refresh" content="4" /> | |
| 151 | - | ) : ( | |
| 152 | - | "" | |
| 153 | - | )) as unknown as JSX.Element | |
| 154 | - | } | |
| 148 | + | {isRunning ? ( | |
| 149 | + | <meta http-equiv="refresh" content="4" /> | |
| 150 | + | ) : null} | |
| 155 | 151 | <div class="container"> | |
| 156 | 152 | <RepoHeader repo={repo} /> | |
| 157 | 153 | <RepoNav repo={repo} active="ci" user={user} /> | |
| @@ -228,28 +224,27 @@ export function CiHistory({ | |||
|---|---|---|---|
| 228 | 224 | </span> | |
| 229 | 225 | </a> | |
| 230 | 226 | <div class="release-item-meta"> | |
| 231 | - | {run.commit_sha && ( | |
| 232 | - | <code class="ci-sha"> | |
| 227 | + | {!!run.commit_sha && ( | |
| 228 | + | <code class="ci-sha" safe> | |
| 233 | 229 | {run.commit_sha.slice(0, 8)} | |
| 234 | 230 | </code> | |
| 235 | 231 | )} | |
| 236 | - | {run.commit_branch && ( | |
| 237 | - | <span class="badge"> | |
| 232 | + | {!!run.commit_branch && ( | |
| 233 | + | <span class="badge" safe> | |
| 238 | 234 | {run.commit_branch} | |
| 239 | 235 | </span> | |
| 240 | 236 | )} | |
| 241 | - | {run.commit_tag && ( | |
| 242 | - | <span class="badge"> | |
| 237 | + | {!!run.commit_tag && ( | |
| 238 | + | <span class="badge" safe> | |
| 243 | 239 | {run.commit_tag} | |
| 244 | 240 | </span> | |
| 245 | 241 | )} | |
| 246 | - | <span class="text-muted"> | |
| 242 | + | <span class="text-muted" safe> | |
| 247 | 243 | {run.trigger_source} | |
| 248 | 244 | </span> | |
| 249 | - | {run.triggered_by_username && ( | |
| 250 | - | <span class="text-muted"> | |
| 251 | - | by{" "} | |
| 252 | - | {run.triggered_by_username} | |
| 245 | + | {!!run.triggered_by_username && ( | |
| 246 | + | <span class="text-muted" safe> | |
| 247 | + | by {run.triggered_by_username} | |
| 253 | 248 | </span> | |
| 254 | 249 | )} | |
| 255 | 250 | {run.artifact_count > 0 && ( | |
| @@ -261,9 +256,9 @@ export function CiHistory({ | |||
|---|---|---|---|
| 261 | 256 | : ""} | |
| 262 | 257 | </span> | |
| 263 | 258 | )} | |
| 264 | - | {run.started_at && | |
| 265 | - | run.finished_at && ( | |
| 266 | - | <span class="text-muted"> | |
| 259 | + | {!!run.started_at && | |
| 260 | + | !!run.finished_at && ( | |
| 261 | + | <span class="text-muted" safe> | |
| 267 | 262 | {duration( | |
| 268 | 263 | run.started_at, | |
| 269 | 264 | run.finished_at, | |
| @@ -273,7 +268,7 @@ export function CiHistory({ | |||
|---|---|---|---|
| 273 | 268 | </div> | |
| 274 | 269 | </div> | |
| 275 | 270 | <div class="release-item-date"> | |
| 276 | - | <time datetime={run.created_at}> | |
| 271 | + | <time datetime={run.created_at} safe> | |
| 277 | 272 | {formatDateTime(run.created_at)} | |
| 278 | 273 | </time> | |
| 279 | 274 | </div> | |
Msrc/views/ci/CiRunDetail.tsx
| @@ -87,13 +87,9 @@ export function CiRunDetail({ | |||
|---|---|---|---|
| 87 | 87 | ||
| 88 | 88 | return ( | |
| 89 | 89 | <Layout user={user} title={`Pipeline #${displayId} — ${repo.name}`}> | |
| 90 | - | { | |
| 91 | - | (isActive && autoRefresh ? ( | |
| 92 | - | <meta http-equiv="refresh" content="3" /> | |
| 93 | - | ) : ( | |
| 94 | - | "" | |
| 95 | - | )) as unknown as JSX.Element | |
| 96 | - | } | |
| 90 | + | {isActive && autoRefresh ? ( | |
| 91 | + | <meta http-equiv="refresh" content="3" /> | |
| 92 | + | ) : null} | |
| 97 | 93 | <div class="container"> | |
| 98 | 94 | <RepoHeader repo={repo} /> | |
| 99 | 95 | <RepoNav repo={repo} active="ci" user={user} /> | |
| @@ -108,38 +104,40 @@ export function CiRunDetail({ | |||
|---|---|---|---|
| 108 | 104 | Pipeline #{displayId} | |
| 109 | 105 | </h2> | |
| 110 | 106 | <div class="release-item-meta"> | |
| 111 | - | {run.commit_sha && ( | |
| 112 | - | <code class="ci-sha"> | |
| 107 | + | {!!run.commit_sha && ( | |
| 108 | + | <code class="ci-sha" safe> | |
| 113 | 109 | {run.commit_sha.slice(0, 8)} | |
| 114 | 110 | </code> | |
| 115 | 111 | )} | |
| 116 | - | {run.commit_branch && ( | |
| 112 | + | {!!run.commit_branch && ( | |
| 117 | 113 | <a | |
| 118 | 114 | href={`/${repo.name}/tree/${run.commit_branch}`} | |
| 119 | 115 | class="badge" | |
| 116 | + | safe | |
| 120 | 117 | > | |
| 121 | 118 | {run.commit_branch} | |
| 122 | 119 | </a> | |
| 123 | 120 | )} | |
| 124 | - | {run.commit_tag && ( | |
| 121 | + | {!!run.commit_tag && ( | |
| 125 | 122 | <a | |
| 126 | 123 | href={`/${repo.name}/tree/${run.commit_tag}`} | |
| 127 | 124 | class="badge" | |
| 125 | + | safe | |
| 128 | 126 | > | |
| 129 | 127 | {run.commit_tag} | |
| 130 | 128 | </a> | |
| 131 | 129 | )} | |
| 132 | - | <span class="text-muted"> | |
| 130 | + | <span class="text-muted" safe> | |
| 133 | 131 | triggered by {run.trigger_source} | |
| 134 | - | {run.triggered_by_username && | |
| 132 | + | {!!run.triggered_by_username && | |
| 135 | 133 | ` (${run.triggered_by_username})`} | |
| 136 | 134 | </span> | |
| 137 | - | {run.started_at && run.finished_at && ( | |
| 138 | - | <span class="text-muted"> | |
| 135 | + | {!!run.started_at && !!run.finished_at && ( | |
| 136 | + | <span class="text-muted" safe> | |
| 139 | 137 | {duration(run.started_at, run.finished_at)} | |
| 140 | 138 | </span> | |
| 141 | 139 | )} | |
| 142 | - | <time datetime={run.created_at} class="text-muted"> | |
| 140 | + | <time datetime={run.created_at} class="text-muted" safe> | |
| 143 | 141 | {formatDateTime(run.created_at)} | |
| 144 | 142 | </time> | |
| 145 | 143 | </div> | |
| @@ -194,9 +192,9 @@ export function CiRunDetail({ | |||
|---|---|---|---|
| 194 | 192 | {Object.entries(variableOverrides).map(([k, v]) => ( | |
| 195 | 193 | <> | |
| 196 | 194 | <dt> | |
| 197 | - | <code>{k}</code> | |
| 195 | + | <code safe>{k}</code> | |
| 198 | 196 | </dt> | |
| 199 | - | <dd>{v}</dd> | |
| 197 | + | <dd safe>{v}</dd> | |
| 200 | 198 | </> | |
| 201 | 199 | ))} | |
| 202 | 200 | </dl> | |
| @@ -221,11 +219,11 @@ export function CiRunDetail({ | |||
|---|---|---|---|
| 221 | 219 | > | |
| 222 | 220 | <summary class="ci-step-summary"> | |
| 223 | 221 | <CiStatusPill status={step.status} /> | |
| 224 | - | <span class="ci-step-name"> | |
| 222 | + | <span class="ci-step-name" safe> | |
| 225 | 223 | {step.name} | |
| 226 | 224 | </span> | |
| 227 | - | {step.started_at && step.finished_at && ( | |
| 228 | - | <span class="ci-step-duration text-muted"> | |
| 225 | + | {!!step.started_at && !!step.finished_at && ( | |
| 226 | + | <span class="ci-step-duration text-muted" safe> | |
| 229 | 227 | {duration( | |
| 230 | 228 | step.started_at, | |
| 231 | 229 | step.finished_at, | |
| @@ -234,7 +232,9 @@ export function CiRunDetail({ | |||
|---|---|---|---|
| 234 | 232 | )} | |
| 235 | 233 | </summary> | |
| 236 | 234 | {step.log ? ( | |
| 237 | - | <pre class="ci-step-log">{step.log}</pre> | |
| 235 | + | <pre class="ci-step-log" safe> | |
| 236 | + | {step.log} | |
| 237 | + | </pre> | |
| 238 | 238 | ) : step.status === "running" ? ( | |
| 239 | 239 | <div class="ci-step-running-indicator text-muted"> | |
| 240 | 240 | Running… | |
| @@ -254,10 +254,11 @@ export function CiRunDetail({ | |||
|---|---|---|---|
| 254 | 254 | <a | |
| 255 | 255 | href={`/${repo.name}/ci/${run.id}/artifacts/${artifact.id}`} | |
| 256 | 256 | class="ci-artifact-name" | |
| 257 | + | safe | |
| 257 | 258 | > | |
| 258 | 259 | {artifact.filename} | |
| 259 | 260 | </a> | |
| 260 | - | <span class="ci-artifact-size text-muted"> | |
| 261 | + | <span class="ci-artifact-size text-muted" safe> | |
| 261 | 262 | {formatBytes(artifact.size)} | |
| 262 | 263 | </span> | |
| 263 | 264 | </li> | |
Msrc/views/ci/CiStatusPill.tsx
| @@ -16,7 +16,7 @@ interface CiStatusPillProps { | |||
|---|---|---|---|
| 16 | 16 | export function CiStatusPill({ status, title }: CiStatusPillProps) { | |
| 17 | 17 | const cls = statusStyles[status] ?? "ci-status-pending"; | |
| 18 | 18 | return ( | |
| 19 | - | <span class={`ci-status-pill ${cls}`} title={title}> | |
| 19 | + | <span class={`ci-status-pill ${cls}`} title={title} safe> | |
| 20 | 20 | {status} | |
| 21 | 21 | </span> | |
| 22 | 22 | ); | |
Msrc/views/issues/IssueDetail.tsx
| @@ -71,7 +71,7 @@ export function IssueDetail({ | |||
|---|---|---|---|
| 71 | 71 | bodyFieldName="edit_body" | |
| 72 | 72 | bodyValue={issue.body} | |
| 73 | 73 | /> | |
| 74 | - | <span class={`issue-badge ${issue.status}`}> | |
| 74 | + | <span class={`issue-badge ${issue.status}`} safe> | |
| 75 | 75 | {issue.status} | |
| 76 | 76 | </span> | |
| 77 | 77 | {canEditIssue && ( | |
| @@ -144,7 +144,7 @@ export function IssueDetail({ | |||
|---|---|---|---|
| 144 | 144 | version={issue.author_avatar_version} | |
| 145 | 145 | size={24} | |
| 146 | 146 | /> | |
| 147 | - | <strong> | |
| 147 | + | <strong safe> | |
| 148 | 148 | {displayName(issue.author_username)} | |
| 149 | 149 | </strong> | |
| 150 | 150 | <div class="timeline-author-right"> | |
| @@ -187,6 +187,7 @@ export function IssueDetail({ | |||
|---|---|---|---|
| 187 | 187 | maxlength={ | |
| 188 | 188 | config.MAX_TEXT_BODY_BYTES | |
| 189 | 189 | } | |
| 190 | + | safe | |
| 190 | 191 | > | |
| 191 | 192 | {issue.body} | |
| 192 | 193 | </textarea> | |
| @@ -203,7 +204,7 @@ export function IssueDetail({ | |||
|---|---|---|---|
| 203 | 204 | </div> | |
| 204 | 205 | )} | |
| 205 | 206 | <div class="timeline-body markdown-body"> | |
| 206 | - | {bodyHtml || ( | |
| 207 | + | {(bodyHtml as "safe") || ( | |
| 207 | 208 | <em class="text-muted"> | |
| 208 | 209 | No description provided. | |
| 209 | 210 | </em> | |
Msrc/views/issues/IssueList.tsx
| @@ -115,6 +115,7 @@ export function IssueList({ | |||
|---|---|---|---|
| 115 | 115 | <span | |
| 116 | 116 | class="label-badge" | |
| 117 | 117 | style={`background:${label.color};color:${labelTextColor(label.color)}`} | |
| 118 | + | safe | |
| 118 | 119 | > | |
| 119 | 120 | {label.name} | |
| 120 | 121 | </span> | |
| @@ -139,7 +140,7 @@ export function IssueList({ | |||
|---|---|---|---|
| 139 | 140 | </div> | |
| 140 | 141 | </details> | |
| 141 | 142 | )} | |
| 142 | - | {user && ( | |
| 143 | + | {!!user && ( | |
| 143 | 144 | <a | |
| 144 | 145 | href={`/${repo.name}/issues/new`} | |
| 145 | 146 | class="btn btn-primary btn-sm" | |
| @@ -157,6 +158,9 @@ export function IssueList({ | |||
|---|---|---|---|
| 157 | 158 | <ul class="issue-list"> | |
| 158 | 159 | {issues.map((issue) => { | |
| 159 | 160 | const labels = labelsByIssueId.get(issue.id) ?? []; | |
| 161 | + | const unsafeAuthor = displayName( | |
| 162 | + | issue.author_username, | |
| 163 | + | ); | |
| 160 | 164 | return ( | |
| 161 | 165 | <li class="issue-item"> | |
| 162 | 166 | <div class="issue-main"> | |
| @@ -166,6 +170,7 @@ export function IssueList({ | |||
|---|---|---|---|
| 166 | 170 | <a | |
| 167 | 171 | href={`/${repo.name}/issues/${issue.number}`} | |
| 168 | 172 | class="issue-title" | |
| 173 | + | safe | |
| 169 | 174 | > | |
| 170 | 175 | {issue.title} | |
| 171 | 176 | </a> | |
| @@ -179,6 +184,7 @@ export function IssueList({ | |||
|---|---|---|---|
| 179 | 184 | <span | |
| 180 | 185 | class="label-badge" | |
| 181 | 186 | style={`background:${label.color};color:${labelTextColor(label.color)}`} | |
| 187 | + | safe | |
| 182 | 188 | > | |
| 183 | 189 | {label.name} | |
| 184 | 190 | </span> | |
| @@ -193,11 +199,10 @@ export function IssueList({ | |||
|---|---|---|---|
| 193 | 199 | } | |
| 194 | 200 | size={20} | |
| 195 | 201 | /> | |
| 196 | - | <span class="issue-author"> | |
| 197 | - | opened by{" "} | |
| 198 | - | {displayName(issue.author_username)} | |
| 202 | + | <span class="issue-author" safe> | |
| 203 | + | opened by {unsafeAuthor} | |
| 199 | 204 | </span> | |
| 200 | - | <time datetime={issue.created_at}> | |
| 205 | + | <time datetime={issue.created_at} safe> | |
| 201 | 206 | {formatDate(issue.created_at)} | |
| 202 | 207 | </time> | |
| 203 | 208 | </div> | |
Msrc/views/issues/NewIssue.tsx
| @@ -27,7 +27,11 @@ export function NewIssue({ | |||
|---|---|---|---|
| 27 | 27 | <RepoHeader repo={repo} /> | |
| 28 | 28 | <RepoNav repo={repo} active="issues" user={user} /> | |
| 29 | 29 | <h2 class="section-title">New issue</h2> | |
| 30 | - | {error && <p class="form-error">{error}</p>} | |
| 30 | + | {!!error && ( | |
| 31 | + | <p class="form-error" safe> | |
| 32 | + | {error} | |
| 33 | + | </p> | |
| 34 | + | )} | |
| 31 | 35 | <form | |
| 32 | 36 | method="POST" | |
| 33 | 37 | action={`/${repo.name}/issues`} | |
| @@ -57,6 +61,7 @@ export function NewIssue({ | |||
|---|---|---|---|
| 57 | 61 | rows="10" | |
| 58 | 62 | maxlength={config.MAX_TEXT_BODY_BYTES} | |
| 59 | 63 | placeholder="Describe the issue..." | |
| 64 | + | safe | |
| 60 | 65 | > | |
| 61 | 66 | {template ?? ""} | |
| 62 | 67 | </textarea> | |
| @@ -76,6 +81,7 @@ export function NewIssue({ | |||
|---|---|---|---|
| 76 | 81 | <span | |
| 77 | 82 | class="label-badge" | |
| 78 | 83 | style={`background:${label.color};color:${labelTextColor(label.color)}`} | |
| 84 | + | safe | |
| 79 | 85 | > | |
| 80 | 86 | {label.name} | |
| 81 | 87 | </span> | |
Msrc/views/layout.tsx
| @@ -1,3 +1,4 @@ | |||
|---|---|---|---|
| 1 | + | import type { Children } from "@kitajs/html"; | |
| 1 | 2 | import config from "../config.ts"; | |
| 2 | 3 | import { displayName } from "../lib/users.ts"; | |
| 3 | 4 | import type { SessionUser } from "../middleware/session.ts"; | |
| @@ -6,7 +7,7 @@ import { Avatar } from "./Avatar.tsx"; | |||
|---|---|---|---|
| 6 | 7 | interface LayoutProps { | |
| 7 | 8 | user: SessionUser | null; | |
| 8 | 9 | title?: string; | |
| 9 | - | children?: JSX.Element | JSX.Element[] | string | null; | |
| 10 | + | children: Children; | |
| 10 | 11 | } | |
| 11 | 12 | ||
| 12 | 13 | export function Layout({ user, title, children }: LayoutProps) { | |
| @@ -16,7 +17,7 @@ export function Layout({ user, title, children }: LayoutProps) { | |||
|---|---|---|---|
| 16 | 17 | <head> | |
| 17 | 18 | <meta charset="UTF-8" /> | |
| 18 | 19 | <meta name="viewport" content="width=500" /> | |
| 19 | - | <title>{pageTitle}</title> | |
| 20 | + | <title safe>{pageTitle}</title> | |
| 20 | 21 | <script src="/assets/theme.js"></script> | |
| 21 | 22 | <link | |
| 22 | 23 | rel="icon" | |
| @@ -48,7 +49,9 @@ export function Layout({ user, title, children }: LayoutProps) { | |||
|---|---|---|---|
| 48 | 49 | version={user.avatar_version} | |
| 49 | 50 | size={24} | |
| 50 | 51 | /> | |
| 51 | - | {displayName(user.username)} | |
| 52 | + | <span safe> | |
| 53 | + | {displayName(user.username)} | |
| 54 | + | </span> | |
| 52 | 55 | </a> | |
| 53 | 56 | <form | |
| 54 | 57 | method="POST" | |
| @@ -81,7 +84,9 @@ export function Layout({ user, title, children }: LayoutProps) { | |||
|---|---|---|---|
| 81 | 84 | </header> | |
| 82 | 85 | <main class="site-main">{children}</main> | |
| 83 | 86 | <footer class="site-footer"> | |
| 84 | - | <p>Hearthforge — hosted by {config.OWNER_DISPLAY_NAME}</p> | |
| 87 | + | <p safe> | |
| 88 | + | Hearthforge — hosted by {config.OWNER_DISPLAY_NAME} | |
| 89 | + | </p> | |
| 85 | 90 | </footer> | |
| 86 | 91 | </body> | |
| 87 | 92 | </html> | |
Msrc/views/patches/NewPatch.tsx
| @@ -27,7 +27,11 @@ export function NewPatch({ | |||
|---|---|---|---|
| 27 | 27 | <RepoHeader repo={repo} /> | |
| 28 | 28 | <RepoNav repo={repo} active="patches" user={user} /> | |
| 29 | 29 | <h2 class="section-title">Upload patch</h2> | |
| 30 | - | {error && <p class="form-error">{error}</p>} | |
| 30 | + | {!!error && ( | |
| 31 | + | <p class="form-error" safe> | |
| 32 | + | {error} | |
| 33 | + | </p> | |
| 34 | + | )} | |
| 31 | 35 | <form | |
| 32 | 36 | method="POST" | |
| 33 | 37 | action={`/${repo.name}/patches`} | |
| @@ -57,6 +61,7 @@ export function NewPatch({ | |||
|---|---|---|---|
| 57 | 61 | name="description" | |
| 58 | 62 | rows="5" | |
| 59 | 63 | maxlength={config.MAX_TEXT_BODY_BYTES} | |
| 64 | + | safe | |
| 60 | 65 | > | |
| 61 | 66 | {template ?? ""} | |
| 62 | 67 | </textarea> | |
| @@ -88,6 +93,7 @@ export function NewPatch({ | |||
|---|---|---|---|
| 88 | 93 | <span | |
| 89 | 94 | class="label-badge" | |
| 90 | 95 | style={`background:${label.color};color:${labelTextColor(label.color)}`} | |
| 96 | + | safe | |
| 91 | 97 | > | |
| 92 | 98 | {label.name} | |
| 93 | 99 | </span> | |
Msrc/views/patches/PatchDetail.tsx
| @@ -91,7 +91,7 @@ export function PatchDetail({ | |||
|---|---|---|---|
| 91 | 91 | bodyFieldName="edit_description" | |
| 92 | 92 | bodyValue={patch.description} | |
| 93 | 93 | /> | |
| 94 | - | <span class={`patch-badge ${patch.status}`}> | |
| 94 | + | <span class={`patch-badge ${patch.status}`} safe> | |
| 95 | 95 | {patch.status} | |
| 96 | 96 | </span> | |
| 97 | 97 | {canEdit && ( | |
| @@ -225,7 +225,7 @@ export function PatchDetail({ | |||
|---|---|---|---|
| 225 | 225 | version={patch.author_avatar_version} | |
| 226 | 226 | size={24} | |
| 227 | 227 | /> | |
| 228 | - | <strong> | |
| 228 | + | <strong safe> | |
| 229 | 229 | {displayName(patch.author_username)} | |
| 230 | 230 | </strong> | |
| 231 | 231 | <div class="timeline-author-right"> | |
| @@ -268,6 +268,7 @@ export function PatchDetail({ | |||
|---|---|---|---|
| 268 | 268 | maxlength={ | |
| 269 | 269 | config.MAX_TEXT_BODY_BYTES | |
| 270 | 270 | } | |
| 271 | + | safe | |
| 271 | 272 | > | |
| 272 | 273 | {patch.description} | |
| 273 | 274 | </textarea> | |
| @@ -284,7 +285,7 @@ export function PatchDetail({ | |||
|---|---|---|---|
| 284 | 285 | </div> | |
| 285 | 286 | )} | |
| 286 | 287 | <div class="timeline-body markdown-body"> | |
| 287 | - | {descriptionHtml || ( | |
| 288 | + | {(descriptionHtml as "safe") || ( | |
| 288 | 289 | <em class="text-muted"> | |
| 289 | 290 | No description provided. | |
| 290 | 291 | </em> | |
| @@ -298,7 +299,7 @@ export function PatchDetail({ | |||
|---|---|---|---|
| 298 | 299 | </div> | |
| 299 | 300 | ||
| 300 | 301 | {/* Apply status */} | |
| 301 | - | {applyResult && ( | |
| 302 | + | {!!applyResult && ( | |
| 302 | 303 | <div class="timeline-item"> | |
| 303 | 304 | <div | |
| 304 | 305 | class={`apply-result apply-${applyResult.status}`} | |
| @@ -315,8 +316,8 @@ export function PatchDetail({ | |||
|---|---|---|---|
| 315 | 316 | : "Has conflicts"} | |
| 316 | 317 | </span> | |
| 317 | 318 | </div> | |
| 318 | - | {applyResult.output && ( | |
| 319 | - | <pre class="apply-output"> | |
| 319 | + | {!!applyResult.output && ( | |
| 320 | + | <pre class="apply-output" safe> | |
| 320 | 321 | {applyResult.output} | |
| 321 | 322 | </pre> | |
| 322 | 323 | )} | |
| @@ -342,7 +343,7 @@ export function PatchDetail({ | |||
|---|---|---|---|
| 342 | 343 | ? escapeHtml(patchMeta.subject) | |
| 343 | 344 | : "No commit message"} | |
| 344 | 345 | </h2> | |
| 345 | - | {patchMeta.body && ( | |
| 346 | + | {!!patchMeta.body && ( | |
| 346 | 347 | <pre class="commit-card-body"> | |
| 347 | 348 | {escapeHtml(patchMeta.body)} | |
| 348 | 349 | </pre> | |
| @@ -363,6 +364,7 @@ export function PatchDetail({ | |||
|---|---|---|---|
| 363 | 364 | <time | |
| 364 | 365 | class="commit-meta-value" | |
| 365 | 366 | datetime={patchMeta.date} | |
| 367 | + | safe | |
| 366 | 368 | > | |
| 367 | 369 | {formatDateTime(patchMeta.date)} | |
| 368 | 370 | </time> | |
Msrc/views/patches/PatchList.tsx
| @@ -113,6 +113,7 @@ export function PatchList({ | |||
|---|---|---|---|
| 113 | 113 | <span | |
| 114 | 114 | class="label-badge" | |
| 115 | 115 | style={`background:${label.color};color:${labelTextColor(label.color)}`} | |
| 116 | + | safe | |
| 116 | 117 | > | |
| 117 | 118 | {label.name} | |
| 118 | 119 | </span> | |
| @@ -137,7 +138,7 @@ export function PatchList({ | |||
|---|---|---|---|
| 137 | 138 | </div> | |
| 138 | 139 | </details> | |
| 139 | 140 | )} | |
| 140 | - | {user && ( | |
| 141 | + | {!!user && ( | |
| 141 | 142 | <a | |
| 142 | 143 | href={`/${repo.name}/patches/new`} | |
| 143 | 144 | class="btn btn-primary btn-sm" | |
| @@ -149,12 +150,15 @@ export function PatchList({ | |||
|---|---|---|---|
| 149 | 150 | </div> | |
| 150 | 151 | {patches.length === 0 ? ( | |
| 151 | 152 | <div class="empty-state"> | |
| 152 | - | <p>No {status} patches.</p> | |
| 153 | + | <p safe>No {status} patches.</p> | |
| 153 | 154 | </div> | |
| 154 | 155 | ) : ( | |
| 155 | 156 | <ul class="issue-list"> | |
| 156 | 157 | {patches.map((patch) => { | |
| 157 | 158 | const labels = labelsByPatchId.get(patch.id) ?? []; | |
| 159 | + | const unsafeAuthor = displayName( | |
| 160 | + | patch.author_username, | |
| 161 | + | ); | |
| 158 | 162 | return ( | |
| 159 | 163 | <li class="issue-item"> | |
| 160 | 164 | <div class="issue-main"> | |
| @@ -164,6 +168,7 @@ export function PatchList({ | |||
|---|---|---|---|
| 164 | 168 | <a | |
| 165 | 169 | href={`/${repo.name}/patches/${patch.number}`} | |
| 166 | 170 | class="issue-title" | |
| 171 | + | safe | |
| 167 | 172 | > | |
| 168 | 173 | {patch.title} | |
| 169 | 174 | </a> | |
| @@ -177,6 +182,7 @@ export function PatchList({ | |||
|---|---|---|---|
| 177 | 182 | <span | |
| 178 | 183 | class="label-badge" | |
| 179 | 184 | style={`background:${label.color};color:${labelTextColor(label.color)}`} | |
| 185 | + | safe | |
| 180 | 186 | > | |
| 181 | 187 | {label.name} | |
| 182 | 188 | </span> | |
| @@ -191,11 +197,8 @@ export function PatchList({ | |||
|---|---|---|---|
| 191 | 197 | } | |
| 192 | 198 | size={20} | |
| 193 | 199 | /> | |
| 194 | - | <span> | |
| 195 | - | by{" "} | |
| 196 | - | {displayName(patch.author_username)} | |
| 197 | - | </span> | |
| 198 | - | <time datetime={patch.created_at}> | |
| 200 | + | <span safe>by {unsafeAuthor}</span> | |
| 201 | + | <time datetime={patch.created_at} safe> | |
| 199 | 202 | {formatDate(patch.created_at)} | |
| 200 | 203 | </time> | |
| 201 | 204 | </div> | |
Msrc/views/releases/NewRelease.tsx
| @@ -30,7 +30,11 @@ export function NewRelease({ user, repo, error, values }: NewReleaseProps) { | |||
|---|---|---|---|
| 30 | 30 | <RepoNav repo={repo} active="releases" user={user} /> | |
| 31 | 31 | <div class="form-page"> | |
| 32 | 32 | <h2 class="page-title">New Release</h2> | |
| 33 | - | {error && <div class="form-error">{error}</div>} | |
| 33 | + | {!!error && ( | |
| 34 | + | <div class="form-error" safe> | |
| 35 | + | {error} | |
| 36 | + | </div> | |
| 37 | + | )} | |
| 34 | 38 | <form | |
| 35 | 39 | method="post" | |
| 36 | 40 | action={`/${repo.name}/releases`} | |
| @@ -115,6 +119,7 @@ export function NewRelease({ user, repo, error, values }: NewReleaseProps) { | |||
|---|---|---|---|
| 115 | 119 | class="form-input form-textarea" | |
| 116 | 120 | rows="8" | |
| 117 | 121 | maxlength={config.MAX_TEXT_BODY_BYTES} | |
| 122 | + | safe | |
| 118 | 123 | > | |
| 119 | 124 | {values?.notes ?? ""} | |
| 120 | 125 | </textarea> | |
Msrc/views/releases/ReleaseDetail.tsx
| @@ -55,29 +55,31 @@ export function ReleaseDetail({ | |||
|---|---|---|---|
| 55 | 55 | <h2 | |
| 56 | 56 | class="page-title" | |
| 57 | 57 | style={`view-transition-name: release-title-${release.id}`} | |
| 58 | + | safe | |
| 58 | 59 | > | |
| 59 | 60 | {release.name} | |
| 60 | 61 | </h2> | |
| 61 | 62 | </div> | |
| 62 | 63 | <div class="release-item-date"> | |
| 63 | - | {release.tag_name && ( | |
| 64 | + | {!!release.tag_name && ( | |
| 64 | 65 | <a | |
| 65 | 66 | href={`/${repo.name}/tree/${release.tag_name}`} | |
| 66 | 67 | class="badge" | |
| 68 | + | safe | |
| 67 | 69 | > | |
| 68 | 70 | {release.tag_name} | |
| 69 | 71 | </a> | |
| 70 | 72 | )} | |
| 71 | - | <time datetime={release.created_at}> | |
| 73 | + | <time datetime={release.created_at} safe> | |
| 72 | 74 | {formatDate(release.created_at)} | |
| 73 | 75 | </time> | |
| 74 | 76 | </div> | |
| 75 | 77 | </div> | |
| 76 | 78 | </div> | |
| 77 | 79 | ||
| 78 | - | {notesHtml && ( | |
| 80 | + | {!!notesHtml && ( | |
| 79 | 81 | <div class="markdown-body release-notes"> | |
| 80 | - | {notesHtml} | |
| 82 | + | {notesHtml as "safe"} | |
| 81 | 83 | </div> | |
| 82 | 84 | )} | |
| 83 | 85 | ||
| @@ -90,10 +92,11 @@ export function ReleaseDetail({ | |||
|---|---|---|---|
| 90 | 92 | <a | |
| 91 | 93 | href={`/${repo.name}/releases/${release.id}/assets/${asset.filename}`} | |
| 92 | 94 | class="asset-name" | |
| 95 | + | safe | |
| 93 | 96 | > | |
| 94 | 97 | {asset.filename} | |
| 95 | 98 | </a> | |
| 96 | - | <span class="asset-size"> | |
| 99 | + | <span class="asset-size" safe> | |
| 97 | 100 | {formatBytes(asset.size)} | |
| 98 | 101 | </span> | |
| 99 | 102 | </li> | |
| @@ -103,13 +106,14 @@ export function ReleaseDetail({ | |||
|---|---|---|---|
| 103 | 106 | <a | |
| 104 | 107 | href={`/${repo.name}/releases/${release.id}/source/${archive.filename}`} | |
| 105 | 108 | class="asset-name" | |
| 109 | + | safe | |
| 106 | 110 | > | |
| 107 | 111 | {archive.filename} | |
| 108 | 112 | </a> | |
| 109 | - | <span class="asset-meta"> | |
| 113 | + | <span class="asset-meta" safe> | |
| 110 | 114 | Source code ({archive.format}) | |
| 111 | 115 | </span> | |
| 112 | - | <span class="asset-size"> | |
| 116 | + | <span class="asset-size" safe> | |
| 113 | 117 | {formatBytes(archive.size)} | |
| 114 | 118 | </span> | |
| 115 | 119 | </li> | |
| @@ -130,7 +134,10 @@ export function ReleaseDetail({ | |||
|---|---|---|---|
| 130 | 134 | ) | |
| 131 | 135 | .map(([format, ext]) => ( | |
| 132 | 136 | <li class="asset-item asset-item-pending"> | |
| 133 | - | <span class="asset-name asset-name-pending"> | |
| 137 | + | <span | |
| 138 | + | class="asset-name asset-name-pending" | |
| 139 | + | safe | |
| 140 | + | > | |
| 134 | 141 | {`${repo.name}-${release.tag_name}${ext}`} | |
| 135 | 142 | </span> | |
| 136 | 143 | <span class="asset-meta"> | |
Msrc/views/releases/ReleaseList.tsx
| @@ -22,10 +22,12 @@ function NotesPreview({ notes }: { notes: string }) { | |||
|---|---|---|---|
| 22 | 22 | return ( | |
| 23 | 23 | <details class="notes-expand"> | |
| 24 | 24 | <summary class="notes-toggle"> | |
| 25 | - | <div class="notes-preview">{plaintext}</div> | |
| 25 | + | <div class="notes-preview" safe> | |
| 26 | + | {plaintext} | |
| 27 | + | </div> | |
| 26 | 28 | <span class="notes-toggle-label" /> | |
| 27 | 29 | </summary> | |
| 28 | - | <div class="notes-full markdown-body">{renderMarkdown(notes)}</div> | |
| 30 | + | <div class="notes-full markdown-body">{renderMarkdown(notes) as "safe"}</div> | |
| 29 | 31 | </details> | |
| 30 | 32 | ); | |
| 31 | 33 | } | |
| @@ -66,6 +68,7 @@ export function ReleaseList({ | |||
|---|---|---|---|
| 66 | 68 | href={`/${repo.name}/releases/${release.id}`} | |
| 67 | 69 | class="release-item-title" | |
| 68 | 70 | style={`view-transition-name: release-title-${release.id}`} | |
| 71 | + | safe | |
| 69 | 72 | > | |
| 70 | 73 | {release.name} | |
| 71 | 74 | </a> | |
| @@ -85,20 +88,21 @@ export function ReleaseList({ | |||
|---|---|---|---|
| 85 | 88 | </div> | |
| 86 | 89 | </div> | |
| 87 | 90 | <div class="release-item-date"> | |
| 88 | - | {release.tag_name && ( | |
| 91 | + | {!!release.tag_name && ( | |
| 89 | 92 | <a | |
| 90 | 93 | href={`/${repo.name}/tree/${release.tag_name}`} | |
| 91 | 94 | class="badge" | |
| 95 | + | safe | |
| 92 | 96 | > | |
| 93 | 97 | {release.tag_name} | |
| 94 | 98 | </a> | |
| 95 | 99 | )} | |
| 96 | - | <time datetime={release.created_at}> | |
| 100 | + | <time datetime={release.created_at} safe> | |
| 97 | 101 | {formatDate(release.created_at)} | |
| 98 | 102 | </time> | |
| 99 | 103 | </div> | |
| 100 | 104 | </div> | |
| 101 | - | {release.notes && ( | |
| 105 | + | {!!release.notes && ( | |
| 102 | 106 | <div class="release-notes-section"> | |
| 103 | 107 | <p class="release-notes-label"> | |
| 104 | 108 | Release Notes | |
Msrc/views/repos/BranchList.tsx
| @@ -32,8 +32,16 @@ export function BranchList({ | |||
|---|---|---|---|
| 32 | 32 | <div class="container"> | |
| 33 | 33 | <RepoHeader repo={repo} /> | |
| 34 | 34 | <RepoNav repo={repo} active="branches" user={user} /> | |
| 35 | - | {success && <p class="form-success">{success}</p>} | |
| 36 | - | {error && <p class="form-error">{error}</p>} | |
| 35 | + | {!!success && ( | |
| 36 | + | <p class="form-success" safe> | |
| 37 | + | {success} | |
| 38 | + | </p> | |
| 39 | + | )} | |
| 40 | + | {!!error && ( | |
| 41 | + | <p class="form-error" safe> | |
| 42 | + | {error} | |
| 43 | + | </p> | |
| 44 | + | )} | |
| 37 | 45 | <div class="list-header"> | |
| 38 | 46 | <h2 class="list-heading">Branches</h2> | |
| 39 | 47 | {user?.isAdmin && ( | |
| @@ -97,6 +105,7 @@ export function BranchList({ | |||
|---|---|---|---|
| 97 | 105 | <a | |
| 98 | 106 | href={`/${repo.name}/tree/${b.name}`} | |
| 99 | 107 | class="ref-name" | |
| 108 | + | safe | |
| 100 | 109 | > | |
| 101 | 110 | {b.name} | |
| 102 | 111 | </a> | |
| @@ -105,28 +114,30 @@ export function BranchList({ | |||
|---|---|---|---|
| 105 | 114 | )} | |
| 106 | 115 | </div> | |
| 107 | 116 | <div class="ref-meta-row"> | |
| 108 | - | {b.authorName && ( | |
| 109 | - | <span class="ref-author"> | |
| 117 | + | {!!b.authorName && ( | |
| 118 | + | <span class="ref-author" safe> | |
| 110 | 119 | {b.authorName} | |
| 111 | 120 | </span> | |
| 112 | 121 | )} | |
| 113 | - | {b.shortHash && ( | |
| 122 | + | {!!b.shortHash && ( | |
| 114 | 123 | <a | |
| 115 | 124 | href={`/${repo.name}/commit/${b.shortHash}`} | |
| 116 | 125 | class="ref-hash mono" | |
| 126 | + | safe | |
| 117 | 127 | > | |
| 118 | 128 | {b.shortHash} | |
| 119 | 129 | </a> | |
| 120 | 130 | )} | |
| 121 | - | {b.subject && ( | |
| 122 | - | <span class="ref-subject"> | |
| 131 | + | {!!b.subject && ( | |
| 132 | + | <span class="ref-subject" safe> | |
| 123 | 133 | {b.subject} | |
| 124 | 134 | </span> | |
| 125 | 135 | )} | |
| 126 | - | {b.date && ( | |
| 136 | + | {!!b.date && ( | |
| 127 | 137 | <time | |
| 128 | 138 | class="ref-date" | |
| 129 | 139 | datetime={b.date} | |
| 140 | + | safe | |
| 130 | 141 | > | |
| 131 | 142 | {formatDateTime(b.date)} | |
| 132 | 143 | </time> | |
| @@ -174,7 +185,7 @@ export function BranchList({ | |||
|---|---|---|---|
| 174 | 185 | </summary> | |
| 175 | 186 | <div class="confirm-popup"> | |
| 176 | 187 | Delete branch{" "} | |
| 177 | - | <strong> | |
| 188 | + | <strong safe> | |
| 178 | 189 | {b.name} | |
| 179 | 190 | </strong> | |
| 180 | 191 | ? | |
Msrc/views/repos/BranchSelector.tsx
| @@ -30,11 +30,11 @@ export function BranchSelector({ | |||
|---|---|---|---|
| 30 | 30 | class="branch-selector" | |
| 31 | 31 | > | |
| 32 | 32 | <input type="hidden" name="view" value={view} /> | |
| 33 | - | {path && <input type="hidden" name="path" value={path} />} | |
| 33 | + | {!!path && <input type="hidden" name="path" value={path} />} | |
| 34 | 34 | <span class="branch-selector-icon">⎇</span> | |
| 35 | 35 | <select name="rev" class="branch-select" data-autosubmit> | |
| 36 | 36 | {isDetached && ( | |
| 37 | - | <option value={currentRef} selected> | |
| 37 | + | <option value={currentRef} selected safe> | |
| 38 | 38 | {shortRef} (detached) | |
| 39 | 39 | </option> | |
| 40 | 40 | )} | |
| @@ -43,6 +43,7 @@ export function BranchSelector({ | |||
|---|---|---|---|
| 43 | 43 | <option | |
| 44 | 44 | value={b} | |
| 45 | 45 | selected={b === currentRef ? true : undefined} | |
| 46 | + | safe | |
| 46 | 47 | > | |
| 47 | 48 | {b} | |
| 48 | 49 | </option> | |
| @@ -54,6 +55,7 @@ export function BranchSelector({ | |||
|---|---|---|---|
| 54 | 55 | <option | |
| 55 | 56 | value={t} | |
| 56 | 57 | selected={t === currentRef ? true : undefined} | |
| 58 | + | safe | |
| 57 | 59 | > | |
| 58 | 60 | {t} | |
| 59 | 61 | </option> | |
Msrc/views/repos/CommitDetail.tsx
| @@ -44,6 +44,7 @@ export function CommitDetail({ | |||
|---|---|---|---|
| 44 | 44 | href={`/${repo.name}/tree/${sha}`} | |
| 45 | 45 | class="btn btn-secondary btn-sm" | |
| 46 | 46 | title={`Browse tree at ${sha.slice(0, 7)}`} | |
| 47 | + | safe | |
| 47 | 48 | > | |
| 48 | 49 | @ {sha.slice(0, 7)} | |
| 49 | 50 | </a> | |
| @@ -53,11 +54,14 @@ export function CommitDetail({ | |||
|---|---|---|---|
| 53 | 54 | <div class="commit-card"> | |
| 54 | 55 | <h2 | |
| 55 | 56 | class={`commit-card-subject${meta.subject ? "" : " commit-card-subject-empty"}`} | |
| 57 | + | safe | |
| 56 | 58 | > | |
| 57 | 59 | {meta.subject || "No commit message"} | |
| 58 | 60 | </h2> | |
| 59 | - | {meta.body && ( | |
| 60 | - | <pre class="commit-card-body">{meta.body}</pre> | |
| 61 | + | {!!meta.body && ( | |
| 62 | + | <pre class="commit-card-body" safe> | |
| 63 | + | {meta.body} | |
| 64 | + | </pre> | |
| 61 | 65 | )} | |
| 62 | 66 | <div class="commit-card-meta"> | |
| 63 | 67 | <div class="commit-card-meta-row"> | |
| @@ -71,6 +75,7 @@ export function CommitDetail({ | |||
|---|---|---|---|
| 71 | 75 | <time | |
| 72 | 76 | class="commit-meta-value" | |
| 73 | 77 | datetime={meta.date} | |
| 78 | + | safe | |
| 74 | 79 | > | |
| 75 | 80 | {formatDateTime(meta.date)} | |
| 76 | 81 | </time> | |
| @@ -95,6 +100,7 @@ export function CommitDetail({ | |||
|---|---|---|---|
| 95 | 100 | <time | |
| 96 | 101 | class="commit-meta-value" | |
| 97 | 102 | datetime={meta.committerDate} | |
| 103 | + | safe | |
| 98 | 104 | > | |
| 99 | 105 | {formatDateTime(meta.committerDate)} | |
| 100 | 106 | </time> | |
| @@ -103,7 +109,7 @@ export function CommitDetail({ | |||
|---|---|---|---|
| 103 | 109 | )} | |
| 104 | 110 | <div class="commit-card-meta-row"> | |
| 105 | 111 | <span class="commit-meta-label">Commit</span> | |
| 106 | - | <code class="commit-meta-value commit-sha-full mono"> | |
| 112 | + | <code class="commit-meta-value commit-sha-full mono" safe> | |
| 107 | 113 | {meta.hash} | |
| 108 | 114 | </code> | |
| 109 | 115 | </div> | |
| @@ -117,6 +123,7 @@ export function CommitDetail({ | |||
|---|---|---|---|
| 117 | 123 | <a | |
| 118 | 124 | href={`/${repo.name}/commit/${p}`} | |
| 119 | 125 | class="commit-hash mono" | |
| 126 | + | safe | |
| 120 | 127 | > | |
| 121 | 128 | {p.slice(0, 7)} | |
| 122 | 129 | </a> | |
| @@ -145,7 +152,7 @@ export function CommitDetail({ | |||
|---|---|---|---|
| 145 | 152 | ||
| 146 | 153 | {tooLarge !== undefined ? ( | |
| 147 | 154 | <div class="file-download-notice"> | |
| 148 | - | <p> | |
| 155 | + | <p safe> | |
| 149 | 156 | Diff is too large to render inline ( | |
| 150 | 157 | {(tooLarge / 1024 / 1024).toFixed(1)} MB). Browse | |
| 151 | 158 | individual files at the tree below. | |
Msrc/views/repos/CommitLog.tsx
| @@ -66,11 +66,12 @@ export function CommitLog({ | |||
|---|---|---|---|
| 66 | 66 | <a | |
| 67 | 67 | href={`/${repo.name}/commit/${c.hash}`} | |
| 68 | 68 | class="commit-subject" | |
| 69 | + | safe | |
| 69 | 70 | > | |
| 70 | 71 | {c.subject} | |
| 71 | 72 | </a> | |
| 72 | 73 | <div class="commit-meta"> | |
| 73 | - | <span class="commit-author"> | |
| 74 | + | <span class="commit-author" safe> | |
| 74 | 75 | {c.author} | |
| 75 | 76 | </span> | |
| 76 | 77 | <span class="commit-meta-right"> | |
| @@ -87,12 +88,14 @@ export function CommitLog({ | |||
|---|---|---|---|
| 87 | 88 | <a | |
| 88 | 89 | href={`/${repo.name}/commit/${c.hash}`} | |
| 89 | 90 | class="commit-hash mono" | |
| 91 | + | safe | |
| 90 | 92 | > | |
| 91 | 93 | {c.hash.slice(0, 7)} | |
| 92 | 94 | </a> | |
| 93 | 95 | <time | |
| 94 | 96 | class="commit-date" | |
| 95 | 97 | datetime={c.date} | |
| 98 | + | safe | |
| 96 | 99 | > | |
| 97 | 100 | {formatDateTime(c.date)} | |
| 98 | 101 | </time> | |
| @@ -103,20 +106,20 @@ export function CommitLog({ | |||
|---|---|---|---|
| 103 | 106 | </ul> | |
| 104 | 107 | </> | |
| 105 | 108 | )} | |
| 106 | - | {(newerUrl || olderUrl) && ( | |
| 109 | + | {(!!newerUrl || !!olderUrl) && ( | |
| 107 | 110 | <nav | |
| 108 | 111 | class="commit-cursor-nav" | |
| 109 | 112 | aria-label="Commit history navigation" | |
| 110 | 113 | > | |
| 111 | 114 | <div class="commit-cursor-prev"> | |
| 112 | - | {newerUrl && ( | |
| 115 | + | {!!newerUrl && ( | |
| 113 | 116 | <a href={newerUrl} class="pagination-btn"> | |
| 114 | 117 | ← Newer | |
| 115 | 118 | </a> | |
| 116 | 119 | )} | |
| 117 | 120 | </div> | |
| 118 | 121 | <div class="commit-cursor-next"> | |
| 119 | - | {olderUrl && ( | |
| 122 | + | {!!olderUrl && ( | |
| 120 | 123 | <a href={olderUrl} class="pagination-btn"> | |
| 121 | 124 | Older → | |
| 122 | 125 | </a> | |
Msrc/views/repos/FileBlob.tsx
| @@ -35,7 +35,9 @@ export function FileBlob({ | |||
|---|---|---|---|
| 35 | 35 | <RepoHeader repo={repo} /> | |
| 36 | 36 | <RepoNav repo={repo} active="code" user={user} /> | |
| 37 | 37 | <div class="breadcrumb"> | |
| 38 | - | <a href={`/${repo.name}/tree/${blobRef}`}>{repo.name}</a> | |
| 38 | + | <a href={`/${repo.name}/tree/${blobRef}`} safe> | |
| 39 | + | {repo.name} | |
| 40 | + | </a> | |
| 39 | 41 | {parts.map((part, i) => { | |
| 40 | 42 | const partPath = parts.slice(0, i + 1).join("/"); | |
| 41 | 43 | const isLast = i === parts.length - 1; | |
| @@ -43,12 +45,13 @@ export function FileBlob({ | |||
|---|---|---|---|
| 43 | 45 | <> | |
| 44 | 46 | <span class="breadcrumb-sep">/</span> | |
| 45 | 47 | {isLast ? ( | |
| 46 | - | <span class="breadcrumb-current"> | |
| 48 | + | <span class="breadcrumb-current" safe> | |
| 47 | 49 | {part} | |
| 48 | 50 | </span> | |
| 49 | 51 | ) : ( | |
| 50 | 52 | <a | |
| 51 | 53 | href={`/${repo.name}/tree/${blobRef}/${partPath}`} | |
| 54 | + | safe | |
| 52 | 55 | > | |
| 53 | 56 | {part} | |
| 54 | 57 | </a> | |
| @@ -58,7 +61,9 @@ export function FileBlob({ | |||
|---|---|---|---|
| 58 | 61 | })} | |
| 59 | 62 | </div> | |
| 60 | 63 | <div class="file-blob-header"> | |
| 61 | - | <span class="file-blob-name">{filename}</span> | |
| 64 | + | <span class="file-blob-name" safe> | |
| 65 | + | {filename} | |
| 66 | + | </span> | |
| 62 | 67 | <div class="file-blob-actions file-blob-actions-wrap"> | |
| 63 | 68 | <BranchSelector | |
| 64 | 69 | repoName={repo.name} | |
| @@ -90,7 +95,7 @@ export function FileBlob({ | |||
|---|---|---|---|
| 90 | 95 | Delete | |
| 91 | 96 | </summary> | |
| 92 | 97 | <div class="confirm-popup"> | |
| 93 | - | Delete <strong>{filename}</strong>? | |
| 98 | + | Delete <strong safe>{filename}</strong>? | |
| 94 | 99 | <form | |
| 95 | 100 | method="POST" | |
| 96 | 101 | action={`/${repo.name}/delete-file/${blobRef}/${filePath}`} | |
| @@ -119,9 +124,9 @@ export function FileBlob({ | |||
|---|---|---|---|
| 119 | 124 | </div> | |
| 120 | 125 | <div class="file-blob-body"> | |
| 121 | 126 | {markdownHtml ? ( | |
| 122 | - | <div class="markdown-body">{markdownHtml}</div> | |
| 127 | + | <div class="markdown-body">{markdownHtml as "safe"}</div> | |
| 123 | 128 | ) : view.type === "inline" ? ( | |
| 124 | - | <div class="shiki-wrapper">{view.html}</div> | |
| 129 | + | <div class="shiki-wrapper">{view.html as "safe"}</div> | |
| 125 | 130 | ) : view.type === "media" ? ( | |
| 126 | 131 | <div class="file-media"> | |
| 127 | 132 | {view.mimeType.startsWith("image/") ? ( | |
| @@ -148,7 +153,7 @@ export function FileBlob({ | |||
|---|---|---|---|
| 148 | 153 | </div> | |
| 149 | 154 | ) : view.type === "binary" ? ( | |
| 150 | 155 | <div class="file-download-notice"> | |
| 151 | - | <p>Binary file ({formatSize(view.size)})</p> | |
| 156 | + | <p safe>Binary file ({formatSize(view.size)})</p> | |
| 152 | 157 | <a | |
| 153 | 158 | href={`/${repo.name}/raw/${blobRef}/${filePath}`} | |
| 154 | 159 | class="btn btn-primary" | |
| @@ -158,7 +163,7 @@ export function FileBlob({ | |||
|---|---|---|---|
| 158 | 163 | </div> | |
| 159 | 164 | ) : ( | |
| 160 | 165 | <div class="file-download-notice"> | |
| 161 | - | <p> | |
| 166 | + | <p safe> | |
| 162 | 167 | File too large to display inline ( | |
| 163 | 168 | {formatSize(view.size)}) | |
| 164 | 169 | </p> | |
Msrc/views/repos/FileEdit.tsx
| @@ -32,7 +32,9 @@ export function FileEdit({ | |||
|---|---|---|---|
| 32 | 32 | <RepoHeader repo={repo} /> | |
| 33 | 33 | <RepoNav repo={repo} active="code" user={user} /> | |
| 34 | 34 | <div class="breadcrumb"> | |
| 35 | - | <a href={`/${repo.name}/tree/${editRef}`}>{repo.name}</a> | |
| 35 | + | <a href={`/${repo.name}/tree/${editRef}`} safe> | |
| 36 | + | {repo.name} | |
| 37 | + | </a> | |
| 36 | 38 | {parts.map((part, i) => { | |
| 37 | 39 | const partPath = parts.slice(0, i + 1).join("/"); | |
| 38 | 40 | const isLast = i === parts.length - 1; | |
| @@ -40,12 +42,13 @@ export function FileEdit({ | |||
|---|---|---|---|
| 40 | 42 | <> | |
| 41 | 43 | <span class="breadcrumb-sep">/</span> | |
| 42 | 44 | {isLast ? ( | |
| 43 | - | <span class="breadcrumb-current"> | |
| 45 | + | <span class="breadcrumb-current" safe> | |
| 44 | 46 | {part} | |
| 45 | 47 | </span> | |
| 46 | 48 | ) : ( | |
| 47 | 49 | <a | |
| 48 | 50 | href={`/${repo.name}/tree/${editRef}/${partPath}`} | |
| 51 | + | safe | |
| 49 | 52 | > | |
| 50 | 53 | {part} | |
| 51 | 54 | </a> | |
| @@ -57,15 +60,19 @@ export function FileEdit({ | |||
|---|---|---|---|
| 57 | 60 | <p class="form-hint"> | |
| 58 | 61 | WARNING: Line endings are normalized to LF (\n) on save. | |
| 59 | 62 | </p> | |
| 60 | - | {(error || queryError) && ( | |
| 61 | - | <p class="form-error">{error ?? queryError}</p> | |
| 63 | + | {(!!error || !!queryError) && ( | |
| 64 | + | <p class="form-error" safe> | |
| 65 | + | {error ?? queryError} | |
| 66 | + | </p> | |
| 62 | 67 | )} | |
| 63 | 68 | <form | |
| 64 | 69 | method="POST" | |
| 65 | 70 | action={`/${repo.name}/edit/${editRef}/${filePath}`} | |
| 66 | 71 | > | |
| 67 | 72 | <div class="file-blob-header"> | |
| 68 | - | <span class="file-blob-name">{filename}</span> | |
| 73 | + | <span class="file-blob-name" safe> | |
| 74 | + | {filename} | |
| 75 | + | </span> | |
| 69 | 76 | <div class="file-blob-actions"> | |
| 70 | 77 | <a | |
| 71 | 78 | href={`/${repo.name}/blob/${editRef}/${filePath}`} | |
| @@ -84,6 +91,7 @@ export function FileEdit({ | |||
|---|---|---|---|
| 84 | 91 | autocorrect="off" | |
| 85 | 92 | autocapitalize="off" | |
| 86 | 93 | {...{ autocomplete: "off" }} | |
| 94 | + | safe | |
| 87 | 95 | > | |
| 88 | 96 | {content} | |
| 89 | 97 | </textarea> | |
| @@ -93,7 +101,8 @@ export function FileEdit({ | |||
|---|---|---|---|
| 93 | 101 | class="form-hint" | |
| 94 | 102 | style="margin-bottom: var(--space-4);" | |
| 95 | 103 | > | |
| 96 | - | Committing directly to <strong>{editRef}</strong> | |
| 104 | + | Committing directly to{" "} | |
| 105 | + | <strong safe>{editRef}</strong> | |
| 97 | 106 | </p> | |
| 98 | 107 | <div class="form-group"> | |
| 99 | 108 | <label for="file-path">File path</label> | |
| @@ -113,6 +122,7 @@ export function FileEdit({ | |||
|---|---|---|---|
| 113 | 122 | name="message" | |
| 114 | 123 | rows="3" | |
| 115 | 124 | required | |
| 125 | + | safe | |
| 116 | 126 | > | |
| 117 | 127 | {`Edited ${filename}`} | |
| 118 | 128 | </textarea> | |
Msrc/views/repos/FileTree.tsx
| @@ -57,9 +57,9 @@ export function FileTree({ | |||
|---|---|---|---|
| 57 | 57 | </a> | |
| 58 | 58 | )} | |
| 59 | 59 | </div> | |
| 60 | - | {subpath && ( | |
| 60 | + | {!!subpath && ( | |
| 61 | 61 | <div class="breadcrumb"> | |
| 62 | - | <a href={`/${repo.name}/tree/${treeRef}`}> | |
| 62 | + | <a href={`/${repo.name}/tree/${treeRef}`} safe> | |
| 63 | 63 | {repo.name} | |
| 64 | 64 | </a> | |
| 65 | 65 | {parts.map((part, i) => { | |
| @@ -69,6 +69,7 @@ export function FileTree({ | |||
|---|---|---|---|
| 69 | 69 | <span class="breadcrumb-sep">/</span> | |
| 70 | 70 | <a | |
| 71 | 71 | href={`/${repo.name}/tree/${treeRef}/${partPath}`} | |
| 72 | + | safe | |
| 72 | 73 | > | |
| 73 | 74 | {part} | |
| 74 | 75 | </a> | |
| @@ -83,11 +84,11 @@ export function FileTree({ | |||
|---|---|---|---|
| 83 | 84 | subpath={subpath} | |
| 84 | 85 | entries={entries} | |
| 85 | 86 | /> | |
| 86 | - | {readmeHtml && ( | |
| 87 | + | {!!readmeHtml && ( | |
| 87 | 88 | <div class="readme-section"> | |
| 88 | 89 | <div class="readme-header"> | |
| 89 | 90 | <span>README</span> | |
| 90 | - | {readmePath && ( | |
| 91 | + | {!!readmePath && ( | |
| 91 | 92 | <a | |
| 92 | 93 | href={`/${repo.name}/raw/${treeRef}/${readmePath}`} | |
| 93 | 94 | class="btn btn-sm" | |
| @@ -96,7 +97,7 @@ export function FileTree({ | |||
|---|---|---|---|
| 96 | 97 | </a> | |
| 97 | 98 | )} | |
| 98 | 99 | </div> | |
| 99 | - | <div class="markdown-body">{readmeHtml}</div> | |
| 100 | + | <div class="markdown-body">{readmeHtml as "safe"}</div> | |
| 100 | 101 | </div> | |
| 101 | 102 | )} | |
| 102 | 103 | </div> | |
Msrc/views/repos/FileTreeTable.tsx
| @@ -27,7 +27,7 @@ export function FileTreeTable({ | |||
|---|---|---|---|
| 27 | 27 | return ( | |
| 28 | 28 | <table class="file-tree"> | |
| 29 | 29 | <tbody> | |
| 30 | - | {subpath && ( | |
| 30 | + | {!!subpath && ( | |
| 31 | 31 | <tr class="file-tree-row file-tree-row-up"> | |
| 32 | 32 | <td class="file-icon file-icon-dir">{DirIcon()}</td> | |
| 33 | 33 | <td class="file-name" colspan="2"> | |
| @@ -58,6 +58,7 @@ export function FileTreeTable({ | |||
|---|---|---|---|
| 58 | 58 | ? "file-name-dir" | |
| 59 | 59 | : undefined | |
| 60 | 60 | } | |
| 61 | + | safe | |
| 61 | 62 | > | |
| 62 | 63 | {entry.name} | |
| 63 | 64 | </a> | |
Msrc/views/repos/NewFileForm.tsx
| @@ -29,7 +29,11 @@ export function NewFileForm({ | |||
|---|---|---|---|
| 29 | 29 | <div class="container"> | |
| 30 | 30 | <RepoHeader repo={repo} /> | |
| 31 | 31 | <RepoNav repo={repo} active="code" user={user} /> | |
| 32 | - | {error && <p class="form-error">{error}</p>} | |
| 32 | + | {!!error && ( | |
| 33 | + | <p class="form-error" safe> | |
| 34 | + | {error} | |
| 35 | + | </p> | |
| 36 | + | )} | |
| 33 | 37 | <form | |
| 34 | 38 | method="POST" | |
| 35 | 39 | action={`/${repo.name}/new-file/${treeRef}`} | |
| @@ -59,7 +63,7 @@ export function NewFileForm({ | |||
|---|---|---|---|
| 59 | 63 | class="form-hint" | |
| 60 | 64 | style="margin-bottom: var(--space-4);" | |
| 61 | 65 | > | |
| 62 | - | Creating file on <strong>{treeRef}</strong> | |
| 66 | + | Creating file on <strong safe>{treeRef}</strong> | |
| 63 | 67 | </p> | |
| 64 | 68 | <div class="form-group"> | |
| 65 | 69 | <label for="file-path">File path</label> | |
Msrc/views/repos/NewRepo.tsx
| @@ -11,7 +11,11 @@ export function NewRepo({ user, error }: NewRepoProps) { | |||
|---|---|---|---|
| 11 | 11 | <Layout user={user} title="New repository"> | |
| 12 | 12 | <div class="container container-narrow"> | |
| 13 | 13 | <h1 class="page-title">Create new repository</h1> | |
| 14 | - | {error && <p class="form-error">{error}</p>} | |
| 14 | + | {!!error && ( | |
| 15 | + | <p class="form-error" safe> | |
| 16 | + | {error} | |
| 17 | + | </p> | |
| 18 | + | )} | |
| 15 | 19 | <form method="POST" action="/new" class="form-card"> | |
| 16 | 20 | <div class="form-group"> | |
| 17 | 21 | <label for="name">Repository name</label> | |
Msrc/views/repos/RepoHeader.tsx
| @@ -9,7 +9,9 @@ export function RepoHeader({ repo }: RepoHeaderProps) { | |||
|---|---|---|---|
| 9 | 9 | <div class="repo-header"> | |
| 10 | 10 | <div class="repo-title-row"> | |
| 11 | 11 | <h1 class="page-title"> | |
| 12 | - | <a href={`/${repo.name}`}>{repo.name}</a> | |
| 12 | + | <a href={`/${repo.name}`} safe> | |
| 13 | + | {repo.name} | |
| 14 | + | </a> | |
| 13 | 15 | </h1> | |
| 14 | 16 | {repo.is_private ? ( | |
| 15 | 17 | <span class="badge badge-private">Private</span> | |
Msrc/views/repos/RepoHome.tsx
| @@ -33,8 +33,10 @@ export function RepoHome({ | |||
|---|---|---|---|
| 33 | 33 | <Layout user={user} title={repo.name}> | |
| 34 | 34 | <div class="container"> | |
| 35 | 35 | <RepoHeader repo={repo} /> | |
| 36 | - | {repo.description && ( | |
| 37 | - | <p class="repo-description">{repo.description}</p> | |
| 36 | + | {!!repo.description && ( | |
| 37 | + | <p class="repo-description" safe> | |
| 38 | + | {repo.description} | |
| 39 | + | </p> | |
| 38 | 40 | )} | |
| 39 | 41 | <RepoNav repo={repo} active="code" user={user} /> | |
| 40 | 42 | {!hasContent ? ( | |
| @@ -42,7 +44,7 @@ export function RepoHome({ | |||
|---|---|---|---|
| 42 | 44 | <h2>This repository is empty.</h2> | |
| 43 | 45 | <p>Push your first commit to get started:</p> | |
| 44 | 46 | <pre class="code-setup"> | |
| 45 | - | <code>{`git clone ${sshUrl(repo.name)} | |
| 47 | + | <code safe>{`git clone ${sshUrl(repo.name)} | |
| 46 | 48 | cd ${repo.name} | |
| 47 | 49 | echo "# ${repo.name}" > README.md | |
| 48 | 50 | git add . | |
| @@ -109,11 +111,11 @@ git push origin main`}</code> | |||
|---|---|---|---|
| 109 | 111 | subpath="" | |
| 110 | 112 | entries={entries} | |
| 111 | 113 | /> | |
| 112 | - | {readmeHtml && ( | |
| 114 | + | {!!readmeHtml && ( | |
| 113 | 115 | <div class="readme-section"> | |
| 114 | 116 | <div class="readme-header"> | |
| 115 | 117 | <span>README</span> | |
| 116 | - | {readmePath && ( | |
| 118 | + | {!!readmePath && ( | |
| 117 | 119 | <a | |
| 118 | 120 | href={`/${repo.name}/raw/${repo.default_branch}/${readmePath}`} | |
| 119 | 121 | class="btn btn-sm" | |
| @@ -122,7 +124,7 @@ git push origin main`}</code> | |||
|---|---|---|---|
| 122 | 124 | </a> | |
| 123 | 125 | )} | |
| 124 | 126 | </div> | |
| 125 | - | <div class="markdown-body">{readmeHtml}</div> | |
| 127 | + | <div class="markdown-body">{readmeHtml as "safe"}</div> | |
| 126 | 128 | </div> | |
| 127 | 129 | )} | |
| 128 | 130 | </> | |
Msrc/views/repos/RepoList.tsx
| @@ -76,7 +76,8 @@ export function RepoList({ | |||
|---|---|---|---|
| 76 | 76 | <div class="empty-state"> | |
| 77 | 77 | {search ? ( | |
| 78 | 78 | <p> | |
| 79 | - | No repositories match <strong>{search}</strong>. | |
| 79 | + | No repositories match{" "} | |
| 80 | + | <strong safe>{search}</strong>. | |
| 80 | 81 | </p> | |
| 81 | 82 | ) : ( | |
| 82 | 83 | <p>No repositories yet.</p> | |
| @@ -91,6 +92,7 @@ export function RepoList({ | |||
|---|---|---|---|
| 91 | 92 | <a | |
| 92 | 93 | href={`/${repo.name}`} | |
| 93 | 94 | class="repo-name" | |
| 95 | + | safe | |
| 94 | 96 | > | |
| 95 | 97 | {repo.name} | |
| 96 | 98 | </a> | |
| @@ -105,8 +107,8 @@ export function RepoList({ | |||
|---|---|---|---|
| 105 | 107 | </span> | |
| 106 | 108 | ) : null} | |
| 107 | 109 | </div> | |
| 108 | - | {repo.description && ( | |
| 109 | - | <p class="repo-description"> | |
| 110 | + | {!!repo.description && ( | |
| 111 | + | <p class="repo-description" safe> | |
| 110 | 112 | {repo.description} | |
| 111 | 113 | </p> | |
| 112 | 114 | )} | |
| @@ -115,6 +117,7 @@ export function RepoList({ | |||
|---|---|---|---|
| 115 | 117 | <time | |
| 116 | 118 | class="repo-date" | |
| 117 | 119 | datetime={repo.created_at} | |
| 120 | + | safe | |
| 118 | 121 | > | |
| 119 | 122 | {formatDate(repo.created_at)} | |
| 120 | 123 | </time> | |
Msrc/views/repos/RepoSettings.tsx
| @@ -36,8 +36,16 @@ export function RepoSettings({ | |||
|---|---|---|---|
| 36 | 36 | <div class="container"> | |
| 37 | 37 | <RepoHeader repo={repo} /> | |
| 38 | 38 | <RepoNav repo={repo} active="settings" user={user} /> | |
| 39 | - | {success && <p class="form-success">{success}</p>} | |
| 40 | - | {error && <p class="form-error">{error}</p>} | |
| 39 | + | {!!success && ( | |
| 40 | + | <p class="form-success" safe> | |
| 41 | + | {success} | |
| 42 | + | </p> | |
| 43 | + | )} | |
| 44 | + | {!!error && ( | |
| 45 | + | <p class="form-error" safe> | |
| 46 | + | {error} | |
| 47 | + | </p> | |
| 48 | + | )} | |
| 41 | 49 | <form | |
| 42 | 50 | method="POST" | |
| 43 | 51 | action={`/${repo.name}/settings`} | |
| @@ -69,6 +77,7 @@ export function RepoSettings({ | |||
|---|---|---|---|
| 69 | 77 | ? true | |
| 70 | 78 | : undefined | |
| 71 | 79 | } | |
| 80 | + | safe | |
| 72 | 81 | > | |
| 73 | 82 | {b} | |
| 74 | 83 | </option> | |
| @@ -128,6 +137,7 @@ export function RepoSettings({ | |||
|---|---|---|---|
| 128 | 137 | name="issue_template" | |
| 129 | 138 | rows="8" | |
| 130 | 139 | placeholder="## Description ## Steps to reproduce ## Expected behavior" | |
| 140 | + | safe | |
| 131 | 141 | > | |
| 132 | 142 | {repo.issue_template ?? ""} | |
| 133 | 143 | </textarea> | |
| @@ -145,6 +155,7 @@ export function RepoSettings({ | |||
|---|---|---|---|
| 145 | 155 | name="patch_template" | |
| 146 | 156 | rows="8" | |
| 147 | 157 | placeholder="## Summary ## Testing" | |
| 158 | + | safe | |
| 148 | 159 | > | |
| 149 | 160 | {repo.patch_template ?? ""} | |
| 150 | 161 | </textarea> | |
| @@ -163,7 +174,7 @@ export function RepoSettings({ | |||
|---|---|---|---|
| 163 | 174 | class="label-settings-swatch" | |
| 164 | 175 | style={`background:${label.color}`} | |
| 165 | 176 | /> | |
| 166 | - | <span class="label-settings-name"> | |
| 177 | + | <span class="label-settings-name" safe> | |
| 167 | 178 | {label.name} | |
| 168 | 179 | </span> | |
| 169 | 180 | <form | |
| @@ -230,10 +241,10 @@ export function RepoSettings({ | |||
|---|---|---|---|
| 230 | 241 | {secrets.map((secret) => ( | |
| 231 | 242 | <div class="label-settings-item"> | |
| 232 | 243 | <span class="label-settings-name"> | |
| 233 | - | <code>{secret.name}</code> | |
| 244 | + | <code safe>{secret.name}</code> | |
| 234 | 245 | </span> | |
| 235 | - | {secret.description && ( | |
| 236 | - | <span class="text-muted"> | |
| 246 | + | {!!secret.description && ( | |
| 247 | + | <span class="text-muted" safe> | |
| 237 | 248 | {secret.description} | |
| 238 | 249 | </span> | |
| 239 | 250 | )} | |
| @@ -293,6 +304,61 @@ export function RepoSettings({ | |||
|---|---|---|---|
| 293 | 304 | <div class="danger-zone"> | |
| 294 | 305 | <h2 class="section-title danger-title">Danger zone</h2> | |
| 295 | 306 | <div class="form-card danger-card"> | |
| 307 | + | <div class="danger-item"> | |
| 308 | + | <div> | |
| 309 | + | <strong>Rename this repository</strong> | |
| 310 | + | <p class="text-muted"> | |
| 311 | + | Changing the name breaks existing clone | |
| 312 | + | URLs and links to this repo. Collaborators | |
| 313 | + | will need to update their remotes. | |
| 314 | + | </p> | |
| 315 | + | </div> | |
| 316 | + | <details class="confirm-details"> | |
| 317 | + | <summary class="btn btn-danger"> | |
| 318 | + | Rename repository | |
| 319 | + | </summary> | |
| 320 | + | <div class="confirm-popup"> | |
| 321 | + | <form | |
| 322 | + | method="POST" | |
| 323 | + | action={`/${repo.name}/settings/rename`} | |
| 324 | + | class="inline-form" | |
| 325 | + | > | |
| 326 | + | <div class="form-group"> | |
| 327 | + | <label for="new_name"> | |
| 328 | + | New name | |
| 329 | + | </label> | |
| 330 | + | <input | |
| 331 | + | id="new_name" | |
| 332 | + | name="new_name" | |
| 333 | + | type="text" | |
| 334 | + | required | |
| 335 | + | pattern="[A-Za-z0-9._\-]+" | |
| 336 | + | autocomplete="off" | |
| 337 | + | /> | |
| 338 | + | </div> | |
| 339 | + | <div class="form-group"> | |
| 340 | + | <label for="confirm_name"> | |
| 341 | + | Type <code safe>{repo.name}</code>{" "} | |
| 342 | + | to confirm | |
| 343 | + | </label> | |
| 344 | + | <input | |
| 345 | + | id="confirm_name" | |
| 346 | + | name="confirm_name" | |
| 347 | + | type="text" | |
| 348 | + | required | |
| 349 | + | autocomplete="off" | |
| 350 | + | /> | |
| 351 | + | </div> | |
| 352 | + | <button | |
| 353 | + | type="submit" | |
| 354 | + | class="btn btn-danger" | |
| 355 | + | > | |
| 356 | + | Rename repository | |
| 357 | + | </button> | |
| 358 | + | </form> | |
| 359 | + | </div> | |
| 360 | + | </details> | |
| 361 | + | </div> | |
| 296 | 362 | <div class="danger-item"> | |
| 297 | 363 | <div> | |
| 298 | 364 | <strong>Delete this repository</strong> | |
| @@ -305,7 +371,8 @@ export function RepoSettings({ | |||
|---|---|---|---|
| 305 | 371 | Delete repository | |
| 306 | 372 | </summary> | |
| 307 | 373 | <div class="confirm-popup"> | |
| 308 | - | Delete {repo.name}? This cannot be undone. | |
| 374 | + | Delete <span safe>{repo.name}</span>? This | |
| 375 | + | cannot be undone. | |
| 309 | 376 | <form | |
| 310 | 377 | method="POST" | |
| 311 | 378 | action={`/${repo.name}/settings/delete`} | |
Msrc/views/repos/TagList.tsx
| @@ -37,8 +37,16 @@ export function TagList({ | |||
|---|---|---|---|
| 37 | 37 | <div class="container"> | |
| 38 | 38 | <RepoHeader repo={repo} /> | |
| 39 | 39 | <RepoNav repo={repo} active="tags" user={user} /> | |
| 40 | - | {success && <p class="form-success">{success}</p>} | |
| 41 | - | {error && <p class="form-error">{error}</p>} | |
| 40 | + | {!!success && ( | |
| 41 | + | <p class="form-success" safe> | |
| 42 | + | {success} | |
| 43 | + | </p> | |
| 44 | + | )} | |
| 45 | + | {!!error && ( | |
| 46 | + | <p class="form-error" safe> | |
| 47 | + | {error} | |
| 48 | + | </p> | |
| 49 | + | )} | |
| 42 | 50 | <div class="list-header"> | |
| 43 | 51 | <h2 class="list-heading">Tags</h2> | |
| 44 | 52 | {user?.isAdmin && ( | |
| @@ -117,6 +125,7 @@ export function TagList({ | |||
|---|---|---|---|
| 117 | 125 | <a | |
| 118 | 126 | href={`/${repo.name}/tree/${tag.name}`} | |
| 119 | 127 | class="ref-name" | |
| 128 | + | safe | |
| 120 | 129 | > | |
| 121 | 130 | {tag.name} | |
| 122 | 131 | </a> | |
| @@ -130,28 +139,30 @@ export function TagList({ | |||
|---|---|---|---|
| 130 | 139 | )} | |
| 131 | 140 | </div> | |
| 132 | 141 | <div class="ref-meta-row"> | |
| 133 | - | {tag.taggerName && ( | |
| 134 | - | <span class="ref-author"> | |
| 142 | + | {!!tag.taggerName && ( | |
| 143 | + | <span class="ref-author" safe> | |
| 135 | 144 | {tag.taggerName} | |
| 136 | 145 | </span> | |
| 137 | 146 | )} | |
| 138 | - | {tag.shortHash && ( | |
| 147 | + | {!!tag.shortHash && ( | |
| 139 | 148 | <a | |
| 140 | 149 | href={`/${repo.name}/commit/${tag.shortHash}`} | |
| 141 | 150 | class="ref-hash mono" | |
| 151 | + | safe | |
| 142 | 152 | > | |
| 143 | 153 | {tag.shortHash} | |
| 144 | 154 | </a> | |
| 145 | 155 | )} | |
| 146 | - | {tag.subject && ( | |
| 147 | - | <span class="ref-subject"> | |
| 156 | + | {!!tag.subject && ( | |
| 157 | + | <span class="ref-subject" safe> | |
| 148 | 158 | {tag.subject} | |
| 149 | 159 | </span> | |
| 150 | 160 | )} | |
| 151 | - | {tag.date && ( | |
| 161 | + | {!!tag.date && ( | |
| 152 | 162 | <time | |
| 153 | 163 | class="ref-date" | |
| 154 | 164 | datetime={tag.date} | |
| 165 | + | safe | |
| 155 | 166 | > | |
| 156 | 167 | {formatDateTime(tag.date)} | |
| 157 | 168 | </time> | |
| @@ -166,7 +177,7 @@ export function TagList({ | |||
|---|---|---|---|
| 166 | 177 | {releaseId ? ( | |
| 167 | 178 | <p class="confirm-warning"> | |
| 168 | 179 | Tag{" "} | |
| 169 | - | <strong> | |
| 180 | + | <strong safe> | |
| 170 | 181 | {tag.name} | |
| 171 | 182 | </strong>{" "} | |
| 172 | 183 | is linked to{" "} | |
| @@ -184,7 +195,7 @@ export function TagList({ | |||
|---|---|---|---|
| 184 | 195 | ) : ( | |
| 185 | 196 | <> | |
| 186 | 197 | Delete tag{" "} | |
| 187 | - | <strong> | |
| 198 | + | <strong safe> | |
| 188 | 199 | {tag.name} | |
| 189 | 200 | </strong> | |
| 190 | 201 | ? | |
Mtests/e2e.settings.test.ts
| @@ -338,6 +338,101 @@ describe('repository deletion', () => { | |||
|---|---|---|---|
| 338 | 338 | }); | |
| 339 | 339 | }); | |
| 340 | 340 | ||
| 341 | + | // ─── Repository rename ─────────────────────────────────────────────────────── | |
| 342 | + | ||
| 343 | + | describe('repository rename', () => { | |
| 344 | + | let adminCtx: BrowserContext; | |
| 345 | + | ||
| 346 | + | beforeAll(async () => { | |
| 347 | + | adminCtx = await loggedInContext(); | |
| 348 | + | ||
| 349 | + | const page = await adminCtx.newPage(); | |
| 350 | + | try { | |
| 351 | + | await page.goto(`${BASE}/new`); | |
| 352 | + | await page.fill('[name=name]', 'renameme-repo'); | |
| 353 | + | await page.click('form[action="/new"] button[type=submit]'); | |
| 354 | + | await page.waitForURL(`${BASE}/renameme-repo`); | |
| 355 | + | ||
| 356 | + | await page.goto(`${BASE}/new`); | |
| 357 | + | await page.fill('[name=name]', 'rename-other'); | |
| 358 | + | await page.click('form[action="/new"] button[type=submit]'); | |
| 359 | + | await page.waitForURL(`${BASE}/rename-other`); | |
| 360 | + | } finally { await page.close(); } | |
| 361 | + | }); | |
| 362 | + | ||
| 363 | + | afterAll(async () => { await adminCtx.close(); }); | |
| 364 | + | ||
| 365 | + | test('rejects wrong confirmation', async () => { | |
| 366 | + | const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 367 | + | form: { new_name: 'whatever', confirm_name: 'wrong' }, | |
| 368 | + | maxRedirects: 0, | |
| 369 | + | }); | |
| 370 | + | expect(resp.status()).toBe(302); | |
| 371 | + | const loc = resp.headers()['location']!; | |
| 372 | + | expect(loc).toContain('/renameme-repo/settings?error='); | |
| 373 | + | expect(decodeURIComponent(loc)).toContain('Confirmation'); | |
| 374 | + | ||
| 375 | + | const check = await adminCtx.request.get(`${BASE}/renameme-repo`); | |
| 376 | + | expect(check.status()).toBe(200); | |
| 377 | + | }); | |
| 378 | + | ||
| 379 | + | test('rejects invalid name', async () => { | |
| 380 | + | const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 381 | + | form: { new_name: 'bad name', confirm_name: 'renameme-repo' }, | |
| 382 | + | maxRedirects: 0, | |
| 383 | + | }); | |
| 384 | + | expect(resp.status()).toBe(302); | |
| 385 | + | const loc = resp.headers()['location']!; | |
| 386 | + | expect(loc).toContain('/renameme-repo/settings?error='); | |
| 387 | + | expect(decodeURIComponent(loc)).toContain('Invalid'); | |
| 388 | + | }); | |
| 389 | + | ||
| 390 | + | test('rejects no-op rename', async () => { | |
| 391 | + | const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 392 | + | form: { new_name: 'renameme-repo', confirm_name: 'renameme-repo' }, | |
| 393 | + | maxRedirects: 0, | |
| 394 | + | }); | |
| 395 | + | expect(resp.status()).toBe(302); | |
| 396 | + | expect(decodeURIComponent(resp.headers()['location']!)).toContain('same as the current name'); | |
| 397 | + | }); | |
| 398 | + | ||
| 399 | + | test('rejects duplicate name', async () => { | |
| 400 | + | const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 401 | + | form: { new_name: 'rename-other', confirm_name: 'renameme-repo' }, | |
| 402 | + | maxRedirects: 0, | |
| 403 | + | }); | |
| 404 | + | expect(resp.status()).toBe(302); | |
| 405 | + | expect(decodeURIComponent(resp.headers()['location']!)).toContain('already taken'); | |
| 406 | + | }); | |
| 407 | + | ||
| 408 | + | test('non-admin cannot rename', async () => { | |
| 409 | + | const aliceCtx = await loggedInContext('alice', 'password123'); | |
| 410 | + | try { | |
| 411 | + | const resp = await aliceCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 412 | + | form: { new_name: 'hijack', confirm_name: 'renameme-repo' }, | |
| 413 | + | maxRedirects: 0, | |
| 414 | + | }); | |
| 415 | + | expect(resp.status()).toBe(403); | |
| 416 | + | } finally { await aliceCtx.close(); } | |
| 417 | + | }); | |
| 418 | + | ||
| 419 | + | test('admin can rename repository', async () => { | |
| 420 | + | const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 421 | + | form: { new_name: 'renamed-repo', confirm_name: 'renameme-repo' }, | |
| 422 | + | maxRedirects: 0, | |
| 423 | + | }); | |
| 424 | + | expect(resp.status()).toBe(302); | |
| 425 | + | const loc = resp.headers()['location']!; | |
| 426 | + | expect(loc).toContain('/renamed-repo/settings?success='); | |
| 427 | + | ||
| 428 | + | const old = await adminCtx.request.get(`${BASE}/renameme-repo`); | |
| 429 | + | expect(old.status()).toBe(404); | |
| 430 | + | ||
| 431 | + | const next = await adminCtx.request.get(`${BASE}/renamed-repo`); | |
| 432 | + | expect(next.status()).toBe(200); | |
| 433 | + | }); | |
| 434 | + | }); | |
| 435 | + | ||
| 341 | 436 | // ─── 404 handling ───────────────────────────────────────────────────────────── | |
| 342 | 437 | ||
| 343 | 438 | describe('404 handling', () => { | |