Skip to content

Commit 700fcb1

Browse files
authored
refactor(client): Meteor-less absoluteUrl() (#40324)
1 parent 0d34fc1 commit 700fcb1

10 files changed

Lines changed: 185 additions & 15 deletions

File tree

apps/meteor/app/ui/client/lib/recorderjs/AudioEncoder.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Emitter } from '@rocket.chat/emitter';
2-
import { Meteor } from 'meteor/meteor';
2+
3+
import { absoluteUrl } from '../../../../../client/lib/absoluteUrl';
34

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

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

1415
this.worker = new Worker(workerPath);
1516
this.worker.onmessage = this.handleWorkerMessage;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { absoluteUrl, _relativeToSiteRootUrl } from './absoluteUrl';
2+
3+
jest.mock('./baseURI', () => ({
4+
baseURI: 'http://localhost:3000/',
5+
}));
6+
7+
beforeEach(() => {
8+
absoluteUrl.defaultOptions = { rootUrl: 'http://localhost:3000/' };
9+
});
10+
11+
describe('absoluteUrl', () => {
12+
it('should return the root URL with a trailing slash when no path is given', () => {
13+
expect(absoluteUrl(undefined, { rootUrl: 'http://example.com' })).toBe('http://example.com/');
14+
});
15+
16+
it('should append the path to the root URL', () => {
17+
expect(absoluteUrl('foo/bar', { rootUrl: 'http://example.com' })).toBe('http://example.com/foo/bar');
18+
});
19+
20+
it('should strip leading slashes from the path', () => {
21+
expect(absoluteUrl('///foo', { rootUrl: 'http://example.com' })).toBe('http://example.com/foo');
22+
});
23+
24+
it('should prepend http:// when the rootUrl has no protocol', () => {
25+
expect(absoluteUrl(undefined, { rootUrl: 'example.com' })).toBe('http://example.com/');
26+
});
27+
28+
it('should preserve https:// when already present in rootUrl', () => {
29+
expect(absoluteUrl('path', { rootUrl: 'https://example.com' })).toBe('https://example.com/path');
30+
});
31+
32+
it('should throw when rootUrl is not provided and defaultOptions.rootUrl is unset', () => {
33+
absoluteUrl.defaultOptions = {};
34+
expect(() => absoluteUrl()).toThrow('Must pass options.rootUrl or set ROOT_URL in the server environment');
35+
});
36+
37+
it('should use defaultOptions.rootUrl when no rootUrl option is given', () => {
38+
absoluteUrl.defaultOptions = { rootUrl: 'http://default.example.com' };
39+
expect(absoluteUrl('test')).toBe('http://default.example.com/test');
40+
});
41+
42+
it('should upgrade http to https when secure is true and host is not localhost', () => {
43+
expect(absoluteUrl('path', { rootUrl: 'http://example.com', secure: true })).toBe('https://example.com/path');
44+
});
45+
46+
it('should not upgrade to https for localhost when secure is true', () => {
47+
expect(absoluteUrl('path', { rootUrl: 'http://localhost:3000', secure: true })).toBe('http://localhost:3000/path');
48+
});
49+
50+
it('should not upgrade to https for 127.0.0.1 when secure is true', () => {
51+
expect(absoluteUrl('path', { rootUrl: 'http://127.0.0.1:3000', secure: true })).toBe('http://127.0.0.1:3000/path');
52+
});
53+
54+
it('should replace localhost with 127.0.0.1 when replaceLocalhost is true', () => {
55+
expect(absoluteUrl('path', { rootUrl: 'http://localhost:3000', replaceLocalhost: true })).toBe('http://127.0.0.1:3000/path');
56+
});
57+
58+
it('should not replace localhost when replaceLocalhost is false', () => {
59+
expect(absoluteUrl('path', { rootUrl: 'http://localhost:3000', replaceLocalhost: false })).toBe('http://localhost:3000/path');
60+
});
61+
62+
it('should accept options as the first argument when path is omitted', () => {
63+
expect(absoluteUrl({ rootUrl: 'http://example.com' } as any)).toBe('http://example.com/');
64+
});
65+
66+
it('should not duplicate trailing slash', () => {
67+
expect(absoluteUrl(undefined, { rootUrl: 'http://example.com/' })).toBe('http://example.com/');
68+
});
69+
});
70+
71+
describe('_relativeToSiteRootUrl', () => {
72+
const originalConfig = (globalThis as any).__meteor_runtime_config__;
73+
74+
afterEach(() => {
75+
(globalThis as any).__meteor_runtime_config__ = originalConfig;
76+
});
77+
78+
it('should prepend ROOT_URL_PATH_PREFIX to links starting with /', () => {
79+
(globalThis as any).__meteor_runtime_config__ = { ROOT_URL_PATH_PREFIX: '/subdir' };
80+
expect(_relativeToSiteRootUrl('/route')).toBe('/subdir/route');
81+
});
82+
83+
it('should return the link unchanged when it does not start with /', () => {
84+
(globalThis as any).__meteor_runtime_config__ = { ROOT_URL_PATH_PREFIX: '/subdir' };
85+
expect(_relativeToSiteRootUrl('route')).toBe('route');
86+
});
87+
88+
it('should return the link unchanged when __meteor_runtime_config__ is not an object', () => {
89+
delete (globalThis as any).__meteor_runtime_config__;
90+
expect(_relativeToSiteRootUrl('/route')).toBe('/route');
91+
});
92+
93+
it('should handle empty ROOT_URL_PATH_PREFIX', () => {
94+
(globalThis as any).__meteor_runtime_config__ = { ROOT_URL_PATH_PREFIX: '' };
95+
expect(_relativeToSiteRootUrl('/route')).toBe('/route');
96+
});
97+
});
98+
99+
describe('defaultOptions', () => {
100+
it('should initialize rootUrl from baseURI', () => {
101+
expect(absoluteUrl.defaultOptions.rootUrl).toBe('http://localhost:3000/');
102+
});
103+
104+
it('should initialize secure from window.isSecureContext', () => {
105+
expect(absoluteUrl.defaultOptions.secure).toBe(window.isSecureContext);
106+
});
107+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// There is a good chance this module may be promoted to root lib/ in the future
2+
3+
import { baseURI } from './baseURI';
4+
5+
type AbsoluteUrlOptions = {
6+
rootUrl?: string;
7+
secure?: boolean;
8+
replaceLocalhost?: boolean;
9+
};
10+
11+
export function absoluteUrl(path?: string, options?: AbsoluteUrlOptions): string {
12+
if (!options && typeof path === 'object') {
13+
options = path;
14+
path = undefined;
15+
}
16+
17+
options = { ...absoluteUrl.defaultOptions, ...options };
18+
19+
let { rootUrl } = options;
20+
21+
if (!rootUrl) throw Error('Must pass options.rootUrl or set ROOT_URL in the server environment');
22+
23+
if (!/^http[s]?:\/\//i.test(rootUrl)) {
24+
rootUrl = `http://${rootUrl}`;
25+
}
26+
27+
if (!rootUrl.endsWith('/')) {
28+
rootUrl += '/';
29+
}
30+
31+
if (path) {
32+
while (path.startsWith('/')) path = path.slice(1);
33+
rootUrl += path;
34+
}
35+
36+
if (options.secure && /^http:/.test(rootUrl) && !/http:\/\/localhost[:/]/.test(rootUrl) && !/http:\/\/127\.0\.0\.1[:/]/.test(rootUrl)) {
37+
rootUrl = rootUrl.replace(/^http:/, 'https:');
38+
}
39+
40+
if (options.replaceLocalhost) {
41+
rootUrl = rootUrl.replace(/^http:\/\/localhost([:/].*)/, 'http://127.0.0.1$1');
42+
}
43+
44+
return rootUrl;
45+
}
46+
47+
absoluteUrl.defaultOptions = {
48+
rootUrl: baseURI,
49+
secure: window.isSecureContext,
50+
} as AbsoluteUrlOptions;
51+
52+
export function _relativeToSiteRootUrl(link: string): string {
53+
if (typeof __meteor_runtime_config__ === 'object' && link.slice(0, 1) === '/') {
54+
link = (__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '') + link;
55+
}
56+
57+
return link;
58+
}

apps/meteor/client/lib/openCASLoginPopup.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { Meteor } from 'meteor/meteor';
2-
1+
import { absoluteUrl } from './absoluteUrl';
32
import { settings } from './settings';
43

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

35-
const appUrl = Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
34+
const appUrl = absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX;
3635
const serviceUrl = `${appUrl}/_cas/${credentialToken}`;
3736
const url = new URL(loginUrl);
3837
url.searchParams.set('service', serviceUrl);

apps/meteor/client/lib/rooms/roomCoordinator.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { IRoom, RoomType, IUser, AtLeast, ValueOf, ISubscription } from '@rocket.chat/core-typings';
22
import type { RouteName } from '@rocket.chat/ui-contexts';
3-
import { Meteor } from 'meteor/meteor';
43

54
import { hasPermission } from '../../../app/authorization/client';
65
import type {
@@ -17,6 +16,7 @@ import { router } from '../../providers/RouterProvider';
1716
import { Subscriptions } from '../../stores';
1817
import RoomRoute from '../../views/room/RoomRoute';
1918
import MainLayout from '../../views/root/MainLayout';
19+
import { absoluteUrl } from '../absoluteUrl';
2020
import { appLayout } from '../appLayout';
2121

2222
class RoomCoordinatorClient extends RoomCoordinator {
@@ -203,7 +203,7 @@ class RoomCoordinatorClient extends RoomCoordinator {
203203
return false;
204204
}
205205

206-
return Meteor.absoluteUrl(
206+
return absoluteUrl(
207207
router.buildRoutePath({
208208
name: config.route.name,
209209
params: routeData,

apps/meteor/client/meteor/login/saml.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Accounts } from 'meteor/accounts-base';
33
import { Meteor } from 'meteor/meteor';
44

55
import { type LoginCallback, callLoginMethod, handleLogin } from '../../lib/2fa/overrideLoginMethod';
6+
import { absoluteUrl } from '../../lib/absoluteUrl';
67
import { settings } from '../../lib/settings';
78

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

7778
// A nasty bounce: 'result' has the SAML LogoutRequest but we need a proper 302 to redirected from the server.
78-
window.location.replace(Meteor.absoluteUrl(`_saml/sloRedirect/${provider}/?redirect=${encodeURIComponent(result)}`));
79+
window.location.replace(absoluteUrl(`_saml/sloRedirect/${provider}/?redirect=${encodeURIComponent(result)}`));
7980
})
8081
.catch(() => logout.apply(Meteor));
8182
return;

apps/meteor/client/meteor/login/twitter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Twitter } from 'meteor/twitter-oauth';
88

99
import { createOAuthTotpLoginMethod } from './oauth';
1010
import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod';
11+
import { absoluteUrl } from '../../lib/absoluteUrl';
1112
import { wrapRequestCredentialFn } from '../../lib/wrapRequestCredentialFn';
1213

1314
const { loginWithTwitter } = Meteor;
@@ -45,7 +46,7 @@ Twitter.requestCredential = wrapRequestCredentialFn<TwitterOAuthConfiguration>(
4546
});
4647
}
4748

48-
const loginUrl = Meteor.absoluteUrl(loginPath);
49+
const loginUrl = absoluteUrl(loginPath);
4950

5051
OAuth.launchLogin({
5152
loginService: 'twitter',
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { Meteor } from 'meteor/meteor';
22

3-
import { baseURI } from '../../lib/baseURI';
3+
import { _relativeToSiteRootUrl, absoluteUrl } from '../../lib/absoluteUrl';
44

5-
Meteor.absoluteUrl.defaultOptions.rootUrl = baseURI;
5+
Object.assign(Meteor, {
6+
absoluteUrl,
7+
_relativeToSiteRootUrl,
8+
});

apps/meteor/client/providers/ServerProvider.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ import { useMemo, type ReactNode } from 'react';
1717
import { sdk } from '../../app/utils/client/lib/SDKClient';
1818
import { Info as info } from '../../app/utils/rocketchat.info';
1919
import { useReactiveValue } from '../hooks/useReactiveValue';
20-
21-
const absoluteUrl = (path: string): string => Meteor.absoluteUrl(path);
20+
import { absoluteUrl } from '../lib/absoluteUrl';
2221

2322
const callMethod = <MethodName extends ServerMethodName>(
2423
methodName: MethodName,
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useSetting } from '@rocket.chat/ui-contexts';
2-
import { Meteor } from 'meteor/meteor';
32
import { useEffect } from 'react';
43

4+
import { absoluteUrl } from '../../../lib/absoluteUrl';
5+
56
export const useCorsSSLConfig = () => {
67
const forceSSlSetting = useSetting('Force_SSL');
78

89
useEffect(() => {
9-
Meteor.absoluteUrl.defaultOptions.secure = Boolean(forceSSlSetting);
10+
absoluteUrl.defaultOptions.secure = Boolean(forceSSlSetting);
1011
}, [forceSSlSetting]);
1112
};

0 commit comments

Comments
 (0)