release: prepare August 1 Android RC

This commit is contained in:
key
2026-07-29 00:35:15 +08:00
parent 9ae5f750c8
commit 9ea5d79a62
41 changed files with 2322 additions and 71 deletions
+81
View File
@@ -0,0 +1,81 @@
export type LegalDocumentKind = 'terms' | 'privacy';
const placeholderPattern = /__|change[-_ ]?me|your[-_ ]?|example\.(com|org|net)/i;
const reservedHostnamePattern = /(?:^|\.)(?:example|invalid|test)$/i;
const ipv4Parts = (hostname: string) => {
const parts = hostname.split('.').map(Number);
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
return null;
}
return parts;
};
const isReservedIpv4 = (parts: number[]) => {
return parts[0] === 0
|| parts[0] === 10
|| parts[0] === 127
|| (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127)
|| (parts[0] === 169 && parts[1] === 254)
|| (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
|| (parts[0] === 192 && parts[1] === 168)
|| (parts[0] === 192 && parts[1] === 0 && (parts[2] === 0 || parts[2] === 2))
|| (parts[0] === 198 && (parts[1] === 18 || parts[1] === 19))
|| (parts[0] === 198 && parts[1] === 51 && parts[2] === 100)
|| (parts[0] === 203 && parts[1] === 0 && parts[2] === 113)
|| parts[0] >= 224;
};
const normalizePublicHttpsUrl = (rawValue?: string) => {
const value = String(rawValue || '').trim();
if (!value || placeholderPattern.test(value)) return '';
if (/[\u0000-\u0020\u007f]/.test(value)) return '';
const match = /^https:\/\/([^/?#]+)(?:[/?#]|$)/i.exec(value);
if (!match || match[1].includes('@')) return '';
const authority = match[1];
let hostname = '';
if (authority.startsWith('[')) {
const end = authority.indexOf(']');
if (end <= 1 || (authority.slice(end + 1) && !/^:\d+$/.test(authority.slice(end + 1)))) return '';
hostname = authority.slice(1, end).toLowerCase();
} else {
const hostAndPort = authority.toLowerCase();
const colon = hostAndPort.lastIndexOf(':');
if (colon >= 0) {
if (!/^\d+$/.test(hostAndPort.slice(colon + 1)) || hostAndPort.slice(0, colon).includes(':')) return '';
hostname = hostAndPort.slice(0, colon);
} else {
hostname = hostAndPort;
}
}
const numericIpv4 = /^[0-9.]+$/.test(hostname);
const parsedIpv4 = numericIpv4 ? ipv4Parts(hostname) : null;
if (
!hostname
|| reservedHostnamePattern.test(hostname)
|| hostname === 'localhost'
|| hostname.endsWith('.localhost')
|| hostname === '::1'
|| (numericIpv4 && (!parsedIpv4 || isReservedIpv4(parsedIpv4)))
|| (hostname.includes(':') && (!/^[0-9a-f:]+$/i.test(hostname) || /^f[cd]/.test(hostname) || hostname.startsWith('fe80:')))
|| (!numericIpv4 && !hostname.includes(':') && (
!hostname.includes('.')
|| !/^[a-z0-9.-]+$/i.test(hostname)
|| hostname.startsWith('.')
|| hostname.endsWith('.')
))
) return '';
return value;
};
const termsUrl = normalizePublicHttpsUrl(import.meta.env.VITE_APP_TERMS_URL);
const privacyUrl = normalizePublicHttpsUrl(import.meta.env.VITE_APP_PRIVACY_URL);
export const legalLinksReady = Boolean(termsUrl && privacyUrl && termsUrl !== privacyUrl);
export const legalDocumentUrl = (kind: LegalDocumentKind) => {
if (!legalLinksReady) return '';
return kind === 'terms' ? termsUrl : privacyUrl;
};