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: 5 additions & 0 deletions .changeset/emoji-zwj-tag-sequences.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/message-parser': patch
---

Fixes an issue in which some combined emojis like 😶‍🌫️, 😮‍💨 and 😵‍💫 were being displayed as two separate emojis, and the flags of some countries like England 🏴󠁧󠁢󠁥󠁮󠁧󠁿, Scotland 🏴󠁧󠁢󠁳󠁣󠁴󠁿 and Wales 🏴󠁧󠁢󠁷󠁬󠁳󠁿 were being displayed as a plain black flag
6 changes: 6 additions & 0 deletions .changeset/empty-garlics-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/core-typings': patch
'@rocket.chat/meteor': patch
---

Fixes the password policy allowing a maximum length lower than the minimum length to be saved — a combination that made it impossible to set any valid password. The server now rejects such configurations when password policy settings are saved and shows an error explaining the constraint.
6 changes: 6 additions & 0 deletions .changeset/offline-license-no-egress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/meteor': minor
'@rocket.chat/license': minor
---

Adds support for the `offline` license flag, suppressing every outbound connection to Rocket.Chat Cloud services and the Push Gateway at its source, so air-gapped workspaces never initiate calls that would violate their security compliance.
5 changes: 5 additions & 0 deletions .changeset/ready-taxis-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Restores Away as a selectable preset in the quick status menu, custom status modal, and account profile page.
6 changes: 6 additions & 0 deletions .changeset/strict-find-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/meteor': patch
'@rocket.chat/model-typings': patch
---

Fixes broken pagination on `rooms.bannedUsers` and on omnichannel department listing endpoints, which ignored the `offset` parameter and always returned results from the first page. Also reduces payload over-fetching on several endpoints that unintentionally loaded full documents (`rooms.hide`, private group lookups, direct email replies, omnichannel auto-transfer), and removes the permissive model query typings that allowed these invalid find options to compile unnoticed.
4 changes: 4 additions & 0 deletions .github/workflows/ci-test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,10 @@ jobs:
if: failure()
run: docker compose -f docker-compose-ci.yml logs mongo

- name: Show traefik logs if E2E test failed
if: failure()
run: docker compose -f docker-compose-ci.yml logs traefik

- name: Store coverage
if: inputs.coverage == matrix.mongodb-version
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
15 changes: 12 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ on:
- '**.md'

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
# merge_group runs group by the merge-group ref so a recreated group cancels the stale run;
# push/release runs group by run_id on purpose (never cancel a deploy in progress)
group: ${{ github.workflow }}-${{ github.head_ref || (github.event_name == 'merge_group' && github.ref) || github.run_id }}
cancel-in-progress: true

env:
Expand Down Expand Up @@ -155,6 +157,7 @@ jobs:
name: 🚀 Notify external services - draft
runs-on: ubuntu-24.04-arm
needs: [release-versions]
if: github.event_name == 'release'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
Expand Down Expand Up @@ -195,7 +198,7 @@ jobs:

packages-build:
name: 📦 Build Packages
needs: [release-versions, notify-draft-services]
needs: [release-versions]
runs-on: ubuntu-24.04-arm
steps:
- name: Cache build
Expand Down Expand Up @@ -327,12 +330,18 @@ jobs:
type:
# if running in a PR build with coverage
- ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}
exclude:
# when images aren't published (merge queue, fork PRs) only amd64 tars are uploaded
# as artifacts, so arm64 builds would be discarded — skip them
- arch: ${{ (github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'arm64' || '' }}
include:
# if not, build with coverage for tests
- arch: amd64
service: [rocketchat]
type: coverage
- arch: arm64
# resolves to amd64 when arm64 is excluded above, matching the entry before this
# one instead of re-adding an arm64 job (includes bypass exclude)
- arch: ${{ (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && 'arm64' || 'amd64' }}
service: [rocketchat]
type: coverage

Expand Down
9 changes: 2 additions & 7 deletions apps/meteor/client/components/UserStatusMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,16 @@ const UserStatusMenu = ({

const statuses: Array<[value: UserStatusType, label: ReactNode]> = [
[UserStatusType.ONLINE, renderOption(UserStatusType.ONLINE, t('Online'))],
[UserStatusType.AWAY, renderOption(UserStatusType.AWAY, t('Away'))],
[UserStatusType.BUSY, renderOption(UserStatusType.BUSY, t('Busy'))],
];

// Away is no longer manually selectable, but surface it if the user is currently on it
// (e.g., set in a previous version or auto-applied by the server) so they can switch off.
if (status === UserStatusType.AWAY) {
statuses.push([UserStatusType.AWAY, renderOption(UserStatusType.AWAY, t('Away'))]);
}

if (allowInvisibleStatus) {
statuses.push([UserStatusType.OFFLINE, renderOption(UserStatusType.OFFLINE, t('Offline'))]);
}

return statuses;
}, [t, allowInvisibleStatus, status]);
}, [t, allowInvisibleStatus]);

const [cursor, handleKeyDown, handleKeyUp, reset, [visible, hide, show]] = useCursor(-1, options, ([selected], [, hide]) => {
setStatus(selected);
Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/lib/getUserInitialStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export const getUserStatusInitialValues = (user: IUser | null, initialStatusText

return {
statusText: user?.statusText ?? initialStatusText ?? '',
statusType:
user?.status === UserStatusType.AWAY ? (user?.statusDefault ?? UserStatusType.ONLINE) : (user?.status ?? UserStatusType.ONLINE),
statusType: user?.status ?? UserStatusType.ONLINE,
statusDuration: initialExpiration ? 'custom' : '',
statusCustomDate: initialDate.toLocaleDateString('en-CA'),
statusCustomTime: initialDate.toTimeString().slice(0, 5),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ICustomUserStatus, IUser } from '@rocket.chat/core-typings';
import { UserStatus as UserStatusEnum } from '@rocket.chat/core-typings';
import type { ICustomUserStatus, IUser, UserStatus as UserStatusEnum } from '@rocket.chat/core-typings';
import { Box, Icon, RadioButton } from '@rocket.chat/fuselage';
import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
import { clientCallbacks } from '@rocket.chat/ui-client';
Expand Down Expand Up @@ -131,12 +130,10 @@ export const useStatusItems = (user?: IUser): GenericMenuItemProps[] => {
});
}

// Presets: filter to Online / Busy / Offline. Keep Away only if user is currently on Away (legacy).
const isPresetSelected = (statusType: UserStatusEnum): boolean =>
!user?.statusText && !customStatusExpiration && user?.status === statusType;
const presetItems = (statuses ?? [])
.filter((s) => userStatuses.isValidType(s.id))
.filter((s) => s.statusType !== UserStatusEnum.AWAY || isPresetSelected(UserStatusEnum.AWAY))
.map(
(status): GenericMenuItemProps => ({
id: status.id,
Expand Down
8 changes: 4 additions & 4 deletions apps/meteor/client/views/root/AppErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ const AppErrorPage = () => {
<StatesAction
onClick={() => {
const result = indexedDB.deleteDatabase('MeteorDynamicImportCache');
result.onsuccess = () => {
window.location.reload();
};
result.onerror = () => {
const reload = () => {
window.location.reload();
};
result.onsuccess = reload;
result.onerror = reload;
result.onblocked = reload;
}}
>
Reload Application
Expand Down
5 changes: 5 additions & 0 deletions apps/meteor/ee/server/apps/appRequestsCron.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cronJobs } from '@rocket.chat/cron';
import { License } from '@rocket.chat/license';
import type { ExtendedFetchOptions } from '@rocket.chat/server-fetch';

import { appRequestNotififyForUsers } from './marketplace/appRequestNotifyUsers';
Expand All @@ -8,6 +9,10 @@ import { settings } from '../../../server/settings';

const appsNotifyAppRequests = async function _appsNotifyAppRequests() {
try {
if (License.hasOfflineLicense()) {
return;
}

const installedApps = await Apps.installedApps({ enabled: true });
if (!installedApps || installedApps.length === 0) {
return;
Expand Down
12 changes: 10 additions & 2 deletions apps/meteor/ee/server/apps/communication/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { registerAppLogsExportHandler } from './endpoints/appLogsExportHandler';
import { registerAppLogsHandler } from './endpoints/appLogsHandler';
import { registerAppsCountHandler } from './endpoints/appsCountHandler';
import { Info } from '../../../../app/utils/rocketchat.info';
import { CloudOfflineLicenseError } from '../../../../lib/errors/CloudOfflineLicenseError';
import { API } from '../../../../server/api';
import type { APIClass } from '../../../../server/api/ApiClass';
import { getUploadFormData } from '../../../../server/api/lib/getUploadFormData';
Expand Down Expand Up @@ -96,6 +97,13 @@ export class AppsRestApi {
const manager = this._manager;

const handleError = (message: string, err: any) => {
// Offline (air-gapped) licenses suppress marketplace requests at the source;
// report the real reason instead of a generic connectivity failure.
if (err instanceof CloudOfflineLicenseError) {
orchestrator.getRocketChatLogger().info({ msg: err.message });
return API.v1.failure({ error: err.message });
}

// when there is no `response` field in the error, it means the request
// couldn't even make it to the server
if (!err.hasOwnProperty('response')) {
Expand Down Expand Up @@ -149,7 +157,7 @@ export class AppsRestApi {
const apps = await fetchMarketplaceApps({ ...(this.queryParams.isAdminUser === 'false' && { endUserID: this.user._id }) });
return API.v1.success(apps);
} catch (err) {
if (err instanceof MarketplaceConnectionError) {
if (err instanceof MarketplaceConnectionError || err instanceof CloudOfflineLicenseError) {
return handleError('Unable to access Marketplace. Does the server has access to the internet?', err);
}

Expand Down Expand Up @@ -178,7 +186,7 @@ export class AppsRestApi {
return API.v1.success(categories);
} catch (err) {
orchestrator.getRocketChatLogger().error({ msg: 'Error fetching categories from Marketplace:', err });
if (err instanceof MarketplaceConnectionError) {
if (err instanceof MarketplaceConnectionError || err instanceof CloudOfflineLicenseError) {
return handleError('Unable to access Marketplace. Does the server has access to the internet?', err);
}

Expand Down
5 changes: 5 additions & 0 deletions apps/meteor/ee/server/apps/cron.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ProxiedApp } from '@rocket.chat/apps/dist/server/ProxiedApp';
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import { cronJobs } from '@rocket.chat/cron';
import { License } from '@rocket.chat/license';
import { Settings, Users } from '@rocket.chat/models';

import { Apps } from './orchestrator';
Expand Down Expand Up @@ -75,6 +76,10 @@ const notifyAdminsAboutRenewedApps = async function _notifyAdminsAboutRenewedApp
};

const appsUpdateMarketplaceInfo = async function _appsUpdateMarketplaceInfo() {
if (License.hasOfflineLicense()) {
return;
}

const token = await getWorkspaceAccessToken();
const workspaceIdSetting = await Settings.getValueById('Cloud_Workspace_Id');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { License } from '@rocket.chat/license';
import { type ExtendedFetchOptions, Response, serverFetch } from '@rocket.chat/server-fetch';

import { isTesting } from './isTesting';
import { CloudOfflineLicenseError } from '../../../../lib/errors/CloudOfflineLicenseError';

export class MarketplaceAPIClient {
#fetchStrategy: (input: string, options?: ExtendedFetchOptions, allowSelfSignedCerts?: boolean) => Promise<Response>;
Expand Down Expand Up @@ -41,6 +43,12 @@ export class MarketplaceAPIClient {
}

public fetch(input: string, options?: ExtendedFetchOptions, allowSelfSignedCerts?: boolean): ReturnType<typeof serverFetch> {
if (License.hasOfflineLicense()) {
return Promise.reject(
new CloudOfflineLicenseError('Marketplace connectivity is disabled by the offline license applied to this workspace'),
);
}

if (!input.startsWith('http://') && !input.startsWith('https://')) {
input = this.getMarketplaceUrl().concat(!input.startsWith('/') ? '/' : '', input);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as z from 'zod';

import { getMarketplaceHeaders } from './getMarketplaceHeaders';
import { MarketplaceAppsError, MarketplaceConnectionError, MarketplaceUnsupportedVersionError } from './marketplaceErrors';
import { CloudOfflineLicenseError } from '../../../../lib/errors/CloudOfflineLicenseError';
import { getWorkspaceAccessToken } from '../../../../server/lib/cloud';
import { settings } from '../../../../server/settings';
import { Apps } from '../orchestrator';
Expand Down Expand Up @@ -157,6 +158,12 @@ export async function fetchMarketplaceApps({ endUserID }: FetchMarketplaceAppsPa
},
});
} catch (error) {
// Offline (air-gapped) licenses reject before any request is made; keep the
// typed error so the REST layer can report the real reason instead of a
// generic connectivity failure.
if (error instanceof CloudOfflineLicenseError) {
throw error;
}
throw new MarketplaceConnectionError('Marketplace_Bad_Marketplace_Connection');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as z from 'zod';

import { getMarketplaceHeaders } from './getMarketplaceHeaders';
import { MarketplaceAppsError, MarketplaceConnectionError, MarketplaceUnsupportedVersionError } from './marketplaceErrors';
import { CloudOfflineLicenseError } from '../../../../lib/errors/CloudOfflineLicenseError';
import { getWorkspaceAccessToken } from '../../../../server/lib/cloud';
import { settings } from '../../../../server/settings';
import { Apps } from '../orchestrator';
Expand Down Expand Up @@ -41,6 +42,12 @@ export async function fetchMarketplaceCategories(): Promise<AppCategory[]> {
allowList: settings.get<string>('SSRF_Allowlist'),
});
} catch (error) {
// Offline (air-gapped) licenses reject before any request is made; keep the
// typed error so the REST layer can report the real reason instead of a
// generic connectivity failure.
if (error instanceof CloudOfflineLicenseError) {
throw error;
}
throw new MarketplaceConnectionError('Marketplace_Bad_Marketplace_Connection');
}

Expand Down
10 changes: 10 additions & 0 deletions apps/meteor/ee/server/lib/license/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import moment from 'moment';
import { getAppCount } from './lib/getAppCount';
import { callbacks } from '../../../../server/lib/callbacks';
import { syncWorkspace } from '../../../../server/lib/cloud/syncWorkspace';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { notifyOnSettingChangedById } from '../../../../server/lib/notifyListener';
import { settings } from '../../../../server/settings';

Expand Down Expand Up @@ -125,6 +126,15 @@ export const startLicense = async () => {
}
}

License.onInstall(() => {
if (License.hasOfflineLicense()) {
// startup level so it is visible at the default Log_Level, like 'License installed'
SystemLogger.startup(
'Offline license detected: outbound connections to Rocket.Chat Cloud services and the Rocket.Chat Push Gateway are disabled',
);
}
});

// After the current license is already loaded, watch the setting value to react to new licenses being applied.
settings.change<string>('Enterprise_License', (license) => applyLicenseOrRemove(license, true));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,7 @@ export class AutoTransferChatSchedulerClass {

private async transferRoom(roomId: string): Promise<void> {
this.logger.debug({ msg: 'Transferring room', roomId });
const room = await LivechatRooms.findOneById(roomId, {
_id: 1,
v: 1,
servedBy: 1,
open: 1,
departmentId: 1,
});
const room = await LivechatRooms.findOneById(roomId);
if (!room?.open || !room?.servedBy?._id) {
throw new Error('Room is not open or is not being served by an agent');
}
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/ee/server/lib/omnichannel/Department.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const findAllDepartmentsAvailable = async (
query = await applyDepartmentRestrictions(query, uid);
}

const { cursor, totalCount } = LivechatDepartment.findPaginated(query, { limit: count, offset, sort: { name: 1 } });
const { cursor, totalCount } = LivechatDepartment.findPaginated(query, { limit: count, skip: offset, sort: { name: 1 } });

const [departments, total] = await Promise.all([cursor.toArray(), totalCount]);

Expand All @@ -40,7 +40,7 @@ export const findAllDepartmentsByUnit = async (
{
ancestors: { $in: [unitId] },
},
{ limit: count, offset },
{ limit: count, skip: offset, sort: { name: 1, _id: 1 } },
);

const [departments, total] = await Promise.all([cursor.toArray(), totalCount]);
Expand Down
5 changes: 5 additions & 0 deletions apps/meteor/lib/errors/CloudOfflineLicenseError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CloudWorkspaceError } from './CloudWorkspaceError';

export class CloudOfflineLicenseError extends CloudWorkspaceError {
override name = CloudOfflineLicenseError.name;
}
2 changes: 1 addition & 1 deletion apps/meteor/server/api/v1/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ async function findPrivateGroupByIdOrName({
}> {
const room = await getRoomFromParams(params);

const user = await Users.findOneById(userId, { projections: { username: 1 } });
const user = await Users.findOneById(userId, { projection: { username: 1, roles: 1, abacAttributes: 1 } });

if (!room || !user || !(await canAccessRoomAsync(room, user))) {
throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group');
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/server/api/v1/omnichannel/lib/customFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export async function findLivechatCustomFields({
pagination: { offset, count, sort },
}: {
text?: string;
pagination: { offset: number; count: number; sort: Record<string, number> };
pagination: { offset: number; count: number; sort: Record<string, 1 | -1> };
}): Promise<PaginatedResult<{ customFields: Array<ILivechatCustomField> }>> {
const query = {
...(text && {
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/server/api/v1/omnichannel/lib/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function findLivechatTransferHistory({
pagination: { offset, count, sort },
}: {
rid: string;
pagination: { offset: number; count: number; sort: Record<string, number> };
pagination: { offset: number; count: number; sort: Record<string, 1 | -1> };
}): Promise<PaginatedResult<{ history: IOmnichannelSystemMessage['transferData'][] }>> {
const { cursor, totalCount } = Messages.findPaginated(
{ rid, t: 'livechat_transfer_history' },
Expand Down
Loading
Loading