Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 43 additions & 22 deletions src/components/ErrorMessageDetails/index.jsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,51 @@
import {isSafeUrl, unescapeHTML} from "@site/src/utils";

/*
This component deals with untrusted inputs. These details may come from externally-controlled
data (tracebacks in bot logic and other types of unhandled errors, branch names, etc). We
need to be careful not to render those inputs as arbitrary elements!

Thankfully, React escapes children strings before rendering, so that should take care of it.
*/
export default function ErrorMessageDetails({ details }) {
console.log(details);
let children = [];
/* Structured error metadata was introduced in 2026-06... */
if (details.messages) {
if (details.kind) {
children.push(<span className="badge badge--warning">
{details.kind && details.kind || "error"}
{details.base_branch && ` @ ${details.base_branch}` || null}
{details.attempts && ` after ${details.attempts.toFixed(1)} attempts` || null}
</span>)
if (details.kind || details.url) {
let header = []
if (details.kind) {
header.push(
<span className="badge badge--warning">
{(details.kind && details.kind != "plain") ? details.kind : "error"}
{details.base_branch ? ` @ ${details.base_branch}` : null}
{details.attempts ? ` after ${details.attempts.toFixed(1)} attempts` : null}
</span>
)
}
if (details.url && isSafeUrl(details.url)) {
header.push(
<a
href={details.url}
target="_blank"
rel="noopener noreferrer"
title="View CI job"
style={{ marginLeft: "1em" }}
>
View CI job{" "}
<small className="fa fa-fw">
<i className="fa fa-fw fa-arrow-up-right-from-square"></i>
</small>
</a>
)
}
children.push(<div style={{ display: "inline-flex"}}>{header}</div>)
}
if (details.url) {
children.push(
<a href={details.url} target="_blank" title="View CI job" style={{ marginLeft: "1em"}}>
View CI job{" "}
<small className="fa fa-fw">
<i className="fa fa-fw fa-arrow-up-right-from-square"></i>
</small>
</a>
)
}
for (const message of details.messages) {
children.push(<pre>{message}</pre>)
}
/* Legacy: string only values */
details.messages.map((message, index) => (
children.push(<pre key={`message-${index}`}>{unescapeHTML(message.toString())}</pre>)
))
/* ... but bot data may still contain legacy, string only values */
} else {
children.push(<pre>{details.toString()}</pre>)
children.push(<pre>{unescapeHTML(details.toString())}</pre>)
}
return <>{children}</>
}
10 changes: 7 additions & 3 deletions src/components/StatusDashboard/cloud_services.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { urls } from "@site/src/constants";
import { React, useEffect, useState } from "react";
import styles from "./styles.module.css";
import { isSafeUrl } from "@site/src/utils";

const OPERATIONAL = "All Systems Operational";

Expand Down Expand Up @@ -53,9 +54,12 @@ function Status({ api, link, title }) {
return (
<tr>
<td>
<a href={link} style={{ display: "inline-block", minWidth: "100%" }}>
{title}
</a>
{link && isSafeUrl(link) ?
<a href={link} style={{ display: "inline-block", minWidth: "100%" }}>
{title}
</a>
: title
}
</td>
<td>
<span className={className} style={{
Expand Down
12 changes: 8 additions & 4 deletions src/components/StatusDashboard/repos_and_bots.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { urls } from "@site/src/constants";
import { React, useEffect, useState } from "react";
import styles from "./styles.module.css";
import { isSafeUrl } from "@site/src/utils";

// If CDN status is updated in this window (20 minutes), status is operational.
const OPERATIONAL_WINDOW = 20 * 60 * 1000;
Expand Down Expand Up @@ -34,9 +35,12 @@ function Badge({ children, link, badge, badgeLink }) {
return (
<tr>
<td>
<a href={link} style={{ display: "inline-block", minWidth: "100%" }}>
{children}
</a>
{link && isSafeUrl(link) ?
<a href={link} style={{ display: "inline-block", minWidth: "100%" }}>
{children}
</a>
: <span>children</span>
}
</td>
<td style={{ textAlign: "right" }}>
<Image alt={`${children} status`} link={badgeLink}>{badge}</Image>
Expand All @@ -53,7 +57,7 @@ function Image({ alt, link, children }) {
<img alt={alt}
style={{ verticalAlign: "bottom" }} onError={onError} src={children} />
);
return link ? <a href={link}>{image}</a> : image;
return link && isSafeUrl(link) ? <a href={link}>{image}</a> : image;
}

function CDNStatus() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/StatusDashboard/version_updates.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default function VersionUpdates({ onLoad }) {
<span className="badge badge--warning">{Object.keys(errors).length}</span>
{" "}
<small className="fa fa-fw">
<a href={urls.versions.api} target="_blank" title="View raw payload">
<a href={urls.versions.api} target="_blank" rel="noopener noreferrer" title="View raw payload">
<i className="fa fa-fw fa-arrow-up-right-from-square"></i>
</a>
</small>
Expand Down
17 changes: 9 additions & 8 deletions src/pages/status/migration/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useSorting, SortableHeader } from '@site/src/components/SortableTable';
import * as d3 from "d3";
import DependencyGraph from "@site/src/components/DependencyGraph";
import ErrorMessageDetails from "@site/src/components/ErrorMessageDetails";
import { isSafeUrl } from "@site/src/utils";

// GitHub GraphQL MergeStateStatus documentation
// Reference: https://docs.github.com/en/graphql/reference/enums#mergestatestatus
Expand Down Expand Up @@ -159,28 +160,28 @@ export default function MigrationDetails() {
<div className={`card margin-top--xs`}>
<div className="card__header">
<div className={styles.migration_details_toggle}>
<div class="tabs-container">
<ul role="tablist" aria-orientation="horizontal" class="tabs">
<div className="tabs-container">
<ul role="tablist" aria-orientation="horizontal" className="tabs">
<li
key="table"
role="tab"
class={["tabs__item", (view == "table" ? "tabs__item--active" : null)].join(" ")}
className={["tabs__item", (view == "table" ? "tabs__item--active" : null)].join(" ")}
onClick={() => toggle("table")}
>
Table
</li>
<li
key="dependencies"
role="tab"
class={["tabs__item", (view == "dependencies" ? "tabs__item--active" : null)].join(" ")}
className={["tabs__item", (view == "dependencies" ? "tabs__item--active" : null)].join(" ")}
onClick={() => toggle("dependencies")}
>
Dependencies
</li>
<li
key="graph"
role="tab"
class={["tabs__item", (view == "graph" ? "tabs__item--active" : null)].join(" ")}
className={["tabs__item", (view == "graph" ? "tabs__item--active" : null)].join(" ")}
onClick={() => toggle("graph")}
>
Graph
Expand All @@ -190,7 +191,7 @@ export default function MigrationDetails() {
<li
key="raw"
role="tab"
class="tabs__item"
className="tabs__item"
>
<span>Raw <i className="fa fa-fw fa-arrow-up-right-from-square"></i></span>
</li>
Expand Down Expand Up @@ -534,7 +535,7 @@ function Table({ details }) {
</thead>
<tbody>
{rows.map((row, i) =>
<Row key={i}>{{ feedstock: feedstock[row.name], name: row.name, status: row.status }}</Row>
<Row key={`row-${i}`}>{{ feedstock: feedstock[row.name], name: row.name, status: row.status }}</Row>
)}
</tbody>
</table>}
Expand All @@ -555,7 +556,7 @@ function Row({ children }) {
return (<>
<tr>
<td>
{href ? (
{href && isSafeUrl(href) ? (
<a href={href}>{name}</a>
) : (
details ? (
Expand Down
16 changes: 16 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function isSafeUrl(urlString) {
try {
const parsed = new URL(urlString);
// Explicitly allow only http: and https: protocols
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch (e) {
// If the URL constructor throws an error, the URL is invalid
return false;
}
}

export function unescapeHTML(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
return doc.documentElement.textContent.replaceAll("&#x27;", "'");
}
Loading