diff --git a/CHANGELOG.md b/CHANGELOG.md index 42bb670be..ee6fa9f6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - **Privacy**: One switch, `NO_THIRD_PARTY_TRAFFIC` or Organisation > Security, stops every connection to services outside your own servers: telemetry, the version check, the built-in push relay, the official extension directory, sender icons, MyMemory translation, map links, OpenPGP key lookups and remote pictures in mail. Plugins that need network access don't run, and the page's CSP allows only the hosts you configured. Push registrations move to your own relay. The admin page lists what stays reachable (#903) - **Team inbox**: The new official Team inbox plugin lets members of a team mailbox assign messages to each other, mark them done, and filter by "Assigned to me" and "Open" - **Plugins**: Plugins can add keyword filters to the message list, shown as chips above it, put icons on row badges, show actions only in team mailboxes or for some keywords, change several keywords in one step, see the user's own account and which mailboxes are team mailboxes, and keep a small shared value in a team mailbox on Stalwart 1.0. Looking people up in the directory needs the new `directory:read` permission. Plugins can no longer set `$draft`, `$junk`, `$notjunk` or `$phishing` +- **Files**: With a document editor configured, "New file" also creates Word, Excel, PowerPoint, OpenDocument and (where the editor edits PDFs) PDF files and opens them in the editor. It offers the formats the editor can edit; admins can narrow the list with `WOPI_NEW_FILE_FORMATS` and add their own blank templates, or other formats, with `WOPI_TEMPLATE_DIR` (#1393) ## 1.18.0 (2026-10-26) diff --git a/app/(main)/admin/_pages/settings.tsx b/app/(main)/admin/_pages/settings.tsx index 99319d8be..e4427b6ca 100644 --- a/app/(main)/admin/_pages/settings.tsx +++ b/app/(main)/admin/_pages/settings.tsx @@ -165,9 +165,14 @@ export function SettingsPage() { + + Point the client URL at a WOPI-compatible office suite (Collabora Online, OnlyOffice, EuroOffice, …) to let users edit documents in Files. The client URL may be an internal address: the browser loads the editor from the address its discovery advertises. The host override is only needed when the editor reaches this webmail under a different address than the browser does (e.g. Docker networks); give the address alone, the webmail's base path is added to it. The editor must also allow this webmail as a WOPI host. + + "New file" in Files offers .txt plus every format the editor can edit that a blank template exists for: .docx, .xlsx, .pptx, .odt, .ods, .odp and .pdf are built in. List extensions separated by commas (e.g. docx,xlsx,pptx) to offer only those. A file named new.<ext> in the template directory on this server replaces the built-in blank file for that format or adds a format, so ONLYOFFICE's document-templates/new/<locale> folder works as one. + diff --git a/app/api/wopi/status/route.ts b/app/api/wopi/status/route.ts index 07fd538bc..87f354ac7 100644 --- a/app/api/wopi/status/route.ts +++ b/app/api/wopi/status/route.ts @@ -1,13 +1,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { getStalwartCredentials } from '@/lib/stalwart/credentials'; import { getWopiActions, getWopiClientUrl } from '@/lib/wopi/discovery'; +import { getNewFileFormats } from '@/lib/wopi/new-file'; /** * GET /api/wopi/status * - * Tells the Files UI whether a WOPI document editor is configured and which - * file extensions it can edit/view (#425). Session-authenticated; the WOPI - * client URL itself stays server-side. + * Tells the Files UI whether a WOPI document editor is configured, which + * file extensions it can edit/view (#425) and which ones "New file" can + * create (#1393). Session-authenticated; the WOPI client URL itself stays + * server-side. */ export async function GET(request: NextRequest) { const creds = await getStalwartCredentials(request); @@ -15,19 +17,21 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); } + const disabled = { enabled: false, editExtensions: [], viewExtensions: [], newExtensions: [] }; const clientUrl = await getWopiClientUrl(); if (!clientUrl) { - return NextResponse.json({ enabled: false, editExtensions: [], viewExtensions: [] }); + return NextResponse.json(disabled); } const actions = await getWopiActions(); if (!actions) { - return NextResponse.json({ enabled: false, editExtensions: [], viewExtensions: [] }); + return NextResponse.json(disabled); } return NextResponse.json({ enabled: true, editExtensions: Object.keys(actions.edit), viewExtensions: Object.keys(actions.view), + newExtensions: await getNewFileFormats(actions), }); } diff --git a/app/api/wopi/template/route.ts b/app/api/wopi/template/route.ts new file mode 100644 index 000000000..0f4c5d0f6 --- /dev/null +++ b/app/api/wopi/template/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getStalwartCredentials } from '@/lib/stalwart/credentials'; +import { getWopiActions } from '@/lib/wopi/discovery'; +import { getBlankTemplate, getNewFileFormats } from '@/lib/wopi/new-file'; + +/** + * GET /api/wopi/template?ext=docx + * + * The blank file "New file" uploads for a document format the editor can + * create (#1393). Only formats /api/wopi/status lists under newExtensions. + * Session-authenticated. + */ +export async function GET(request: NextRequest) { + const creds = await getStalwartCredentials(request); + if (!creds) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); + } + + const ext = (request.nextUrl.searchParams.get('ext') ?? '').toLowerCase(); + const actions = await getWopiActions(); + if (!actions || !(await getNewFileFormats(actions)).includes(ext)) { + return NextResponse.json({ error: 'Unknown format' }, { status: 404 }); + } + + const template = await getBlankTemplate(ext); + if (!template) { + return NextResponse.json({ error: 'Unknown format' }, { status: 404 }); + } + return new NextResponse(new Blob([template.data as BlobPart], { type: template.type }), { + headers: { + 'Content-Type': template.type, + 'Cache-Control': 'private, no-cache', + }, + }); +} diff --git a/components/files/__tests__/file-browser-shared-folders.test.tsx b/components/files/__tests__/file-browser-shared-folders.test.tsx index 7a82a2fc8..da83e776a 100644 --- a/components/files/__tests__/file-browser-shared-folders.test.tsx +++ b/components/files/__tests__/file-browser-shared-folders.test.tsx @@ -105,7 +105,7 @@ function renderBrowser(opts: { resources?: FileResource[]; allFolders?: FileReso onPaste={noop} onMoveTo={noop} onPreviewImage={vi.fn()} - onCreateTextFile={noop} + onCreateFile={noop} onDuplicate={noop} getImageUrl={async () => ''} favorites={[]} diff --git a/components/files/__tests__/files-plugin-surfaces.test.tsx b/components/files/__tests__/files-plugin-surfaces.test.tsx index 93359c09d..2890e8b92 100644 --- a/components/files/__tests__/files-plugin-surfaces.test.tsx +++ b/components/files/__tests__/files-plugin-surfaces.test.tsx @@ -77,7 +77,7 @@ function renderBrowser(selected: string[] = []) { onPaste={noop} onMoveTo={noop} onPreviewImage={vi.fn()} - onCreateTextFile={noop} + onCreateFile={noop} onDuplicate={noop} getImageUrl={async () => ''} favorites={[]} diff --git a/components/files/__tests__/new-file-dialog.test.tsx b/components/files/__tests__/new-file-dialog.test.tsx new file mode 100644 index 000000000..41a5fedb5 --- /dev/null +++ b/components/files/__tests__/new-file-dialog.test.tsx @@ -0,0 +1,52 @@ +import { render, screen, fireEvent, cleanup } from '@testing-library/react'; +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { NewFileDialog, withExtension } from '@/components/files/new-file-dialog'; + +// #1393: with a document editor, "New file" offers its formats next to .txt. + +vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key })); + +afterEach(cleanup); + +const nameField = () => screen.getByLabelText('file_name') as HTMLInputElement; +const checked = () => screen.getAllByRole('radio').filter((r) => r.getAttribute('aria-checked') === 'true').map((r) => r.textContent); + +describe('NewFileDialog', () => { + it('is the plain text-file prompt without an editor', () => { + render( {}} onSubmit={() => {}} formats={[]} />); + expect(screen.getByText('new_text_file')).toBeTruthy(); + expect(screen.queryByRole('radiogroup')).toBeNull(); + expect(nameField().value).toBe('new-file.txt'); + }); + + it('switches the extension with the format and the format with the extension', () => { + const onSubmit = vi.fn(); + render( {}} onSubmit={onSubmit} formats={['docx', 'xlsx', 'pdf']} />); + expect(screen.getByText('new_file')).toBeTruthy(); + expect(screen.getAllByRole('radio').map((r) => r.textContent)).toEqual([ + 'format_text.txt', 'format_document.docx', 'format_spreadsheet.xlsx', 'format_pdf.pdf', + ]); + expect(checked()).toEqual(['format_text.txt']); + + fireEvent.change(nameField(), { target: { value: 'Budget 2027.txt' } }); + fireEvent.click(screen.getByText('format_spreadsheet')); + expect(nameField().value).toBe('Budget 2027.xlsx'); + expect(checked()).toEqual(['format_spreadsheet.xlsx']); + + fireEvent.change(nameField(), { target: { value: 'Budget 2027.DOCX' } }); + expect(checked()).toEqual(['format_document.docx']); + fireEvent.change(nameField(), { target: { value: 'Budget 2027.md' } }); + expect(checked()).toEqual([]); + + fireEvent.click(screen.getByText('create')); + expect(onSubmit).toHaveBeenCalledWith('Budget 2027.md'); + }); + + it('keeps the base name when changing the extension', () => { + expect(withExtension('report.txt', 'docx')).toBe('report.docx'); + expect(withExtension('archive.tar.gz', 'odt')).toBe('archive.tar.odt'); + expect(withExtension('notes', 'pptx')).toBe('notes.pptx'); + expect(withExtension('.hidden', 'ods')).toBe('.hidden.ods'); + expect(withExtension('', 'pdf')).toBe('new-file.pdf'); + }); +}); diff --git a/components/files/file-browser.tsx b/components/files/file-browser.tsx index e6088cb15..1a4bf1d33 100644 --- a/components/files/file-browser.tsx +++ b/components/files/file-browser.tsx @@ -16,6 +16,7 @@ import { ContextMenu, ContextMenuItem, ContextMenuSeparator } from "@/components import { Avatar } from "@/components/ui/avatar"; import { cn } from "@/lib/utils"; import { PromptDialog } from "@/components/ui/prompt-dialog"; +import { NewFileDialog } from "@/components/files/new-file-dialog"; import { FolderPickerDialog } from "@/components/files/folder-picker-dialog"; import { fileNameProblem } from "@/lib/file-name-rules"; import { EmptyFolder, NoSearchResults } from "@/components/files/file-empty-state"; @@ -38,6 +39,8 @@ import type { IJMAPClient } from "@/lib/jmap/client-interface"; import type { FileNodeRights } from "@/lib/jmap/types"; import type { FileResourceView } from "@/lib/plugin-types"; +const NO_FORMATS: string[] = []; + interface ClipboardState { mode: "cut" | "copy"; ids: string[]; @@ -93,7 +96,10 @@ interface FileBrowserProps { /** WOPI document editing (#425): whether a file can open in the document editor. */ isOfficeEditable?: (name: string) => boolean; onEditFile?: (name: string) => void; - onCreateTextFile: (name: string) => Promise; + /** "New file": empty text, or a blank document of a format in `newFileFormats`. */ + onCreateFile: (name: string) => Promise; + /** Document formats a WOPI editor can create besides .txt (#1393). */ + newFileFormats?: string[]; onDuplicate: (name: string) => Promise; getImageUrl: (name: string) => Promise; favorites: string[]; @@ -182,7 +188,8 @@ export function FileBrowser({ onPreviewFile, isOfficeEditable, onEditFile, - onCreateTextFile, + onCreateFile, + newFileFormats = NO_FORMATS, onDuplicate, getImageUrl, favorites, @@ -204,6 +211,7 @@ export function FileBrowser({ onShare, }: FileBrowserProps) { const t = useTranslations("files"); + const newFileLabel = newFileFormats.length > 0 ? t("new_file") : t("new_text_file"); const fileNameRules = useMemo(() => client?.getFileNameRules?.() ?? null, [client]); const validateFileName = useCallback((name: string) => { const problem = fileNameProblem(name, fileNameRules); @@ -940,7 +948,7 @@ export function FileBrowser({ { closeMenu(); folderInputRef.current?.click(); }} /> { closeMenu(); setShowNewFolder(true); }} /> - { closeMenu(); setShowNewTextFile(true); }} /> + { closeMenu(); setShowNewTextFile(true); }} /> ); @@ -978,7 +986,7 @@ export function FileBrowser({ { id: "upload-files", label: t("upload_files"), icon: Upload, run: () => fileInputRef.current?.click(), when: () => canCreate }, { id: "upload-folder", label: t("upload_folder"), icon: FolderUp, run: () => folderInputRef.current?.click(), when: () => canCreate }, { id: "new-folder", label: t("new_folder"), icon: FolderPlus, run: () => setShowNewFolder(true), when: () => canCreate }, - { id: "new-text-file", label: t("new_text_file"), icon: FilePlus, run: () => setShowNewTextFile(true), when: () => canCreate }, + { id: "new-text-file", label: newFileLabel, icon: FilePlus, run: () => setShowNewTextFile(true), when: () => canCreate }, { id: "trash", label: t("trash"), icon: Trash2, run: () => onNavigate(TRASH_PATH, TRASH_LOCATION), when: () => !inTrash && !accountPickerMode }, { id: "empty-trash", label: t("empty_trash"), icon: Trash2, run: () => onEmptyTrash?.(), when: () => inTrash && trashCount > 0 && !!onEmptyTrash }, { id: "refresh", label: t("refresh"), icon: RefreshCw, run: () => void onRefresh() }, @@ -1000,6 +1008,7 @@ export function FileBrowser({ onUploadFolder={fromDrawer(() => folderInputRef.current?.click())} onNewFolder={fromDrawer(() => setShowNewFolder(true))} onNewTextFile={fromDrawer(() => setShowNewTextFile(true))} + newFileLabel={newFileLabel} onToggleFavorite={onToggleFavorite} onDropItems={moveNames} canShare={canShare} @@ -1358,17 +1367,12 @@ export function FileBrowser({ onSubmit={(name) => void onCreateFolder(name)} /> - setShowNewTextFile(false)} - title={t("new_text_file")} - placeholder={t("file_name")} - defaultValue="new-file.txt" - selectBaseName - confirmText={t("create")} - cancelText={t("cancel")} + formats={newFileFormats} validate={validateFileName} - onSubmit={(name) => void onCreateTextFile(name)} + onSubmit={(name) => void onCreateFile(name)} /> { + // A document format the editor can create starts from a blank template + // and opens in the editor (#1393); any other name is an empty text file. + const handleCreateFile = useCallback(async (name: string) => { try { - await createTextFile(name); + const ext = fileExtension(name); + let template: Blob | undefined; + if (wopiStatus?.newExtensions.includes(ext)) { + const res = await apiFetch(`/api/wopi/template?ext=${encodeURIComponent(ext)}`); + if (!res.ok) throw new Error(`Template for .${ext} unavailable (${res.status})`); + template = await res.blob(); + } + const created = await createFile(name, template); toast.success(t("create_file_success")); + if (template && created && isOfficeEditable(created)) setEditFile(created); } catch (err) { console.error("Failed to create file:", err); toast.error(t("create_file_error"), failureReason(err)); } - }, [createTextFile, t, failureReason]); + }, [createFile, wopiStatus, isOfficeEditable, t, failureReason]); const handleDuplicate = useCallback(async (name: string) => { try { @@ -867,7 +878,8 @@ export function FilesApp({ linkSegments: routeSegments }: FilesAppProps = {}) { onPreviewFile={handlePreviewFile} isOfficeEditable={isOfficeEditable} onEditFile={setEditFile} - onCreateTextFile={handleCreateTextFile} + onCreateFile={handleCreateFile} + newFileFormats={wopiStatus?.newExtensions} onDuplicate={handleDuplicate} getImageUrl={getImageUrl} favorites={favorites} diff --git a/components/files/files-sidebar.tsx b/components/files/files-sidebar.tsx index 35407d56b..574bbddb9 100644 --- a/components/files/files-sidebar.tsx +++ b/components/files/files-sidebar.tsx @@ -66,6 +66,8 @@ export interface FilesSidebarProps { onUploadFolder: () => void; onNewFolder: () => void; onNewTextFile: () => void; + /** The label of the new-file entries: "New file" when documents can be made too (#1393). */ + newFileLabel?: string; onToggleFavorite: (path: string) => void; /** Move dragged items from the open folder into a folder (null = the root). */ onDropItems: (names: string[], targetId: string | null) => void; @@ -104,6 +106,7 @@ export function FilesSidebar({ onUploadFolder, onNewFolder, onNewTextFile, + newFileLabel, onToggleFavorite, onDropItems, canShare, @@ -269,7 +272,7 @@ export function FilesSidebar({ <> run(() => onCreateIn(path, id, "folder"))} /> - run(() => onCreateIn(path, id, "text"))} /> + run(() => onCreateIn(path, id, "text"))} /> run(() => onCreateIn(path, id, "files"))} /> run(() => onCreateIn(path, id, "dir"))} /> @@ -443,7 +446,7 @@ export function FilesSidebar({ { newMenu.closeContextMenu(); onUploadFolder(); }} /> { newMenu.closeContextMenu(); onNewFolder(); }} /> - { newMenu.closeContextMenu(); onNewTextFile(); }} /> + { newMenu.closeContextMenu(); onNewTextFile(); }} /> diff --git a/components/files/new-file-dialog.tsx b/components/files/new-file-dialog.tsx new file mode 100644 index 000000000..264161113 --- /dev/null +++ b/components/files/new-file-dialog.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useEffect, useId, useRef, useState } from "react"; +import { useTranslations } from "next-intl"; +import { Button } from "@/components/ui/button"; +import { Dialog } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { nameSelectionRange } from "@/components/ui/prompt-dialog"; +import { getFileIconByName } from "@/components/files/file-icons"; +import { fileExtension } from "@/hooks/use-wopi-status"; +import { cn } from "@/lib/utils"; + +const DEFAULT_BASE_NAME = "new-file"; + +/** The kind of document a format makes, for its label; null for an unknown one. */ +export function formatKind(ext: string): "text" | "document" | "spreadsheet" | "presentation" | "pdf" | null { + if (ext === "txt") return "text"; + if (["docx", "odt", "doc", "rtf"].includes(ext)) return "document"; + if (["xlsx", "ods", "xls"].includes(ext)) return "spreadsheet"; + if (["pptx", "odp", "ppt"].includes(ext)) return "presentation"; + if (ext === "pdf") return "pdf"; + return null; +} + +/** `name` with its extension replaced by `ext` (or added, when it has none). */ +export function withExtension(name: string, ext: string): string { + const dot = name.lastIndexOf("."); + const base = dot > 0 ? name.slice(0, dot) : name; + return `${base || DEFAULT_BASE_NAME}.${ext}`; +} + +interface NewFileDialogProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (name: string) => void; + /** + * Document formats a WOPI editor can create besides .txt (#1393). Without + * any, this is the plain "New text file" prompt. + */ + formats: string[]; + validate?: (value: string) => string | null; +} + +/** + * "New file": a name and, with a document editor, the format to start from. + * The name decides: picking a format rewrites its extension, and typing an + * extension selects the format it names. + */ +export function NewFileDialog({ isOpen, onClose, onSubmit, formats, validate }: NewFileDialogProps) { + const t = useTranslations("files"); + const id = useId(); + const choices = formats.length > 0 ? ["txt", ...formats.filter((ext) => ext !== "txt")] : []; + const [value, setValue] = useState(`${DEFAULT_BASE_NAME}.txt`); + const inputRef = useRef(null); + + const focusBaseName = () => { + const input = inputRef.current; + if (!input) return; + input.focus(); + const [start, end] = nameSelectionRange(input.value, true); + input.setSelectionRange(start, end); + }; + + useEffect(() => { + if (!isOpen) return; + setValue(`${DEFAULT_BASE_NAME}.txt`); + const timer = setTimeout(focusBaseName, 50); + return () => clearTimeout(timer); + }, [isOpen]); + + const trimmed = value.trim(); + const error = trimmed.length > 0 && validate ? validate(trimmed) : null; + const canSubmit = trimmed.length > 0 && !error; + const errorId = `${id}-error`; + const selected = fileExtension(trimmed); + + const pick = (ext: string) => { + setValue(withExtension(trimmed, ext)); + setTimeout(focusBaseName, 0); + }; + + const label = (ext: string) => { + const kind = formatKind(ext); + return kind ? t(`format_${kind}`) : ext.toUpperCase(); + }; + + const handleSubmit = (e?: React.FormEvent) => { + e?.preventDefault(); + if (!canSubmit) return; + try { + onSubmit(trimmed); + } finally { + onClose(); + } + }; + + return ( + 0 ? t("new_file") : t("new_text_file")} + showClose={false} + initialFocus={inputRef} + onSubmit={handleSubmit} + footer={ + <> + + + + } + > + setValue(e.target.value)} + placeholder={t("file_name")} + aria-label={t("file_name")} + aria-invalid={error ? true : undefined} + aria-describedby={error ? errorId : undefined} + className={error ? "mt-4 border-destructive focus-visible:border-destructive" : "mt-4"} + /> + {error && ( + + )} + {choices.length > 0 && ( +
+ {choices.map((ext) => ( + + ))} +
+ )} +
+ ); +} diff --git a/hooks/use-wopi-status.ts b/hooks/use-wopi-status.ts index e3b3b5521..4d6dd014a 100644 --- a/hooks/use-wopi-status.ts +++ b/hooks/use-wopi-status.ts @@ -13,9 +13,11 @@ export interface WopiStatus { enabled: boolean; editExtensions: string[]; viewExtensions: string[]; + /** Formats "New file" can create from a blank template besides .txt (#1393). */ + newExtensions: string[]; } -const DISABLED: WopiStatus = { enabled: false, editExtensions: [], viewExtensions: [] }; +const DISABLED: WopiStatus = { enabled: false, editExtensions: [], viewExtensions: [], newExtensions: [] }; let statusPromise: Promise | null = null; @@ -29,6 +31,7 @@ function fetchWopiStatus(): Promise { enabled: !!data?.enabled, editExtensions: Array.isArray(data?.editExtensions) ? data.editExtensions : [], viewExtensions: Array.isArray(data?.viewExtensions) ? data.viewExtensions : [], + newExtensions: Array.isArray(data?.newExtensions) ? data.newExtensions : [], })) .catch(() => { statusPromise = null; // allow a retry on the next mount diff --git a/lib/__tests__/wopi-new-file.test.ts b/lib/__tests__/wopi-new-file.test.ts new file mode 100644 index 000000000..3080732b2 --- /dev/null +++ b/lib/__tests__/wopi-new-file.test.ts @@ -0,0 +1,140 @@ +// @vitest-environment node +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest'; +import { NextRequest } from 'next/server'; +import JSZip from 'jszip'; + +// #1393: "New file" offers the formats the WOPI editor can edit and has a +// blank template for, and serves that template. + +const config = vi.hoisted(() => ({ values: {} as Record })); + +vi.mock('@/lib/logger', () => ({ + logger: { warn: () => {}, error: () => {}, info: () => {}, debug: () => {} }, +})); +vi.mock('@/lib/admin/config-manager', () => ({ + configManager: { + ensureLoaded: async () => {}, + get: (key: string, fallback: unknown) => config.values[key] ?? fallback, + }, +})); +vi.mock('@/lib/stalwart/credentials', () => ({ getStalwartCredentials: vi.fn() })); + +import { GET as status } from '@/app/api/wopi/status/route'; +import { GET as template } from '@/app/api/wopi/template/route'; +import { getBlankTemplate, getNewFileFormats, parseFormatList } from '@/lib/wopi/new-file'; +import { getStalwartCredentials } from '@/lib/stalwart/credentials'; +import type { WopiActions } from '@/lib/wopi/discovery'; + +const action = (name: string, ext: string) => + ``; + +// What Collabora advertises: PDF can be viewed and commented, not edited. +const COLLABORA = `${[ + action('edit', 'docx'), action('editnew', 'docx'), action('edit', 'xlsx'), action('edit', 'pptx'), + action('edit', 'odt'), action('edit', 'ods'), action('edit', 'odp'), action('edit', 'txt'), + action('edit', 'rtf'), action('view', 'pdf'), action('view_comment', 'pdf'), +].join('')}`; + +const actions = (edit: string[], view: string[] = []): WopiActions => ({ + edit: Object.fromEntries(edit.map((ext) => [ext, 'https://office.example/edit?'])), + view: Object.fromEntries(view.map((ext) => [ext, 'https://office.example/view?'])), +}); + +let editorSeq = 0; +function useEditor(xml: string) { + // A fresh editor URL each time: discovery is cached per URL. + config.values.wopiClientUrl = `https://office${++editorSeq}.example`; + (globalThis.fetch as Mock).mockResolvedValue(new Response(xml, { status: 200 })); +} + +const request = (url: string) => new NextRequest(`https://mail.example${url}`); + +beforeEach(() => { + config.values = {}; + vi.stubGlobal('fetch', vi.fn()); + (getStalwartCredentials as Mock).mockResolvedValue({ serverUrl: 'https://jmap.example', username: 'u', authHeader: 'Basic x' }); +}); +afterEach(() => vi.unstubAllGlobals()); + +describe('new file formats (#1393)', () => { + it('offers the formats the editor can edit and has a template for', async () => { + // rtf has no template and txt needs none, pdf is view-only. + expect(await getNewFileFormats(actions(['txt', 'rtf', 'odt', 'docx', 'pptx', 'xlsx'], ['pdf']))) + .toEqual(['docx', 'xlsx', 'pptx', 'odt']); + expect(await getNewFileFormats(actions(['pdf', 'docx']))).toEqual(['docx', 'pdf']); + }); + + it('takes the admin list instead of discovery, still only with a template', async () => { + config.values.wopiNewFileFormats = ' .DOCX, ods ; vsdx'; + expect(await getNewFileFormats(actions(['docx', 'xlsx', 'pptx']))).toEqual(['docx', 'ods']); + expect(parseFormatList('docx,,docx, ../x, .odt')).toEqual(['docx', 'odt']); + }); + + it('adds and replaces templates from the template directory', async () => { + const dir = mkdtempSync(path.join(tmpdir(), 'bw-wopi-tpl-')); + writeFileSync(path.join(dir, 'new.vsdx'), 'visio'); + writeFileSync(path.join(dir, 'new.docx'), 'letterhead'); + writeFileSync(path.join(dir, 'other.odg'), 'not a template name'); + config.values.wopiTemplateDir = dir; + + expect(await getNewFileFormats(actions(['docx', 'vsdx', 'odg']))).toEqual(['docx', 'vsdx']); + const docx = await getBlankTemplate('docx'); + expect(Buffer.from(docx!.data).toString()).toBe('letterhead'); + expect(docx!.type).toBe('application/vnd.openxmlformats-officedocument.wordprocessingml.document'); + expect(Buffer.from((await getBlankTemplate('vsdx'))!.data).toString()).toBe('visio'); + // Formats the directory doesn't have still come from the shipped set. + expect((await getBlankTemplate('xlsx'))!.data.length).toBeGreaterThan(1000); + expect(await getBlankTemplate('../new')).toBeNull(); + }); + + it('ships blank templates the editors can open', async () => { + const parts: Record = { + docx: 'word/document.xml', xlsx: 'xl/workbook.xml', pptx: 'ppt/presentation.xml', + odt: 'content.xml', ods: 'content.xml', odp: 'content.xml', + }; + for (const [ext, part] of Object.entries(parts)) { + const zip = await JSZip.loadAsync((await getBlankTemplate(ext))!.data); + expect(zip.file(part), `${ext} has ${part}`).not.toBeNull(); + if (ext.startsWith('od')) { + // ODF: an uncompressed mimetype entry first. + expect(Object.keys(zip.files)[0]).toBe('mimetype'); + expect(await zip.file('mimetype')!.async('string')).toBe(`application/vnd.oasis.opendocument.${{ odt: 'text', ods: 'spreadsheet', odp: 'presentation' }[ext]}`); + } + // No language of their own: a new document takes the editor's. + for (const file of Object.values(zip.files).filter((f) => f.name.endsWith('.xml'))) { + expect(await file.async('string'), `${ext} ${file.name}`).not.toMatch(/en-GB|en-US|fo:language=| { + useEditor(COLLABORA); + const body = await (await status(request('/api/wopi/status'))).json(); + expect(body.newExtensions).toEqual(['docx', 'xlsx', 'pptx', 'odt', 'ods', 'odp']); + }); + + it('serves a template only for a format it offers', async () => { + useEditor(COLLABORA); + const res = await template(request('/api/wopi/template?ext=DOCX')); + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toBe('application/vnd.openxmlformats-officedocument.wordprocessingml.document'); + expect((await JSZip.loadAsync(await res.arrayBuffer())).file('word/document.xml')).not.toBeNull(); + + expect((await template(request('/api/wopi/template?ext=pdf'))).status).toBe(404); + expect((await template(request('/api/wopi/template?ext=rtf'))).status).toBe(404); + }); + + it('serves nothing without a session or an editor', async () => { + useEditor(COLLABORA); + (getStalwartCredentials as Mock).mockResolvedValueOnce(null); + expect((await template(request('/api/wopi/template?ext=docx'))).status).toBe(401); + + config.values.wopiClientUrl = ''; + expect((await template(request('/api/wopi/template?ext=docx'))).status).toBe(404); + expect((await (await status(request('/api/wopi/status'))).json()).newExtensions).toEqual([]); + }); +}); diff --git a/lib/admin/types.ts b/lib/admin/types.ts index 7f42af183..b0a54edc4 100644 --- a/lib/admin/types.ts +++ b/lib/admin/types.ts @@ -373,6 +373,12 @@ export const CONFIG_ENV_MAP: Record` + // files replace the shipped blank templates or add formats. + wopiNewFileFormats: { envVar: 'WOPI_NEW_FILE_FORMATS', type: 'string', defaultValue: '' }, + wopiTemplateDir: { envVar: 'WOPI_TEMPLATE_DIR', type: 'string', defaultValue: '' }, }; /** Keys that should never be exposed to the client config endpoint */ diff --git a/lib/wopi/blank-templates.ts b/lib/wopi/blank-templates.ts new file mode 100644 index 000000000..10fb594b8 --- /dev/null +++ b/lib/wopi/blank-templates.ts @@ -0,0 +1,731 @@ +/** + * Blank documents for "New file" with a WOPI editor (#1393): a new .docx + * or .ods is not a zero-byte file, so the file is created from one of these. + * + * Made with LibreOffice (Collabora Online convert-to, lang en-GB for A4 + * pages) from empty files, then stripped of their language tags and + * creation date so a new document takes the editor's language. Generated; + * an admin can replace any of them with `wopiTemplateDir`. + */ + +export const BLANK_TEMPLATES: Record = { + docx: { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + base64: + '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', + }, + xlsx: { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + base64: + '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', + }, + pptx: { + type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + base64: + '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', + }, + odt: { + type: 'application/vnd.oasis.opendocument.text', + base64: + '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', + }, + ods: { + type: 'application/vnd.oasis.opendocument.spreadsheet', + base64: + 'UEsDBAoAAAAAAKqzXF2FbDmKLgAAAC4AAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQub2FzaXMub3BlbmRvY3VtZW50LnNw' + + 'cmVhZHNoZWV0UEsDBAoAAAAAAJmzXF0AAAAAAAAAAAAAAAAQAAAAQ29uZmlndXJhdGlvbnMyL1BLAwQKAAAACACqs1xdr8NKMl8J' + + 'AACJNwAACgAAAHN0eWxlcy54bWzdW1lz47gRfs+vUHEqqaQqNA/5kBRbrt3ZbPKQSVIzk+RxCyYhCjFIsADIsvfXBycJnqI0tseO' + + '7Bmb6G50o48PAAFf3z7mePYAKUOkuPGis9CbwSIhKSqyG+9fX3/2F97t+jfXZLNBCVylJNnlsOA+408YspkQLtgqo2mKb7wt5+Uq' + + 'CPb7/dl+fkZoFsRhOA+yIAUc+A8I7j94RuJxy/NeiWi5XAaKallTkg90HQWCw4cPwh5muVlCUclvvB0tVgQwxFYFyCFb8WRFSlhY' + + '+1cu90qNWrdsCM2nSkteVzYHfDswpkXwSRDVf5/+Zvm1T6dqMxFw9BFCKnVSQHNY75wH+rke2VRNjwz7G+InJC8BR3e4rXQ/qnVP' + + 'EYe0CjRGxf1woCW1CjQF+9EhRWEgeRxLklFLEoCTqvOatdxRrJjSJIAYyoGzIDqLAssr5eCjTSKaVVm/IbtC5LKoFOM/+FhCiiQJ' + + 'YCW2avTg+k0VzOS8lMyuNK/tOSjMW5rJZnqKCbm40glE6MdzbBkoJjeCU3VJXoExrqEpnafTxeepK0tl0Q/aeRFQWBLKqwgzNud9' + + 'Wfn1cyBpvvRhlQ/sIZscuIdsoG5KCpnME65wdlpvrkwDpBDE1lPVWPtSUkTPz5mPClGRpFw50m53yRbQydmlmBupKTNgcm6CllsS' + + 'UmW28Jv4hzG4IxSMjElTHHG3v2KX30E6OYvExNSptRxyMLUDyevKYnICcDRGhOsRmXZncj731nYm3hAxC29AAv0UJpitr/U4quaZ' + + 'fpbqbryPgGLEiTcT+WlZcoSfXIorLml+Bgtho8BNtkeMNThKxBMx1T0AilT9B+Pq/044mX0BBesx4HegJOxPFYd+HDfniXGYP4s9' + + 'sy8fZ59htsOAHjbNYX42K4OhaJp2vcyyo0nhBuywWXzZno2xqrL8BGLsWfa6yS9F+UPKkViybchqT0Hpk1JDUUF8+Vz7rAQUZKJl' + + '60pVPQrlpPRTxDgo5OIlPLtARS0sgbMrp4bXSkVhh2pm6FfRHIUl99rcvqg/UAzGq2aWfVjm/p4kKGP42MzFVgcVj+pCRqfH7VNi' + + 'obyHkioQ5rnhF5FsjFNyL9ViIhDrw/z88gKcezM5MwqQFlGzlKt4uUk23mDotAjbgpTsfZE5DHL/UYYmihaRiE4f/cmlazPlyk1M' + + 'yX5OUhGREmRwWlLIkIMdJ6wEMiNQColmBbjcAtt7uSsSvlMgqKwWJYKkvy1dLAWhf0chEMtF4RmUNKOIihRK/JWbD8UqtalNygZg' + + 'Bt2kVynK6vj12T6erzsGfbE6ke5Syk0gON0JczVCE6meSy/+ts7lY1CVkhwUo/jQLpG4TmwMuZjS/XtIC+UDbVlXm6kJjVc/wf+C' + + 'f++mwayVHIIxS6+NHSrH+NvLMT5cjm4ZaqD5SbN5A7UZDEsKm+CQ2KxKqWoX7Df1jRS9LkNV9cKzBKPUlGYOqAil6E4txH6glOy3' + + 'EKTslzj8JerhEcmZyr1meBZeLa6qCm+wJFCu+mx51KjS1NzAmY34JKGhyIr2M2GJv4Uo23KbZC2iscRoERlbglS+PPBFDSoD58tz' + + 'aaBDuSOcy019LxHDDR8gUW1GTXNgTWQiYkgl4hAY9kg1wNDQv2Eqe95F1bPOkH14cNLK5vmA4ZR52thScR27eqwlB+2vOIZHMLRQ' + + 'MIik9yBmK+L34EvotZhm5ilHhdqsZUIuRRniTOhQCnr6PBr6nCXiCPr9VQCPqLYR4ekYeNwK1PTLthQV9wI//A3iFlkGq7JZJPG5' + + 'TKqqTb9zKQjNAa6b9wbP7ghOB9NT9+QS1TgN1Xbp0HWvlqG/7ypxBnqv6MP9VyxKQyf3DkTVTChmIkWsxODJb3DMopMib7Pmu0U+' + + 'WrQ96mJNL7Eu40VfGU9wZTzuyvidujIec2U87sr4GFd+FXa8PaCZZnvfKnGS7WrQBw2/A8m9WGDtirS5PEvqlAMZKQD277DPqRxc' + + 'ATs0LsgVbdAdUh2hqVwthmdXAppmaok4+7AI5dfJOVXtbNVnumt/JoQXL+veZ60dO07rrtb8gzjA9dahO5kM0quqMhzTIepJWKfP' + + 'Vt6XB0PxgVXcFaOoADUWaN1pti+9LGY3IrcnAxwtTZNd+oUDvmPvFaz+Qkh6ku1m2KfAVZJouHq5XLm8DMMjABvuOHUWVa/kBova' + + 'L+WG5fI4N/wI0td3QZK8pAuSRFbzdBf8B6jXZi/qhjcx0D9TSujr170y8sWcsFGfCdu5Azuyb9tQ/ZDIF2zvbZf8PX01svfUDCdu' + + 'PU0kTknVMHyVVD3BUfGoo+LXdpSzsn1jjpqPOmr+2o5K1cd72dXcZ8jGX++9zTd0fbuhF1zi1yd3U179HdiQHXq717tfOx5BnWMc' + + 'fU5hErL3RKiV8RWPwtGHbCVvXP5I5NnHLJzF4Wwe6vb0xvsUiTbsR7JxG4e/Skv0uSbfwhyaM07d8T/0NcIGg/Yra/B9xIAxdeqt' + + 'WxVLgyMVA4vsAWoLg4NhMSw915GrIWNMXdwWi2F8FV4eVNeRg+ozJgcUanTsNMf6BwU7GqPLRXwxPyw4bwumlzE8n2DqeVvQ3DM4' + + 'KHjRFryAd8togsbLjlfB1cV48Lf1q4x23lyG4/HfEIzJHqb+YB+LMA7TpSrDbnI3G+u7OvZOjnmUQJQDjhLfEg6e/8RD5z8pTFAO' + + 'BBRjkEB244mccE6GxqlHnxvJCx6+wA+y4w3zPpV55PUwdc9CmxdGMPX5XT0NSChqqzRtG0Lk7YXmHCHHYE+aw7N4eTE3x78CDjNB' + + 'MwfDzUZ7JNxsdQ6YlwtzphsMW2XM+R6W2kPytplNk4JOLCbEMP4/i6H7ljo6u6hfUjuLeHNTQEmFV5GV6tmjhvKr8pDDISosg518' + + '6Yz2fSTUG/XZ4ewOBsHVEHLAqi6qy1ymUfY0dh7u1kMP5Gnz19fqHn1pfrIthJp7fXt7ex20G01L2XJCK/oykM3tSmf1bJ2HKDvE' + + 'qt1YGfpPOWzzIEeoMX8dWdOcto61tqtGfCZYazjHrQ060TkUsM/mtv5IvOJOvPQThZm8YShtPzaEuiFY/17/xhHHLqt+/kPHdw2N' + + 'jSZVoi0rUsDtWNWfRbmbM7EwmFVM/gPAO3mlIYwvxULdjxfeOgwD9R2GxgrJuP7jzBosRhGGK/VdGd2XnE373kXG2uAErkAi4Iev' + + 'l0tXQLd9/wwP+pEq6P9LuvX/AFBLAwQKAAAACACZs1xdtPdo0gUBAACDAwAADAAAAG1hbmlmZXN0LnJkZs2TzW6DMBCE7zyFZc7Y' + + 'QC8FBXIoyrlqn8A1hlgFL/KaEt6+jpNWUaSq6p/U465GM9+OtJvtYRzIi7KowVQ0YyklykhotekrOrsuuaXbOtrYtisfmh3xaoOl' + + 'nyq6d24qOV+WhS03DGzPs6IoeJrzPE+8IsHVOHFIDMa0jggJHo1CafXkfBo5zuIJZldRdOugkHn3ID2L3TqpoLIKYbZSvYe2IJGB' + + 'QI0JTMqEdIMcuk5LxTOW81E5waHt4sdgvdODojxg8CuOz9jeiAym5V7gvbDuXIPffJVoeu5jenXTxfHfI5RgnDLuT+q7O3n/5/4u' + + 'z/8Z4q+0dkRsQM6jZ/qQ57TyH1VHr1BLAwQKAAAACACqs1xdMNb62wUEAAC0DgAACwAAAGNvbnRlbnQueG1srVdNj9s2EL33Vxgq' + + '0BuX1jpBbXXtHFL0lO0hmwK90uRIJkKJAklZ9r/vUF+mFMsrFDmsFiLfm3nzwRH98umSq9UZjJW62Efx0zpaQcG1kEW2j/759hfZ' + + 'Rp8Ov7zoNJUcEqF5lUPhCNeFw/8rZBc2yYwQah+dnCsTSuu6fqo3T9pk9Hm93tCMCuYYOUuof406xuXk8ruMeLfb0WZ3gFo5Yzqm' + + '/75+eeMnyBmRhXWs4HBjifdZAzjVJrcz+GfabvdgofNZy4ggcMbEDGjLjSzdPqpMkWhmpU0KloNNHE90CUWf0CREJ00Z2hXveinb' + + 'Y0NuztxpJsdb+oqbzeP1S49vi7zUW9cSgT+t9eDOE1pEn50PtH2/RbbU08UqkmrsubxkTh7V1Gn90GttpAMzlFrJ4vt84/ndodCG' + + '1Q9DitfUYwIl/KESzhQfjN+gZWVUAxKcggIfuKXxU0x7rOfBpW8ikw3HMNVVgWcLj26XP7iUYKTfYqqhJSMLYd6su6rFtW7AIdvd' + + '9LxLdhPPOl3eYsh7HnwyLP3jHtvRBhRWcKkvj8WhFwoVZiOW0zci5Bp/6Gd1fqQGSm3cUGFrN+5eV377Sv0e8Tkc+sGes8WFO2cz' + + '56Y0YH2fuGbwL7MWckZDSoLqMzXEeq8lsXoktzis8UTqMgnY05l3WWbOD1wt0qnFySDkJ2YWt2sDHvW6b6nFzc4meeZ6OCpYCPxT' + + 'ih21YQ+iancCemivqPIjmMVtiV/eHw5vDo4tNeCxIVfp/zGJRhGpW0TdenD9+BAd+rtG+y20dFhI8c5BUsaBCODKHl7awIblVfvu' + + '/e+jz8wo6XS0whPQQ3KpruFOSPd7JIMCReNktrW0doQopeP4MT0zI5sJQx+7/1s7vXpjhb0j4DdWavvHgGhfH8u5Wgf5T9Gzevu8' + + '+gpZpZh5X1oA/mkq6Vw1u3VWOY0XF8lJY2coc/McxcR1PDjr9DenDweeqvIi6pnhIilxSoBxEuwq1cnRAPtOjoADAw16173FDl5L' + + '4e9Q66ftdieLRn8gZ16bmdNmdD0RhiuhqnbLL55AZifnnce/b9H5Y8GVBaJLJ3OmSMh2poLluh27r7tfzHGagCEly4C0jD8hZZVy' + + 'k6CCgNqJKKQtFbt2ejpr/mqG31ySa4GWlCHu+KNUOtsY3cZRi+ttauA3igl7AnCHl9a1v/xgC/vhRCw477FXdbOZykIQnMyg8HdA' + + 'ypRFkS3GJ9a0h4DgVMNPoB9W91C1VIIzI2wfZbtVVEqRKzAc2vHu49pH2G40zx7UJPPN6457YhMnuRVmxOsa+g60ORdd1tviEA4o' + + 'IcT0VRtr8Y1zx6Bv5olvtOcLNeGOV3qKNyLC1AVTYFQuOioonfnNefgPUEsDBAoAAAAIAKqzXF2BxSpDWAEAAJUCAAAIAAAAbWV0' + + 'YS54bWyNUj1PwzAU3PkVkWFN7DQfJFaaDkhMlVhAYqsc+zUYXLtynKY/n+azgDqw+Z3vfO9OLjbng/JOYBtp9BqFAUEeaG6E1PUa' + + 'vb0++xnalHeF2e8lByoMbw+gnX8Ax7yLVDe0tkKoNfpw7kgx7rou6KLA2BqvCIlwjQVzzD9J6O7RpOjFa9RaTQ1rZEM1O0BDHafm' + + 'CHq2oFcuHdYaZ8EXq2Nr1WAkOAYFvajBYRDimXtWUn/d2izM8xwPtzPVGLMQ+y3GvHOMGI/zwh6m/0aYuhtCTOcfhceonNvts5bF' + + 'kHgpunHMycZJ7g24Y5UCn5tWu4sYjSAHpWaMTJipPoG7K4qnh2vQYJkztnwySrHKWLZ7GezxKg1IHETB6mErdXvevWfpLo29raws' + + 'jJTd0Zr+Wcx5koYxTx8hE5DGCRF5xgiEqyQL44gkYp8mpErjAv8xLfCvrPjWvyq/AVBLAwQKAAAACACqs1xdE+JEYrAEAAA5JgAA' + + 'DAAAAHNldHRpbmdzLnhtbO1aW1PiSBR+319h8YwS0HEUlSlkxpVdHSmCzjpvTXKALjt9Ut0dA/Pr93QClhfiIqSt2imfIEn3dy45' + + '9/Txl2kktu5BaY7ypFLf8SpbIAMMuRyfVK4HZ9sHlS+tP45xNOIBNEMMkgik2dZgDC3RW7Rd6maAcsRpQ6JkE5nmuilZBLppgibG' + + 'IBfbmo9XNzNi+Z2p4PLupDIxJm7Wamma7qS7O6jGtfrh4WEte7pYiogPCy12zlm2uOF5e7X8+mF1drUqY3MpM8bm/x+pZq/SWuhh' + + 'IX7reC5L/rPNDURWN1vz25bYSYVYbt5zSB+0Vlm27+meG675UEBbARtgXFk8NLOYHnJpKi3vuPYS5E3AFzAybpB/8NBMlkE3Gp8O' + + 'NkY/Bz6eLOV871NjVfTtiMXbXIYwhfA5JUiXv6JsD5mLmq3CL6Td8BmT2ih6/5WWtYb6mzi1oM/4HDDSx38x+nSLPwEw9RXMr5Mo' + + 'jaqHmhuy/n9KtJKnyLclIrcDw+/BjwU3fSbH8Fz3E1TWAtcDXzBcss8sYPtFFr0hbrmxY4F6isZgVCLwT8RoQCjLX9kGoDdMJLCM' + + '0bq3rg7YGKxvv4q+vya4P8H0T8Wfh40hogAmKy2jEljTOWRAyoRwAFNzRUltJDC9gDELZkW0RkzoNYnlwD6FOgEXXMIZSpOiunNA' + + 'qoNSQmBQXWvwJYv7dOGATHcsSXunlHzu2iMD6jIRhgsrGgcROiBI4gwUsyTG36LYzKwadVdesBkm6whYez2b1V5LO6uG3iwhFWS8' + + 'PPOsp4tzVPwXmRATfqBQiCFThQXG54O9jxC0aQiyBDJb7ymwpYoD+7ZkfoLCjH9dfryz+N/RuIRuJ4aMww3+GaooEUxfMnWnHanf' + + 'TaKxqB0ULxSTm3xjf5dagP0STNPBiz1nmjhPItnH9BxYSK2fEyJZLKRg6QC9q68SY/OSP4uGKLQPxgWRzGvPqWYVtm61OUra2O8i' + + 'EXa1TesD7DNNedcBgRyYZMobTGcU+qDJuArbqXrjs1cO/K0LeD8Zhvye60L2SwK/LRG8q3P49pRrf0YFsELJf4GDoDcP16dMLZ1N' + + 'zIuINcX4wWWIqW+YKSquvKpXPfC86r7nHdWPqtXq0UeT8NEkvFuTMJ+iLV+gwaw+Jc1vJIrZOPaWcWlbkKn2yBvMXzjsMBmAcNBD' + + 'x7GYkc7VV2aYA/jEYIeJgAKJgfLhOxOmWEBm2cEoVqBttC294fkWDSFsa84ktWo8NtapHZQ5GRkrh4Cpe0KvYm/gvhn4Bdn6Oyjr' + + 'Smam60wWh2X/71CWR9S2UDM9AkXp9WuvW+LstKu/zr9i+eThjmrwv0HJzK17iQxMksVnJ4TepX15r7LQee9CCf/uOg4pX1xiWBDJ' + + 'd9eERhb2yduQAocDzolfPpr1mNZU84XkfZMyv1PYQgDUd7pYXjHXVsbosRjUmcKITDGJHShiTmcpPNOwv3fKJVOzVVj+6GR/107W' + + 'Z/cwmCTRUFK172DiSPA3+UGHK9kRqMHRyPH/O9Z8j6Gvw3m1y7Gp61F+nuBsDB4AVfwlNUdFpyryUrzzuBF1dMSCMnZxhnr1g9lm' + + 'H/MKu/Tai9NNtaJzX61/AVBLAwQKAAAACACZs1xd8ru913MAAAATAQAAGAAAAFRodW1ibmFpbHMvdGh1bWJuYWlsLnBuZ+sM8HPn' + + '5ZLiYmBg4PX0cAliYGAMBeKzHMxAkZnPJYyAFGeAT4jr////gUwgWfe+3x8kWOARWczAwC0MwowMs+ZIAAU3eLo4hlTcenuwEchh' + + 'OLzgZ3D8A45QRoZBCu4zpP9hYHRatvAniOfp6ueyzimhCQBQSwMECgAAAAgAqrNcXSPeBaBIAQAAfAQAABUAAABNRVRBLUlORi9t' + + 'YW5pZmVzdC54bWytlE1qwzAQhfc5hdG2WGpLF0XEyaLQE6QHUOSRI5BGQj8huX1lJ45dSmgNWRgkzeh78x7I6+3JmuoIIWqHDXmh' + + 'z6QClK7V2DXka/dZv5PtZrW2ArWCmPi4qMo9jLdtQ3JA7kTUkaOwEHmS3HnA1slsARP/2c8HpdtuNsAbuaKNg9PIDR0fQcplbEUq' + + '3VchOHkIui8Jw51SWgKfES5KlwM5IaWz5TNG7F0Q11t/8eSNt1lVUyRKG6hLezhPhlQ2pvYiHRrC7vqcQoVWizqdPTREeG+0HAyy' + + 'I7Z0yJTOo6TRBxBtPAAkwpaM8uFQ6S6HgR5f2T9HiBlpSYBmTeWcsEw8prOB2IPuyKaSLuvLi7DjGQ2t+oef0vW0WKO4Tn3wD58d' + + 'kng4NEJK5fU+PundIds9Cm0iS+OSeuzuiGgrOmB9vais2a8/yOYbUEsDBAoAAAAAAKqzXF0AAAAAAAAAAAAAAAAJAAAATUVUQS1J' + + 'TkYvUEsBAhQACgAAAAAAqrNcXYVsOYouAAAALgAAAAgAAAAAAAAAAAAAAAAAAAAAAG1pbWV0eXBlUEsBAhQACgAAAAAAmbNcXQAA' + + 'AAAAAAAAAAAAABAAAAAAAAAAAAAQAAAAVAAAAENvbmZpZ3VyYXRpb25zMi9QSwECFAAKAAAACACqs1xdr8NKMl8JAACJNwAACgAA' + + 'AAAAAAAAAAAAAACCAAAAc3R5bGVzLnhtbFBLAQIUAAoAAAAIAJmzXF2092jSBQEAAIMDAAAMAAAAAAAAAAAAAAAAAAkKAABtYW5p' + + 'ZmVzdC5yZGZQSwECFAAKAAAACACqs1xdMNb62wUEAAC0DgAACwAAAAAAAAAAAAAAAAA4CwAAY29udGVudC54bWxQSwECFAAKAAAA' + + 'CACqs1xdgcUqQ1gBAACVAgAACAAAAAAAAAAAAAAAAABmDwAAbWV0YS54bWxQSwECFAAKAAAACACqs1xdE+JEYrAEAAA5JgAADAAA' + + 'AAAAAAAAAAAAAADkEAAAc2V0dGluZ3MueG1sUEsBAhQACgAAAAgAmbNcXfK7vddzAAAAEwEAABgAAAAAAAAAAAAAAAAAvhUAAFRo' + + 'dW1ibmFpbHMvdGh1bWJuYWlsLnBuZ1BLAQIUAAoAAAAIAKqzXF0j3gWgSAEAAHwEAAAVAAAAAAAAAAAAAAAAAGcWAABNRVRBLUlO' + + 'Ri9tYW5pZmVzdC54bWxQSwECFAAKAAAAAACqs1xdAAAAAAAAAAAAAAAACQAAAAAAAAAAABAAAADiFwAATUVUQS1JTkYvUEsFBgAA' + + 'AAAKAAoATwIAAAkYAAAAAA==', + }, + odp: { + type: 'application/vnd.oasis.opendocument.presentation', + base64: + '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' + + 'KXNqGQdVN6rEJ2wVJzSuduoHRc4/4Xuq1NjHW3al1NZoMqt05RkT55Cb5d93MvlPRo5ZMqnpTM5SFAx7ZNmzThdLRqeLpSNxmwxb' + + 'bRVmU+cQbvORM500Rq79wsnpbuF0JGZ257aGd1R6C9AucQGl8BlijSS1MZPyXWv3ft6Yj6bzWQF/A7u8ibzSHynL15HpTOvA39v5' + + 'HmSb4tGK7Qlme15ya9aayJ7aHBmq0Rh6Jd8ZD2dx1Ga/7e8Aqje+/a9DzerDAF3h1YcDysIBxelMxAikaEB+0LJ3nsIfe8jdbsSl' + + 'uuT9/wFQSwMECgAAAAgAqrNcXW/ZWpNXBQAAkxgAAAsAAABjb250ZW50LnhtbM1ZW2/bNhR+368wXGBvEi05N2tJiqHDnpKhaLJh' + + 'r7REyWwpUSAp29mv7yEl0ZRsKWpctCvQFCS/cz/8DpXevt/nbLYlQlJe3M0DfzGfkSLmCS2yu/nfz396N/P397/c8jSlMYkSHlc5' + + 'KZQX80LBvzOQLmSEC5rfzStRRBxLKqMC50RGKo54SYpWJjpgsQJrkTFWb8qcsqkKNBbs5yVoWTPiqslEkoCejVJlhNBut/N3S5+L' + + 'DIWLxRJlKMEKe1tKdu9aif1G5SclgtVqhcyphUo6oDpA/z4+PMUbkmOPFlLhIiYHqeR1KQtOucjlAD5E9XELTng+qBkQHtlCzixa' + + 'xoKWanKODdpNrTY9VVpjXVmo92YgxzfoEQ7Nj8eHFl8321RrTWs69jjn1pwWqBFtdi5QvT5ENtXSXjIv5QO9B0Z3o1Z3gioibKkZ' + + 'Lb4MN54+tYUWeDcaUrBAGuN4Eo96EmMWW+UHaFkJZkBJjAgjOnCJAj9ALVbLkX3bRCKzdJDyqkjqS13nj+xLIqg+wsyIRR0Nnauv' + + 'XtjkWhuwK60O/rwqrHqWeTq9xUAutDYxlH68x1bIgNwKTrWlsUC+rqOJWCbTxZeJKyv0pR/08xIJUnKhbIWlXKpTXfn8CekzT+fQ' + + '9oPcZpMLt80G7k0piNR9oswAmqbNlemQFCWszZSN9VRLQvW8XAJZw43kZeRI9zlvP02dJlyepH2NPSKMN1hMblcD7vartv9a43XJ' + + 'zbTh5AuCe7WJub1eUDz4yxhec4FHMlGfOOKuvqLK10RMbmWY1kcXPicKT1Wgsa4s429gr05E7BBRs+88nS7m9+07qZ6fEtmNFN5L' + + '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', + }, + pdf: { + type: 'application/pdf', + base64: + 'JVBERi0xLjcKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+CnN0cmVhbQp4nDPQ' + + 'M1Qo5ypUMFAw0DMwslAwtTTVMzI3U7AwMdSzMDNUKErlCtdSyOMKVAAAtyYIrwplbmRzdHJlYW0KZW5kb2JqCgozIDAgb2JqCjUw' + + 'CmVuZG9iagoKNSAwIG9iago8PAo+PgplbmRvYmoKCjYgMCBvYmoKPDwvRm9udCA1IDAgUi9Qcm9jU2V0Wy9QREYvVGV4dF0KPj4K' + + 'ZW5kb2JqCgoxIDAgb2JqCjw8L1R5cGUvUGFnZS9QYXJlbnQgNCAwIFIvUmVzb3VyY2VzIDYgMCBSL01lZGlhQm94WzAgMCA1OTUu' + + 'MzAzOTM3MDA3ODc0IDg0MS44ODk3NjM3Nzk1MjhdL1RhYnMvUwovQ29udGVudHMgMiAwIFI+PgplbmRvYmoKCjcgMCBvYmoKPDwv' + + 'VHlwZS9NZXRhZGF0YS9TdWJ0eXBlL1hNTC9MZW5ndGggNDk0Mj4+CnN0cmVhbQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1' + + 'TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIj4KIDxyZGY6UkRGIHht' + + 'bG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgPHJkZjpEZXNjcmlwdGlvbiBy' + + 'ZGY6YWJvdXQ9IiIgeG1sbnM6cGRmPSJodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvIj4KICAgPHBkZjpQcm9kdWNlcj5Db2xs' + + 'YWJvcmEgT2ZmaWNlIDI2LjA0LjMuMiAoWDg2XzY0KTwvcGRmOlByb2R1Y2VyPgogICA8cGRmOlBERlZlcnNpb24+MS43PC9wZGY6' + + 'UERGVmVyc2lvbj4KICA8L3JkZjpEZXNjcmlwdGlvbj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9' + + 'Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICA8eG1wOkNyZWF0b3JUb29sPldyaXRlcjwveG1wOkNyZWF0b3JUb29s' + + 'PgogICA8eG1wOkNyZWF0ZURhdGU+MjAyNi0xMC0yOFQyMjoyODo1MFo8L3htcDpDcmVhdGVEYXRlPgogICA8eG1wOk1vZGlmeURh' + + 'dGU+MjAyNi0xMC0yOFQyMjoyODo1MFo8L3htcDpNb2RpZnlEYXRlPgogICA8eG1wOk1ldGFkYXRhRGF0ZT4yMDI2LTEwLTI4VDIy' + + 'OjI4OjUwWjwveG1wOk1ldGFkYXRhRGF0ZT4KICA8L3JkZjpEZXNjcmlwdGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4KCmVuZHN0cmVhbQplbmRvYmoKCjQgMCBvYmoKPDwvVHlwZS9Q' + + 'YWdlcwovUmVzb3VyY2VzIDYgMCBSCi9LaWRzWyAxIDAgUiBdCi9Db3VudCAxPj4KZW5kb2JqCgo4IDAgb2JqCjw8L1R5cGUvQ2F0' + + 'YWxvZy9QYWdlcyA0IDAgUgovTGFuZyhlbi1HQikKL01ldGFkYXRhIDcgMCBSPj4KZW5kb2JqCgo5IDAgb2JqCjw8L0NyZWF0b3I8' + + 'RkVGRjAwNTcwMDcyMDA2OTAwNzQwMDY1MDA3Mj4vUHJvZHVjZXI8RkVGRjAwNDMwMDZGMDA2QzAwNkMwMDYxMDA2MjAwNkYwMDcy' + + 'MDA2MTAwMjAwMDRGMDA2NjAwNjYwMDY5MDA2MzAwNjUwMDIwMDAzMjAwMzYwMDJFMDAzMDAwMzQwMDJFMDAzMzAwMkUwMDMyMDAy' + + 'MDAwMjgwMDU4MDAzODAwMzYwMDVGMDAzNjAwMzQwMDI5Pi9DcmVhdGlvbkRhdGUoRDoyMDI2MTAyODIyMjg1MFonKT4+CmVuZG9i' + + 'agoKeHJlZgowIDEwCjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDIzMyAwMDAwMCBuIAowMDAwMDAwMDE5IDAwMDAwIG4gCjAw' + + 'MDAwMDAxNDAgMDAwMDAgbiAKMDAwMDAwNTM4MyAwMDAwMCBuIAowMDAwMDAwMTU5IDAwMDAwIG4gCjAwMDAwMDAxODEgMDAwMDAg' + + 'biAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDA1NDU2IDAwMDAwIG4gCjAwMDAwMDU1MzEgMDAwMDAgbiAKdHJhaWxlcgo8PC9T' + + 'aXplIDEwL1Jvb3QgOCAwIFIKL0luZm8gOSAwIFIKL0lEIFsgPDZCRTZEOTlGQzUxNzNDRTREQTQ1N0UwNjg5OTA4NEJBPgo8NkJF' + + 'NkQ5OUZDNTE3M0NFNERBNDU3RTA2ODk5MDg0QkE+IF0KL0RvY0NoZWNrc3VtIC8zQzM4MzIyRDAxMzdGMzZEMUIzMDNGQzUxOEEx' + + 'QjhEQQo+PgpzdGFydHhyZWYKNTc3OAolJUVPRgo=', + }, +}; diff --git a/lib/wopi/new-file.ts b/lib/wopi/new-file.ts new file mode 100644 index 000000000..34d1deae1 --- /dev/null +++ b/lib/wopi/new-file.ts @@ -0,0 +1,86 @@ +import { readFile, readdir } from 'node:fs/promises'; +import path from 'node:path'; +import { configManager } from '@/lib/admin/config-manager'; +import { logger } from '@/lib/logger'; +import { BLANK_TEMPLATES } from '@/lib/wopi/blank-templates'; +import type { WopiActions } from '@/lib/wopi/discovery'; + +/** + * The formats "New file" offers next to .txt when a WOPI editor is + * configured (#1393), and the blank file each one starts from. + * + * A format is offered when the editor has an "edit" action for it (a + * view-only one could be created but not filled in) and there is a blank + * template for it: one shipped in blank-templates.ts, or `new.` in the + * admin's `wopiTemplateDir` (ONLYOFFICE's document-templates/new/ + * folders use that name). `wopiNewFileFormats` replaces the discovery part + * with the admin's own list. + */ + +const EXTENSION = /^[a-z0-9]{1,10}$/; + +export interface BlankTemplate { + type: string; + data: Uint8Array; +} + +async function getTemplateDir(): Promise { + await configManager.ensureLoaded(); + return configManager.get('wopiTemplateDir', '').trim(); +} + +/** The admin's format list, lowercased and without dots; empty when unset. */ +export function parseFormatList(value: string): string[] { + const formats = value + .split(/[\s,;]+/) + .map((ext) => ext.trim().replace(/^\./, '').toLowerCase()) + .filter((ext) => EXTENSION.test(ext)); + return Array.from(new Set(formats)); +} + +/** Extensions with a `new.` file in the template directory. */ +async function templateDirFormats(dir: string): Promise { + if (!dir) return []; + try { + return (await readdir(dir)) + .map((name) => /^new\.([^.]+)$/.exec(name)?.[1] ?? '') + .filter((ext) => EXTENSION.test(ext)); + } catch (error) { + logger.warn('WOPI template directory unreadable', { + dir, + error: error instanceof Error ? error.message : 'Unknown', + }); + return []; + } +} + +/** The extensions "New file" offers besides .txt, in a stable order. */ +export async function getNewFileFormats(actions: WopiActions): Promise { + await configManager.ensureLoaded(); + const override = parseFormatList(configManager.get('wopiNewFileFormats', '')); + const available = new Set([...Object.keys(BLANK_TEMPLATES), ...await templateDirFormats(await getTemplateDir())]); + const wanted = override.length > 0 ? override : Object.keys(actions.edit); + const formats = wanted.filter((ext) => ext !== 'txt' && available.has(ext)); + // Shipped formats first, in their own order (documents, spreadsheets, + // presentations, then ODF), then the admin's extras. + const order = Object.keys(BLANK_TEMPLATES); + const rank = (ext: string) => (order.includes(ext) ? order.indexOf(ext) : order.length); + return formats.sort((a, b) => rank(a) - rank(b)); +} + +/** The blank file a new document of this format starts from, or null. */ +export async function getBlankTemplate(ext: string): Promise { + if (!EXTENSION.test(ext)) return null; + const shipped = BLANK_TEMPLATES[ext]; + const dir = await getTemplateDir(); + if (dir) { + try { + const data = await readFile(path.join(dir, `new.${ext}`)); + return { type: shipped?.type ?? 'application/octet-stream', data: new Uint8Array(data) }; + } catch { + // No admin template for this format: fall back to the shipped one. + } + } + if (!shipped) return null; + return { type: shipped.type, data: new Uint8Array(Buffer.from(shipped.base64, 'base64')) }; +} diff --git a/locales/ar/common.json b/locales/ar/common.json index be39304f1..53d1387de 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -4619,6 +4619,13 @@ "paste_success": "تم اللصق بنجاح", "paste_error": "فشل اللصق", "new_text_file": "ملف نصي جديد", + "new_file": "ملف جديد", + "file_format": "التنسيق", + "format_text": "ملف نصي", + "format_document": "مستند", + "format_spreadsheet": "جدول بيانات", + "format_presentation": "عرض تقديمي", + "format_pdf": "PDF", "file_name": "اسم الملف", "retry": "إعادة المحاولة", "refresh": "تحديث", diff --git a/locales/ca/common.json b/locales/ca/common.json index cfed03780..f6518c120 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -4619,6 +4619,13 @@ "paste_success": "Enganxat correctament", "paste_error": "No s'ha pogut enganxar", "new_text_file": "Fitxer de text nou", + "new_file": "Fitxer nou", + "file_format": "Format", + "format_text": "Fitxer de text", + "format_document": "Document", + "format_spreadsheet": "Full de càlcul", + "format_presentation": "Presentació", + "format_pdf": "PDF", "file_name": "Nom del fitxer", "retry": "Reintenta", "refresh": "Actualitza", diff --git a/locales/cs/common.json b/locales/cs/common.json index 6ae6c02f8..a52d94e20 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -4489,6 +4489,13 @@ "paste_success": "Úspěšně vloženo", "paste_error": "Vložení selhalo", "new_text_file": "Nový textový soubor", + "new_file": "Nový soubor", + "file_format": "Formát", + "format_text": "Textový soubor", + "format_document": "Dokument", + "format_spreadsheet": "Tabulka", + "format_presentation": "Prezentace", + "format_pdf": "PDF", "file_name": "Název souboru", "retry": "Opakovat", "refresh": "Obnovit", diff --git a/locales/da/common.json b/locales/da/common.json index 790f4e62c..3e04e5ef2 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -4618,6 +4618,13 @@ "paste_success": "Indsat", "paste_error": "Kunne ikke indsætte", "new_text_file": "Ny tekstfil", + "new_file": "Ny fil", + "file_format": "Format", + "format_text": "Tekstfil", + "format_document": "Dokument", + "format_spreadsheet": "Regneark", + "format_presentation": "Præsentation", + "format_pdf": "PDF", "file_name": "Filnavn", "retry": "Prøv igen", "refresh": "Opdatér", diff --git a/locales/de/common.json b/locales/de/common.json index 1c23ad534..3a0ce45aa 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -4489,6 +4489,13 @@ "paste_success": "Erfolgreich eingefügt", "paste_error": "Einfügen fehlgeschlagen", "new_text_file": "Neue Textdatei", + "new_file": "Neue Datei", + "file_format": "Format", + "format_text": "Textdatei", + "format_document": "Dokument", + "format_spreadsheet": "Tabelle", + "format_presentation": "Präsentation", + "format_pdf": "PDF", "file_name": "Dateiname", "retry": "Wiederholen", "refresh": "Aktualisieren", diff --git a/locales/en/common.json b/locales/en/common.json index e8dc05efd..e370559a0 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -4619,6 +4619,13 @@ "paste_success": "Pasted", "paste_error": "Couldn't paste", "new_text_file": "New text file", + "new_file": "New file", + "file_format": "Format", + "format_text": "Text file", + "format_document": "Document", + "format_spreadsheet": "Spreadsheet", + "format_presentation": "Presentation", + "format_pdf": "PDF", "file_name": "File name", "retry": "Retry", "refresh": "Refresh", diff --git a/locales/es/common.json b/locales/es/common.json index a63236cba..45d4255d7 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -4489,6 +4489,13 @@ "paste_success": "Pegado correctamente", "paste_error": "Error al pegar", "new_text_file": "Nuevo archivo de texto", + "new_file": "Nuevo archivo", + "file_format": "Formato", + "format_text": "Archivo de texto", + "format_document": "Documento", + "format_spreadsheet": "Hoja de cálculo", + "format_presentation": "Presentación", + "format_pdf": "PDF", "file_name": "Nombre del archivo", "retry": "Reintentar", "refresh": "Actualizar", diff --git a/locales/et/common.json b/locales/et/common.json index b8ef615a2..409dc7914 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -4619,6 +4619,13 @@ "paste_success": "Edukalt kleebitud", "paste_error": "Kleepimine ebaõnnestus", "new_text_file": "Uus tekstifail", + "new_file": "Uus fail", + "file_format": "Vorming", + "format_text": "Tekstifail", + "format_document": "Dokument", + "format_spreadsheet": "Arvutustabel", + "format_presentation": "Esitlus", + "format_pdf": "PDF", "file_name": "Faili nimi", "retry": "Proovi uuesti", "refresh": "Värskenda", diff --git a/locales/fa/common.json b/locales/fa/common.json index 344752ec7..c67b0be54 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -4619,6 +4619,13 @@ "paste_success": "با موفقیت چسبانده شد", "paste_error": "چسباندن ناموفق بود", "new_text_file": "فایل متنی جدید", + "new_file": "فایل جدید", + "file_format": "قالب", + "format_text": "فایل متنی", + "format_document": "سند", + "format_spreadsheet": "صفحه‌گسترده", + "format_presentation": "ارائه", + "format_pdf": "PDF", "file_name": "نام فایل", "retry": "تلاش مجدد", "refresh": "بازنشانی", diff --git a/locales/fr/common.json b/locales/fr/common.json index 0b467e082..05d4a448c 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -4489,6 +4489,13 @@ "paste_success": "Collé avec succès", "paste_error": "Échec du collage", "new_text_file": "Nouveau fichier texte", + "new_file": "Nouveau fichier", + "file_format": "Format", + "format_text": "Fichier texte", + "format_document": "Document", + "format_spreadsheet": "Feuille de calcul", + "format_presentation": "Présentation", + "format_pdf": "PDF", "file_name": "Nom du fichier", "retry": "Réessayer", "refresh": "Actualiser", diff --git a/locales/he/common.json b/locales/he/common.json index 3ee09e4f3..639fb6c7a 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -4406,6 +4406,13 @@ "paste_success": "הודבק בהצלחה", "paste_error": "ההדבקה נכשלה", "new_text_file": "קובץ טקסט חדש", + "new_file": "קובץ חדש", + "file_format": "פורמט", + "format_text": "קובץ טקסט", + "format_document": "מסמך", + "format_spreadsheet": "גיליון אלקטרוני", + "format_presentation": "מצגת", + "format_pdf": "PDF", "file_name": "שם הקובץ", "retry": "נסה שוב", "refresh": "רענן", diff --git a/locales/hu/common.json b/locales/hu/common.json index cfb87fb45..d73d1024c 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -4619,6 +4619,13 @@ "paste_success": "Sikeresen beillesztve", "paste_error": "Nem sikerült beilleszteni", "new_text_file": "Új szöveges fájl", + "new_file": "Új fájl", + "file_format": "Formátum", + "format_text": "Szöveges fájl", + "format_document": "Dokumentum", + "format_spreadsheet": "Táblázat", + "format_presentation": "Bemutató", + "format_pdf": "PDF", "file_name": "Fájl neve", "retry": "Újra", "refresh": "Frissítés", diff --git a/locales/it/common.json b/locales/it/common.json index 643985d11..d233c37f2 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -4489,6 +4489,13 @@ "paste_success": "Incollato con successo", "paste_error": "Incollaggio non riuscito", "new_text_file": "Nuovo file di testo", + "new_file": "Nuovo file", + "file_format": "Formato", + "format_text": "File di testo", + "format_document": "Documento", + "format_spreadsheet": "Foglio di calcolo", + "format_presentation": "Presentazione", + "format_pdf": "PDF", "file_name": "Nome del file", "retry": "Riprova", "refresh": "Aggiorna", diff --git a/locales/ja/common.json b/locales/ja/common.json index 5b7a06f07..14443e59d 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -4489,6 +4489,13 @@ "paste_success": "正常に貼り付けました", "paste_error": "貼り付けに失敗しました", "new_text_file": "新規テキストファイル", + "new_file": "新規ファイル", + "file_format": "形式", + "format_text": "テキストファイル", + "format_document": "ドキュメント", + "format_spreadsheet": "スプレッドシート", + "format_presentation": "プレゼンテーション", + "format_pdf": "PDF", "file_name": "ファイル名", "retry": "再試行", "refresh": "更新", diff --git a/locales/ko/common.json b/locales/ko/common.json index 4513b088a..96e51cd9f 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -4489,6 +4489,13 @@ "paste_success": "붙여넣기 성공", "paste_error": "붙여넣기에 실패했어요", "new_text_file": "새 텍스트 파일", + "new_file": "새 파일", + "file_format": "형식", + "format_text": "텍스트 파일", + "format_document": "문서", + "format_spreadsheet": "스프레드시트", + "format_presentation": "프레젠테이션", + "format_pdf": "PDF", "file_name": "파일 이름", "retry": "재시도", "refresh": "새로고침", diff --git a/locales/lv/common.json b/locales/lv/common.json index eb37e9312..ea3047e14 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -4489,6 +4489,13 @@ "paste_success": "Ielīmēts sekmīgi", "paste_error": "Neizdevās ielīmēt", "new_text_file": "Jauns teksta fails", + "new_file": "Jauns fails", + "file_format": "Formāts", + "format_text": "Teksta fails", + "format_document": "Dokuments", + "format_spreadsheet": "Izklājlapa", + "format_presentation": "Prezentācija", + "format_pdf": "PDF", "file_name": "Faila nosaukums", "retry": "Mēģināt vēlreiz", "refresh": "Atsvaidzināt", diff --git a/locales/mn/common.json b/locales/mn/common.json index 4d9040592..e6ea18a15 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -4619,6 +4619,13 @@ "paste_success": "Амжилттай буулгалаа", "paste_error": "Буулгаж чадсангүй", "new_text_file": "Шинэ текст файл", + "new_file": "Шинэ файл", + "file_format": "Формат", + "format_text": "Текст файл", + "format_document": "Баримт бичиг", + "format_spreadsheet": "Хүснэгт", + "format_presentation": "Танилцуулга", + "format_pdf": "PDF", "file_name": "Файлын нэр", "retry": "Дахин оролдох", "refresh": "Сэргээх", diff --git a/locales/nb/common.json b/locales/nb/common.json index 6d5ba4cc5..6794e637c 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -4619,6 +4619,13 @@ "paste_success": "Limt inn", "paste_error": "Kunne ikke lime inn", "new_text_file": "Ny tekstfil", + "new_file": "Ny fil", + "file_format": "Format", + "format_text": "Tekstfil", + "format_document": "Dokument", + "format_spreadsheet": "Regneark", + "format_presentation": "Presentasjon", + "format_pdf": "PDF", "file_name": "Filnavn", "retry": "Prøv på nytt", "refresh": "Oppdater", diff --git a/locales/nl/common.json b/locales/nl/common.json index a2627db9c..21f2232f5 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -4489,6 +4489,13 @@ "paste_success": "Succesvol geplakt", "paste_error": "Plakken mislukt", "new_text_file": "Nieuw tekstbestand", + "new_file": "Nieuw bestand", + "file_format": "Formaat", + "format_text": "Tekstbestand", + "format_document": "Document", + "format_spreadsheet": "Spreadsheet", + "format_presentation": "Presentatie", + "format_pdf": "PDF", "file_name": "Bestandsnaam", "retry": "Opnieuw proberen", "refresh": "Vernieuwen", diff --git a/locales/pl/common.json b/locales/pl/common.json index 9f6e69908..b0aae170a 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -4489,6 +4489,13 @@ "paste_success": "Wklejono pomyślnie", "paste_error": "Nie udało się wkleić", "new_text_file": "Nowy plik tekstowy", + "new_file": "Nowy plik", + "file_format": "Format", + "format_text": "Plik tekstowy", + "format_document": "Dokument", + "format_spreadsheet": "Arkusz kalkulacyjny", + "format_presentation": "Prezentacja", + "format_pdf": "PDF", "file_name": "Nazwa pliku", "retry": "Ponów", "refresh": "Odśwież", diff --git a/locales/pt/common.json b/locales/pt/common.json index d83c922cb..3603dd30d 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -4489,6 +4489,13 @@ "paste_success": "Colado com sucesso", "paste_error": "Falha ao colar", "new_text_file": "Novo arquivo de texto", + "new_file": "Novo arquivo", + "file_format": "Formato", + "format_text": "Arquivo de texto", + "format_document": "Documento", + "format_spreadsheet": "Planilha", + "format_presentation": "Apresentação", + "format_pdf": "PDF", "file_name": "Nome do arquivo", "retry": "Tentar novamente", "refresh": "Atualizar", diff --git a/locales/ro/common.json b/locales/ro/common.json index b0b4f60cf..6f8502c27 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -4619,6 +4619,13 @@ "paste_success": "S-a lipit cu succes", "paste_error": "Nu s-a putut insera", "new_text_file": "Fișier text nou", + "new_file": "Fișier nou", + "file_format": "Format", + "format_text": "Fișier text", + "format_document": "Document", + "format_spreadsheet": "Foaie de calcul", + "format_presentation": "Prezentare", + "format_pdf": "PDF", "file_name": "Numele fișierului", "retry": "Reîncearcă", "refresh": "Reîmprospătează", diff --git a/locales/ru/common.json b/locales/ru/common.json index d241f7ed4..b88e0e8f0 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -4489,6 +4489,13 @@ "paste_success": "Вставлено успешно", "paste_error": "Не удалось вставить", "new_text_file": "Новый текстовый файл", + "new_file": "Новый файл", + "file_format": "Формат", + "format_text": "Текстовый файл", + "format_document": "Документ", + "format_spreadsheet": "Таблица", + "format_presentation": "Презентация", + "format_pdf": "PDF", "file_name": "Имя файла", "retry": "Повторить", "refresh": "Обновить", diff --git a/locales/sk/common.json b/locales/sk/common.json index 25549343f..7c2579d36 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -4619,6 +4619,13 @@ "paste_success": "Úspešne vložené", "paste_error": "Vloženie zlyhalo", "new_text_file": "Nový textový súbor", + "new_file": "Nový súbor", + "file_format": "Formát", + "format_text": "Textový súbor", + "format_document": "Dokument", + "format_spreadsheet": "Tabuľka", + "format_presentation": "Prezentácia", + "format_pdf": "PDF", "file_name": "Názov súboru", "retry": "Opakovať", "refresh": "Obnoviť", diff --git a/locales/th/common.json b/locales/th/common.json index efa861b78..e8ce9fe3b 100644 --- a/locales/th/common.json +++ b/locales/th/common.json @@ -1820,6 +1820,13 @@ "new_folder_name": "ชื่อโฟลเดอร์", "new_name": "ชื่อใหม่", "new_text_file": "ไฟล์ข้อความใหม่", + "new_file": "ไฟล์ใหม่", + "file_format": "รูปแบบ", + "format_text": "ไฟล์ข้อความ", + "format_document": "เอกสาร", + "format_spreadsheet": "สเปรดชีต", + "format_presentation": "งานนำเสนอ", + "format_pdf": "PDF", "no_accounts": "ไม่มีบัญชีที่เชื่อมต่ออยู่", "no_results": "ไม่พบไฟล์ที่ตรงกับการค้นหา", "no_results_for": "ไม่มีไฟล์ในโฟลเดอร์นี้ที่ตรงกับ “{query}”", diff --git a/locales/tr/common.json b/locales/tr/common.json index 55cd94cec..848d215f2 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -4618,6 +4618,13 @@ "paste_success": "Başarıyla yapıştırıldı", "paste_error": "Yapıştırılamadı", "new_text_file": "Yeni Metin Dosyası", + "new_file": "Yeni Dosya", + "file_format": "Biçim", + "format_text": "Metin dosyası", + "format_document": "Belge", + "format_spreadsheet": "Elektronik tablo", + "format_presentation": "Sunu", + "format_pdf": "PDF", "file_name": "Dosya adı", "retry": "Yeniden dene", "refresh": "Yenile", diff --git a/locales/uk/common.json b/locales/uk/common.json index 2b40f12db..f6acc947b 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -4489,6 +4489,13 @@ "paste_success": "Вставлено успішно", "paste_error": "Не вдалося вставити", "new_text_file": "Новий текстовий файл", + "new_file": "Новий файл", + "file_format": "Формат", + "format_text": "Текстовий файл", + "format_document": "Документ", + "format_spreadsheet": "Таблиця", + "format_presentation": "Презентація", + "format_pdf": "PDF", "file_name": "Ім'я файлу", "retry": "Повторіть спробу", "refresh": "Оновити", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index c0901bf5c..f3aa1b8b8 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -4619,6 +4619,13 @@ "paste_success": "貼上成功", "paste_error": "貼上失敗", "new_text_file": "新增文字檔案", + "new_file": "新增檔案", + "file_format": "格式", + "format_text": "文字檔案", + "format_document": "文件", + "format_spreadsheet": "試算表", + "format_presentation": "簡報", + "format_pdf": "PDF", "file_name": "檔名", "retry": "重試", "refresh": "重新整理", diff --git a/locales/zh/common.json b/locales/zh/common.json index 2281d3eee..3e0ae974b 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -4489,6 +4489,13 @@ "paste_success": "粘贴成功", "paste_error": "粘贴失败", "new_text_file": "新建文本文件", + "new_file": "新建文件", + "file_format": "格式", + "format_text": "文本文件", + "format_document": "文档", + "format_spreadsheet": "电子表格", + "format_presentation": "演示文稿", + "format_pdf": "PDF", "file_name": "文件名", "retry": "重试", "refresh": "刷新", diff --git a/stores/__tests__/file-store.test.ts b/stores/__tests__/file-store.test.ts index f79954b91..6cb46aab6 100644 --- a/stores/__tests__/file-store.test.ts +++ b/stores/__tests__/file-store.test.ts @@ -582,7 +582,7 @@ describe('file-store "Shared with me"', () => { const refusal = (promise: Promise) => promise.then(() => null, (error: unknown) => fileErrorKind(error)); expect(await refusal(store.createDirectory('New'))).toBe('forbidden'); - expect(await refusal(store.createTextFile('a.txt'))).toBe('forbidden'); + expect(await refusal(store.createFile('a.txt'))).toBe('forbidden'); expect(await refusal(store.uploadFiles([new File(['x'], 'a.txt')]))).toBe('forbidden'); expect(await refusal(store.uploadFolder([new File(['x'], 'a.txt')]))).toBe('forbidden'); expect(await refusal(store.duplicateResource('notes.txt'))).toBe('forbidden'); @@ -1077,3 +1077,37 @@ describe('file-store preview content', () => { expect(contentType).toBe('text/plain'); }); }); + +describe('file-store new file (#1393)', () => { + const DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + + it('creates a document from its blank template and returns the name the server gave it', async () => { + const client = makeMockClient([file('a', 'Report.docx', null)]); + const uploads: File[] = []; + const upload = client.uploadBlob.bind(client); + const create = client.createFileNode.bind(client); + Object.assign(client, { + uploadBlob: async (f: File) => { uploads.push(f); return upload(f); }, + // Stalwart's onExists: "rename". + createFileNode: (name: string, ...rest: [string, string, number, string | null]) => + create(name === 'Report.docx' ? 'Report (2).docx' : name, ...rest), + }); + useFileStore.getState().initClient(client); + await useFileStore.getState().navigate(null); + + const blank = new Blob(['PK blank docx'], { type: DOCX }); + expect(await useFileStore.getState().createFile('Report.docx', blank)).toBe('Report (2).docx'); + expect(await uploads[0].text()).toBe('PK blank docx'); + expect(client._nodes().find(n => n.name === 'Report (2).docx')).toMatchObject({ type: DOCX, size: 13 }); + expect(useFileStore.getState().resources.map(r => r.name)).toContain('Report (2).docx'); + }); + + it('creates an empty text file without a template', async () => { + const client = makeMockClient(); + useFileStore.getState().initClient(client); + await useFileStore.getState().navigate(null); + + expect(await useFileStore.getState().createFile('notes.txt')).toBe('notes.txt'); + expect(client._nodes()[0]).toMatchObject({ name: 'notes.txt', type: 'text/plain', size: 0 }); + }); +}); diff --git a/stores/file-store.ts b/stores/file-store.ts index ce5b1256b..6247a14fc 100644 --- a/stores/file-store.ts +++ b/stores/file-store.ts @@ -152,7 +152,12 @@ interface FileState { downloadZip: (ids: string[], zipName: string, options?: { signal?: AbortSignal; onProgress?: (p: ZipProgress) => void }) => Promise; getImageUrl: (name: string) => Promise; getFileContent: (name: string) => Promise<{ blob: Blob; contentType: string }>; - createTextFile: (name: string) => Promise; + /** + * A new file in the open folder: empty text, or a copy of `template` (a + * blank document, #1393). Resolves to the name it got, which differs from + * `name` when the server renamed it to avoid a clash. + */ + createFile: (name: string, template?: Blob) => Promise; duplicateResource: (name: string) => Promise; moveToFolder: (names: string[], targetFolder: string) => Promise; moveToParent: (names: string[]) => Promise; @@ -1380,15 +1385,17 @@ export const useFileStore = create((set, get) => ({ return { blob, contentType: resource.contentType || 'application/octet-stream' }; }, - createTextFile: async (name: string) => { + createFile: async (name: string, template?: Blob) => { const { client, currentParentId, refresh } = get(); - if (!client) return; + if (!client) return null; assertCanAddTo(currentParentId); - const emptyBlob = new File([''], name, { type: 'text/plain' }); - const { blobId } = await client.uploadBlob(emptyBlob); - await client.createFileNode(name, blobId, 'text/plain', 0, currentParentId); + const type = template?.type || 'text/plain'; + const content = new File(template ? [template] : [''], name, { type }); + const { blobId } = await client.uploadBlob(content); + const node = await client.createFileNode(name, blobId, type, content.size, currentParentId); await refresh(); + return node.name || name; }, duplicateResource: async (name: string) => {