Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c11bd3b8e | ||
|
|
906a034e26 | ||
|
|
b855eb505d | ||
|
|
cc8e7444f6 | ||
|
|
d7ea408fd0 | ||
|
|
d3f01b5116 | ||
|
|
f84c0d6e9e | ||
|
|
a6c7333ea3 | ||
|
|
f0b7400d1b | ||
|
|
a86947be48 | ||
|
|
b6f9af3f58 |
Generated
+4
-4
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "4.13.0",
|
"@axe-core/playwright": "4.13.0",
|
||||||
"@lotusguild/element-call-embedded": "0.25.0-lotus.22",
|
"@lotusguild/element-call-embedded": "0.25.0-lotus.23",
|
||||||
"@playwright/test": "1.63.0",
|
"@playwright/test": "1.63.0",
|
||||||
"@rollup/plugin-inject": "5.0.5",
|
"@rollup/plugin-inject": "5.0.5",
|
||||||
"@rollup/plugin-wasm": "6.2.2",
|
"@rollup/plugin-wasm": "6.2.2",
|
||||||
@@ -2695,9 +2695,9 @@
|
|||||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||||
},
|
},
|
||||||
"node_modules/@lotusguild/element-call-embedded": {
|
"node_modules/@lotusguild/element-call-embedded": {
|
||||||
"version": "0.25.0-lotus.22",
|
"version": "0.25.0-lotus.23",
|
||||||
"resolved": "https://code.lotusguild.org/api/packages/LotusGuild/npm/%40lotusguild%2Felement-call-embedded/-/0.25.0-lotus.22/element-call-embedded-0.25.0-lotus.22.tgz",
|
"resolved": "https://code.lotusguild.org/api/packages/LotusGuild/npm/%40lotusguild%2Felement-call-embedded/-/0.25.0-lotus.23/element-call-embedded-0.25.0-lotus.23.tgz",
|
||||||
"integrity": "sha512-AimDKZA4xUKEEyl2XUFUSUDl03tBKFv88T/3igKyH2VqNSPMGnqcQJC5HXIkHLTWrOmg8kLcJ//FFbqlUZiLcA==",
|
"integrity": "sha512-vRmcWeJcU0Accyl2x3mqNEzrIe8djE5BekyYbYOSCsMflC98rZgD1mO7KOe7bA1cX7gparRf6sAt1fotHoU+bA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
||||||
|
|||||||
+1
-1
@@ -108,7 +108,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "4.13.0",
|
"@axe-core/playwright": "4.13.0",
|
||||||
"@lotusguild/element-call-embedded": "0.25.0-lotus.22",
|
"@lotusguild/element-call-embedded": "0.25.0-lotus.23",
|
||||||
"@playwright/test": "1.63.0",
|
"@playwright/test": "1.63.0",
|
||||||
"@rollup/plugin-inject": "5.0.5",
|
"@rollup/plugin-inject": "5.0.5",
|
||||||
"@rollup/plugin-wasm": "6.2.2",
|
"@rollup/plugin-wasm": "6.2.2",
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ export function AccountDataEditor({
|
|||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
<IconButton type="button" onClick={requestClose} variant="Surface" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
|
|||||||
Setup Device Verification
|
Setup Device Verification
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
<IconButton type="button" size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
@@ -361,7 +361,7 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
|
|||||||
Reset Device Verification
|
Reset Device Verification
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
<IconButton type="button" size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -139,8 +139,11 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
|
|||||||
data.allowFederation,
|
data.allowFederation,
|
||||||
data.additionalCreators,
|
data.additionalCreators,
|
||||||
),
|
),
|
||||||
power_level_content_override:
|
// [Gitea #328] Every non-space room gets call.member at user level, not just
|
||||||
data.type === RoomType.Call ? createVoiceRoomPowerLevelsOverride() : undefined,
|
// voice rooms: the call button is shown in private group rooms too, and with
|
||||||
|
// Synapse's state_default (50) only admins could start or join a call there
|
||||||
|
// ("You don't have permission to join!").
|
||||||
|
power_level_content_override: createVoiceRoomPowerLevelsOverride(),
|
||||||
initial_state: initialState,
|
initial_state: initialState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
GIF_PREVIEW_MAX_WIDTH,
|
GIF_PREVIEW_MAX_WIDTH,
|
||||||
gifPreviewBox,
|
gifPreviewBox,
|
||||||
} from '../../utils/gifPreviewSize';
|
} from '../../utils/gifPreviewSize';
|
||||||
|
import { sameProvider } from './embedProvider';
|
||||||
|
|
||||||
const linkStyles = { color: color.Success.Main };
|
const linkStyles = { color: color.Success.Main };
|
||||||
|
|
||||||
@@ -2497,7 +2498,10 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
|||||||
const resolveEmbed = (prev: IPreviewUrlResponse): MediaEmbed | null => {
|
const resolveEmbed = (prev: IPreviewUrlResponse): MediaEmbed | null => {
|
||||||
if (embed) return embed;
|
if (embed) return embed;
|
||||||
const ogUrl = prev['og:url'];
|
const ogUrl = prev['og:url'];
|
||||||
return typeof ogUrl === 'string' && ogUrl !== url
|
// [Gitea #326] …but only from an og:url on the posted link's own
|
||||||
|
// provider: the page sets og:url, so any site could otherwise show up
|
||||||
|
// as a playable YouTube (etc.) card that links to itself.
|
||||||
|
return typeof ogUrl === 'string' && ogUrl !== url && sameProvider(url, ogUrl)
|
||||||
? parseMediaEmbed(ogUrl, window.location.hostname)
|
? parseMediaEmbed(ogUrl, window.location.hostname)
|
||||||
: null;
|
: null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { sameProvider } from './embedProvider';
|
||||||
|
|
||||||
|
test("sameProvider: a page cannot borrow another provider's embed via og:url (#326)", () => {
|
||||||
|
const yt = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
|
||||||
|
assert.equal(sameProvider('https://evil.example/free-gift', yt), false);
|
||||||
|
assert.equal(sameProvider('https://youtube.com.evil.example/x', yt), false);
|
||||||
|
assert.equal(sameProvider('https://youtu.be/abc', yt), true);
|
||||||
|
assert.equal(sameProvider('https://m.youtube.com/shorts/abc', yt), true);
|
||||||
|
assert.equal(
|
||||||
|
sameProvider('https://vm.tiktok.com/ZM123/', 'https://www.tiktok.com/@a/video/1'),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(sameProvider('https://spotify.link/abc', 'https://open.spotify.com/track/1'), true);
|
||||||
|
assert.equal(sameProvider('https://on.soundcloud.com/x', 'https://soundcloud.com/a/b'), true);
|
||||||
|
assert.equal(
|
||||||
|
sameProvider('https://redd.it/abc', 'https://www.reddit.com/r/x/comments/abc/'),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(sameProvider('not a url', yt), false);
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* [Gitea #326] A page's og:url is chosen by the page itself, so a phishing
|
||||||
|
* link could serve `og:url = youtube.com/watch?v=…` and render as a playable
|
||||||
|
* YouTube card linking to the attacker. Only trust an og:url on the same
|
||||||
|
* provider as the link actually posted (including the provider's own
|
||||||
|
* short-link domains).
|
||||||
|
*/
|
||||||
|
const PROVIDER_ALIASES: Record<string, string> = {
|
||||||
|
'youtu.be': 'youtube.com',
|
||||||
|
'youtube-nocookie.com': 'youtube.com',
|
||||||
|
'spotify.link': 'spotify.com',
|
||||||
|
'redd.it': 'reddit.com',
|
||||||
|
'x.com': 'twitter.com',
|
||||||
|
'twitch.tv': 'twitch.tv',
|
||||||
|
'apple.co': 'apple.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure: the provider a host belongs to (last two labels, aliases folded). Hosts
|
||||||
|
* under two-part public suffixes (`*.co.uk`) share a value, which is harmless
|
||||||
|
* here: no embeddable provider lives under one.
|
||||||
|
*/
|
||||||
|
export const providerOf = (host: string): string => {
|
||||||
|
const parts = host.toLowerCase().replace(/\.$/, '').split('.');
|
||||||
|
const base = parts.slice(-2).join('.');
|
||||||
|
return PROVIDER_ALIASES[base] ?? base;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Pure: whether `ogUrl` is on the same provider as the posted `url`. */
|
||||||
|
export function sameProvider(url: string, ogUrl: string): boolean {
|
||||||
|
try {
|
||||||
|
return providerOf(new URL(url).hostname) === providerOf(new URL(ogUrl).hostname);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -275,7 +275,7 @@ export function StateEventEditor({ type, stateKey, requestClose }: StateEventEdi
|
|||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
<IconButton type="button" onClick={requestClose} variant="Surface" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export function PollCreator({ roomId, threadRootId, onClose }: PollCreatorProps)
|
|||||||
Create Poll
|
Create Poll
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={onClose} aria-label="Close">
|
<IconButton type="button" size="300" radii="300" onClick={onClose} aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export function ReportRoomModal({ roomId, onClose }: ReportRoomModalProps) {
|
|||||||
Report Room
|
Report Room
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close">
|
<IconButton type="button" size="300" onClick={onClose} radii="300" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function ReportUserModal({ userId, onClose }: ReportUserModalProps) {
|
|||||||
Report User
|
Report User
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close">
|
<IconButton type="button" size="300" onClick={onClose} radii="300" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ export function ScheduleMessageModal({
|
|||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={onClose} aria-label="Close">
|
<IconButton type="button" size="300" radii="300" onClick={onClose} aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -725,7 +725,13 @@ export const MessageDeleteItem = as<
|
|||||||
Delete Message
|
Delete Message
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={handleClose} radii="300" aria-label="Close">
|
<IconButton
|
||||||
|
type="button"
|
||||||
|
size="300"
|
||||||
|
onClick={handleClose}
|
||||||
|
radii="300"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
@@ -865,7 +871,13 @@ export const MessageReportItem = as<
|
|||||||
Report Message
|
Report Message
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" onClick={handleClose} radii="300" aria-label="Close">
|
<IconButton
|
||||||
|
type="button"
|
||||||
|
size="300"
|
||||||
|
onClick={handleClose}
|
||||||
|
radii="300"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { verifyDenoiseAssets } from './denoiseSmokeCheck';
|
|||||||
import { canDelegateCapability } from './utils';
|
import { canDelegateCapability } from './utils';
|
||||||
import { getCallPageUrl } from './callPageUrl';
|
import { getCallPageUrl } from './callPageUrl';
|
||||||
import { restrictWidgetMessages } from '../widgetTransport';
|
import { restrictWidgetMessages } from '../widgetTransport';
|
||||||
|
import { isRingableRoom } from '../../utils/callRing';
|
||||||
|
|
||||||
// Maximum time to wait for the embedded Element Call iframe to progress from
|
// Maximum time to wait for the embedded Element Call iframe to progress from
|
||||||
// initial load to a ready/joined state. If it hasn't by then, we assume the
|
// initial load to a ready/joined state. If it hasn't by then, we assume the
|
||||||
@@ -251,8 +252,18 @@ export class CallEmbed {
|
|||||||
verifyDenoiseAssets(denoiseModel).catch(() => undefined);
|
verifyDenoiseAssets(denoiseModel).catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Gitea #295] Only ask Element Call to notify where a call can ring at all
|
||||||
|
// (DMs, invite-only groups outside a space). In a voice channel or space
|
||||||
|
// room the notification carries an @room mention, so every member got a
|
||||||
|
// highlighted ping (and phone push) whenever a moderator hopped in, and
|
||||||
|
// other clients showed it as a missed call.
|
||||||
|
// `none` needs element-call-embedded ≥ 0.25.0-lotus.23 (older builds ignore it).
|
||||||
if (CallEmbed.startingCall(intent)) {
|
if (CallEmbed.startingCall(intent)) {
|
||||||
params.append('sendNotificationType', CallEmbed.dmCall(intent) ? 'ring' : 'notification');
|
let notify = 'none';
|
||||||
|
if (isRingableRoom(room, CallEmbed.dmCall(intent))) {
|
||||||
|
notify = CallEmbed.dmCall(intent) ? 'ring' : 'notification';
|
||||||
|
}
|
||||||
|
params.append('sendNotificationType', notify);
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Gitea #43] On its own origin when config.json sets elementCallUrl.
|
// [Gitea #43] On its own origin when config.json sets elementCallUrl.
|
||||||
|
|||||||
Reference in New Issue
Block a user