diff --git a/.github/workflows/repeat-recent-requests.yml b/.github/workflows/repeat-recent-requests.yml
index 39220a8c91bcd..536199b728b70 100644
--- a/.github/workflows/repeat-recent-requests.yml
+++ b/.github/workflows/repeat-recent-requests.yml
@@ -13,6 +13,8 @@ on:
- cron: "15,45 * * * *"
workflow_dispatch:
+permissions: {}
+
jobs:
triggerRepeatRecent:
if: |
diff --git a/packages/core/src/api/gist.js b/packages/core/src/api/gist.js
index 0cb9610262ca8..8d5a28e11836f 100644
--- a/packages/core/src/api/gist.js
+++ b/packages/core/src/api/gist.js
@@ -1,4 +1,5 @@
import { renderGistCard } from "../cards/gist.js";
+import { findInvalidColor } from "../common/color.js";
import {
MissingParamError,
retrieveSecondaryMessage,
@@ -26,6 +27,23 @@ export default async (
},
pat = null,
) => {
+ const invalidColorInput = findInvalidColor({
+ title_color,
+ icon_color,
+ text_color,
+ bg_color,
+ border_color,
+ });
+ if (invalidColorInput) {
+ return {
+ status: "error - permanent",
+ content: renderError({
+ message: "Something went wrong",
+ secondaryMessage: `Invalid color input for parameter "${invalidColorInput}"`,
+ }),
+ };
+ }
+
if (locale && !isLocaleAvailable(locale)) {
return {
status: "error - permanent",
diff --git a/packages/core/src/api/index.js b/packages/core/src/api/index.js
index 4751560082eab..87f6bfd9056cf 100644
--- a/packages/core/src/api/index.js
+++ b/packages/core/src/api/index.js
@@ -1,4 +1,5 @@
import { renderStatsCard } from "../cards/stats.js";
+import { findInvalidColor } from "../common/color.js";
import {
MissingParamError,
retrieveSecondaryMessage,
@@ -44,6 +45,24 @@ export default async (
},
pat = null,
) => {
+ const invalidColorInput = findInvalidColor({
+ title_color,
+ ring_color,
+ icon_color,
+ text_color,
+ bg_color,
+ border_color,
+ });
+ if (invalidColorInput) {
+ return {
+ status: "error - permanent",
+ content: renderError({
+ message: "Something went wrong",
+ secondaryMessage: `Invalid color input for parameter "${invalidColorInput}"`,
+ }),
+ };
+ }
+
if (locale && !isLocaleAvailable(locale)) {
return {
status: "error - permanent",
diff --git a/packages/core/src/api/pin.js b/packages/core/src/api/pin.js
index 118aa70cc2204..fe59500dfe228 100644
--- a/packages/core/src/api/pin.js
+++ b/packages/core/src/api/pin.js
@@ -1,4 +1,5 @@
import { renderRepoCard } from "../cards/repo.js";
+import { findInvalidColor } from "../common/color.js";
import {
MissingParamError,
retrieveSecondaryMessage,
@@ -34,6 +35,23 @@ export default async (
},
pat = null,
) => {
+ const invalidColorInput = findInvalidColor({
+ title_color,
+ icon_color,
+ text_color,
+ bg_color,
+ border_color,
+ });
+ if (invalidColorInput) {
+ return {
+ status: "error - permanent",
+ content: renderError({
+ message: "Something went wrong",
+ secondaryMessage: `Invalid color input for parameter "${invalidColorInput}"`,
+ }),
+ };
+ }
+
if (locale && !isLocaleAvailable(locale)) {
return {
status: "error - permanent",
diff --git a/packages/core/src/api/top-langs.js b/packages/core/src/api/top-langs.js
index 46f6633c013e6..c4599351d9816 100644
--- a/packages/core/src/api/top-langs.js
+++ b/packages/core/src/api/top-langs.js
@@ -1,4 +1,5 @@
import { renderTopLanguages } from "../cards/top-languages.js";
+import { findInvalidColor } from "../common/color.js";
import {
MissingParamError,
retrieveSecondaryMessage,
@@ -38,6 +39,23 @@ export default async (
},
pat = null,
) => {
+ const invalidColorInput = findInvalidColor({
+ title_color,
+ text_color,
+ bg_color,
+ prog_bar_bg_color,
+ border_color,
+ });
+ if (invalidColorInput) {
+ return {
+ status: "error - permanent",
+ content: renderError({
+ message: "Something went wrong",
+ secondaryMessage: `Invalid color input for parameter "${invalidColorInput}"`,
+ }),
+ };
+ }
+
if (locale && !isLocaleAvailable(locale)) {
return {
status: "error - permanent",
diff --git a/packages/core/src/api/wakatime.js b/packages/core/src/api/wakatime.js
index 19307f104aaf8..80a5421d4de61 100644
--- a/packages/core/src/api/wakatime.js
+++ b/packages/core/src/api/wakatime.js
@@ -1,4 +1,5 @@
import { renderWakatimeCard } from "../cards/wakatime.js";
+import { findInvalidColor } from "../common/color.js";
import {
MissingParamError,
retrieveSecondaryMessage,
@@ -32,6 +33,23 @@ export default async ({
display_format,
disable_animations,
}) => {
+ const invalidColorInput = findInvalidColor({
+ title_color,
+ icon_color,
+ text_color,
+ bg_color,
+ border_color,
+ });
+ if (invalidColorInput) {
+ return {
+ status: "error - permanent",
+ content: renderError({
+ message: "Something went wrong",
+ secondaryMessage: `Invalid color input for parameter "${invalidColorInput}"`,
+ }),
+ };
+ }
+
if (locale && !isLocaleAvailable(locale)) {
return {
status: "error - permanent",
diff --git a/packages/core/src/cards/repo.js b/packages/core/src/cards/repo.js
index a352f357c1ea5..6c9a6c86790fb 100644
--- a/packages/core/src/cards/repo.js
+++ b/packages/core/src/cards/repo.js
@@ -1,6 +1,6 @@
import { Card } from "../common/Card.js";
import { I18n } from "../common/I18n.js";
-import { getCardColors } from "../common/color.js";
+import { getCardColors, isValidHexColor } from "../common/color.js";
import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
import { encodeHTML } from "../common/html.js";
import { icons } from "../common/icons.js";
@@ -32,20 +32,29 @@ const DESCRIPTION_MAX_LINES = 3;
* @param {string} textColor The color of the text.
* @returns {string} Wrapped repo description SVG object.
*/
-const getBadgeSVG = (label, textColor, xOffset = 0) => `
-
-
-
- ${label}
-
-
-`;
+const getBadgeSVG = (label, textColor, xOffset = 0) => {
+ if (!isValidHexColor(textColor, true)) {
+ throw new Error(`Invalid text color: "${textColor}"`);
+ }
+ if (!Number.isFinite(xOffset)) {
+ throw new Error(`Invalid xOffset: "${xOffset}"`);
+ }
+
+ return `
+
+
+
+ ${encodeHTML(label)}
+
+
+ `;
+};
/**
* @typedef {import("../fetchers/types").RepositoryData} RepositoryData Repository data.
@@ -110,6 +119,7 @@ const renderRepoCard = (repo, options = {}) => {
});
let repoFilter = encodeURIComponent(buildSearchFilter([nameWithOwner], []));
+ const encodedUsername = encodeURIComponent(username);
const STATS = {};
if (show.includes("prs_authored")) {
STATS.prs_authored = {
@@ -117,7 +127,7 @@ const renderRepoCard = (repo, options = {}) => {
label: i18n.t("repocard.prs-authored"),
value: totalPRsAuthored,
id: "prs_authored",
- link: `https://github.com/search?q=${repoFilter}author%3A${username}&type=pullrequests`,
+ link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&type=pullrequests`,
};
}
if (show.includes("prs_commented")) {
@@ -126,7 +136,7 @@ const renderRepoCard = (repo, options = {}) => {
label: i18n.t("repocard.prs-commented"),
value: totalPRsCommented,
id: "prs_commented",
- link: `https://github.com/search?q=${repoFilter}commenter%3A${username}+-author%3A${username}&type=pullrequests`,
+ link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&type=pullrequests`,
};
}
if (show.includes("prs_reviewed")) {
@@ -135,7 +145,7 @@ const renderRepoCard = (repo, options = {}) => {
label: i18n.t("repocard.prs-reviewed"),
value: totalPRsReviewed,
id: "prs_reviewed",
- link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${username}+-author%3A${username}&type=pullrequests`,
+ link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${encodedUsername}+-author%3A${encodedUsername}&type=pullrequests`,
};
}
if (show.includes("issues_authored")) {
@@ -144,7 +154,7 @@ const renderRepoCard = (repo, options = {}) => {
label: i18n.t("repocard.issues-authored"),
value: totalIssuesAuthored,
id: "issues_authored",
- link: `https://github.com/search?q=${repoFilter}author%3A${username}&type=issues`,
+ link: `https://github.com/search?q=${repoFilter}author%3A${encodedUsername}&type=issues`,
};
}
if (show.includes("issues_commented")) {
@@ -153,7 +163,7 @@ const renderRepoCard = (repo, options = {}) => {
label: i18n.t("repocard.issues-commented"),
value: totalIssuesCommented,
id: "issues_commented",
- link: `https://github.com/search?q=${repoFilter}commenter%3A${username}+-author%3A${username}&type=issues`,
+ link: `https://github.com/search?q=${repoFilter}commenter%3A${encodedUsername}+-author%3A${encodedUsername}&type=issues`,
};
}
diff --git a/packages/core/src/cards/stats.js b/packages/core/src/cards/stats.js
index cbaf2b61f3e43..cd65180eed967 100644
--- a/packages/core/src/cards/stats.js
+++ b/packages/core/src/cards/stats.js
@@ -3,6 +3,7 @@ import { I18n } from "../common/I18n.js";
import { getCardColors } from "../common/color.js";
import { CustomError } from "../common/error.js";
import { kFormatter } from "../common/fmt.js";
+import { encodeHTML } from "../common/html.js";
import { icons, rankIcon } from "../common/icons.js";
import { buildSearchFilter, clampValue } from "../common/ops.js";
import { flexLayout, measureText } from "../common/render.js";
@@ -52,8 +53,10 @@ const LONG_LOCALES = [
/**
* Create a stats card text item.
*
+ * The caller must ensure that the passed `icon` and `link` are properly sanitized!
+ *
* @param {object} params Object that contains the createTextNode parameters.
- * @param {string} params.icon The icon to display.
+ * @param {string} params.icon The sanitized icon to display.
* @param {string} params.label The label to display.
* @param {number} params.value The value to display.
* @param {string} params.id The id of the stat.
@@ -64,7 +67,7 @@ const LONG_LOCALES = [
* @param {boolean} params.bold Whether to bold the label.
* @param {string} params.numberFormat The format of numbers on card.
* @param {number=} params.numberPrecision The precision of numbers on card.
- * @param {string} params.link Url to link to.
+ * @param {string} params.link Sanitized url to link to.
* @param {number} params.labelXOffset horizontal offset for label.
* @returns {string} The stats card text item SVG object.
*/
@@ -83,6 +86,16 @@ const createTextNode = ({
link,
labelXOffset = 25,
}) => {
+ if (!Number.isFinite(labelXOffset)) {
+ throw new Error(`Invalid labelXOffset: "${labelXOffset}"`);
+ }
+ if (!Number.isFinite(shiftValuePos)) {
+ throw new Error(`Invalid shiftValuePos: "${shiftValuePos}"`);
+ }
+ if (!Number.isFinite(index)) {
+ throw new Error(`Invalid index: "${index}"`);
+ }
+
const precision =
typeof numberPrecision === "number" && !isNaN(numberPrecision)
? clampValue(numberPrecision, 0, 2)
@@ -109,7 +122,7 @@ const createTextNode = ({
${iconSvg}
${label}:
+ }" ${labelOffset} y="12.5">${encodeHTML(label)}:
- ${name}
- ${hideValues ? "" : `${displayValue}`}
+ ${encodeHTML(name)}
+ ${hideValues ? "" : `${encodeHTML(displayValue)}`}
${createProgressNode({
x: 0,
y: 25,
@@ -285,11 +290,15 @@ const createCompactLangNode = ({
const staggerDelay = (index + 3) * 150;
const color = lang.color || "#858585";
+ if (!isValidHexColor(color, true)) {
+ throw new Error(`Invalid language color: "${color}"`);
+ }
+
return `
- ${lang.name} ${hideProgress || hideValues ? "" : displayValue}
+ ${encodeHTML(lang.name)} ${hideProgress || hideValues ? "" : encodeHTML(displayValue)}
`;
@@ -439,6 +448,10 @@ const renderCompactLayout = (
let progressOffset = 0;
const compactProgressBar = langs
.map((lang) => {
+ if (!isValidHexColor(lang.color, true)) {
+ throw new Error(`Invalid language color: "${lang.color}"`);
+ }
+
const percentage = parseFloat(
((lang.size / totalLanguageSize) * offsetWidth).toFixed(2),
);
@@ -514,13 +527,17 @@ const renderDonutVerticalLayout = (
// Generate each donut vertical chart part
for (const lang of langs) {
+ if (!isValidHexColor(lang.color, true)) {
+ throw new Error(`Invalid language color: "${lang.color}"`);
+ }
+
const percentage = (lang.size / totalLanguageSize) * 100;
const circleLength = totalCircleLength * (percentage / 100);
const delay = startDelayCoefficient * 100;
circles.push(`
- {
// Generate each pie chart part
for (const lang of langs) {
+ if (!isValidHexColor(lang.color, true)) {
+ throw new Error(`Invalid language color: "${lang.color}"`);
+ }
+
if (langs.length === 1) {
paths.push(`
{
+ if (!Number.isFinite(width)) {
+ throw new Error(`Invalid width: "${width}"`);
+ }
+
const centerX = width / 3;
const centerY = width / 3;
const radius = centerX - 60;
const strokeWidth = 12;
- const colors = langs.map((lang) => lang.color);
+ const colors = langs.map((lang) => {
+ if (!isValidHexColor(lang.color, true)) {
+ throw new Error(`Invalid language color: "${lang.color}"`);
+ }
+ return lang.color;
+ });
const langsPercents = langs.map((lang) =>
parseFloat(((lang.size / totalLanguageSize) * 100).toFixed(2)),
);
@@ -783,10 +813,14 @@ const renderDonutLayout = (
* @returns {string} No languages data SVG node string.
*/
const noLanguagesDataNode = ({ color, text, layout }) => {
+ if (!isValidHexColor(color, true)) {
+ throw new Error(`Invalid text color: "${color}"`);
+ }
+
return `
${text}
+ }" y="11" class="stat bold" fill="${color}">${encodeHTML(text)}
`;
};
diff --git a/packages/core/src/cards/wakatime.js b/packages/core/src/cards/wakatime.js
index a717e753587fc..72bdfff69fa15 100644
--- a/packages/core/src/cards/wakatime.js
+++ b/packages/core/src/cards/wakatime.js
@@ -1,6 +1,7 @@
import { Card } from "../common/Card.js";
import { I18n } from "../common/I18n.js";
-import { getCardColors } from "../common/color.js";
+import { getCardColors, isValidHexColor } from "../common/color.js";
+import { encodeHTML } from "../common/html.js";
import languageColors from "../common/languageColors.json" with { type: "json" };
import { clampValue, lowercaseTrim } from "../common/ops.js";
import { createProgressNode, flexLayout } from "../common/render.js";
@@ -24,8 +25,12 @@ const TOTAL_TEXT_WIDTH = 275;
* @returns {string} No coding activity SVG node string.
*/
const noCodingActivityNode = ({ color, text }) => {
+ if (!isValidHexColor(color, true)) {
+ throw new Error(`Invalid text color: "${color}"`);
+ }
+
return `
- ${text}
+ ${encodeHTML(text)}
`;
};
@@ -58,6 +63,13 @@ const formatLanguageValue = ({ display_format, lang }) => {
* @returns {string} The compact layout language SVG node.
*/
const createCompactLangNode = ({ lang, x, y, display_format }) => {
+ if (!Number.isFinite(x)) {
+ throw new Error(`Invalid x: "${x}"`);
+ }
+ if (!Number.isFinite(y)) {
+ throw new Error(`Invalid y: "${y}"`);
+ }
+
// @ts-ignore
const color = languageColors[lang.name] || "#858585";
const value = formatLanguageValue({ display_format, lang });
@@ -66,7 +78,7 @@ const createCompactLangNode = ({ lang, x, y, display_format }) => {
- ${lang.name} - ${value}
+ ${encodeHTML(lang.name)} - ${encodeHTML(value)}
`;
@@ -125,6 +137,13 @@ const createTextNode = ({
progressBarBackgroundColor,
progressBarWidth,
}) => {
+ if (!Number.isFinite(index)) {
+ throw new Error(`Invalid index: "${index}"`);
+ }
+ if (!Number.isFinite(progressBarWidth)) {
+ throw new Error(`Invalid progressBarWidth: "${progressBarWidth}"`);
+ }
+
const staggerDelay = (index + 3) * 150;
const cardProgress = hideProgress
? null
@@ -142,12 +161,12 @@ const createTextNode = ({
return `
- ${label}:
+ ${encodeHTML(label)}:
${value}
+ >${encodeHTML(value)}
${cardProgress}
`;
@@ -179,11 +198,15 @@ const recalculatePercentages = (languages) => {
* @param {string} colors.textColor The text color.
* @returns {string} Card CSS styles.
*/
-const getStyles = ({
+const getStyles = function ({
// eslint-disable-next-line no-unused-vars
titleColor,
textColor,
-}) => {
+}) {
+ if (!isValidHexColor(textColor, true)) {
+ throw new Error(`Invalid text color: "${textColor}"`);
+ }
+
return `
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: ${textColor};
diff --git a/packages/core/src/common/Card.ts b/packages/core/src/common/Card.ts
index 7ae629f83c8cf..f19d1716bd755 100644
--- a/packages/core/src/common/Card.ts
+++ b/packages/core/src/common/Card.ts
@@ -1,3 +1,4 @@
+import { isValidGradient, isValidHexColor } from "./color.js";
import { encodeHTML } from "./html.js";
import { flexLayout } from "./render.js";
@@ -33,6 +34,8 @@ class Card {
/**
* Creates a new card instance.
*
+ * The caller must ensure that the passed `titlePrefixIcon` is properly sanitized!
+ *
* @param props Card arguments.
* @param props.width Card width.
* @param props.height Card height.
@@ -40,7 +43,7 @@ class Card {
* @param props.colors Card colors arguments.
* @param props.customTitle Card custom title.
* @param props.defaultTitle Card default title.
- * @param props.titlePrefixIcon Card title prefix icon.
+ * @param props.titlePrefixIcon Sanitized card title prefix icon.
*/
constructor({
width = 100,
@@ -65,13 +68,11 @@ class Card {
this.hideBorder = false;
this.hideTitle = false;
- this.border_radius = border_radius;
+ this.border_radius = parseFloat(String(border_radius));
// returns theme based colors with proper overrides and defaults
this.colors = colors;
- this.title = encodeHTML(
- customTitle === undefined ? defaultTitle : customTitle,
- );
+ this.title = customTitle === undefined ? defaultTitle : customTitle;
this.css = "";
@@ -104,7 +105,9 @@ class Card {
}
/**
- * @param value The CSS to add to the card.
+ * The caller must ensure that the passed `css` string is properly sanitized!
+ *
+ * @param value The sanitized CSS to add to the card.
*/
setCSS(value: string): void {
this.css = value;
@@ -121,10 +124,13 @@ class Card {
* @param value Whether to hide the title or not.
*/
setHideTitle(value: boolean): void {
- this.hideTitle = value;
- if (value) {
+ if (value && !this.hideTitle) {
this.height -= 30;
}
+ if (!value && this.hideTitle) {
+ this.height += 30;
+ }
+ this.hideTitle = value;
}
/**
@@ -144,7 +150,7 @@ class Card {
y="0"
class="header"
data-testid="header"
- >${this.title}
+ >${encodeHTML(this.title)}
`;
const prefixIcon = `
@@ -180,10 +186,12 @@ class Card {
if (typeof this.colors.bgColor !== "object") {
return "";
}
+ if (!isValidGradient(this.colors.bgColor)) {
+ throw new Error(`Invalid gradient: ${this.colors.bgColor.join(",")}`);
+ }
const gradients = this.colors.bgColor.slice(1);
- return typeof this.colors.bgColor === "object"
- ? `
+ return `
- `
- : "";
+ `;
}
/**
@@ -230,10 +237,38 @@ class Card {
};
/**
- * @param body The inner body of the card.
+ * The caller must ensure that the passed `body` string is properly sanitized!
+ *
+ * @param body The sanitized inner body of the card.
* @returns The rendered card.
*/
render(body: string): string {
+ if (!Number.isFinite(this.border_radius)) {
+ throw new Error(`Invalid border radius: "${this.border_radius}"`);
+ }
+ if (
+ this.colors.titleColor !== undefined &&
+ !isValidHexColor(this.colors.titleColor, true)
+ ) {
+ throw new Error(`Invalid title color: "${this.colors.titleColor}"`);
+ }
+ if (
+ this.colors.borderColor !== undefined &&
+ !isValidHexColor(this.colors.borderColor, true)
+ ) {
+ throw new Error(`Invalid border color: "${this.colors.borderColor}"`);
+ }
+ if (
+ this.colors.bgColor !== undefined &&
+ !(typeof this.colors.bgColor === "object"
+ ? isValidGradient(this.colors.bgColor)
+ : isValidHexColor(this.colors.bgColor, true))
+ ) {
+ throw new Error(
+ `Invalid background color: ${String(this.colors.bgColor)}`,
+ );
+ }
+
return `