Merge remote-tracking branch 'gitea/develop' into fix/review-batch-2

# Conflicts:
#	AGENTS.md
#	CHANGELOG.md
#	README.md
#	apps/server/src/collaboration/collaboration.handler.ts
#	apps/server/src/common/helpers/prosemirror/html-embed.spec.ts
#	apps/server/src/common/helpers/prosemirror/html-embed.util.ts
#	apps/server/src/core/ai-chat/public-share-chat.service.ts
#	apps/server/src/core/ai-chat/public-share-chat.spec.ts
#	apps/server/src/core/ai-chat/public-share-workspace-limiter.ts
#	apps/server/src/core/page/services/page.service.ts
#	apps/server/src/core/page/transclusion/transclusion.service.ts
#	apps/server/src/integrations/import/services/file-import-task.service.ts
#	apps/server/src/integrations/import/services/import.service.ts
This commit is contained in:
claude code agent 227
2026-06-21 05:32:44 +03:00
65 changed files with 1448 additions and 2927 deletions

View File

@@ -5,6 +5,8 @@ import {
IsBoolean,
IsInt,
IsOptional,
IsString,
MaxLength,
Min,
} from 'class-validator';
@@ -53,12 +55,22 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
@IsBoolean()
aiDictation: boolean;
// Workspace feature toggle for the admin-only HTML embed feature. Persisted at
// settings.htmlEmbed. ABSENT/false => OFF (default).
// Workspace master toggle that enables/disables the HTML embed block type.
// Persisted at settings.htmlEmbed. ABSENT/false => OFF (default). The block
// itself renders in a sandboxed iframe, so this is a feature switch, not a
// security gate.
@IsOptional()
@IsBoolean()
htmlEmbed: boolean;
// Admin-only analytics/tracker snippet (raw HTML/JS) injected verbatim into
// the <head> of PUBLIC SHARE pages only (same-origin). Persisted at
// settings.trackerHead. Admin-authored trusted content.
@IsOptional()
@IsString()
@MaxLength(20000)
trackerHead?: string;
@IsOptional()
@IsBoolean()
aiPublicShareAssistant: boolean;

View File

@@ -108,4 +108,38 @@ describe('WorkspaceService.update — htmlEmbed toggle persistence (real code)',
expect(logged.changes.before.htmlEmbed).toBe(false);
expect(logged.changes.after.htmlEmbed).toBe(true);
});
it('persists trackerHead via updateSetting with the trackerHead key', async () => {
const { service, updateSetting } = buildService({});
await service.update('w1', { trackerHead: '<script>ga()</script>' } as any);
expect(updateSetting).toHaveBeenCalledWith(
'w1',
'trackerHead',
'<script>ga()</script>',
expect.anything(),
);
});
it('does NOT call updateSetting when trackerHead is undefined in the dto', async () => {
const { service, updateSetting } = buildService({});
await service.update('w1', { name: 'New name' } as any);
expect(updateSetting).not.toHaveBeenCalled();
});
it('audits the trackerHead change (before/after) when the value changes', async () => {
const { service, auditService } = buildService({
settingsBefore: { trackerHead: '' },
});
await service.update('w1', { trackerHead: '<script>m()</script>' } as any);
expect(auditService.log).toHaveBeenCalledTimes(1);
const logged = auditService.log.mock.calls[0][0];
expect(logged.changes.before.trackerHead).toBe('');
expect(logged.changes.after.trackerHead).toBe('<script>m()</script>');
});
});

View File

@@ -525,6 +525,22 @@ export class WorkspaceService {
);
}
if (typeof updateWorkspaceDto.trackerHead !== 'undefined') {
// Admin-only analytics/tracker snippet injected into the <head> of
// public share pages (same-origin). Persisted at settings.trackerHead.
const prev = (settingsBefore as any)?.trackerHead ?? '';
if (prev !== updateWorkspaceDto.trackerHead) {
before.trackerHead = prev;
after.trackerHead = updateWorkspaceDto.trackerHead;
}
await this.workspaceRepo.updateSetting(
workspaceId,
'trackerHead',
updateWorkspaceDto.trackerHead,
trx,
);
}
if (typeof updateWorkspaceDto.aiPublicShareAssistant !== 'undefined') {
const prev = settingsBefore?.ai?.publicShareAssistant ?? false;
if (prev !== updateWorkspaceDto.aiPublicShareAssistant) {
@@ -549,6 +565,7 @@ export class WorkspaceService {
delete updateWorkspaceDto.aiChat;
delete updateWorkspaceDto.aiDictation;
delete updateWorkspaceDto.htmlEmbed;
delete updateWorkspaceDto.trackerHead;
delete updateWorkspaceDto.aiPublicShareAssistant;
await this.workspaceRepo.updateWorkspace(