feat(file_sharing): Provide template creator list in public shares

Signed-off-by: Louis Chemineau <louis@chmn.me>
pull/54949/head
Louis Chemineau 2025-09-04 12:14:04 +07:00
parent a181276f5d
commit eaa18379ec
No known key found for this signature in database
4 changed files with 94 additions and 47 deletions

@ -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<TemplateFile[]>('files', 'templates', [])
let templates: TemplateFile[]
if (isPublicShare()) {
templates = loadState<TemplateFile[]>('files_sharing', 'templates', [])
} else {
templates = loadState<TemplateFile[]>('files', 'templates', [])
}
// Init template files menu
templates.forEach((provider, index) => {

@ -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<FileStat>
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,

@ -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();

@ -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)
)
);