Skip to content

Commit e380c3a

Browse files
committed
Handles ambiguous slash URLs in web paths
Addresses an issue where URLs with a slash but without a leading "@" could be misinterpreted. Implements a new `resolveSlashUrl` function to handle cases such as malformed channel URLs (missing "@" prefix) and names with claim IDs (using "/" instead of ":"). This change ensures proper resolution and redirection for these ambiguous URLs, improving user experience and preventing broken links, particularly when shared on platforms like Grok and Twitter. Removes now-unnecessary code related to correcting channel paths within the `withResolvedClaimRender` higher-order component and lbryURI.js. Prioritizes full claim ID resolution Improves slash URL resolution by prioritizing the name#claimid interpretation when the second part of the URL is a 40-character hexadecimal string (full claim ID). This prevents misinterpretation of content names that might coincidentally look like claim IDs and ensures that content can be reliably resolved.
1 parent c8685fc commit e380c3a

File tree

5 files changed

+114
-175
lines changed

5 files changed

+114
-175
lines changed

ui/hocs/withResolvedClaimRender/view.jsx

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// @flow
22
import React from 'react';
3-
import { useHistory, useLocation } from 'react-router-dom';
43

54
import Spinner from 'component/spinner';
65
import Button from 'component/button';
@@ -34,47 +33,6 @@ type Props = {
3433
doOpenModal: (string, {}) => void,
3534
};
3635

37-
/**
38-
* Checks if a web URL path might be missing the @ prefix for the channel name.
39-
* Returns a corrected path with @ if applicable, or null if not applicable.
40-
* This handles cases like "/channel:id/content:id" which should be "/@channel:id/content:id"
41-
* (e.g., malformed URLs from Grok/Twitter that omit the @)
42-
*/
43-
function getCorrectedChannelPath(pathname: string): ?string {
44-
try {
45-
// Remove leading slash
46-
const webPath = pathname.startsWith('/') ? pathname.slice(1) : pathname;
47-
48-
// Must have a slash with content after it (channel/content pattern)
49-
const slashIndex = webPath.indexOf('/');
50-
if (slashIndex === -1 || slashIndex === webPath.length - 1) {
51-
return null;
52-
}
53-
54-
const firstPart = webPath.substring(0, slashIndex);
55-
const secondPart = webPath.substring(slashIndex + 1);
56-
57-
// First part should not already start with @
58-
if (firstPart.startsWith('@')) {
59-
return null;
60-
}
61-
62-
// Both parts should have content (non-empty name before any modifiers)
63-
// Modifiers in web URLs use : instead of #
64-
const firstNameMatch = firstPart.match(/^[^#:$*]+/);
65-
const secondNameMatch = secondPart.match(/^[^#:$*]+/);
66-
67-
if (!firstNameMatch || !firstNameMatch[0] || !secondNameMatch || !secondNameMatch[0]) {
68-
return null;
69-
}
70-
71-
// Return the corrected path with @ prefix
72-
return `/@${webPath}`;
73-
} catch (e) {
74-
return null;
75-
}
76-
}
77-
7836
const withResolvedClaimRender = (ClaimRenderComponent: FunctionalComponentParam) => {
7937
const ResolvedClaimRender = (props: Props) => {
8038
const {
@@ -102,19 +60,8 @@ const withResolvedClaimRender = (ClaimRenderComponent: FunctionalComponentParam)
10260
...otherProps
10361
} = props;
10462

105-
const { replace: historyReplace } = useHistory();
106-
const location = useLocation();
10763
const { streamName, /* channelName, */ isChannel } = parseURI(uri);
10864

109-
// Check if the browser URL is missing @ prefix and redirect if needed
110-
// This handles malformed URLs from Grok/Twitter like "/channel:id/content:id"
111-
const correctedPath = getCorrectedChannelPath(location.pathname);
112-
React.useEffect(() => {
113-
if (correctedPath) {
114-
historyReplace(correctedPath + location.search + location.hash);
115-
}
116-
}, [correctedPath, historyReplace, location.search, location.hash]);
117-
11865
const claimIsRestricted = !claimIsMine && (geoRestriction !== null || isClaimBlackListed || isClaimFiltered);
11966

12067
const resolveRequired =
@@ -158,11 +105,6 @@ const withResolvedClaimRender = (ClaimRenderComponent: FunctionalComponentParam)
158105
}
159106
}, [resolveRequired, resolveClaim]);
160107

161-
// If URL needs correction, show loading while redirect happens
162-
if (correctedPath) {
163-
return <LoadingSpinner text={__('Resolving...')} />;
164-
}
165-
166108
if (!hasClaim) {
167109
if (hasClaim === undefined) {
168110
return <LoadingSpinner text={__('Resolving...')} />;

web/src/html.js

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ const {
2121
const { fetchStreamUrl } = require('./fetchStreamUrl');
2222
const { lbryProxy: Lbry } = require('../lbry');
2323
const { getHomepageJsonV1 } = require('./getHomepageJSON');
24-
const { buildURI, parseURI, normalizeClaimUrl, getCorrectedChannelWebPath } = require('./lbryURI');
24+
const { buildURI, parseURI, normalizeClaimUrl } = require('./lbryURI');
25+
const { resolveSlashUrl } = require('./resolveSlashUrl');
2526
const fs = require('fs');
2627
const PAGES = require('../../ui/constants/pages');
2728
const path = require('path');
@@ -665,16 +666,6 @@ async function getHtml(ctx) {
665666
let parsedUri, claimUri;
666667
let pathToUse = requestPath.slice(1);
667668

668-
// Check if URL is missing @ prefix (e.g., malformed URLs from Grok/Twitter)
669-
const correctedPath = getCorrectedChannelWebPath(pathToUse);
670-
if (correctedPath) {
671-
// Redirect to the corrected URL with @ prefix, preserving query string
672-
// Use encodeURI to handle special characters like parentheses
673-
const queryString = ctx.querystring ? `?${ctx.querystring}` : '';
674-
ctx.redirect(encodeURI(`/${correctedPath}`) + queryString);
675-
return;
676-
}
677-
678669
try {
679670
parsedUri = parseURI(normalizeClaimUrl(pathToUse));
680671
claimUri = buildURI({ ...parsedUri, startTime: undefined }, true);
@@ -683,7 +674,36 @@ async function getHtml(ctx) {
683674
return err.message;
684675
}
685676

686-
const claim = await resolveClaimOrRedirect(ctx, claimUri);
677+
// If the path has "/" without "@", parseURI drops the second segment
678+
// (e.g., "Creator/video" → lbry://Creator). Try alternate interpretations first.
679+
let claim;
680+
const resolved = await resolveSlashUrl(pathToUse);
681+
682+
if (resolved) {
683+
const queryString = ctx.querystring ? `?${ctx.querystring}` : '';
684+
685+
if (resolved.type === 'channel') {
686+
// Malformed channel URL — redirect to the corrected @-prefixed path
687+
ctx.redirect(encodeURI(`/@${pathToUse}`) + queryString);
688+
return;
689+
}
690+
691+
if (resolved.type === 'claimid') {
692+
// name/claimid — redirect to canonical URL so the client-side app works
693+
const canonicalPath = resolved.claim.canonical_url?.replace('lbry://', '').replace(/#/g, ':');
694+
if (canonicalPath) {
695+
ctx.redirect(`/${canonicalPath}` + queryString);
696+
return;
697+
}
698+
claim = resolved.claim;
699+
claimUri = resolved.uri;
700+
}
701+
}
702+
703+
if (!claim) {
704+
claim = await resolveClaimOrRedirect(ctx, claimUri);
705+
}
706+
687707
const referrerQuery = escapeHtmlProperty(getParameterByName('r', ctx.request.url));
688708

689709
if (claim) {

web/src/lbryURI.js

Lines changed: 0 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -359,95 +359,6 @@ function normalizeClaimUrl(url) {
359359
return normalizeURI(url.replace(/:/g, '#'));
360360
}
361361

362-
/**
363-
* Checks if a URI might be missing the @ prefix for the channel name.
364-
* Returns a corrected URI with @ if applicable, or null if not applicable.
365-
* This handles cases like "channel:id/content:id" which should be "@channel:id/content:id"
366-
* (e.g., malformed URLs from Grok/Twitter that omit the @)
367-
*/
368-
function getCorrectedChannelUri(uri) {
369-
try {
370-
// Check if the URI has a path separator (indicating channel/content pattern)
371-
// but doesn't have a channel name (meaning no @ was detected)
372-
const parsed = parseURI(uri);
373-
374-
// If it already has a channelName, the @ is present - no correction needed
375-
if (parsed.channelName) {
376-
return null;
377-
}
378-
379-
// Check if the raw URI contains a "/" followed by more content (channel/content pattern)
380-
// Remove lbry:// protocol for checking
381-
const uriWithoutProtocol = uri.replace(/^lbry:\/\//, '');
382-
const slashIndex = uriWithoutProtocol.indexOf('/');
383-
384-
// Must have a slash with content after it (channel/content pattern)
385-
if (slashIndex === -1 || slashIndex === uriWithoutProtocol.length - 1) {
386-
return null;
387-
}
388-
389-
const firstPart = uriWithoutProtocol.substring(0, slashIndex);
390-
const secondPart = uriWithoutProtocol.substring(slashIndex + 1);
391-
392-
// First part should not already start with @
393-
if (firstPart.startsWith('@')) {
394-
return null;
395-
}
396-
397-
// Both parts should have content (non-empty after any modifiers are stripped)
398-
const firstNameMatch = firstPart.match(/^[^#:$*]+/);
399-
const secondNameMatch = secondPart.match(/^[^#:$*]+/);
400-
401-
if (!firstNameMatch || !firstNameMatch[0] || !secondNameMatch || !secondNameMatch[0]) {
402-
return null;
403-
}
404-
405-
// Build the corrected URI with @ prefix
406-
return `lbry://@${uriWithoutProtocol}`;
407-
} catch (e) {
408-
return null;
409-
}
410-
}
411-
412-
/**
413-
* Checks if a web URL path might be missing the @ prefix for the channel name.
414-
* Returns a corrected path with @ if applicable, or null if not applicable.
415-
* This works on raw web paths BEFORE they are parsed into lbry:// URIs.
416-
*
417-
* Example: "JustMe:05/content:0" -> "@JustMe:05/content:0"
418-
*/
419-
function getCorrectedChannelWebPath(webPath) {
420-
try {
421-
// Must have a slash with content after it (channel/content pattern)
422-
const slashIndex = webPath.indexOf('/');
423-
if (slashIndex === -1 || slashIndex === webPath.length - 1) {
424-
return null;
425-
}
426-
427-
const firstPart = webPath.substring(0, slashIndex);
428-
const secondPart = webPath.substring(slashIndex + 1);
429-
430-
// First part should not already start with @
431-
if (firstPart.startsWith('@')) {
432-
return null;
433-
}
434-
435-
// Both parts should have content (non-empty name before any modifiers)
436-
// Modifiers in web URLs use : instead of #
437-
const firstNameMatch = firstPart.match(/^[^#:$*]+/);
438-
const secondNameMatch = secondPart.match(/^[^#:$*]+/);
439-
440-
if (!firstNameMatch || !firstNameMatch[0] || !secondNameMatch || !secondNameMatch[0]) {
441-
return null;
442-
}
443-
444-
// Return the corrected path with @ prefix
445-
return `@${webPath}`;
446-
} catch (e) {
447-
return null;
448-
}
449-
}
450-
451362
module.exports = {
452363
parseURI,
453364
buildURI,
@@ -459,6 +370,4 @@ module.exports = {
459370
isURIClaimable,
460371
splitBySeparator,
461372
convertToShareLink,
462-
getCorrectedChannelUri,
463-
getCorrectedChannelWebPath,
464373
};

web/src/oEmbed.js

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const {
99
escapeHtmlProperty,
1010
} = require('../../ui/util/web');
1111
const { lbryProxy: Lbry } = require('../lbry');
12-
const { getCorrectedChannelWebPath } = require('./lbryURI');
12+
const { resolveSlashUrl } = require('./resolveSlashUrl');
1313

1414
Lbry.setDaemonConnectionString(PROXY_URL);
1515

@@ -18,22 +18,26 @@ Lbry.setDaemonConnectionString(PROXY_URL);
1818
// ****************************************************************************
1919

2020
async function getClaim(requestUrl) {
21-
// Extract the path from the URL and check for missing @ prefix
22-
let webPath = requestUrl.replace(`${URL}/`, '');
23-
const correctedPath = getCorrectedChannelWebPath(webPath);
24-
if (correctedPath) {
25-
webPath = correctedPath;
26-
}
27-
21+
const webPath = requestUrl.replace(`${URL}/`, '');
2822
const uri = `lbry://${webPath}`;
2923

3024
let claim, error;
31-
try {
32-
const response = await Lbry.resolve({ urls: [uri] });
33-
if (response && response[uri] && !response[uri].error) {
34-
claim = response[uri];
35-
}
36-
} catch {}
25+
26+
// Try alternate interpretations for ambiguous slash URLs (missing @, name/claimid)
27+
const resolved = await resolveSlashUrl(webPath);
28+
if (resolved) {
29+
claim = resolved.claim;
30+
}
31+
32+
// Fall back to resolving as-is (handles @channel/content, single-segment, etc.)
33+
if (!claim) {
34+
try {
35+
const response = await Lbry.resolve({ urls: [uri] });
36+
if (response && response[uri] && !response[uri].error) {
37+
claim = response[uri];
38+
}
39+
} catch {}
40+
}
3741

3842
if (!claim) {
3943
error = 'The URL is invalid or is not associated with any claim.';

web/src/resolveSlashUrl.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const { lbryProxy: Lbry } = require('../lbry');
2+
3+
/**
4+
* Resolves ambiguous URLs that have "/" without "@".
5+
*
6+
* In LBRY URLs, "/" is always a channel/content separator (requiring @).
7+
* When we see "foo/bar" without "@", it could be:
8+
* 1) A name with claim ID: foo:bar (someone used "/" instead of ":")
9+
* 2) A malformed channel URL: @foo/bar (missing @ prefix, e.g., from Grok/Twitter)
10+
*
11+
* Priority:
12+
* - If second part is exactly 40 hex chars (full claim ID), try name:claimid first
13+
* - Otherwise, try @channel/content first
14+
* - Falls back to the other interpretation if the first doesn't resolve
15+
*
16+
* @param {string} webPath - The web path without leading slash (e.g., "Creator/video-title")
17+
* @returns {Promise<{ claim: Object, uri: string, type: 'channel'|'claimid' } | null>}
18+
*/
19+
async function resolveSlashUrl(webPath) {
20+
const slashIdx = webPath.indexOf('/');
21+
if (slashIdx <= 0 || webPath.startsWith('@')) {
22+
return null;
23+
}
24+
25+
const secondPart = webPath.substring(slashIdx + 1);
26+
const isHexClaimId = /^[0-9a-f]+$/.test(secondPart) && secondPart.length <= 40;
27+
const isFullClaimId = isHexClaimId && secondPart.length === 40;
28+
29+
// If the second part is exactly 40 hex chars (full claim ID), prioritize name:claimid
30+
// interpretation since content names don't look like "ca00cbe17cb76...".
31+
if (isFullClaimId) {
32+
try {
33+
const fixedUri = `lbry://${webPath.substring(0, slashIdx)}#${secondPart}`;
34+
const response = await Lbry.resolve({ urls: [fixedUri] });
35+
if (response && response[fixedUri] && !response[fixedUri].error) {
36+
return { claim: response[fixedUri], uri: fixedUri, type: 'claimid' };
37+
}
38+
} catch {}
39+
}
40+
41+
// Try as @channel/content (malformed URLs from Grok/Twitter)
42+
try {
43+
const withAtUri = `lbry://@${webPath}`;
44+
const response = await Lbry.resolve({ urls: [withAtUri] });
45+
if (response && response[withAtUri] && !response[withAtUri].error) {
46+
return { claim: response[withAtUri], uri: withAtUri, type: 'channel' };
47+
}
48+
} catch {}
49+
50+
// Try as name#claimid (partial claim ID, or full ID that didn't resolve above)
51+
if (isHexClaimId && !isFullClaimId) {
52+
try {
53+
const fixedUri = `lbry://${webPath.substring(0, slashIdx)}#${secondPart}`;
54+
const response = await Lbry.resolve({ urls: [fixedUri] });
55+
if (response && response[fixedUri] && !response[fixedUri].error) {
56+
return { claim: response[fixedUri], uri: fixedUri, type: 'claimid' };
57+
}
58+
} catch {}
59+
}
60+
61+
return null;
62+
}
63+
64+
module.exports = { resolveSlashUrl };

0 commit comments

Comments
 (0)