Skip to content
Open
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
51 changes: 44 additions & 7 deletions app/src/views/main/profile/WebLinkAdd.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
autovalidate
@invalid="() => (isValidLink = false)"
@input="handleInput"
type="url"
type="text"
required
>
<j-box pr="300" v-if="loadingMeta" slot="end">
Expand Down Expand Up @@ -72,27 +72,64 @@ const loadingMeta = ref(false);
const isAddingLink = ref(false);
const isValidLink = ref(false);
const titleEl = ref<HTMLElement | null>(null);
let metaDebounce: ReturnType<typeof setTimeout> | null = null;

Comment on lines +75 to +76
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Clear debounce on unmount and track requests to avoid leaks/races

Ensure pending timers don’t fire after unmount and prep a request id for stale-response protection.

-let metaDebounce: ReturnType<typeof setTimeout> | null = null;
+let metaDebounce: ReturnType<typeof setTimeout> | null = null;
+let metaReqId = 0;

Add (outside this block) to imports and lifecycle:

// add to imports
import { ref, onBeforeUnmount } from "vue";

// add after declarations
onBeforeUnmount(() => {
  if (metaDebounce) clearTimeout(metaDebounce);
});
🤖 Prompt for AI Agents
In app/src/views/main/profile/WebLinkAdd.vue around lines 75 to 76, the debounce
timer assigned to metaDebounce can fire after the component unmounts and there’s
no request-id tracking to ignore stale async responses; import ref and
onBeforeUnmount from "vue", add an onBeforeUnmount handler that clears
metaDebounce if set, and add a simple incrementing request id (e.g., a numeric
ref or local variable updated when issuing the request) that you pass into the
async metadata fetch and check on response to ignore stale results so timers and
responses cannot cause leaks or racey state updates after unmount.

function normalizeUrl(input: string): string {
const value = (input || "").trim();
if (!value) return value;
// If already has a scheme, keep as is
const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
return hasScheme ? value : `https://${value}`;
}

Comment on lines +77 to +84
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Allow-list http/https in normalizeUrl

Match WebLinkCard hardening; block non-web schemes early.

-function normalizeUrl(input: string): string {
-  const value = (input || "").trim();
-  if (!value) return value;
-  // If already has a scheme, keep as is
-  const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
-  return hasScheme ? value : `https://${value}`;
-}
+function normalizeUrl(input: string): string {
+  const value = (input || "").trim();
+  if (!value) return "";
+  const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
+  const withScheme = hasScheme ? value : `https://${value}`;
+  try {
+    const u = new URL(withScheme);
+    return u.protocol === "http:" || u.protocol === "https:" ? withScheme : "";
+  } catch {
+    return "";
+  }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function normalizeUrl(input: string): string {
const value = (input || "").trim();
if (!value) return value;
// If already has a scheme, keep as is
const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
return hasScheme ? value : `https://${value}`;
}
function normalizeUrl(input: string): string {
const value = (input || "").trim();
if (!value) return "";
const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
const withScheme = hasScheme ? value : `https://${value}`;
try {
const u = new URL(withScheme);
return u.protocol === "http:" || u.protocol === "https:" ? withScheme : "";
} catch {
return "";
}
}
🤖 Prompt for AI Agents
In app/src/views/main/profile/WebLinkAdd.vue around lines 77 to 84, normalizeUrl
currently accepts any URI scheme; change it to allow-list only http and https:
detect a leading scheme, and if one exists but is not http or https return an
empty string (block non-web schemes early), otherwise if no scheme prepend
"https://" as before; ensure case-insensitive matching and keep the function
signature returning a string.

function looksLikeHost(urlStr: string): boolean {
try {
const u = new URL(urlStr);
const host = u.hostname;
// Require a dot or be localhost to reduce early validations like "https://exa"
return host === "localhost" || host.includes(".");
} catch {
return false;
}
}

async function getMeta() {
async function getMeta(urlForMeta: string) {
try {
loadingMeta.value = true;
const data = await fetch("https://jsonlink.io/api/extract?url=" + link.value).then((res) => res.json());
const data = await fetch("https://jsonlink.io/api/extract?url=" + encodeURIComponent(urlForMeta)).then((res) =>
res.json()
);

title.value = data.title || "";
description.value = data.description || "";
imageUrl.value = data.images[0] || "";
Comment on lines +96 to 105
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prevent stale meta overwrites; make images access safe; only fetch on http(s)

Concurrent fetches can race and older responses can clobber newer input; also data.images may be undefined; and we should skip meta for non-http(s).

-async function getMeta(urlForMeta: string) {
+async function getMeta(urlForMeta: string, reqId: number) {
   try {
     loadingMeta.value = true;
-    const data = await fetch("https://jsonlink.io/api/extract?url=" + encodeURIComponent(urlForMeta)).then((res) =>
-      res.json()
-    );
+    const data = await fetch(
+      "https://jsonlink.io/api/extract?url=" + encodeURIComponent(urlForMeta)
+    ).then((res) => res.json());
+
+    // Drop if a newer request started after this one
+    if (reqId !== metaReqId) return;
 
     title.value = data.title || "";
     description.value = data.description || "";
-    imageUrl.value = data.images[0] || "";
+    imageUrl.value = (data.images?.[0] as string | undefined) || "";
   } finally {
     loadingMeta.value = false;
   }
 }
 
@@
-    // Validate
-    // new URL will throw if not valid
+    // Validate (throws if invalid)
     // We intentionally do not mutate the visible input to avoid caret jumps
     // Use a debounced meta fetch to avoid interrupting typing
-    new URL(normalized);
-    isValidLink.value = true;
+    const u = new URL(normalized);
+    // Only accept http(s)
+    if (u.protocol !== "http:" && u.protocol !== "https:") {
+      isValidLink.value = false;
+      if (metaDebounce) clearTimeout(metaDebounce);
+      return;
+    }
+    isValidLink.value = true;
     if (metaDebounce) clearTimeout(metaDebounce);
-    metaDebounce = setTimeout(() => {
-      getMeta(normalized);
+    metaReqId += 1;
+    const reqId = metaReqId;
+    metaDebounce = setTimeout(() => {
+      getMeta(normalized, reqId);
     }, 500);

Also applies to: 123-133

🤖 Prompt for AI Agents
In app/src/views/main/profile/WebLinkAdd.vue around lines 96-105 (and similarly
at 123-133), the meta fetch needs three fixes: first, only run the fetch when
the URL uses http or https (skip otherwise); second, prevent stale response
overwrites by capturing a local token (requestId or the current url value)
before awaiting and only apply results if the token still matches the latest
input; third, guard access to images (check data.images && data.images.length >
0) before reading images[0] and fall back to an empty string. Also ensure
loadingMeta is properly cleared (in a finally block) so UI state is consistent.

} finally {
loadingMeta.value = false;
titleEl.value?.focus();
}
}

async function handleInput(e: any) {
try {
link.value = e.target.value;
await new URL(e.target.value);
getMeta();
const normalized = normalizeUrl(link.value);

// If it doesn't look like a host yet, consider invalid and skip meta fetch
if (!looksLikeHost(normalized)) {
isValidLink.value = false;
if (metaDebounce) clearTimeout(metaDebounce);
return;
}

// Validate
// new URL will throw if not valid
// We intentionally do not mutate the visible input to avoid caret jumps
// Use a debounced meta fetch to avoid interrupting typing
new URL(normalized);
isValidLink.value = true;
if (metaDebounce) clearTimeout(metaDebounce);
metaDebounce = setTimeout(() => {
getMeta(normalized);
}, 500);
} catch (e) {
isValidLink.value = false;
}
Expand All @@ -108,7 +145,7 @@ async function createLink() {
title: title.value,
description: description.value,
imageUrl: imageUrl.value,
url: link.value,
url: normalizeUrl(link.value),
});

appStore.showSuccessToast({ message: "Link added to agent perspective" });
Expand Down
15 changes: 12 additions & 3 deletions app/src/views/main/profile/WebLinkCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</div>

<div class="link-card__content">
<a :href="url" target="_blank" class="link-card__info">
<a :href="safeUrl" target="_blank" rel="noopener noreferrer" class="link-card__info">
<h2 class="link-card__title">{{ title || url }}</h2>

<div class="link-card__description" v-if="description">{{ description }}</div>
Expand Down Expand Up @@ -39,10 +39,19 @@ const emit = defineEmits(["delete", "edit"]);

const { ad4mClient } = useAppStore();

function normalizeUrl(input: string): string {
const value = (input || "").trim();
if (!value) return "";
const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
return hasScheme ? value : `https://${value}`;
}
Comment on lines +42 to +47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Allow-list http/https to block javascript: and other unsafe schemes

Current normalizeUrl passes through any scheme, enabling clickable javascript: URLs. Restrict to http(s) and return empty for others.

-function normalizeUrl(input: string): string {
-  const value = (input || "").trim();
-  if (!value) return "";
-  const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
-  return hasScheme ? value : `https://${value}`;
-}
+function normalizeUrl(input: string): string {
+  const value = (input || "").trim();
+  if (!value) return "";
+  const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
+  const withScheme = hasScheme ? value : `https://${value}`;
+  try {
+    const u = new URL(withScheme);
+    return u.protocol === "http:" || u.protocol === "https:" ? withScheme : "";
+  } catch {
+    return "";
+  }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function normalizeUrl(input: string): string {
const value = (input || "").trim();
if (!value) return "";
const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
return hasScheme ? value : `https://${value}`;
}
function normalizeUrl(input: string): string {
const value = (input || "").trim();
if (!value) return "";
const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
const withScheme = hasScheme ? value : `https://${value}`;
try {
const u = new URL(withScheme);
return u.protocol === "http:" || u.protocol === "https:" ? withScheme : "";
} catch {
return "";
}
}
🤖 Prompt for AI Agents
In app/src/views/main/profile/WebLinkCard.vue around lines 42 to 47,
normalizeUrl currently accepts any URL scheme (allowing unsafe schemes like
javascript:). Change it so that after trimming and early-return for empty, it
detects a scheme case-insensitively; if no scheme, prefix with "https://"; if a
scheme is present, only allow "http" or "https" (case-insensitive) and otherwise
return an empty string to block unsafe schemes. Ensure regex/check is updated
accordingly and tests/uses of normalizeUrl handle the empty-string case.


const safeUrl = computed(() => normalizeUrl(props.url));

const hostname = computed(() => {
if (props.url) {
if (safeUrl.value) {
try {
return new URL(props.url).hostname;
return new URL(safeUrl.value).hostname;
} catch (e) {
return "";
}
Expand Down