mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 01:13:55 +00:00
1849b4d39f
* chore: migrate eslint to oxlint * chore: migrate prettier to oxfmt * chore: migrate typescript * chore: toThrow should have a expected value * chore: cast test value as Day * chore: small title fix * chore: mocks should be hoisted * chore: incorrect async useage * chore: test should be inside description * chore: test sohuld include an expeced * chore: oxfmt --------- Co-authored-by: alex-Arc <omnivox@LAPTOP-RC5SNBVV.localdomain>
211 lines
6.7 KiB
TypeScript
211 lines
6.7 KiB
TypeScript
import { OntimeView, OntimeViewPresettable, URLPreset } from 'ontime-types';
|
|
import { checkRegex } from 'ontime-utils';
|
|
import { Path, resolvePath } from 'react-router';
|
|
|
|
/**
|
|
* Validates a preset against defined parameters
|
|
* Used in the context of form validation
|
|
*/
|
|
export function validateUrlPresetPath(preset: string): { message: string; isValid: boolean } {
|
|
if (preset === '' || preset == null) {
|
|
return { isValid: false, message: 'Path cannot be empty' };
|
|
}
|
|
|
|
if (preset.includes('http') || preset.includes('https') || preset.includes('www')) {
|
|
return { isValid: false, message: 'Path should not include http, https, www' };
|
|
}
|
|
|
|
if (preset.includes('127.0.0.1') || preset.includes('localhost') || preset.includes('0.0.0.0')) {
|
|
return { isValid: false, message: 'Path should not include hostname' };
|
|
}
|
|
|
|
if (preset.includes('editor')) {
|
|
// no editor
|
|
return { isValid: false, message: 'No path to editor page allowed' };
|
|
}
|
|
|
|
return { isValid: true, message: 'ok' };
|
|
}
|
|
|
|
/**
|
|
* Checks whether the current location corresponds to a preset and returns the new path if necessary
|
|
*/
|
|
export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): string | null {
|
|
// if we're already on a preset path, no need to redirect
|
|
if (isPresetPath(location)) {
|
|
return null;
|
|
}
|
|
|
|
// NOTE: verify that this resolves correctly in cloud
|
|
const currentPath = `${location.pathname}${location.search}`.substring(1);
|
|
const currentURL = getCurrentPath(location);
|
|
const locationParams = new URLSearchParams(location.search);
|
|
const token = locationParams.get('token');
|
|
|
|
for (const preset of urlPresets) {
|
|
if (!preset.enabled) continue;
|
|
const presetParams = new URLSearchParams(preset.search);
|
|
const isLocked = locationParams.get('n') === '1' || presetParams.get('n') === '1';
|
|
/**
|
|
* If the page is a known alias it would be like
|
|
* /preset/{alias} <- locked to a preset
|
|
* or
|
|
* /{target}?alias={alias} <- unwrapped preset options
|
|
*
|
|
* we need to compare the saved preset to the current path to see if we need to redirect
|
|
*/
|
|
if (preset.alias === currentURL || preset.target === currentURL) {
|
|
const newPath = shouldMaskPresetPath(preset)
|
|
? generateMaskedPathFromPreset(preset.alias, isLocked, token)
|
|
: generatePathFromPreset(preset.target, preset.search, preset.alias, isLocked, token);
|
|
/**
|
|
* if the current path is equivalent to the new path, we return null
|
|
* this means we will not redirect
|
|
*/
|
|
return arePathsEquivalent(currentPath, newPath) ? null : newPath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Resolves the current path accounting for the base URI
|
|
* Returns the alias if it's a preset path, or the last segment otherwise
|
|
*/
|
|
export function getCurrentPath(location: Path): string {
|
|
// 1. get path without query parameters
|
|
const pathWithoutQuery = location.pathname.split('?')[0];
|
|
// 2. split path into segments and filter out empty segments
|
|
const segments = pathWithoutQuery.split('/').filter(Boolean);
|
|
|
|
// If this is a preset path, return the alias (last segment)
|
|
if (segments[0] === 'preset' && segments.length > 1) {
|
|
return segments[1];
|
|
}
|
|
|
|
// Otherwise return the last segment (view name)
|
|
return segments[segments.length - 1] || '';
|
|
}
|
|
|
|
/**
|
|
* Handles generating a path and search parameters from a preset
|
|
* This is done when we want to keep the current navigation and unwrap the search params
|
|
*/
|
|
export function generatePathFromPreset(
|
|
target: Omit<OntimeView, 'editor'>,
|
|
search: string,
|
|
alias: string,
|
|
locked: boolean,
|
|
token: string | null,
|
|
): string {
|
|
const path = resolvePath(`${target}?${search}`);
|
|
const searchParams = new URLSearchParams(path.search);
|
|
|
|
// save the alias so we have a reference to it being a preset and can update if necessary
|
|
searchParams.set('alias', alias);
|
|
|
|
// maintain params from the URL search feature
|
|
if (locked) {
|
|
searchParams.set('n', '1');
|
|
}
|
|
|
|
if (token) {
|
|
searchParams.set('token', token);
|
|
}
|
|
|
|
// return path concatenated without the leading slash
|
|
return `${path.pathname}?${searchParams}`.substring(1);
|
|
}
|
|
|
|
function generateMaskedPathFromPreset(alias: string, locked: boolean, token: string | null): string {
|
|
const searchParams = new URLSearchParams();
|
|
|
|
if (locked) {
|
|
searchParams.set('n', '1');
|
|
}
|
|
|
|
if (token) {
|
|
searchParams.set('token', token);
|
|
}
|
|
|
|
const path = `preset/${alias}`;
|
|
const search = searchParams.toString();
|
|
if (!search) {
|
|
return path;
|
|
}
|
|
|
|
return `${path}?${search}`;
|
|
}
|
|
|
|
function shouldMaskPresetPath(preset: URLPreset): boolean {
|
|
return preset.target === OntimeView.Cuesheet;
|
|
}
|
|
|
|
/**
|
|
* Utility checks if two paths are equivalent
|
|
* For preset paths, only compares the path (since params are stored in session)
|
|
* For regular paths, compares path and search params (ignoring token)
|
|
*/
|
|
export function arePathsEquivalent(currentPath: string, newPath: string): boolean {
|
|
const currentUrl = new URL(currentPath, document.location.origin);
|
|
const newUrl = new URL(newPath, document.location.origin);
|
|
|
|
if (currentUrl.pathname !== newUrl.pathname) {
|
|
return false;
|
|
}
|
|
|
|
// For preset paths, only n and token are meaningful — ignore everything else
|
|
if (currentUrl.pathname.startsWith('/preset/')) {
|
|
return (
|
|
currentUrl.searchParams.get('n') === newUrl.searchParams.get('n') &&
|
|
currentUrl.searchParams.get('token') === newUrl.searchParams.get('token')
|
|
);
|
|
}
|
|
|
|
// For regular paths, compare all search params except n and token
|
|
currentUrl.searchParams.delete('token');
|
|
currentUrl.searchParams.delete('n');
|
|
newUrl.searchParams.delete('token');
|
|
newUrl.searchParams.delete('n');
|
|
|
|
return currentUrl.searchParams.toString() === newUrl.searchParams.toString();
|
|
}
|
|
|
|
/**
|
|
* Generates a URL preset from a user given alias and URL.
|
|
*/
|
|
export function generateUrlPresetOptions(alias: string, userUrl: string): URLPreset {
|
|
let sanitisedUrl = userUrl;
|
|
// we need to ensure the URL has a protocol, but it doesnt matter which
|
|
if (!checkRegex.startsWithHttp(userUrl.toLowerCase())) {
|
|
sanitisedUrl = `http://${sanitisedUrl}`;
|
|
}
|
|
|
|
const url = new URL(sanitisedUrl);
|
|
const path = getCurrentPath(url);
|
|
|
|
if (!isPresettableView(path)) {
|
|
throw new Error(`Invalid target view: ${path}`);
|
|
}
|
|
|
|
return {
|
|
alias,
|
|
target: path,
|
|
search: url.searchParams.toString(),
|
|
enabled: true,
|
|
};
|
|
}
|
|
|
|
function isPresettableView(view: string): view is OntimeViewPresettable {
|
|
return view !== OntimeView.Editor && Object.values(OntimeView).includes(view as OntimeView);
|
|
}
|
|
|
|
/**
|
|
* Check if current location is a preset path
|
|
*/
|
|
export function isPresetPath(location: Path): boolean {
|
|
const segments = location.pathname.split('/').filter(Boolean);
|
|
return segments[0] === 'preset';
|
|
}
|