/* * Copyright (C) 2024-present Puter Technologies Inc. * * This file is part of Puter. * * Puter is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ /** * Links that open a shared item: the dashboard's Files tab, on Shared, with the * item highlighted. Not derived from `ResolvedShare.path`: that is masked for * the requester, and the issuer owns the entry, so it comes back as the owner's * real path — which mailing would leak. */ /** The query parameter the GUI routes on. */ export const SHARE_DEEP_LINK_PARAM = 'shared'; export interface ShareTarget { /** The entry's own name, which the masked path's last segment must be. */ name: string; /** The entry's uuid. */ uid: string; /** Whose entry it is. */ ownerUsername: string; } /** The owner out of either form of share path; both name it first. */ export const ownerFromSharePath = (path: string): string | null => { if (typeof path !== 'string') return null; const owner = path.split('/')[1]; return owner ? owner : null; }; /** * `///`, built without a request context so every reader * gets the same. See `sharePathMask.ts` for how it is read back. */ export const maskedSharePath = (target: ShareTarget): string | null => { const { name, uid, ownerUsername } = target; if (!name || !uid || !ownerUsername) return null; return `/${ownerUsername}/${uid}/${name}`; }; /** * Items one link will highlight, at most. Past this the link still opens * Shared, just without picking the rest out. */ export const SHARE_DEEP_LINK_ITEMS_LIMIT = 20; /** * How long a link may run, in characters. Somewhere past two thousand, older * mail clients cut a URL off or stop making it clickable — and this is the * button — so items are added only while the whole link stays within this. The * first item goes in regardless: a link that names nothing is no better than * the origin, and one long name (hundreds of characters, tripled by encoding * when non-ASCII) is still the item the mail is about. */ export const SHARE_DEEP_LINK_MAX_LENGTH = 2000; /** * A link that opens the recipient's Shared view with `paths` highlighted, once * they are signed in. Only masked paths travel — each one's second segment is * the uuid, so a rename is recoverable and there is no second copy to disagree * with the first. With no paths the link still lands on Shared. */ export const sharedViewLink = (origin: string, paths: string[]): string => { const base = `${origin.replace(/\/+$/, '')}/?`; // The first items that fit, in order — never a later one over an // earlier, so what is highlighted reads as the top of the list. const params: string[] = []; let length = base.length; for (const path of new Set(paths)) { if (params.length === SHARE_DEEP_LINK_ITEMS_LIMIT) break; const param = `${SHARE_DEEP_LINK_PARAM}=${encodeURIComponent(path)}`; const added = param.length + (params.length === 0 ? 0 : '&'.length); const overLength = length + added > SHARE_DEEP_LINK_MAX_LENGTH; if (params.length > 0 && overLength) break; params.push(param); length += added; } return ( base + (params.length === 0 ? `${SHARE_DEEP_LINK_PARAM}=` : params.join('&')) ); }; /** A link that opens `path`: the Shared view with that one item highlighted. */ export const shareDeepLink = (origin: string, path: string): string => sharedViewLink(origin, [path]); /** The link for a target, or `null` when it isn't addressable. */ export const shareTargetLink = ( origin: string, target: ShareTarget, ): string | null => { const path = maskedSharePath(target); return path === null ? null : shareDeepLink(origin, path); };