mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-27 04:46:42 +00:00
Follow up #37327. See the comments. * Root problem: the design of OAuth2 providers is a mess, the display name is used as provider's name and used in the URL directly * The regressions: * When trying to fix https://github.com/go-gitea/gitea/issues/36409 , it introduced inconsistent URL escaping for the "path" part. * This fix: always use "path escaping" for the path part, add more tests to cover all escaping cases. Now, frontend "pathEscape" and "pathEscapeSegments" generate exactly the same result as backend.
76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
export function urlQueryEscape(s: string) {
|
|
// See "TestQueryEscape" in backend
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
|
|
return encodeURIComponent(s).replace(
|
|
/[!'()*]/g,
|
|
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
).replaceAll('%20', '+');
|
|
}
|
|
|
|
export function pathEscape(s: string): string {
|
|
// See "TestPathEscape" in backend
|
|
return encodeURIComponent(s).replace(
|
|
/[!'()*]/g,
|
|
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
).replaceAll(/%(\w\w)/g, (v) => {
|
|
switch (v) {
|
|
case '%24': return '$';
|
|
case '%26': return '&';
|
|
case '%2B': return '+';
|
|
case '%3A': return ':';
|
|
case '%3D': return '=';
|
|
case '%40': return '@';
|
|
default: return v;
|
|
}
|
|
});
|
|
}
|
|
|
|
export function pathEscapeSegments(s: string): string {
|
|
// The same as backend's PathEscapeSegments
|
|
return s.split('/').map(pathEscape).join('/');
|
|
}
|
|
|
|
// Match HTML tags (to skip) or URLs (to linkify) in HTML content
|
|
const urlLinkifyPattern = /(<([-\w]+)[^>]*>)|(<\/([-\w]+)[^>]*>)|(https?:\/\/[^\s<>"'`|(){}[\]]+)/gi;
|
|
const trailingPunctPattern = /[.,;:!?]+$/;
|
|
|
|
// Convert URLs to clickable links in HTML, preserving existing HTML tags
|
|
export function linkifyURLs(html: string): string {
|
|
let inAnchor = false;
|
|
return html.replace(urlLinkifyPattern, (match, _openTagFull, openTag, _closeTagFull, closeTag, url) => {
|
|
// skip URLs inside existing <a> tags
|
|
if (openTag === 'a') {
|
|
inAnchor = true;
|
|
return match;
|
|
} else if (closeTag === 'a') {
|
|
inAnchor = false;
|
|
return match;
|
|
}
|
|
if (inAnchor || !url) {
|
|
return match;
|
|
}
|
|
|
|
const trailingPunct = url.match(trailingPunctPattern);
|
|
const cleanUrl = trailingPunct ? url.slice(0, -trailingPunct[0].length) : url;
|
|
const trailing = trailingPunct ? trailingPunct[0] : '';
|
|
// safe because regexp only matches valid URLs (no quotes or angle brackets)
|
|
return `<a href="${cleanUrl}" target="_blank">${cleanUrl}</a>${trailing}`; // eslint-disable-line github/unescaped-html-literal
|
|
});
|
|
}
|
|
|
|
/** Convert an absolute or relative URL to an absolute URL with the current origin. It only
|
|
* processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'. */
|
|
export function toOriginUrl(urlStr: string) {
|
|
try {
|
|
if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {
|
|
const {origin, protocol, hostname, port} = window.location;
|
|
const url = new URL(urlStr, origin);
|
|
url.protocol = protocol;
|
|
url.hostname = hostname;
|
|
url.port = port || (protocol === 'https:' ? '443' : '80');
|
|
return url.toString();
|
|
}
|
|
} catch {}
|
|
return urlStr;
|
|
}
|