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 e2fb815380d..06250643758 100644 --- a/apps/files/src/views/TemplatePicker.vue +++ b/apps/files/src/views/TemplatePicker.vue @@ -50,7 +50,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' @@ -62,6 +63,7 @@ import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' 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 @@ -165,6 +167,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) { @@ -216,56 +224,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) - - 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, - }, - }) + // 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 ?? '') + + await client.putFileContents(filename, '') + const response = await client.stat(filename, { data: getDefaultPropfind(), details: true }) as ResponseDataDetailed + logger.debug('Created new file', { fileInfo: response.data }) + + const node = resultToNode(response.data) - // 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' }, - ) - - // 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 + 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 }) + + 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() + } + if (this.selectedTemplate?.fields?.length > 0) { spawnDialog(TemplateFiller, { fields: this.selectedTemplate.fields, diff --git a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php index 1daea5cc0c9..d45ba16dc44 100644 --- a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php +++ b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php @@ -25,6 +25,7 @@ use OCP\Defaults; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\FileInfo; use OCP\Files\Folder; +use OCP\Files\Template\ITemplateManager; use OCP\IConfig; use OCP\IL10N; use OCP\IPreview; @@ -50,6 +51,7 @@ class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider private Defaults $defaults, private IConfig $config, private IRequest $request, + private ITemplateManager $templateManager, private IInitialState $initialState, ) { } @@ -219,6 +221,8 @@ class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider Util::addHeader('meta', ['property' => 'og:type', 'content' => 'object']); Util::addHeader('meta', ['property' => 'og:image', 'content' => $ogPreview]); + $this->initialState->provideInitialState('templates', $this->templateManager->listCreators()); + $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($share)); $csp = new ContentSecurityPolicy(); diff --git a/apps/files_sharing/tests/Controller/ShareControllerTest.php b/apps/files_sharing/tests/Controller/ShareControllerTest.php index 975c647b783..e6c82a5a01f 100644 --- a/apps/files_sharing/tests/Controller/ShareControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareControllerTest.php @@ -31,6 +31,7 @@ use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Files\Storage; +use OCP\Files\Template\ITemplateManager; use OCP\IConfig; use OCP\IL10N; use OCP\IPreview; @@ -72,6 +73,8 @@ class ShareControllerTest extends \Test\TestCase { private $shareManager; /** @var IUserManager|MockObject */ private $userManager; + /** @var ITemplateManager&MockObject */ + private $templateManager; /** @var FederatedShareProvider|MockObject */ private $federatedShareProvider; /** @var IAccountManager|MockObject */ @@ -97,6 +100,7 @@ class ShareControllerTest extends \Test\TestCase { $this->previewManager = $this->createMock(IPreview::class); $this->config = $this->createMock(IConfig::class); $this->userManager = $this->createMock(IUserManager::class); + $this->templateManager = $this->createMock(ITemplateManager::class); $this->federatedShareProvider = $this->createMock(FederatedShareProvider::class); $this->federatedShareProvider->expects($this->any()) ->method('isOutgoingServer2serverShareEnabled')->willReturn(true); @@ -123,6 +127,7 @@ class ShareControllerTest extends \Test\TestCase { $this->defaults, $this->config, $this->createMock(IRequest::class), + $this->templateManager, $this->createMock(IInitialState::class) ) );