Skip to content
Merged
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
41 changes: 41 additions & 0 deletions apps/mobile/src/lib/navigation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import { HOME, normalizeUrl } from './navigation';

describe('normalizeUrl', () => {
it('returns home for blank input', () => {
expect(normalizeUrl(' ')).toBe(HOME);
});

it('keeps valid HTTP(S) URLs', () => {
expect(normalizeUrl('https://example.com/path?q=1')).toBe(
'https://example.com/path?q=1',
);
expect(normalizeUrl('http://localhost:3000/health')).toBe(
'http://localhost:3000/health',
);
});

it('promotes a complete domain to HTTPS', () => {
expect(normalizeUrl('docs.example.com/path')).toBe(
'https://docs.example.com/path',
);
});

it('searches ordinary text', () => {
expect(normalizeUrl('privacy first browser')).toBe(
'https://duckduckgo.com/?q=privacy%20first%20browser',
);
});

it('searches unsupported schemes instead of loading them', () => {
expect(normalizeUrl('javascript:alert(1)')).toBe(
'https://duckduckgo.com/?q=javascript%3Aalert(1)',
);
});

it('does not treat domain-looking text with spaces as a URL', () => {
expect(normalizeUrl('example.com malicious suffix')).toBe(
'https://duckduckgo.com/?q=example.com%20malicious%20suffix',
);
});
});
41 changes: 41 additions & 0 deletions apps/mobile/src/lib/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const HOME = 'https://tronbrowser.dev';

const DOMAIN_OR_IP =
/^(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}|(?:\d{1,3}\.){3}\d{1,3}|localhost)(?::\d{1,5})?(?:[/?#][^\s]*)?$/i;

function searchUrl(query: string): string {
return `https://duckduckgo.com/?q=${encodeURIComponent(query)}`;
}

/**
* Convert address-bar input into a safe, loadable URL.
*
* Only explicit HTTP(S) URLs and complete domain/IP inputs are navigated to.
* Everything else, including unsupported schemes and domain-looking text with
* spaces, becomes a search query instead of reaching the WebView as a URL.
*/
export function normalizeUrl(input: string): string {
const trimmed = input.trim();
if (!trimmed) return HOME;

if (/^https?:\/\//i.test(trimmed)) {
try {
const parsed = new URL(trimmed);
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
? parsed.toString()
: searchUrl(trimmed);
} catch {
return searchUrl(trimmed);
}
}

if (DOMAIN_OR_IP.test(trimmed)) {
try {
return new URL(`https://${trimmed}`).toString();
} catch {
return searchUrl(trimmed);
}
}

return searchUrl(trimmed);
}
12 changes: 1 addition & 11 deletions apps/mobile/src/screens/BrowserScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
View,
} from 'react-native';
import { WebView } from 'react-native-webview';
import { HOME, normalizeUrl } from '../lib/navigation';
import { theme } from '../theme';

/**
Expand All @@ -19,17 +20,6 @@ import { theme } from '../theme';
* Ungoogled Chromium engine (see docs/mobile-architecture.md — the engine ships
* via the native Android build and the Linux-phone desktop build, not Expo).
*/
const HOME = 'https://tronbrowser.dev';

function normalizeUrl(input: string): string {
const trimmed = input.trim();
if (!trimmed) return HOME;
if (/^https?:\/\//i.test(trimmed)) return trimmed;
// A bare domain-looking string → https; otherwise treat as a search query.
if (/^[\w-]+(\.[\w-]+)+/.test(trimmed)) return `https://${trimmed}`;
return `https://duckduckgo.com/?q=${encodeURIComponent(trimmed)}`;
}

export function BrowserScreen() {
const webRef = useRef<WebView>(null);
const [address, setAddress] = useState(HOME);
Expand Down
Loading