198 files
This commit is contained in:
@@ -18,6 +18,7 @@ import type { HistoryEntry, MaintenanceObjectResponse } from "../types";
|
||||
import { entrySpend } from "./parts-cost";
|
||||
import { intervalSpanDays } from "./interval";
|
||||
import { statusRank } from "../status-constants";
|
||||
import { taskIsFor } from "./assignees";
|
||||
import { addDaysIso, stampDate } from "./ha-time";
|
||||
|
||||
export const MAX_OCCURRENCES_PER_TASK = 5;
|
||||
@@ -201,7 +202,8 @@ function projectTask(input: ProjectionInput): CalendarEvent[] {
|
||||
* next_due and the overdue status count from that day,
|
||||
* not from the browser's).
|
||||
* @param windowDays Number of days to include (7 / 14 / 30).
|
||||
* @param userFilter null/empty = all; otherwise filter tasks by responsible_user_id.
|
||||
* @param userFilter null/empty = all; otherwise only the tasks meant for that
|
||||
* user (helpers/assignees: theirs, or shared with nobody on duty).
|
||||
*/
|
||||
export function buildCalendarBuckets(
|
||||
objects: MaintenanceObjectResponse[],
|
||||
@@ -220,7 +222,7 @@ export function buildCalendarBuckets(
|
||||
const tasks = obj.tasks || [];
|
||||
for (const task of tasks) {
|
||||
// User filter
|
||||
if (userFilter && task.responsible_user_id !== userFilter) continue;
|
||||
if (userFilter && !taskIsFor(task, userFilter)) continue;
|
||||
// Disabled tasks never produce calendar events
|
||||
if (task.enabled === false) continue;
|
||||
const projected = projectTask({
|
||||
@@ -368,7 +370,7 @@ export function buildPastBuckets(
|
||||
const entryId = obj.entry_id;
|
||||
const tasks = obj.tasks || [];
|
||||
for (const task of tasks) {
|
||||
if (userFilter && task.responsible_user_id !== userFilter) continue;
|
||||
if (userFilter && !taskIsFor(task, userFilter)) continue;
|
||||
const history = fullHistory[pastHistoryKey(entryId, task.id)] ?? ((task.history || []) as HistoryEntryShape[]);
|
||||
for (const h of history) {
|
||||
if (typeof h?.timestamp !== "string") continue;
|
||||
|
||||
@@ -1,11 +1,62 @@
|
||||
/** Shared URL-safety guard for rendering user-supplied links.
|
||||
*
|
||||
* A user-entered URL (object/task documentation, spare-part shopping link,
|
||||
* document weblink) must only ever be linkified when it is an absolute http(s)
|
||||
* URL — never `javascript:`/`data:`/protocol-relative — else it is a stored-XSS
|
||||
* vector. The `X && /^https?:\/\//i.test(X)` idiom was inlined at ~8 render
|
||||
* sites; one forgotten copy is a hole, so it lives here once.
|
||||
* document weblink) must never be linkified as `javascript:`/`data:`/
|
||||
* protocol-relative, else it is a stored-XSS vector. The
|
||||
* `X && /^https?:\/\//i.test(X)` idiom was inlined at ~8 render sites; one
|
||||
* forgotten copy is a hole, so it lives here once.
|
||||
*
|
||||
* Since #207 a documentation link or a document's web link may also open an
|
||||
* app (`obsidian://`, `onenote:`, `mailto:`): every scheme passes except the
|
||||
* blocked ones, the twin of `helpers/url_safety.py` (a tripwire pins the two).
|
||||
*/
|
||||
|
||||
/** Schemes a link may never use: they run code in the page, open the
|
||||
* device's own files or the browser's internals, or start Windows handlers
|
||||
* attackers have abused. */
|
||||
export const BLOCKED_LINK_SCHEMES: ReadonlySet<string> = new Set([
|
||||
"about",
|
||||
"blob",
|
||||
"data",
|
||||
"file",
|
||||
"filesystem",
|
||||
"javascript",
|
||||
"livescript",
|
||||
"ms-appinstaller",
|
||||
"ms-cxh",
|
||||
"ms-cxh-full",
|
||||
"ms-msdt",
|
||||
"ms-officecmd",
|
||||
"search",
|
||||
"search-ms",
|
||||
"view-source",
|
||||
"vbscript",
|
||||
]);
|
||||
|
||||
/** The link as a browser reads it before looking at the scheme: ASCII tabs
|
||||
* and newlines anywhere dropped, control characters and spaces at either
|
||||
* end trimmed ("java\tscript:" is javascript:). */
|
||||
export function cleanLink(url: string): string {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
return url.replace(/[\t\n\r]/g, "").replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, "");
|
||||
}
|
||||
|
||||
/** The lower-cased scheme of an absolute link, else null. Two characters at
|
||||
* least: "C:\manual.pdf" is a drive path, not a link. */
|
||||
export function linkScheme(url: string | null | undefined): string | null {
|
||||
const match = /^([a-z][a-z0-9+.-]+):/i.exec(cleanLink(url || ""));
|
||||
return match ? match[1].toLowerCase() : null;
|
||||
}
|
||||
|
||||
/** An absolute link safe to open: http(s) or an app link — never one of the
|
||||
* blocked schemes, never relative. */
|
||||
export function isSafeLink(url: string | null | undefined): url is string {
|
||||
const scheme = linkScheme(url);
|
||||
return scheme !== null && !BLOCKED_LINK_SCHEMES.has(scheme);
|
||||
}
|
||||
|
||||
/** Only an absolute http(s) URL — for links that must be web pages (a
|
||||
* spare part's shop page). */
|
||||
export function isSafeHttpUrl(url: string | null | undefined): url is string {
|
||||
return !!url && /^https?:\/\//i.test(url);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user