Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/files/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@
'url' => '/api/v1/directEditing/create',
'verb' => 'POST'
],
[
'name' => 'OrganizationTemplate#getPath',
'url' => '/api/v1/templates/organization',
'verb' => 'GET',
],
[
'name' => 'OrganizationTemplate#setPath',
'url' => '/api/v1/templates/organization',
'verb' => 'PUT',
],
[
'name' => 'OrganizationTemplate#preview',
'url' => '/api/v1/templates/organization/{id}/preview',
'verb' => 'GET',
],
[
'name' => 'Template#list',
'url' => '/api/v1/templates',
Expand Down
2 changes: 2 additions & 0 deletions apps/files/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php',
'OCA\\Files\\Controller\\FilenamesController' => $baseDir . '/../lib/Controller/FilenamesController.php',
'OCA\\Files\\Controller\\OpenLocalEditorController' => $baseDir . '/../lib/Controller/OpenLocalEditorController.php',
'OCA\\Files\\Controller\\OrganizationTemplateController' => $baseDir . '/../lib/Controller/OrganizationTemplateController.php',
'OCA\\Files\\Controller\\TemplateController' => $baseDir . '/../lib/Controller/TemplateController.php',
'OCA\\Files\\Controller\\TransferOwnershipController' => $baseDir . '/../lib/Controller/TransferOwnershipController.php',
'OCA\\Files\\Controller\\ViewController' => $baseDir . '/../lib/Controller/ViewController.php',
Expand Down Expand Up @@ -107,4 +108,5 @@
'OCA\\Files\\Sharing\\Property\\NodeGridViewSharePropertyType' => $baseDir . '/../lib/Sharing/Property/NodeGridViewSharePropertyType.php',
'OCA\\Files\\Sharing\\Source\\NodeShareSourceMetadata' => $baseDir . '/../lib/Sharing/Source/NodeShareSourceMetadata.php',
'OCA\\Files\\Sharing\\Source\\NodeShareSourceType' => $baseDir . '/../lib/Sharing/Source/NodeShareSourceType.php',
'OCA\\Files\\Template\\OrganizationTemplateProvider' => $baseDir . '/../lib/Template/OrganizationTemplateProvider.php',
);
2 changes: 2 additions & 0 deletions apps/files/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class ComposerStaticInitFiles
'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php',
'OCA\\Files\\Controller\\FilenamesController' => __DIR__ . '/..' . '/../lib/Controller/FilenamesController.php',
'OCA\\Files\\Controller\\OpenLocalEditorController' => __DIR__ . '/..' . '/../lib/Controller/OpenLocalEditorController.php',
'OCA\\Files\\Controller\\OrganizationTemplateController' => __DIR__ . '/..' . '/../lib/Controller/OrganizationTemplateController.php',
'OCA\\Files\\Controller\\TemplateController' => __DIR__ . '/..' . '/../lib/Controller/TemplateController.php',
'OCA\\Files\\Controller\\TransferOwnershipController' => __DIR__ . '/..' . '/../lib/Controller/TransferOwnershipController.php',
'OCA\\Files\\Controller\\ViewController' => __DIR__ . '/..' . '/../lib/Controller/ViewController.php',
Expand Down Expand Up @@ -122,6 +123,7 @@ class ComposerStaticInitFiles
'OCA\\Files\\Sharing\\Property\\NodeGridViewSharePropertyType' => __DIR__ . '/..' . '/../lib/Sharing/Property/NodeGridViewSharePropertyType.php',
'OCA\\Files\\Sharing\\Source\\NodeShareSourceMetadata' => __DIR__ . '/..' . '/../lib/Sharing/Source/NodeShareSourceMetadata.php',
'OCA\\Files\\Sharing\\Source\\NodeShareSourceType' => __DIR__ . '/..' . '/../lib/Sharing/Source/NodeShareSourceType.php',
'OCA\\Files\\Template\\OrganizationTemplateProvider' => __DIR__ . '/..' . '/../lib/Template/OrganizationTemplateProvider.php',
);

public static function getInitializer(ClassLoader $loader)
Expand Down
1 change: 1 addition & 0 deletions apps/files/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public function register(IRegistrationContext $context): void {
/*
* Register capabilities
*/
$context->registerTemplateProvider(\OCA\Files\Template\OrganizationTemplateProvider::class);
$context->registerCapability(Capabilities::class);
$context->registerCapability(AdvancedCapabilities::class);
$context->registerCapability(DirectEditingCapabilities::class);
Expand Down
8 changes: 8 additions & 0 deletions apps/files/lib/ConfigLexicon.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class ConfigLexicon implements ILexicon {
public const RECENT_FILES_GROUP_MIME_TYPES = 'recent_files_group_mime_types';
public const RECENT_FILES_GROUP_TIMESPAN_MINUTES = 'recent_files_group_timespan_minutes';
public const LOCAL_CLIENT_INTEGRATION = 'local_client_integration';
public const ORGANIZATION_TEMPLATE_FOLDER = 'organization_template_folder';

#[\Override]
public function getStrictness(): Strictness {
Expand All @@ -36,6 +37,13 @@ public function getStrictness(): Strictness {
#[\Override]
public function getAppConfigs(): array {
return [
new Entry(
self::ORGANIZATION_TEMPLATE_FOLDER,
ValueType::ARRAY,
defaultRaw: [],
definition: 'Owner and folder ID of the template directory published to all users',
lazy: false,
),
new Entry(
self::OVERWRITES_HOME_FOLDERS,
ValueType::ARRAY,
Expand Down
113 changes: 113 additions & 0 deletions apps/files/lib/Controller/OrganizationTemplateController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files\Controller;

use OC\User\NoUserException;
use OCA\Files\Template\OrganizationTemplateProvider;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\Files\Folder;
use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IPreview;
use OCP\IRequest;

class OrganizationTemplateController extends OCSController {
public function __construct(
IRequest $request,
private OrganizationTemplateProvider $provider,
private IRootFolder $rootFolder,
private IPreview $preview,
private string $userId,
) {
parent::__construct('files', $request);
}

/**
* Get the folder published as organization templates
*
* Only administrators may access this setting.
*
* @return DataResponse<Http::STATUS_OK, array{template_path: string, available: bool, owner: string}, array{}>
*
* 200: Organization template folder returned
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
public function getPath(): DataResponse {
$selection = $this->provider->getSelection();
$path = '';
$available = false;
try {
$folder = $this->provider->getFolder();
$path = $this->rootFolder->getUserFolder($selection['owner'])->getRelativePath($folder->getPath()) ?? '';
$available = true;
} catch (NotFoundException|NotPermittedException|NoUserException $e) {
}
return new DataResponse(['template_path' => $path, 'available' => $available, 'owner' => $selection['owner']]);
}

/**
* Publish a folder's contents as templates for every authenticated user
*
* @param string $templatePath Folder in the administrator's Files, or empty to disable
* @return DataResponse<Http::STATUS_OK, array{template_path: string, available: bool, owner: string}, array{}>
* @throws OCSBadRequestException The folder is unavailable
*
* 200: Organization template folder updated
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
public function setPath(string $templatePath): DataResponse {
if ($templatePath === '') {
$this->provider->setSelection('', 0);
} else {
try {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
$folder = $userFolder->get($templatePath);
if (!$folder instanceof Folder || !$folder->isReadable() || $userFolder->getRelativePath($folder->getPath()) === null) {
throw new OCSBadRequestException('Choose a readable folder');
}
$this->provider->setSelection($this->userId, $folder->getId());
} catch (NotFoundException|NotPermittedException|InvalidPathException $e) {
throw new OCSBadRequestException('Choose a readable folder');
}
}
return $this->getPath();
}

/**
* Preview a published organization template
*
* @param string $id Template file ID
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>
* @throws OCSNotFoundException Template or preview unavailable
*
* 200: Preview returned
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function preview(string $id): FileDisplayResponse {
try {
$file = $this->provider->getCustomTemplate($id);
$preview = $this->preview->getPreview($file, 512, 512);
return new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]);
} catch (NotFoundException|NotPermittedException|NoUserException $e) {
throw new OCSNotFoundException('Template preview unavailable');
}
}
}
11 changes: 7 additions & 4 deletions apps/files/lib/Controller/TemplateController.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,36 +100,39 @@
}
}
$this->templateManager->setTemplatePath($templatePath);
return new DataResponse(['template_path' => $templatePath, 'available' => $templatePath !== '']);

Check failure on line 103 in apps/files/lib/Controller/TemplateController.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

RedundantCondition

apps/files/lib/Controller/TemplateController.php:103:77: RedundantCondition: '' can never contain non-falsy-string (see https://psalm.dev/122)
}

/**
* List the available templates
*
* @param ?string $targetPath Destination folder relative to the user root
*
* @return DataResponse<Http::STATUS_OK, list<FilesTemplateFileCreatorWithTemplates>, array{}>
*
* 200: Available templates returned
*/
#[NoAdminRequired]
public function list(): DataResponse {
public function list(?string $targetPath = null): DataResponse {
/* Convert embedded Template instances to arrays to match return type */
return new DataResponse(array_map(static function (array $templateFileCreator) {
$templateFileCreator['templates'] = array_map(static fn (Template $template) => $template->jsonSerialize(), $templateFileCreator['templates']);
return $templateFileCreator;
}, $this->templateManager->listTemplates()));
}, $this->templateManager->listTemplates($targetPath)));
}

/**
* List the fields for the template specified by the given file ID
*
* @param int $fileId File ID of the template
* @param ?string $targetPath Destination folder relative to the user root
* @return DataResponse<Http::STATUS_OK, array<string, FilesTemplateField>, array{}>
*
* 200: Fields returned
*/
#[NoAdminRequired]
public function listTemplateFields(int $fileId): DataResponse {
$fields = $this->templateManager->listTemplateFields($fileId);
public function listTemplateFields(int $fileId, ?string $targetPath = null): DataResponse {
$fields = $this->templateManager->listTemplateFields($fileId, $targetPath);

return new DataResponse(
array_merge([], ...$fields),
Expand Down
85 changes: 85 additions & 0 deletions apps/files/lib/Template/OrganizationTemplateProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files\Template;

use OC\User\NoUserException;
use OCA\Files\ConfigLexicon;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\Template\ICustomTemplateProvider;
use OCP\Files\Template\Template;
use OCP\IAppConfig;
use OCP\IURLGenerator;

/** An administrator explicitly publishes this folder's contents to all users. */
class OrganizationTemplateProvider implements ICustomTemplateProvider {
public function __construct(
private IRootFolder $rootFolder,
private IAppConfig $config,
private IURLGenerator $urlGenerator,
) {
}

/** @return array{owner: string, folder: int} */
public function getSelection(): array {
$selection = $this->config->getValueArray('files', ConfigLexicon::ORGANIZATION_TEMPLATE_FOLDER, []);
return ['owner' => (string)($selection['owner'] ?? ''), 'folder' => (int)($selection['folder'] ?? 0)];
}

public function setSelection(string $owner, int $folder): void {
$this->config->setValueArray('files', ConfigLexicon::ORGANIZATION_TEMPLATE_FOLDER, ['owner' => $owner, 'folder' => $folder]);
}

public function getFolder(): Folder {
$selection = $this->getSelection();
if ($selection['owner'] !== '' && $selection['folder'] > 0) {
foreach ($this->rootFolder->getUserFolder($selection['owner'])->getById($selection['folder']) as $folder) {
if ($folder instanceof Folder && $folder->isReadable()) {
return $folder;
}
}
}
throw new NotFoundException('No organization template folder available');
}

public function getCustomTemplates(string $mimetype): array {

Check failure on line 55 in apps/files/lib/Template/OrganizationTemplateProvider.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

MissingOverrideAttribute

apps/files/lib/Template/OrganizationTemplateProvider.php:55:2: MissingOverrideAttribute: Method OCA\Files\Template\OrganizationTemplateProvider::getcustomtemplates should have the "Override" attribute (see https://psalm.dev/358)
try {
$folder = $this->getFolder();
$templates = [];
foreach ($folder->searchByMime($mimetype) as $file) {
if ($file instanceof File && $file->isReadable()) {
$template = new Template(self::class, (string)$file->getId(), $file);
$template->setHasPreview(true);
$template->setCustomPreviewUrl($this->urlGenerator->linkToOCSRouteAbsolute('files.OrganizationTemplate.preview', ['id' => (string)$file->getId(), 'etag' => $file->getEtag()]));
$templates[] = $template;
}
}
return $templates;
} catch (NotFoundException|NotPermittedException|NoUserException $e) {
return [];
}
}

public function getCustomTemplate(string $template): File {

Check failure on line 73 in apps/files/lib/Template/OrganizationTemplateProvider.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

MissingOverrideAttribute

apps/files/lib/Template/OrganizationTemplateProvider.php:73:2: MissingOverrideAttribute: Method OCA\Files\Template\OrganizationTemplateProvider::getcustomtemplate should have the "Override" attribute (see https://psalm.dev/358)
if (!ctype_digit($template)) {
throw new NotFoundException('Invalid template identifier');
}
$folder = $this->getFolder();
foreach ($folder->getById((int)$template) as $file) {
if ($file instanceof File && $file->isReadable() && $folder->getRelativePath($file->getPath()) !== null) {
return $file;
}
}
throw new NotFoundException('Template no longer available');
}
}
Loading
Loading