From 75bc352bd629fec2a966ea6f414e8eac7bf11ace Mon Sep 17 00:00:00 2001 From: Andi Pieper Date: Wed, 27 Nov 2024 17:44:29 +0100 Subject: [PATCH 1/5] first cut --- client/src/observatory/results.scss | 39 ++++ client/src/observatory/results.tsx | 10 +- client/src/observatory/results/cookies.tsx | 197 ++++++++++-------- client/src/observatory/results/csp.tsx | 145 +++++++------ .../observatory/results/human-duration.tsx | 46 ++-- client/src/observatory/utils.tsx | 2 +- 6 files changed, 273 insertions(+), 166 deletions(-) diff --git a/client/src/observatory/results.scss b/client/src/observatory/results.scss index cc1bacbfcf52..83fa03ab0ae3 100644 --- a/client/src/observatory/results.scss +++ b/client/src/observatory/results.scss @@ -99,6 +99,31 @@ margin-top: 1rem; } + .detail-header { + display: flex; + gap: 0.5rem; + padding: 0 1.5rem 0 0; + + .arrow { + color: var(--observatory-color-secondary); + } + + .detail-header-title { + font-weight: 600; + padding-right: 0.2rem; + } + } + + .iso-date { + code { + font-weight: initial; + } + } + + .humanized-duration { + font-size: var(--type-smaller-font-size); + } + table { background: var(--observatory-table-bg); border: none; @@ -213,6 +238,16 @@ width: 1.3rem; } + @media (max-width: #{$screen-sm - 0.02}) { + td { + .iso-date { + code { + font-size: x-small; + } + } + } + } + @media (max-width: #{$screen-lg - 0.02}) { // responsive table min-width: 0; @@ -240,6 +275,10 @@ grid-auto-flow: column; grid-column: span 2; grid-template-columns: subgrid; + + .humanized-duration { + display: none; + } } td:before { diff --git a/client/src/observatory/results.tsx b/client/src/observatory/results.tsx index 5a2002c2b9fd..ab7d7e5a1431 100644 --- a/client/src/observatory/results.tsx +++ b/client/src/observatory/results.tsx @@ -132,16 +132,16 @@ function ObservatoryScanResults({ result, host }) { key: "csp", element: , }, - { - label: "Raw server headers", - key: "headers", - element: , - }, { label: "Cookies", key: "cookies", element: , }, + { + label: "Raw server headers", + key: "headers", + element: , + }, { label: "Scan history", key: "history", diff --git a/client/src/observatory/results/cookies.tsx b/client/src/observatory/results/cookies.tsx index c72c647b44c3..3f7f4d880237 100644 --- a/client/src/observatory/results/cookies.tsx +++ b/client/src/observatory/results/cookies.tsx @@ -1,93 +1,122 @@ import { ObservatoryResult } from "../types"; -import { formatDateTime, PassIcon } from "../utils"; +import { Link, PassIcon } from "../utils"; +import { HumanDuration } from "./human-duration"; export function ObservatoryCookies({ result }: { result: ObservatoryResult }) { const cookies = result.tests["cookies"]?.data; return cookies && Object.keys(cookies).length !== 0 ? ( - - - - - - - - - - - - - - {Object.entries(cookies).map(([key, value]) => ( - - - - - - - - + <> +
+
+

+
+
None

`, + }} + /> +
+
Name - - Expires - - - - Path - - - - Secure - - - - HttpOnly - - - - SameSite - - - - Prefix - -
- {key} - - {value.expires - ? formatDateTime(new Date(value.expires)) - : "Session"} - - {value.path} - - - - - - {value.samesite ? {capitalize(value.samesite)} : "-"} - - -
+ + + + + + + + + - ))} - -
Name + + Expires + + + + Path + + + + Secure + + + + HttpOnly + + + + SameSite + + + + Prefix + +
+ + + {Object.entries(cookies).map(([key, value]) => ( + + + {key} + + + {value.expires ? ( + <> +
+ {value.expires} +
+
+ () +
+ + ) : ( + "Session" + )} + + + {value.path} + + + + + + + + + {value.samesite ? ( + {capitalize(value.samesite)} + ) : ( + "-" + )} + + + + + + ))} + + + ) : ( diff --git a/client/src/observatory/results/csp.tsx b/client/src/observatory/results/csp.tsx index 93f61265420f..23d49185a9fc 100644 --- a/client/src/observatory/results/csp.tsx +++ b/client/src/observatory/results/csp.tsx @@ -36,72 +36,91 @@ export default function ObservatoryCSP({ "unsafeObjects", ]; - return ( -
- {policy ? ( - <> - - - - - - - - - {policyTests.map((pt) => { - return policy[pt] ? ( - - - - - ) : ( - [] - ); - })} - - - ) : ( - + // cookies && Object.keys(cookies).length !== 0 ? + return policy ? ( + <> +
+
+

+
+
None

`, + }} + /> +
+ +
TestResultInfo
- - -
+ - + + + + + + {policyTests.map((pt) => { + return policy[pt] ? ( + + + + + ) : ( + [] + ); + })} - )} +
-

- {result.tests["content-security-policy"]?.result === - "csp-not-implemented-but-reporting-enabled" ? ( - <> - Content-Security-Policy-Report-Only header - detected. Implement an enforced policy; see{" "} - - MDN's Content Security Policy (CSP) documentation - - . - - ) : ( - "No CSP headers detected" - )} -

-
TestResultInfo
+ + +
+ + ) : result.tests["content-security-policy"]?.result === + "csp-not-implemented-but-reporting-enabled" ? ( + + + + + + +
+

+ Content-Security-Policy-Report-Only header detected. + Implement an enforced policy; see{" "} + + MDN's Content Security Policy (CSP) documentation + + . +

+
+ ) : ( + + + + + +
+

No CSP headers detected

+
); } diff --git a/client/src/observatory/results/human-duration.tsx b/client/src/observatory/results/human-duration.tsx index 5eb5dacdadae..e1b6e08f13e6 100644 --- a/client/src/observatory/results/human-duration.tsx +++ b/client/src/observatory/results/human-duration.tsx @@ -24,28 +24,48 @@ function displayString(date: Date) { const currentTime = new Date().getTime(); const targetTime = date.getTime(); const diffSecs = Math.round((currentTime - targetTime) / 1000); + let direction_postfix = " ago"; + let direction_prefix = ""; if (diffSecs < 0) { - return formatDateTime(date); + direction_postfix = ""; + direction_prefix = "in about "; + // return formatDateTime(date); } - if (diffSecs < 60) { + if (Math.abs(diffSecs) < 60) { return `Just now`; } - if (diffSecs < 60 * 60) { - const minutes = Math.floor(diffSecs / 60); - return minutes === 1 ? `1 minute ago` : `${minutes} minutes ago`; + if (Math.abs(diffSecs) < 60 * 60) { + const minutes = Math.abs(Math.floor(diffSecs / 60)); + return minutes === 1 + ? `${direction_prefix}1 minute${direction_postfix}` + : `${direction_prefix}${minutes} minutes${direction_postfix}`; } - if (diffSecs < 60 * 60 * 24) { - const hours = Math.floor(diffSecs / 3600); - return hours === 1 ? `1 hour ago` : `${hours} hours ago`; + if (Math.abs(diffSecs) < 60 * 60 * 24) { + const hours = Math.abs(Math.floor(diffSecs / 3600)); + return hours === 1 + ? `${direction_prefix}1 hour${direction_postfix}` + : `${direction_prefix}${hours} hours${direction_postfix}`; } // up to 30 days as days - if (diffSecs < 60 * 60 * 24 * 30) { - const days = Math.floor(diffSecs / 86400); - return days === 1 ? `1 day ago` : `${days} days ago`; + if (Math.abs(diffSecs) < 60 * 60 * 24 * 30) { + const days = Math.abs(Math.floor(diffSecs / 86400)); + return days === 1 + ? `${direction_prefix}1 day${direction_postfix}` + : `${direction_prefix}${days} days${direction_postfix}`; + } + // up to 350 days as months + if (Math.abs(diffSecs) < 60 * 60 * 24 * 350) { + const months = Math.abs(Math.floor(diffSecs / 2592000)); + return months === 1 + ? `${direction_prefix}1 month${direction_postfix}` + : `${direction_prefix}${months} months${direction_postfix}`; } - // after a week, return the formatted date - return formatDateTime(date); + // after 350 days return as years + const years = Math.abs(Math.floor(diffSecs / 31622400)); + return years === 1 + ? `${direction_prefix}1 year${direction_postfix}` + : `${direction_prefix}${years} years${direction_postfix}`; } diff --git a/client/src/observatory/utils.tsx b/client/src/observatory/utils.tsx index f05232c7570d..b718563e11fa 100644 --- a/client/src/observatory/utils.tsx +++ b/client/src/observatory/utils.tsx @@ -130,7 +130,7 @@ export function formatDateTime(date: Date): string { }); } -export function hostAsRedirectChain(host, result: ObservatoryResult) { +export function hostAsRedirectChain(host: string, result: ObservatoryResult) { const chain = result.tests.redirection?.route; if (!chain || chain.length < 1) { return host; From e61c38320cda61c77089fe5784ce5f742c421dd0 Mon Sep 17 00:00:00 2001 From: Andi Pieper Date: Thu, 28 Nov 2024 14:14:27 +0100 Subject: [PATCH 2/5] some refinements, fixed a type bug on the observatory response --- client/src/observatory/results/cookies.tsx | 17 ++++++----------- client/src/observatory/results/csp.tsx | 6 +++++- client/src/observatory/types.ts | 2 +- client/src/observatory/utils.tsx | 19 +++++++++++++++++++ 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/client/src/observatory/results/cookies.tsx b/client/src/observatory/results/cookies.tsx index 3f7f4d880237..6ef1e8bb1ec7 100644 --- a/client/src/observatory/results/cookies.tsx +++ b/client/src/observatory/results/cookies.tsx @@ -1,14 +1,16 @@ import { ObservatoryResult } from "../types"; -import { Link, PassIcon } from "../utils"; -import { HumanDuration } from "./human-duration"; +import { PassIcon, Timestamp } from "../utils"; export function ObservatoryCookies({ result }: { result: ObservatoryResult }) { const cookies = result.tests["cookies"]?.data; + const pass = result.tests["cookies"]?.pass; return cookies && Object.keys(cookies).length !== 0 ? ( <>
-

+

+ +

{value.expires ? ( - <> -
- {value.expires} -
-
- () -
- + ) : ( "Session" )} diff --git a/client/src/observatory/results/csp.tsx b/client/src/observatory/results/csp.tsx index 23d49185a9fc..4f5c81aec47a 100644 --- a/client/src/observatory/results/csp.tsx +++ b/client/src/observatory/results/csp.tsx @@ -36,12 +36,16 @@ export default function ObservatoryCSP({ "unsafeObjects", ]; + const pass = result.tests["content-security-policy"]?.pass; + // cookies && Object.keys(cookies).length !== 0 ? return policy ? ( <>
-

+

+ +

(res: Response): Promise { return await res.json(); } +export function Timestamp({ expires }: { expires: string }) { + const d = new Date(expires); + if (d.toString() === "Invalid Date") { + return
{expires}
; + } + const ts = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} UTC`; + return ( + <> +
+ {ts} +
+
+ () +
+ + ); +} + export function formatDateTime(date: Date): string { return date.toLocaleString([], { dateStyle: "medium", From cf13caf7c783101205d313e6c13c4f5c16ce692f Mon Sep 17 00:00:00 2001 From: Andi Pieper Date: Tue, 3 Dec 2024 11:43:18 +0100 Subject: [PATCH 3/5] fix date formatting --- client/src/observatory/utils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/observatory/utils.tsx b/client/src/observatory/utils.tsx index c0a1532d9e96..3a51362b6668 100644 --- a/client/src/observatory/utils.tsx +++ b/client/src/observatory/utils.tsx @@ -129,7 +129,7 @@ export function Timestamp({ expires }: { expires: string }) { if (d.toString() === "Invalid Date") { return
{expires}
; } - const ts = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} UTC`; + const ts = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")} UTC`; return ( <>
From 422428b92199497c0faee508c0870b88caa93377 Mon Sep 17 00:00:00 2001 From: Andi Pieper Date: Thu, 5 Dec 2024 17:03:44 +0100 Subject: [PATCH 4/5] improved duration display and time stamps --- .../observatory/results/human-duration.tsx | 71 +++++++------------ client/src/observatory/utils.tsx | 5 +- 2 files changed, 29 insertions(+), 47 deletions(-) diff --git a/client/src/observatory/results/human-duration.tsx b/client/src/observatory/results/human-duration.tsx index e1b6e08f13e6..fed9748eeee8 100644 --- a/client/src/observatory/results/human-duration.tsx +++ b/client/src/observatory/results/human-duration.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from "react"; - import { formatDateTime } from "../utils"; export function HumanDuration({ date }: { date: Date }) { @@ -8,7 +7,7 @@ export function HumanDuration({ date }: { date: Date }) { useEffect(() => { const interval = setInterval(() => { setText(displayString(date)); - }, 1000); + }, 10000); return () => clearInterval(interval); }); @@ -20,52 +19,32 @@ export function HumanDuration({ date }: { date: Date }) { ); } +// breakpoints for humanized time durations +const MINUTE = 60; +const HOUR = MINUTE * 60; +const DAY = HOUR * 24; +const MONTH = DAY * 30; +const YEAR = DAY * 364; + function displayString(date: Date) { const currentTime = new Date().getTime(); const targetTime = date.getTime(); - const diffSecs = Math.round((currentTime - targetTime) / 1000); - let direction_postfix = " ago"; - let direction_prefix = ""; - - if (diffSecs < 0) { - direction_postfix = ""; - direction_prefix = "in about "; - // return formatDateTime(date); - } - - if (Math.abs(diffSecs) < 60) { - return `Just now`; + const diffSecs = Math.round((targetTime - currentTime) / 1000); + + const rtf = new Intl.RelativeTimeFormat("en", { style: "long" }); + const absSecs = Math.abs(diffSecs); + + if (absSecs < MINUTE) { + return diffSecs < 0 ? "Just now" : "Very soon"; + } else if (absSecs < HOUR) { + return rtf.format(Math.floor(diffSecs / MINUTE), "minute"); + } else if (absSecs < DAY) { + return rtf.format(Math.floor(diffSecs / HOUR), "hour"); + } else if (absSecs < MONTH) { + return rtf.format(Math.floor(diffSecs / DAY), "day"); + } else if (absSecs < YEAR) { + return rtf.format(Math.floor(diffSecs / MONTH), "month"); + } else { + return rtf.format(Math.floor(diffSecs / YEAR), "year"); } - if (Math.abs(diffSecs) < 60 * 60) { - const minutes = Math.abs(Math.floor(diffSecs / 60)); - return minutes === 1 - ? `${direction_prefix}1 minute${direction_postfix}` - : `${direction_prefix}${minutes} minutes${direction_postfix}`; - } - if (Math.abs(diffSecs) < 60 * 60 * 24) { - const hours = Math.abs(Math.floor(diffSecs / 3600)); - return hours === 1 - ? `${direction_prefix}1 hour${direction_postfix}` - : `${direction_prefix}${hours} hours${direction_postfix}`; - } - // up to 30 days as days - if (Math.abs(diffSecs) < 60 * 60 * 24 * 30) { - const days = Math.abs(Math.floor(diffSecs / 86400)); - return days === 1 - ? `${direction_prefix}1 day${direction_postfix}` - : `${direction_prefix}${days} days${direction_postfix}`; - } - // up to 350 days as months - if (Math.abs(diffSecs) < 60 * 60 * 24 * 350) { - const months = Math.abs(Math.floor(diffSecs / 2592000)); - return months === 1 - ? `${direction_prefix}1 month${direction_postfix}` - : `${direction_prefix}${months} months${direction_postfix}`; - } - - // after 350 days return as years - const years = Math.abs(Math.floor(diffSecs / 31622400)); - return years === 1 - ? `${direction_prefix}1 year${direction_postfix}` - : `${direction_prefix}${years} years${direction_postfix}`; } diff --git a/client/src/observatory/utils.tsx b/client/src/observatory/utils.tsx index 3a51362b6668..734b067c4e33 100644 --- a/client/src/observatory/utils.tsx +++ b/client/src/observatory/utils.tsx @@ -129,7 +129,10 @@ export function Timestamp({ expires }: { expires: string }) { if (d.toString() === "Invalid Date") { return
{expires}
; } - const ts = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")} UTC`; + const ts = d + .toISOString() + .replace("T", " ") + .replace(/\....Z/, " UTC"); return ( <>
From 6b2ac86f27c434657337ca54b6f55e591d213074 Mon Sep 17 00:00:00 2001 From: Florian Dieminger Date: Thu, 5 Dec 2024 18:38:10 +0100 Subject: [PATCH 5/5] nits --- client/src/observatory/results.scss | 4 ++++ client/src/observatory/results/cookies.tsx | 8 +++----- client/src/observatory/results/csp.tsx | 8 +++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/src/observatory/results.scss b/client/src/observatory/results.scss index 83fa03ab0ae3..956f588b5163 100644 --- a/client/src/observatory/results.scss +++ b/client/src/observatory/results.scss @@ -112,6 +112,10 @@ font-weight: 600; padding-right: 0.2rem; } + + p { + margin: 1rem 0; + } } .iso-date { diff --git a/client/src/observatory/results/cookies.tsx b/client/src/observatory/results/cookies.tsx index 6ef1e8bb1ec7..dd0deacbe6a6 100644 --- a/client/src/observatory/results/cookies.tsx +++ b/client/src/observatory/results/cookies.tsx @@ -7,11 +7,9 @@ export function ObservatoryCookies({ result }: { result: ObservatoryResult }) { return cookies && Object.keys(cookies).length !== 0 ? ( <>
-
-

- -

-
+

+ +

-
-

- -

-
+

+ +