Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion frontend/src/core/components/tools/FullscreenToolList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "@app/data/toolsTaxonomy";
import { ToolId } from "@app/types/toolId";
import { useToolSections } from "@app/hooks/useToolSections";
import { openUrl } from "@app/utils/urlUtils";
import NoToolsFound from "@app/components/tools/shared/NoToolsFound";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import StarRoundedIcon from "@mui/icons-material/StarRounded";
Expand Down Expand Up @@ -105,7 +106,7 @@ const FullscreenToolList = ({
if (!tool.component && !tool.link && id !== "read" && id !== "multiTool")
return;
if (tool.link) {
window.open(tool.link, "_blank", "noopener,noreferrer");
openUrl(tool.link, "_blank", "noopener,noreferrer");
return;
}
onSelect(id as ToolId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { ToolIcon } from "@app/components/shared/ToolIcon";
import { ToolRegistryEntry } from "@app/data/toolsTaxonomy";
import { useToolNavigation } from "@app/hooks/useToolNavigation";
import { handleUnlessSpecialClick } from "@app/utils/clickHandlers";
import { openUrl } from "@app/utils/urlUtils";
import FitText from "@app/components/shared/FitText";
import { useHotkeys } from "@app/contexts/HotkeyContext";
import HotkeyDisplay from "@app/components/hotkeys/HotkeyDisplay";
import FavoriteStar from "@app/components/tools/toolPicker/FavoriteStar";
import FavoriteStar from "@app/components/tools/toolpicker/FavoriteStar";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This shouldn't have been changed. It'll only work on case-insensitive OSs

import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import type { ToolId } from "@app/types/toolId";
import {
Expand Down Expand Up @@ -80,7 +81,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({
}
if (tool.link) {
// Open external link in new tab
window.open(tool.link, "_blank", "noopener,noreferrer");
openUrl(tool.link, "_blank", "noopener,noreferrer");
return;
}
// Normal tool selection
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/core/components/viewer/BookmarkSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import LocalIcon from "@app/components/shared/LocalIcon";
import { useViewer } from "@app/contexts/ViewerContext";
import { PdfBookmarkObject, PdfActionType } from "@embedpdf/models";
import { openUrl } from "@app/utils/urlUtils";
import BookmarksIcon from "@mui/icons-material/BookmarksRounded";
import "@app/components/viewer/SidebarBase.css";
import "@app/components/viewer/BookmarkSidebar.css";
Expand Down Expand Up @@ -330,12 +331,12 @@ export const BookmarkSidebar = ({
const action = target.action;
if (action.type === PdfActionType.URI && action.uri) {
event.preventDefault();
window.open(action.uri, "_blank", "noopener");
openUrl(action.uri, "_blank", "noopener");
return;
}
if (action.type === PdfActionType.LaunchAppOrOpenFile && action.path) {
event.preventDefault();
window.open(action.path, "_blank", "noopener");
openUrl(action.path, "_blank", "noopener");
return;
}
}
Expand Down
25 changes: 11 additions & 14 deletions frontend/src/core/components/viewer/LinkLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PdfActionType,
type PdfLinkAnnoObject,
} from "@embedpdf/models";
import { openUrl, isSafeUrlProtocol } from "@app/utils/urlUtils";

// ---------------------------------------------------------------------------
// Inline SVG icons (thin-stroke, modern)
Expand Down Expand Up @@ -310,18 +311,13 @@ export const LinkLayer: React.FC<LinkLayerProps> = ({
});
} else if (action.type === PdfActionType.URI) {
const uri = action.uri;
try {
const url = new URL(uri, window.location.href);
if (["http:", "https:", "mailto:"].includes(url.protocol)) {
window.open(uri, "_blank", "noopener,noreferrer");
} else {
console.warn(
"[LinkLayer] Blocked unsafe URL protocol:",
url.protocol,
);
}
} catch {
window.open(uri, "_blank", "noopener,noreferrer");
if (isSafeUrlProtocol(uri)) {
openUrl(uri, "_blank", "noopener,noreferrer");
} else {
console.warn(
"[LinkLayer] Blocked unsafe URL protocol:",
uri,
);
}
}
}
Expand Down Expand Up @@ -362,8 +358,8 @@ export const LinkLayer: React.FC<LinkLayerProps> = ({
return (
<React.Fragment key={annotationLink.id}>
{/* Hit-area overlay */}
<a
href="#"
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
Expand All @@ -384,6 +380,7 @@ export const LinkLayer: React.FC<LinkLayerProps> = ({
role="link"
tabIndex={0}
aria-label={getLinkLabel(annotationLink)}
title={getLinkLabel(annotationLink)}
/>

{/* Floating toolbar */}
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/core/tools/formFill/FormFieldOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
useFieldValue,
} from "@app/tools/formFill/FormFillContext";
import { useViewer } from "@app/contexts/ViewerContext";
import { openUrl } from "@app/utils/urlUtils";
import type {
FormField,
WidgetCoordinates,
Expand Down Expand Up @@ -87,7 +88,7 @@ function executePdfJs(
try {
const u = new URL(url);
if (["http:", "https:", "mailto:"].includes(u.protocol)) {
window.open(url, "_blank", "noopener,noreferrer");
openUrl(url, "_blank", "noopener,noreferrer");
}
} catch {
/* invalid URL — ignore */
Expand Down Expand Up @@ -667,7 +668,7 @@ export function FormFieldOverlay({
try {
const u = new URL(url);
if (["http:", "https:", "mailto:"].includes(u.protocol)) {
window.open(url, "_blank", "noopener,noreferrer");
openUrl(url, "_blank", "noopener,noreferrer");
}
} catch {
/* invalid URL */
Expand Down
59 changes: 59 additions & 0 deletions frontend/src/core/utils/urlUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Utility functions for handling URLs in both web and Tauri environments
*/

/**
* Check if we're running in a Tauri desktop environment
* @returns True if running in Tauri
*/
export function isTauriEnvironment(): boolean {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please review frontend/DeveloperGuide.md to see how to design desktop fixes

return typeof window !== 'undefined' &&
(window as any).__TAURI__ !== undefined;
}

/**
* Open a URL in the appropriate way for the current environment
* @param url - The URL to open
* @param target - The target window (default: "_blank")
* @param features - Window features (default: "noopener,noreferrer")
*/
export function openUrl(
url: string,
target: string = "_blank",
features: string = "noopener,noreferrer"
): void {
// Check if we're in Tauri environment
if (isTauriEnvironment()) {
// Use void to avoid returning Promise from this function
void (async () => {
try {
// Dynamically import Tauri shell plugin only in Tauri environment
const { open } = await import('@tauri-apps/plugin-shell');
await open(url);
return;
} catch (error) {
console.warn('[urlUtils] Failed to open URL with Tauri shell, falling back to window.open:', error);
// Fall back to window.open if Tauri fails
window.open(url, target, features);
}
})();
} else {
// Use standard window.open for web environment
window.open(url, target, features);
}
}

/**
* Check if a URL protocol is safe to open
* @param url - The URL to check
* @returns True if the URL protocol is safe (http, https, mailto)
*/
export function isSafeUrlProtocol(url: string): boolean {
try {
const urlObj = new URL(url, window.location.href);
return ['http:', 'https:', 'mailto:'].includes(urlObj.protocol);
} catch {
// If URL parsing fails, assume it's unsafe
return false;
}
}
Loading