diff --git a/apps/files/src/newMenu/newFromTemplate.ts b/apps/files/src/newMenu/newFromTemplate.ts index 356fc5e1611..ea640ad8b01 100644 --- a/apps/files/src/newMenu/newFromTemplate.ts +++ b/apps/files/src/newMenu/newFromTemplate.ts @@ -9,6 +9,7 @@ import type { TemplateFile } from '../types.ts' import { Folder, Node, Permission, addNewFileMenuEntry } from '@nextcloud/files' import { loadState } from '@nextcloud/initial-state' +import { isPublicShare } from '@nextcloud/sharing/public' import { newNodeName } from '../utils/newNodeDialog' import { translate as t } from '@nextcloud/l10n' import Vue, { defineAsyncComponent } from 'vue' @@ -46,7 +47,12 @@ const getTemplatePicker = async (context: Folder) => { * Register all new-file-menu entries for all template providers */ export function registerTemplateEntries() { - const templates = loadState('files', 'templates', []) + let templates: TemplateFile[] + if (isPublicShare()) { + templates = loadState('files_sharing', 'templates', []) + } else { + templates = loadState('files', 'templates', []) + } // Init template files menu templates.forEach((provider, index) => { diff --git a/apps/files/src/views/TemplatePicker.vue b/apps/files/src/views/TemplatePicker.vue index cddacc863e1..0f30c091114 100644 --- a/apps/files/src/views/TemplatePicker.vue +++ b/apps/files/src/views/TemplatePicker.vue @@ -52,7 +52,8 @@ import type { TemplateFile } from '../types.ts' import { getCurrentUser } from '@nextcloud/auth' import { showError, spawnDialog } from '@nextcloud/dialogs' import { emit } from '@nextcloud/event-bus' -import { File } from '@nextcloud/files' +import { File, Node } from '@nextcloud/files' +import { getClient, getRootPath, resultToNode, getDefaultPropfind } from '@nextcloud/files/dav' import { translate as t } from '@nextcloud/l10n' import { generateRemoteUrl } from '@nextcloud/router' import { normalize, extname, join } from 'path' @@ -64,6 +65,7 @@ import NcModal from '@nextcloud/vue/components/NcModal' import TemplatePreview from '../components/TemplatePreview.vue' import TemplateFiller from '../components/TemplateFiller.vue' import logger from '../logger.ts' +import type { FileStat, ResponseDataDetailed } from 'webdav' const border = 2 const margin = 8 @@ -167,6 +169,12 @@ export default defineComponent({ this.name = name this.provider = provider + // Skip templates logic for external users. + if (getCurrentUser() === null) { + this.onSubmit() + return + } + const templates = await getTemplates() const fetchedProvider = templates.find((fetchedProvider) => fetchedProvider.app === provider.app && fetchedProvider.label === provider.label) if (fetchedProvider === null) { @@ -224,56 +232,80 @@ export default defineComponent({ this.name = `${this.name}${this.provider?.extension ?? ''}` } - try { - const fileInfo = await createFromTemplate( - normalize(`${currentDirectory}/${this.name}`), - this.selectedTemplate?.filename as string ?? '', - this.selectedTemplate?.templateType as string ?? '', - templateFields, - ) - logger.debug('Created new file', fileInfo) + // Create a blank file for external users as we can't use the templates. + if (getCurrentUser() === null) { + const client = getClient() + const filename = join(getRootPath(), currentDirectory, this.name ?? '') - const owner = getCurrentUser()?.uid || null - const node = new File({ - id: fileInfo.fileid, - source: generateRemoteUrl(join(`dav/files/${owner}`, fileInfo.filename)), - root: `/files/${owner}`, - mime: fileInfo.mime, - mtime: new Date(fileInfo.lastmod * 1000), - owner, - size: fileInfo.size, - permissions: fileInfo.permissions, - attributes: { - // Inherit some attributes from parent folder like the mount type and real owner - 'mount-type': this.parent?.attributes?.['mount-type'], - 'owner-id': this.parent?.attributes?.['owner-id'], - 'owner-display-name': this.parent?.attributes?.['owner-display-name'], - ...fileInfo, - 'has-preview': fileInfo.hasPreview, - }, - }) + await client.putFileContents(filename, '') + const response = await client.stat(filename, { data: getDefaultPropfind(), details: true }) as ResponseDataDetailed + logger.debug('Created new file', { fileInfo: response.data }) - // Update files list - emit('files:node:created', node) + const node = resultToNode(response.data) - // Open the new file - window.OCP.Files.Router.goToRoute( - null, // use default route - { view: 'files', fileid: node.fileid }, - { dir: node.dirname, openfile: 'true' }, - ) + this.handleFileCreation(node) + } else { + try { + const fileInfo = await createFromTemplate( + normalize(`${currentDirectory}/${this.name}`), + this.selectedTemplate?.filename as string ?? '', + this.selectedTemplate?.templateType as string ?? '', + templateFields, + ) + logger.debug('Created new file', { fileInfo }) - // Close the picker - this.close() - } catch (error) { - logger.error('Error while creating the new file from template', { error }) - showError(t('files', 'Unable to create new file from template')) - } finally { - this.loading = false + const owner = getCurrentUser()?.uid || null + const node = new File({ + id: fileInfo.fileid, + source: generateRemoteUrl(join(`dav/files/${owner}`, fileInfo.filename)), + root: `/files/${owner}`, + mime: fileInfo.mime, + mtime: new Date(fileInfo.lastmod * 1000), + owner, + size: fileInfo.size, + permissions: fileInfo.permissions, + attributes: { + // Inherit some attributes from parent folder like the mount type and real owner + 'mount-type': this.parent?.attributes?.['mount-type'], + 'owner-id': this.parent?.attributes?.['owner-id'], + 'owner-display-name': this.parent?.attributes?.['owner-display-name'], + ...fileInfo, + 'has-preview': fileInfo.hasPreview, + }, + }) + + this.handleFileCreation(node) + + // Close the picker + this.close() + } catch (error) { + logger.error('Error while creating the new file from template', { error }) + showError(t('files', 'Unable to create new file from template')) + } finally { + this.loading = false + } } }, + handleFileCreation(node: Node) { + // Update files list + emit('files:node:created', node) + + // Open the new file + window.OCP.Files.Router.goToRoute( + null, // use default route + { view: 'files', fileid: node.fileid }, + { dir: node.dirname, openfile: 'true' }, + ) + }, + async onSubmit() { + // Skip templates logic for external users. + if (getCurrentUser() === null) { + this.loading = true + return this.createFile() + } + const fileId = this.selectedTemplate?.fileid // Only request field extraction if there is a valid template diff --git a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php index afba45cac4a..f75922fc37d 100644 --- a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php +++ b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php @@ -24,6 +24,7 @@ use OCP\Constants; use OCP\Defaults; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; +use OCP\Files\Template\ITemplateManager; use OCP\IAppConfig; use OCP\IConfig; use OCP\IL10N; @@ -49,6 +50,7 @@ class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider private Defaults $defaults, private IConfig $config, private IRequest $request, + private ITemplateManager $templateManager, private IInitialState $initialState, private IAppConfig $appConfig, ) { @@ -119,6 +121,8 @@ class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider $this->eventDispatcher->dispatchTyped(new LoadViewer()); } + $this->initialState->provideInitialState('templates', $this->templateManager->listCreators()); + // Allow external apps to register their scripts $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($share)); diff --git a/apps/files_sharing/tests/Controller/ShareControllerTest.php b/apps/files_sharing/tests/Controller/ShareControllerTest.php index 011210aff42..32fd0f09637 100644 --- a/apps/files_sharing/tests/Controller/ShareControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareControllerTest.php @@ -31,6 +31,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; +use OCP\Files\Template\ITemplateManager; use OCP\IAppConfig; use OCP\IConfig; use OCP\IL10N; @@ -68,6 +69,7 @@ class ShareControllerTest extends \Test\TestCase { private Manager&MockObject $shareManager; private IPreview&MockObject $previewManager; private IUserManager&MockObject $userManager; + private ITemplateManager&MockObject $templateManager; private IInitialState&MockObject $initialState; private IURLGenerator&MockObject $urlGenerator; private ISecureRandom&MockObject $secureRandom; @@ -87,6 +89,7 @@ class ShareControllerTest extends \Test\TestCase { $this->config = $this->createMock(IConfig::class); $this->appConfig = $this->createMock(IAppConfig::class); $this->userManager = $this->createMock(IUserManager::class); + $this->templateManager = $this->createMock(ITemplateManager::class); $this->initialState = $this->createMock(IInitialState::class); $this->federatedShareProvider = $this->createMock(FederatedShareProvider::class); $this->federatedShareProvider->expects($this->any()) @@ -114,6 +117,7 @@ class ShareControllerTest extends \Test\TestCase { $this->defaults, $this->config, $this->createMock(IRequest::class), + $this->templateManager, $this->initialState, $this->appConfig, ) @@ -338,6 +342,7 @@ class ShareControllerTest extends \Test\TestCase { 'owner' => 'ownerUID', 'ownerDisplayName' => 'ownerDisplay', 'isFileRequest' => false, + 'templates' => [], ]; $response = $this->shareController->showShare(); @@ -485,6 +490,7 @@ class ShareControllerTest extends \Test\TestCase { 'isFileRequest' => false, 'note' => 'The note', 'label' => 'A label', + 'templates' => [], ]; $response = $this->shareController->showShare();