Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Emitter } from '@rocket.chat/emitter';
import { Meteor } from 'meteor/meteor';

import { absoluteUrl } from '../../../../../client/lib/absoluteUrl';

export class AudioEncoder extends Emitter {
private worker: Worker;
Expand All @@ -9,7 +10,7 @@ export class AudioEncoder extends Emitter {
constructor(source: MediaStreamAudioSourceNode, { bufferLen = 4096, numChannels = 1, bitRate = 32 } = {}) {
super();

const workerPath = Meteor.absoluteUrl('workers/mp3-encoder/index.js');
const workerPath = absoluteUrl('workers/mp3-encoder/index.js');

this.worker = new Worker(workerPath);
this.worker.onmessage = this.handleWorkerMessage;
Expand Down
107 changes: 107 additions & 0 deletions apps/meteor/client/lib/absoluteUrl.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { absoluteUrl, _relativeToSiteRootUrl } from './absoluteUrl';

jest.mock('./baseURI', () => ({
baseURI: 'http://localhost:3000/',
}));

beforeEach(() => {
absoluteUrl.defaultOptions = { rootUrl: 'http://localhost:3000/' };
});

describe('absoluteUrl', () => {
it('should return the root URL with a trailing slash when no path is given', () => {
expect(absoluteUrl(undefined, { rootUrl: 'http://example.com' })).toBe('http://example.com/');
});

it('should append the path to the root URL', () => {
expect(absoluteUrl('foo/bar', { rootUrl: 'http://example.com' })).toBe('http://example.com/foo/bar');
});

it('should strip leading slashes from the path', () => {
expect(absoluteUrl('///foo', { rootUrl: 'http://example.com' })).toBe('http://example.com/foo');
});

it('should prepend http:// when the rootUrl has no protocol', () => {
expect(absoluteUrl(undefined, { rootUrl: 'example.com' })).toBe('http://example.com/');
});

it('should preserve https:// when already present in rootUrl', () => {
expect(absoluteUrl('path', { rootUrl: 'https://example.com' })).toBe('https://example.com/path');
});

it('should throw when rootUrl is not provided and defaultOptions.rootUrl is unset', () => {
absoluteUrl.defaultOptions = {};
expect(() => absoluteUrl()).toThrow('Must pass options.rootUrl or set ROOT_URL in the server environment');
});

it('should use defaultOptions.rootUrl when no rootUrl option is given', () => {
absoluteUrl.defaultOptions = { rootUrl: 'http://default.example.com' };
expect(absoluteUrl('test')).toBe('http://default.example.com/test');
});

it('should upgrade http to https when secure is true and host is not localhost', () => {
expect(absoluteUrl('path', { rootUrl: 'http://example.com', secure: true })).toBe('https://example.com/path');
});

it('should not upgrade to https for localhost when secure is true', () => {
expect(absoluteUrl('path', { rootUrl: 'http://localhost:3000', secure: true })).toBe('http://localhost:3000/path');
});

it('should not upgrade to https for 127.0.0.1 when secure is true', () => {
expect(absoluteUrl('path', { rootUrl: 'http://127.0.0.1:3000', secure: true })).toBe('http://127.0.0.1:3000/path');
});

it('should replace localhost with 127.0.0.1 when replaceLocalhost is true', () => {
expect(absoluteUrl('path', { rootUrl: 'http://localhost:3000', replaceLocalhost: true })).toBe('http://127.0.0.1:3000/path');
});

it('should not replace localhost when replaceLocalhost is false', () => {
expect(absoluteUrl('path', { rootUrl: 'http://localhost:3000', replaceLocalhost: false })).toBe('http://localhost:3000/path');
});

it('should accept options as the first argument when path is omitted', () => {
expect(absoluteUrl({ rootUrl: 'http://example.com' } as any)).toBe('http://example.com/');
});

it('should not duplicate trailing slash', () => {
expect(absoluteUrl(undefined, { rootUrl: 'http://example.com/' })).toBe('http://example.com/');
});
});

describe('_relativeToSiteRootUrl', () => {
const originalConfig = (globalThis as any).__meteor_runtime_config__;

afterEach(() => {
(globalThis as any).__meteor_runtime_config__ = originalConfig;
});

it('should prepend ROOT_URL_PATH_PREFIX to links starting with /', () => {
(globalThis as any).__meteor_runtime_config__ = { ROOT_URL_PATH_PREFIX: '/subdir' };
expect(_relativeToSiteRootUrl('/route')).toBe('/subdir/route');
});

it('should return the link unchanged when it does not start with /', () => {
(globalThis as any).__meteor_runtime_config__ = { ROOT_URL_PATH_PREFIX: '/subdir' };
expect(_relativeToSiteRootUrl('route')).toBe('route');
});

it('should return the link unchanged when __meteor_runtime_config__ is not an object', () => {
delete (globalThis as any).__meteor_runtime_config__;
expect(_relativeToSiteRootUrl('/route')).toBe('/route');
});

it('should handle empty ROOT_URL_PATH_PREFIX', () => {
(globalThis as any).__meteor_runtime_config__ = { ROOT_URL_PATH_PREFIX: '' };
expect(_relativeToSiteRootUrl('/route')).toBe('/route');
});
});

describe('defaultOptions', () => {
it('should initialize rootUrl from baseURI', () => {
expect(absoluteUrl.defaultOptions.rootUrl).toBe('http://localhost:3000/');
});

it('should initialize secure from window.isSecureContext', () => {
expect(absoluteUrl.defaultOptions.secure).toBe(window.isSecureContext);
});
});
58 changes: 58 additions & 0 deletions apps/meteor/client/lib/absoluteUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// There is a good chance this module may be promoted to root lib/ in the future

import { baseURI } from './baseURI';

type AbsoluteUrlOptions = {
rootUrl?: string;
secure?: boolean;
replaceLocalhost?: boolean;
};

export function absoluteUrl(path?: string, options?: AbsoluteUrlOptions): string {
if (!options && typeof path === 'object') {
options = path;
path = undefined;
}

options = { ...absoluteUrl.defaultOptions, ...options };

let { rootUrl } = options;

if (!rootUrl) throw Error('Must pass options.rootUrl or set ROOT_URL in the server environment');

if (!/^http[s]?:\/\//i.test(rootUrl)) {
rootUrl = `http://${rootUrl}`;
}

if (!rootUrl.endsWith('/')) {
rootUrl += '/';
}

if (path) {
while (path.startsWith('/')) path = path.slice(1);
rootUrl += path;
}

if (options.secure && /^http:/.test(rootUrl) && !/http:\/\/localhost[:/]/.test(rootUrl) && !/http:\/\/127\.0\.0\.1[:/]/.test(rootUrl)) {
rootUrl = rootUrl.replace(/^http:/, 'https:');
}

if (options.replaceLocalhost) {
rootUrl = rootUrl.replace(/^http:\/\/localhost([:/].*)/, 'http://127.0.0.1$1');
}

return rootUrl;
}

absoluteUrl.defaultOptions = {
rootUrl: baseURI,
secure: window.isSecureContext,
} as AbsoluteUrlOptions;

export function _relativeToSiteRootUrl(link: string): string {
if (typeof __meteor_runtime_config__ === 'object' && link.slice(0, 1) === '/') {
link = (__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '') + link;
}

return link;
}
5 changes: 2 additions & 3 deletions apps/meteor/client/lib/openCASLoginPopup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Meteor } from 'meteor/meteor';

import { absoluteUrl } from './absoluteUrl';
import { settings } from './settings';

const openCenteredPopup = (url: string, width: number, height: number) => {
Expand Down Expand Up @@ -32,7 +31,7 @@ const getPopupUrl = (credentialToken: string): string => {
throw new Error('CAS_login_url not set');
}

const appUrl = Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
const appUrl = absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
const serviceUrl = `${appUrl}/_cas/${credentialToken}`;
const url = new URL(loginUrl);
url.searchParams.set('service', serviceUrl);
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/lib/rooms/roomCoordinator.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { IRoom, RoomType, IUser, AtLeast, ValueOf, ISubscription } from '@rocket.chat/core-typings';
import type { RouteName } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';

import { hasPermission } from '../../../app/authorization/client';
import type {
Expand All @@ -17,6 +16,7 @@ import { router } from '../../providers/RouterProvider';
import { Subscriptions } from '../../stores';
import RoomRoute from '../../views/room/RoomRoute';
import MainLayout from '../../views/root/MainLayout';
import { absoluteUrl } from '../absoluteUrl';
import { appLayout } from '../appLayout';

class RoomCoordinatorClient extends RoomCoordinator {
Expand Down Expand Up @@ -203,7 +203,7 @@ class RoomCoordinatorClient extends RoomCoordinator {
return false;
}

return Meteor.absoluteUrl(
return absoluteUrl(
router.buildRoutePath({
name: config.route.name,
params: routeData,
Expand Down
3 changes: 2 additions & 1 deletion apps/meteor/client/meteor/login/saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';

import { type LoginCallback, callLoginMethod, handleLogin } from '../../lib/2fa/overrideLoginMethod';
import { absoluteUrl } from '../../lib/absoluteUrl';
import { settings } from '../../lib/settings';

declare module 'meteor/meteor' {
Expand Down Expand Up @@ -75,7 +76,7 @@ Meteor.logout = async function (...args) {
Accounts.storageLocation.removeItem(Accounts.USER_ID_KEY);

// A nasty bounce: 'result' has the SAML LogoutRequest but we need a proper 302 to redirected from the server.
window.location.replace(Meteor.absoluteUrl(`_saml/sloRedirect/${provider}/?redirect=${encodeURIComponent(result)}`));
window.location.replace(absoluteUrl(`_saml/sloRedirect/${provider}/?redirect=${encodeURIComponent(result)}`));
})
.catch(() => logout.apply(Meteor));
return;
Expand Down
3 changes: 2 additions & 1 deletion apps/meteor/client/meteor/login/twitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Twitter } from 'meteor/twitter-oauth';

import { createOAuthTotpLoginMethod } from './oauth';
import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod';
import { absoluteUrl } from '../../lib/absoluteUrl';
import { wrapRequestCredentialFn } from '../../lib/wrapRequestCredentialFn';

const { loginWithTwitter } = Meteor;
Expand Down Expand Up @@ -45,7 +46,7 @@ Twitter.requestCredential = wrapRequestCredentialFn<TwitterOAuthConfiguration>(
});
}

const loginUrl = Meteor.absoluteUrl(loginPath);
const loginUrl = absoluteUrl(loginPath);

OAuth.launchLogin({
loginService: 'twitter',
Expand Down
7 changes: 5 additions & 2 deletions apps/meteor/client/meteor/startup/absoluteUrl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Meteor } from 'meteor/meteor';

import { baseURI } from '../../lib/baseURI';
import { _relativeToSiteRootUrl, absoluteUrl } from '../../lib/absoluteUrl';

Meteor.absoluteUrl.defaultOptions.rootUrl = baseURI;
Object.assign(Meteor, {
absoluteUrl,
_relativeToSiteRootUrl,
});
3 changes: 1 addition & 2 deletions apps/meteor/client/providers/ServerProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import { useMemo, type ReactNode } from 'react';
import { sdk } from '../../app/utils/client/lib/SDKClient';
import { Info as info } from '../../app/utils/rocketchat.info';
import { useReactiveValue } from '../hooks/useReactiveValue';

const absoluteUrl = (path: string): string => Meteor.absoluteUrl(path);
import { absoluteUrl } from '../lib/absoluteUrl';
Comment thread
tassoevan marked this conversation as resolved.

const callMethod = <MethodName extends ServerMethodName>(
methodName: MethodName,
Expand Down
5 changes: 3 additions & 2 deletions apps/meteor/client/views/root/hooks/useCorsSSLConfig.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useSetting } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';
import { useEffect } from 'react';

import { absoluteUrl } from '../../../lib/absoluteUrl';

export const useCorsSSLConfig = () => {
const forceSSlSetting = useSetting('Force_SSL');

useEffect(() => {
Meteor.absoluteUrl.defaultOptions.secure = Boolean(forceSSlSetting);
absoluteUrl.defaultOptions.secure = Boolean(forceSSlSetting);
}, [forceSSlSetting]);
};
Loading