chore: parse app status link (#18439)

No actual exploit here as far as I can tell, but doing a string check
without parsing was flagged by a scanner.
This commit is contained in:
Asher
2025-06-26 13:04:11 -08:00
committed by GitHub
parent d5e34195b0
commit 05f6d69455
3 changed files with 139 additions and 46 deletions
+2 -46
View File
@@ -1,4 +1,3 @@
import GitHub from "@mui/icons-material/GitHub";
import type { WorkspaceApp } from "api/typesGenerated";
import { Button } from "components/Button/Button";
import {
@@ -14,19 +13,13 @@ import {
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import {
ArrowLeftIcon,
BugIcon,
EllipsisVerticalIcon,
ExternalLinkIcon,
GitPullRequestArrowIcon,
} from "lucide-react";
import { ArrowLeftIcon, EllipsisVerticalIcon } from "lucide-react";
import type { Task } from "modules/tasks/tasks";
import type { FC } from "react";
import { Link as RouterLink } from "react-router-dom";
import { cn } from "utils/cn";
import { truncateURI } from "utils/uri";
import { TaskAppIFrame } from "./TaskAppIframe";
import { TaskStatusLink } from "./TaskStatusLink";
type TaskSidebarProps = {
task: Task;
@@ -179,40 +172,3 @@ export const TaskSidebar: FC<TaskSidebarProps> = ({ task }) => {
</aside>
);
};
type TaskStatusLinkProps = {
uri: string;
};
const TaskStatusLink: FC<TaskStatusLinkProps> = ({ uri }) => {
let icon = <ExternalLinkIcon />;
let label = truncateURI(uri);
if (uri.startsWith("https://github.com")) {
const issueNumber = uri.split("/").pop();
const [org, repo] = uri.split("/").slice(3, 5);
const prefix = `${org}/${repo}`;
if (uri.includes("pull/")) {
icon = <GitPullRequestArrowIcon />;
label = issueNumber
? `${prefix}#${issueNumber}`
: `${prefix} Pull Request`;
} else if (uri.includes("issues/")) {
icon = <BugIcon />;
label = issueNumber ? `${prefix}#${issueNumber}` : `${prefix} Issue`;
} else {
icon = <GitHub />;
label = `${org}/${repo}`;
}
}
return (
<Button asChild variant="outline" size="sm" className="min-w-0">
<a href={uri} target="_blank" rel="noreferrer">
{icon}
{label}
</a>
</Button>
);
};
@@ -0,0 +1,72 @@
import type { Meta, StoryObj } from "@storybook/react";
import { TaskStatusLink } from "./TaskStatusLink";
const meta: Meta<typeof TaskStatusLink> = {
title: "pages/TaskPage/TaskStatusLink",
component: TaskStatusLink,
// Add a wrapper to test truncation.
decorators: [
(Story) => (
<div style={{ display: "flex", width: "200px" }}>
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof TaskStatusLink>;
export const GithubPRNumber: Story = {
args: {
uri: "https://github.com/org/repo/pull/1234",
},
};
export const GitHubPRNoNumber: Story = {
args: {
uri: "https://github.com/org/repo/pull",
},
};
export const GithubIssueNumber: Story = {
args: {
uri: "https://github.com/org/repo/issues/4321",
},
};
export const GithubIssueNoNumber: Story = {
args: {
uri: "https://github.com/org/repo/issues",
},
};
export const GithubOrgRepo: Story = {
args: {
uri: "https://github.com/org/repo",
},
};
export const GithubOrg: Story = {
args: {
uri: "https://github.com/org",
},
};
export const Github: Story = {
args: {
uri: "https://github.com",
},
};
export const File: Story = {
args: {
uri: "file:///path/to/file",
},
};
export const Long: Story = {
args: {
uri: "https://dev.coder.com/this-is-a/long-url/to-test/how-the-truncation/looks",
},
};
@@ -0,0 +1,65 @@
import GitHub from "@mui/icons-material/GitHub";
import { Button } from "components/Button/Button";
import {
BugIcon,
ExternalLinkIcon,
GitPullRequestArrowIcon,
} from "lucide-react";
import type { FC } from "react";
type TaskStatusLinkProps = {
uri: string;
};
export const TaskStatusLink: FC<TaskStatusLinkProps> = ({ uri }) => {
let icon = <ExternalLinkIcon />;
let label = uri;
try {
const parsed = new URL(uri);
switch (parsed.protocol) {
// For file URIs, strip off the `file://`.
case "file:":
label = uri.replace(/^file:\/\//, "");
break;
case "http:":
case "https:":
// For GitHub URIs, use a short representation.
if (parsed.host === "github.com") {
const [_, org, repo, type, number] = parsed.pathname.split("/");
switch (type) {
case "pull":
icon = <GitPullRequestArrowIcon />;
label = number
? `${org}/${repo}#${number}`
: `${org}/${repo} pull request`;
break;
case "issues":
icon = <BugIcon />;
label = number
? `${org}/${repo}#${number}`
: `${org}/${repo} issue`;
break;
default:
icon = <GitHub />;
if (org && repo) {
label = `${org}/${repo}`;
}
break;
}
}
break;
}
} catch (error) {
// Invalid URL, probably.
}
return (
<Button asChild variant="outline" size="sm" className="min-w-0">
<a href={uri} target="_blank" rel="noreferrer">
{icon}
<span className="truncate">{label}</span>
</a>
</Button>
);
};