From 7652ccb000fea8489a54d097bc2b49ab64f260c6 Mon Sep 17 00:00:00 2001 From: tofarr Date: Wed, 4 Jun 2025 09:23:48 -0600 Subject: [PATCH] Fix VSCode iframe SameSite cookie issue with cross-origin fallback (#8881) Co-authored-by: openhands --- .../scripts/check-unlocalized-strings.cjs | 3 + frontend/src/i18n/declaration.ts | 4 +- frontend/src/i18n/translation.json | 62 ++++++++++++++----- frontend/src/routes/vscode-tab.tsx | 53 ++++++++++++++-- 4 files changed, 102 insertions(+), 20 deletions(-) diff --git a/frontend/scripts/check-unlocalized-strings.cjs b/frontend/scripts/check-unlocalized-strings.cjs index 2623f2360e..86425d61fb 100755 --- a/frontend/scripts/check-unlocalized-strings.cjs +++ b/frontend/scripts/check-unlocalized-strings.cjs @@ -47,6 +47,7 @@ const SCAN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"]; // Attributes that typically don't contain user-facing text const NON_TEXT_ATTRIBUTES = [ + "allow", "className", "i18nKey", "testId", @@ -69,6 +70,7 @@ const NON_TEXT_ATTRIBUTES = [ "aria-describedby", "aria-hidden", "role", + "sandbox", ]; function shouldIgnorePath(filePath) { @@ -114,6 +116,7 @@ const EXCLUDED_TECHNICAL_STRINGS = [ "add-secret-form", // Test ID for secret form "edit-secret-form", // Test ID for secret form "search-api-key-input", // Input name for search API key + "noopener,noreferrer", // Options for window.open ]; function isExcludedTechnicalString(str) { diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts index fcbcf04c92..b822c56ed8 100644 --- a/frontend/src/i18n/declaration.ts +++ b/frontend/src/i18n/declaration.ts @@ -138,7 +138,9 @@ export enum I18nKey { VSCODE$LOADING = "VSCODE$LOADING", VSCODE$URL_NOT_AVAILABLE = "VSCODE$URL_NOT_AVAILABLE", VSCODE$FETCH_ERROR = "VSCODE$FETCH_ERROR", - VSCODE$IFRAME_PERMISSIONS = "VSCODE$IFRAME_PERMISSIONS", + VSCODE$CROSS_ORIGIN_WARNING = "VSCODE$CROSS_ORIGIN_WARNING", + VSCODE$URL_PARSE_ERROR = "VSCODE$URL_PARSE_ERROR", + VSCODE$OPEN_IN_NEW_TAB = "VSCODE$OPEN_IN_NEW_TAB", INCREASE_TEST_COVERAGE = "INCREASE_TEST_COVERAGE", AUTO_MERGE_PRS = "AUTO_MERGE_PRS", FIX_README = "FIX_README", diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 45ba9821a4..26dace3726 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -2207,21 +2207,53 @@ "tr": "VS Code URL'si alınamadı", "uk": "Не вдалося отримати VS Code URL" }, - "VSCODE$IFRAME_PERMISSIONS": { - "en": "clipboard-read; clipboard-write", - "ja": "clipboard-read; clipboard-write", - "zh-CN": "clipboard-read; clipboard-write", - "zh-TW": "clipboard-read; clipboard-write", - "ko-KR": "clipboard-read; clipboard-write", - "de": "clipboard-read; clipboard-write", - "no": "clipboard-read; clipboard-write", - "it": "clipboard-read; clipboard-write", - "pt": "clipboard-read; clipboard-write", - "es": "clipboard-read; clipboard-write", - "ar": "clipboard-read; clipboard-write", - "fr": "clipboard-read; clipboard-write", - "tr": "clipboard-read; clipboard-write", - "uk": "clipboard-read; clipboard-write" + "VSCODE$CROSS_ORIGIN_WARNING": { + "en": "The code editor cannot be embedded due to browser security restrictions. Cross-origin cookies are being blocked.", + "ja": "ブラウザのセキュリティ制限により、コードエディタを埋め込むことができません。クロスオリジンCookieがブロックされています。", + "zh-CN": "由于浏览器安全限制,无法嵌入代码编辑器。跨源Cookie被阻止。", + "zh-TW": "由於瀏覽器安全限制,無法嵌入代碼編輯器。跨源Cookie被阻止。", + "ko-KR": "브라우저 보안 제한으로 인해 코드 편집기를 삽입할 수 없습니다. 교차 출처 쿠키가 차단되고 있습니다.", + "de": "Der Code-Editor kann aufgrund von Browser-Sicherheitsbeschränkungen nicht eingebettet werden. Cross-Origin-Cookies werden blockiert.", + "no": "Koderedigereren kan ikke bygges inn på grunn av nettleserens sikkerhetsbegrensninger. Cross-origin cookies blir blokkert.", + "it": "L'editor di codice non può essere incorporato a causa delle restrizioni di sicurezza del browser. I cookie cross-origin vengono bloccati.", + "pt": "O editor de código não pode ser incorporado devido a restrições de segurança do navegador. Cookies de origem cruzada estão sendo bloqueados.", + "es": "El editor de código no se puede incrustar debido a las restricciones de seguridad del navegador. Las cookies de origen cruzado están siendo bloqueadas.", + "ar": "لا يمكن تضمين محرر التعليمات البرمجية بسبب قيود أمان المتصفح. يتم حظر ملفات تعريف الارتباط عبر المصدر.", + "fr": "L'éditeur de code ne peut pas être intégré en raison des restrictions de sécurité du navigateur. Les cookies cross-origin sont bloqués.", + "tr": "Tarayıcı güvenlik kısıtlamaları nedeniyle kod düzenleyici yerleştirilemiyor. Çapraz kaynaklı çerezler engelleniyor.", + "uk": "Редактор коду не може бути вбудований через обмеження безпеки браузера. Блокуються файли cookie з різних джерел." + }, + "VSCODE$URL_PARSE_ERROR": { + "en": "Error parsing URL", + "ja": "URLの解析エラー", + "zh-CN": "URL解析错误", + "zh-TW": "URL解析錯誤", + "ko-KR": "URL 구문 분석 오류", + "de": "Fehler beim Parsen der URL", + "no": "Feil ved parsing av URL", + "it": "Errore durante l'analisi dell'URL", + "pt": "Erro ao analisar URL", + "es": "Error al analizar URL", + "ar": "خطأ في تحليل عنوان URL", + "fr": "Erreur d'analyse de l'URL", + "tr": "URL ayrıştırma hatası", + "uk": "Помилка аналізу URL" + }, + "VSCODE$OPEN_IN_NEW_TAB": { + "en": "Open in New Tab", + "ja": "新しいタブで開く", + "zh-CN": "在新标签页中打开", + "zh-TW": "在新標籤頁中打開", + "ko-KR": "새 탭에서 열기", + "de": "In neuem Tab öffnen", + "no": "Åpne i ny fane", + "it": "Apri in una nuova scheda", + "pt": "Abrir em nova aba", + "es": "Abrir en nueva pestaña", + "ar": "فتح في علامة تبويب جديدة", + "fr": "Ouvrir dans un nouvel onglet", + "tr": "Yeni Sekmede Aç", + "uk": "Відкрити в новій вкладці" }, "INCREASE_TEST_COVERAGE": { "en": "Increase test coverage", diff --git a/frontend/src/routes/vscode-tab.tsx b/frontend/src/routes/vscode-tab.tsx index 79dca3869a..139e1506f6 100644 --- a/frontend/src/routes/vscode-tab.tsx +++ b/frontend/src/routes/vscode-tab.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; import { I18nKey } from "#/i18n/declaration"; @@ -12,6 +12,29 @@ function VSCodeTab() { const { curAgentState } = useSelector((state: RootState) => state.agent); const isRuntimeInactive = RUNTIME_INACTIVE_STATES.includes(curAgentState); const iframeRef = React.useRef(null); + const [isCrossProtocol, setIsCrossProtocol] = useState(false); + const [iframeError, setIframeError] = useState(null); + + useEffect(() => { + if (data?.url) { + try { + const iframeProtocol = new URL(data.url).protocol; + const currentProtocol = window.location.protocol; + + // Check if the iframe URL has a different protocol than the current page + setIsCrossProtocol(iframeProtocol !== currentProtocol); + } catch (e) { + // Silently handle URL parsing errors + setIframeError(t("VSCODE$URL_PARSE_ERROR")); + } + } + }, [data?.url]); + + const handleOpenInNewTab = () => { + if (data?.url) { + window.open(data.url, "_blank", "noopener,noreferrer"); + } + }; if (isRuntimeInactive) { return ( @@ -29,14 +52,36 @@ function VSCodeTab() { ); } - if (error || (data && data.error) || !data?.url) { + if (error || (data && data.error) || !data?.url || iframeError) { return (
- {data?.error || String(error) || t(I18nKey.VSCODE$URL_NOT_AVAILABLE)} + {iframeError || + data?.error || + String(error) || + t(I18nKey.VSCODE$URL_NOT_AVAILABLE)}
); } + // If cross-origin, show a button to open in new tab + if (isCrossProtocol) { + return ( +
+
+ {t("VSCODE$CROSS_ORIGIN_WARNING")} +
+ +
+ ); + } + + // If same origin, use the iframe return (