diff --git a/apps/client/src/features/page/services/page-service.ts b/apps/client/src/features/page/services/page-service.ts index 8d76438a..c5b6f252 100644 --- a/apps/client/src/features/page/services/page-service.ts +++ b/apps/client/src/features/page/services/page-service.ts @@ -118,7 +118,14 @@ export async function exportPage(data: IExportPageParams): Promise { .split("filename=")[1] .replace(/"/g, ""); - saveAs(req.data, decodeURIComponent(fileName)); + let decodedFileName = fileName; + try { + decodedFileName = decodeURIComponent(fileName); + } catch (err) { + // fallback to raw filename + } + + saveAs(req.data, decodedFileName); } export async function importPage(file: File, spaceId: string) { diff --git a/apps/client/src/features/space/components/sidebar/switch-space.tsx b/apps/client/src/features/space/components/sidebar/switch-space.tsx index 60349ecc..89e0f64e 100644 --- a/apps/client/src/features/space/components/sidebar/switch-space.tsx +++ b/apps/client/src/features/space/components/sidebar/switch-space.tsx @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import { SpaceSelect } from "./space-select"; import { getSpaceUrl } from "@/lib/config"; import { Button, Popover, Text } from "@mantine/core"; -import { IconChevronDown } from "@tabler/icons-react"; +import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { useDisclosure } from "@mantine/hooks"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts"; @@ -21,7 +21,7 @@ export function SwitchSpace({ spaceIcon, }: SwitchSpaceProps) { const navigate = useNavigate(); - const [opened, { close, open, toggle }] = useDisclosure(false); + const [opened, { close, toggle }] = useDisclosure(false); const handleSelect = (value: string) => { if (value) { @@ -44,9 +44,9 @@ export function SwitchSpace({ variant="subtle" fullWidth justify="space-between" - rightSection={} + rightSection={opened ? : } color="gray" - onClick={open} + onClick={toggle} > { .split("filename=")[1] .replace(/"/g, ""); - saveAs(req.data, decodeURIComponent(fileName)); + let decodedFileName = fileName; + try { + decodedFileName = decodeURIComponent(fileName); + } catch (err) { + // fallback to raw filename + } + + saveAs(req.data, decodedFileName); } diff --git a/apps/server/src/common/logger/pino.config.ts b/apps/server/src/common/logger/pino.config.ts index 9d9a14f7..7299a8e9 100644 --- a/apps/server/src/common/logger/pino.config.ts +++ b/apps/server/src/common/logger/pino.config.ts @@ -5,13 +5,14 @@ const CONTEXTS_TO_IGNORE = [ 'InstanceLoader', 'RoutesResolver', 'RouterExplorer', + 'LegacyRouteConverter', 'WebSocketsController', ]; export function createPinoConfig(): Params { - const isProduction = process.env.NODE_ENV === 'production'; - const isDebugMode = process.env.DEBUG_MODE === 'true'; - const logHttp = process.env.LOG_HTTP === 'true'; + const isProduction = process.env.NODE_ENV?.toLowerCase() === 'production'; + const isDebugMode = process.env.DEBUG_MODE?.toLowerCase() === 'true'; + const logHttp = process.env.LOG_HTTP?.toLowerCase() === 'true'; const level = isProduction && !isDebugMode ? 'info' : 'debug'; @@ -32,14 +33,20 @@ export function createPinoConfig(): Params { : undefined, formatters: { level: (label) => ({ level: label }), - log: (object: Record) => { + }, + hooks: { + logMethod(inputArgs, method) { if (isProduction && !isDebugMode) { - const context = object['context'] as string | undefined; - if (context && CONTEXTS_TO_IGNORE.includes(context)) { - return { filtered: true }; + for (const arg of inputArgs) { + if (typeof arg === 'object' && arg !== null && 'context' in arg) { + const context = (arg as Record)['context']; + if (typeof context === 'string' && CONTEXTS_TO_IGNORE.includes(context)) { + return; + } + } } } - return object; + return method.apply(this, inputArgs); }, }, serializers: { diff --git a/apps/server/src/ee b/apps/server/src/ee index b6844b01..88e3d01f 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit b6844b019c3778d51ff1bb236f30284a0bf8f403 +Subproject commit 88e3d01f8135c2dbc628b9636ba91bb9ffd2f0eb diff --git a/apps/server/src/integrations/export/utils.ts b/apps/server/src/integrations/export/utils.ts index fe1815b0..266141c2 100644 --- a/apps/server/src/integrations/export/utils.ts +++ b/apps/server/src/integrations/export/utils.ts @@ -1,4 +1,5 @@ import { jsonToNode } from 'src/collaboration/collaboration.util'; +import { Logger } from '@nestjs/common'; import { ExportFormat } from './dto/export-dto'; import { Node } from '@tiptap/pm/model'; import { validate as isValidUUID } from 'uuid'; @@ -88,7 +89,7 @@ export function replaceInternalLinks( // if link and text are same, use page title if (markLink === node.text) { //@ts-expect-error - node.text = getInternalLinkPageName(relativePath); + node.text = getInternalLinkPageName(relativePath, currentPagePath); } } } @@ -99,10 +100,19 @@ export function replaceInternalLinks( return doc.toJSON(); } -export function getInternalLinkPageName(path: string): string { - return decodeURIComponent( - path?.split('/').pop().split('.').slice(0, -1).join('.'), - ); +export function getInternalLinkPageName(path: string, currentFilePath?: string): string { + const name = path?.split('/').pop().split('.').slice(0, -1).join('.'); + try { + return decodeURIComponent(name); + } catch (err) { + if (currentFilePath) { + Logger.warn( + `URI malformed in page ${currentFilePath}: ${name}. Falling back to raw name.`, + 'ExportUtils', + ); + } + return name; + } } export function extractPageSlugId(input: string): string { diff --git a/apps/server/src/integrations/import/utils/import-formatter.ts b/apps/server/src/integrations/import/utils/import-formatter.ts index 14a2530c..59f5eeec 100644 --- a/apps/server/src/integrations/import/utils/import-formatter.ts +++ b/apps/server/src/integrations/import/utils/import-formatter.ts @@ -1,4 +1,5 @@ import { getEmbedUrlAndProvider } from '@docmost/editor-ext'; +import { Logger } from '@nestjs/common'; import * as path from 'path'; import { v7 } from 'uuid'; import { InsertableBacklink } from '@docmost/db/types/entity.types'; @@ -280,8 +281,18 @@ export async function rewriteInternalLinksToMentionHtml( const $a = $(el); const raw = $a.attr('href')!; if (raw.startsWith('http') || raw.startsWith('/api/')) return; + let decodedRaw = raw; + try { + decodedRaw = decodeURIComponent(raw); + } catch (err) { + Logger.warn( + `URI malformed in page ${currentFilePath}: ${raw}. Falling back to raw path.`, + 'ImportFormatter', + ); + } + const resolved = normalize( - path.join(path.dirname(currentFilePath), decodeURIComponent(raw)), + path.join(path.dirname(currentFilePath), decodedRaw), ); const meta = filePathToPageMetaMap.get(resolved); if (!meta) return; diff --git a/apps/server/src/integrations/import/utils/import.utils.ts b/apps/server/src/integrations/import/utils/import.utils.ts index 1fa10d7a..c8f5fe51 100644 --- a/apps/server/src/integrations/import/utils/import.utils.ts +++ b/apps/server/src/integrations/import/utils/import.utils.ts @@ -1,3 +1,4 @@ +import { Logger } from '@nestjs/common'; import { promises as fs } from 'fs'; import * as path from 'path'; @@ -30,8 +31,13 @@ export function resolveRelativeAttachmentPath( pageDir: string, attachmentCandidates: Map, ): string | null { - const mainRel = decodeURIComponent(raw.replace(/^\.?\/+/, '')); - const fallback = path.normalize(path.join(pageDir, mainRel)); + let mainRel = raw.replace(/^\.?\/+/, ''); + try { + mainRel = decodeURIComponent(mainRel); + } catch (err) { + Logger.warn(`URI malformed for attachment path: ${mainRel}. Falling back to raw path.`, 'ImportUtils'); + } + const fallback = path.normalize(path.join(pageDir, mainRel)).split(path.sep).join('/'); if (attachmentCandidates.has(mainRel)) { return mainRel;