refactor(template-manager): Modernize template manager API

And correct openapi types for the size.

Signed-off-by: Carl Schwan <carl.schwan@nextcloud.com>
pull/56251/head
Carl Schwan 2025-11-06 17:47:32 +07:00
parent ff5041fc8e
commit 38fd84aa6a
No known key found for this signature in database
GPG Key ID: 02325448204E452A
9 changed files with 117 additions and 80 deletions

@ -17,9 +17,10 @@ namespace OCA\Files;
* filename: ?string, * filename: ?string,
* lastmod: int, * lastmod: int,
* mime: string, * mime: string,
* size: int, * size: int|float,
* type: string, * type: string,
* hasPreview: bool, * hasPreview: bool,
* permissions: int,
* } * }
* *
* @psalm-type FilesTemplateField = array{ * @psalm-type FilesTemplateField = array{

@ -323,7 +323,8 @@
"mime", "mime",
"size", "size",
"type", "type",
"hasPreview" "hasPreview",
"permissions"
], ],
"properties": { "properties": {
"basename": { "basename": {
@ -348,14 +349,26 @@
"type": "string" "type": "string"
}, },
"size": { "size": {
"type": "integer", "anyOf": [
"format": "int64" {
"type": "integer",
"format": "int64"
},
{
"type": "number",
"format": "double"
}
]
}, },
"type": { "type": {
"type": "string" "type": "string"
}, },
"hasPreview": { "hasPreview": {
"type": "boolean" "type": "boolean"
},
"permissions": {
"type": "integer",
"format": "int64"
} }
} }
}, },

@ -277,7 +277,7 @@ class InstalledVersions
if (null === self::$installed) { if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location, // only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') { if (substr(__DIR__, -8, 1) !== 'C' && is_file(__DIR__ . '/installed.php')) {
self::$installed = include __DIR__ . '/installed.php'; self::$installed = include __DIR__ . '/installed.php';
} else { } else {
self::$installed = array(); self::$installed = array();
@ -378,7 +378,7 @@ class InstalledVersions
if (null === self::$installed) { if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location, // only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') { if (substr(__DIR__, -8, 1) !== 'C' && is_file(__DIR__ . '/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php'; $required = require __DIR__ . '/installed.php';
self::$installed = $required; self::$installed = $required;

@ -1,4 +1,3 @@
Copyright (c) Nils Adermann, Jordi Boggiano Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
@ -18,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.

@ -3,7 +3,7 @@
'name' => '__root__', 'name' => '__root__',
'pretty_version' => 'dev-master', 'pretty_version' => 'dev-master',
'version' => 'dev-master', 'version' => 'dev-master',
'reference' => '3fce359f4c606737b21b1b4213efd5bc5536e867', 'reference' => '8c12590cf6f93ce7aa41f17817b3791e524da39e',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../../', 'install_path' => __DIR__ . '/../../../',
'aliases' => array(), 'aliases' => array(),
@ -13,7 +13,7 @@
'__root__' => array( '__root__' => array(
'pretty_version' => 'dev-master', 'pretty_version' => 'dev-master',
'version' => 'dev-master', 'version' => 'dev-master',
'reference' => '3fce359f4c606737b21b1b4213efd5bc5536e867', 'reference' => '8c12590cf6f93ce7aa41f17817b3791e524da39e',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../../', 'install_path' => __DIR__ . '/../../../',
'aliases' => array(), 'aliases' => array(),

@ -11,16 +11,15 @@ namespace OC\Files\Template;
use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Bootstrap\Coordinator;
use OC\Files\Cache\Scanner; use OC\Files\Cache\Scanner;
use OC\Files\Filesystem; use OC\Files\Filesystem;
use OCA\Files\ResponseDefinitions;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File; use OCP\Files\File;
use OCP\Files\Folder; use OCP\Files\Folder;
use OCP\Files\GenericFileException; use OCP\Files\GenericFileException;
use OCP\Files\IFilenameValidator; use OCP\Files\IFilenameValidator;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException; use OCP\Files\NotFoundException;
use OCP\Files\Template\BeforeGetTemplatesEvent; use OCP\Files\Template\BeforeGetTemplatesEvent;
use OCP\Files\Template\Field;
use OCP\Files\Template\FileCreatedFromTemplateEvent; use OCP\Files\Template\FileCreatedFromTemplateEvent;
use OCP\Files\Template\ICustomTemplateProvider; use OCP\Files\Template\ICustomTemplateProvider;
use OCP\Files\Template\ITemplateManager; use OCP\Files\Template\ITemplateManager;
@ -28,65 +27,54 @@ use OCP\Files\Template\RegisterTemplateCreatorEvent;
use OCP\Files\Template\Template; use OCP\Files\Template\Template;
use OCP\Files\Template\TemplateFileCreator; use OCP\Files\Template\TemplateFileCreator;
use OCP\IConfig; use OCP\IConfig;
use OCP\IL10N;
use OCP\IPreview; use OCP\IPreview;
use OCP\IServerContainer;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\L10N\IFactory; use OCP\L10N\IFactory;
use Override;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/**
* @psalm-import-type FilesTemplateFile from ResponseDefinitions
*/
class TemplateManager implements ITemplateManager { class TemplateManager implements ITemplateManager {
private $registeredTypes = []; /** @var list<callable(): TemplateFileCreator> */
private $types = []; private array $registeredTypes = [];
/** @var list<TemplateFileCreator> */
/** @var array|null */ private array $types = [];
private $providers = null; /** @var array<class-string<ICustomTemplateProvider>, ICustomTemplateProvider>|null */
private ?array $providers = null;
private $serverContainer; private IL10n $l10n;
private $eventDispatcher; private ?string $userId;
private $rootFolder;
private $userManager;
private $previewManager;
private $config;
private $l10n;
private $logger;
private $userId;
private $l10nFactory;
/** @var Coordinator */
private $bootstrapCoordinator;
public function __construct( public function __construct(
IServerContainer $serverContainer, private readonly ContainerInterface $serverContainer,
IEventDispatcher $eventDispatcher, private readonly IEventDispatcher $eventDispatcher,
Coordinator $coordinator, private readonly Coordinator $bootstrapCoordinator,
IRootFolder $rootFolder, private readonly IRootFolder $rootFolder,
IUserSession $userSession, IUserSession $userSession,
IUserManager $userManager, private readonly IUserManager $userManager,
IPreview $previewManager, private readonly IPreview $previewManager,
IConfig $config, private readonly IConfig $config,
IFactory $l10nFactory, private readonly IFactory $l10nFactory,
LoggerInterface $logger, private readonly LoggerInterface $logger,
private IFilenameValidator $filenameValidator, private readonly IFilenameValidator $filenameValidator,
) { ) {
$this->serverContainer = $serverContainer;
$this->eventDispatcher = $eventDispatcher;
$this->bootstrapCoordinator = $coordinator;
$this->rootFolder = $rootFolder;
$this->userManager = $userManager;
$this->previewManager = $previewManager;
$this->config = $config;
$this->l10nFactory = $l10nFactory;
$this->l10n = $l10nFactory->get('lib'); $this->l10n = $l10nFactory->get('lib');
$this->logger = $logger; $this->userId = $userSession->getUser()?->getUID();
$user = $userSession->getUser();
$this->userId = $user ? $user->getUID() : null;
} }
#[Override]
public function registerTemplateFileCreator(callable $callback): void { public function registerTemplateFileCreator(callable $callback): void {
$this->registeredTypes[] = $callback; $this->registeredTypes[] = $callback;
} }
public function getRegisteredProviders(): array { /**
* @return array<class-string<ICustomTemplateProvider>, ICustomTemplateProvider>
*/
private function getRegisteredProviders(): array {
if ($this->providers !== null) { if ($this->providers !== null) {
return $this->providers; return $this->providers;
} }
@ -101,7 +89,10 @@ class TemplateManager implements ITemplateManager {
return $this->providers; return $this->providers;
} }
public function getTypes(): array { /**
* @return list<TemplateFileCreator>
*/
private function getTypes(): array {
if (!empty($this->types)) { if (!empty($this->types)) {
return $this->types; return $this->types;
} }
@ -112,6 +103,7 @@ class TemplateManager implements ITemplateManager {
return $this->types; return $this->types;
} }
#[Override]
public function listCreators(): array { public function listCreators(): array {
$types = $this->getTypes(); $types = $this->getTypes();
usort($types, function (TemplateFileCreator $a, TemplateFileCreator $b) { usort($types, function (TemplateFileCreator $a, TemplateFileCreator $b) {
@ -120,6 +112,7 @@ class TemplateManager implements ITemplateManager {
return $types; return $types;
} }
#[Override]
public function listTemplates(): array { public function listTemplates(): array {
return array_values(array_map(function (TemplateFileCreator $entry) { return array_values(array_map(function (TemplateFileCreator $entry) {
return array_merge($entry->jsonSerialize(), [ return array_merge($entry->jsonSerialize(), [
@ -128,6 +121,7 @@ class TemplateManager implements ITemplateManager {
}, $this->listCreators())); }, $this->listCreators()));
} }
#[Override]
public function listTemplateFields(int $fileId): array { public function listTemplateFields(int $fileId): array {
foreach ($this->listCreators() as $creator) { foreach ($this->listCreators() as $creator) {
$fields = $this->getTemplateFields($creator, $fileId); $fields = $this->getTemplateFields($creator, $fileId);
@ -141,13 +135,7 @@ class TemplateManager implements ITemplateManager {
return []; return [];
} }
/** #[Override]
* @param string $filePath
* @param string $templateId
* @param array $templateFields
* @return array
* @throws GenericFileException
*/
public function createFromTemplate(string $filePath, string $templateId = '', string $templateType = 'user', array $templateFields = []): array { public function createFromTemplate(string $filePath, string $templateId = '', string $templateType = 'user', array $templateFields = []): array {
$userFolder = $this->rootFolder->getUserFolder($this->userId); $userFolder = $this->rootFolder->getUserFolder($this->userId);
try { try {
@ -159,6 +147,7 @@ class TemplateManager implements ITemplateManager {
if (!$userFolder->nodeExists(dirname($filePath))) { if (!$userFolder->nodeExists(dirname($filePath))) {
throw new GenericFileException($this->l10n->t('Invalid path')); throw new GenericFileException($this->l10n->t('Invalid path'));
} }
/** @var Folder $folder */
$folder = $userFolder->get(dirname($filePath)); $folder = $userFolder->get(dirname($filePath));
$template = null; $template = null;
if ($templateType === 'user' && $templateId !== '') { if ($templateType === 'user' && $templateId !== '') {
@ -178,7 +167,9 @@ class TemplateManager implements ITemplateManager {
$targetFile = $folder->newFile($filename, ($template instanceof File ? $template->fopen('rb') : null)); $targetFile = $folder->newFile($filename, ($template instanceof File ? $template->fopen('rb') : null));
$this->eventDispatcher->dispatchTyped(new FileCreatedFromTemplateEvent($template, $targetFile, $templateFields)); $this->eventDispatcher->dispatchTyped(new FileCreatedFromTemplateEvent($template, $targetFile, $templateFields));
return $this->formatFile($userFolder->get($filePath)); /** @var File $file */
$file = $userFolder->get($filePath);
return $this->formatFile($file);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]); $this->logger->error($e->getMessage(), ['exception' => $e]);
throw new GenericFileException($this->l10n->t('Failed to create file from template')); throw new GenericFileException($this->l10n->t('Failed to create file from template'));
@ -186,7 +177,6 @@ class TemplateManager implements ITemplateManager {
} }
/** /**
* @return Folder
* @throws \OCP\Files\NotFoundException * @throws \OCP\Files\NotFoundException
* @throws \OCP\Files\NotPermittedException * @throws \OCP\Files\NotPermittedException
* @throws \OC\User\NoUserException * @throws \OC\User\NoUserException
@ -245,6 +235,9 @@ class TemplateManager implements ITemplateManager {
foreach ($type->getMimetypes() as $mimetype) { foreach ($type->getMimetypes() as $mimetype) {
foreach ($userTemplateFolder->searchByMime($mimetype) as $templateFile) { foreach ($userTemplateFolder->searchByMime($mimetype) as $templateFile) {
if (!($templateFile instanceof File)) {
continue;
}
$template = new Template( $template = new Template(
'user', 'user',
$this->rootFolder->getUserFolder($this->userId)->getRelativePath($templateFile->getPath()), $this->rootFolder->getUserFolder($this->userId)->getRelativePath($templateFile->getPath()),
@ -267,9 +260,7 @@ class TemplateManager implements ITemplateManager {
$matchedTemplates = array_filter( $matchedTemplates = array_filter(
array_merge($providerTemplates, $userTemplates), array_merge($providerTemplates, $userTemplates),
function (Template $template) use ($fileId) { fn (Template $template): bool => $template->jsonSerialize()['fileid'] === $fileId);
return $template->jsonSerialize()['fileid'] === $fileId;
});
if (empty($matchedTemplates)) { if (empty($matchedTemplates)) {
return []; return [];
@ -277,22 +268,19 @@ class TemplateManager implements ITemplateManager {
$this->eventDispatcher->dispatchTyped(new BeforeGetTemplatesEvent($matchedTemplates, true)); $this->eventDispatcher->dispatchTyped(new BeforeGetTemplatesEvent($matchedTemplates, true));
return array_values(array_map(function (Template $template) { return array_values(array_map(static fn (Template $template): array => $template->jsonSerialize()['fields'] ?? [], $matchedTemplates));
return $template->jsonSerialize()['fields'] ?? [];
}, $matchedTemplates));
} }
/** /**
* @param Node|File $file * @return FilesTemplateFile
* @return array
* @throws NotFoundException * @throws NotFoundException
* @throws \OCP\Files\InvalidPathException * @throws \OCP\Files\InvalidPathException
*/ */
private function formatFile(Node $file): array { private function formatFile(File $file): array {
return [ return [
'basename' => $file->getName(), 'basename' => $file->getName(),
'etag' => $file->getEtag(), 'etag' => $file->getEtag(),
'fileid' => $file->getId(), 'fileid' => $file->getId() ?? -1,
'filename' => $this->rootFolder->getUserFolder($this->userId)->getRelativePath($file->getPath()), 'filename' => $this->rootFolder->getUserFolder($this->userId)->getRelativePath($file->getPath()),
'lastmod' => $file->getMTime(), 'lastmod' => $file->getMTime(),
'mime' => $file->getMimetype(), 'mime' => $file->getMimetype(),
@ -312,14 +300,17 @@ class TemplateManager implements ITemplateManager {
return false; return false;
} }
#[Override]
public function setTemplatePath(string $path): void { public function setTemplatePath(string $path): void {
$this->config->setUserValue($this->userId, 'core', 'templateDirectory', $path); $this->config->setUserValue($this->userId, 'core', 'templateDirectory', $path);
} }
#[Override]
public function getTemplatePath(): string { public function getTemplatePath(): string {
return $this->config->getUserValue($this->userId, 'core', 'templateDirectory', ''); return $this->config->getUserValue($this->userId, 'core', 'templateDirectory', '');
} }
#[Override]
public function initializeTemplateDirectory(?string $path = null, ?string $userId = null, $copyTemplates = true): string { public function initializeTemplateDirectory(?string $path = null, ?string $userId = null, $copyTemplates = true): string {
if ($userId !== null) { if ($userId !== null) {
$this->userId = $userId; $this->userId = $userId;
@ -365,8 +356,10 @@ class TemplateManager implements ITemplateManager {
} }
try { try {
/** @var Folder $folder */
$folder = $userFolder->get($userTemplatePath); $folder = $userFolder->get($userTemplatePath);
} catch (NotFoundException $e) { } catch (NotFoundException $e) {
/** @var Folder $folder */
$folder = $userFolder->get(dirname($userTemplatePath)); $folder = $userFolder->get(dirname($userTemplatePath));
$folder = $folder->newFolder(basename($userTemplatePath)); $folder = $folder->newFolder(basename($userTemplatePath));
} }
@ -407,7 +400,7 @@ class TemplateManager implements ITemplateManager {
return $this->getTemplatePath(); return $this->getTemplatePath();
} }
private function getLocalizedTemplatePath(string $skeletonTemplatePath, string $userLang) { private function getLocalizedTemplatePath(string $skeletonTemplatePath, string $userLang): string {
$localizedSkeletonTemplatePath = str_replace('{lang}', $userLang, $skeletonTemplatePath); $localizedSkeletonTemplatePath = str_replace('{lang}', $userLang, $skeletonTemplatePath);
if (!file_exists($localizedSkeletonTemplatePath)) { if (!file_exists($localizedSkeletonTemplatePath)) {

@ -8,11 +8,25 @@ declare(strict_types=1);
*/ */
namespace OCP\Files\Template; namespace OCP\Files\Template;
use OCP\AppFramework\Attribute\Consumable;
use OCP\Files\GenericFileException; use OCP\Files\GenericFileException;
/** /**
* @since 21.0.0 * @since 21.0.0
* @psalm-type FilesTemplateFile = array{
* basename: string,
* etag: string,
* fileid: int,
* filename: ?string,
* lastmod: int,
* mime: string,
* size: int|float,
* type: string,
* hasPreview: bool,
* permissions: int,
* }
*/ */
#[Consumable(since: '21.0.0')]
interface ITemplateManager { interface ITemplateManager {
/** /**
* Register a template type support * Register a template type support
@ -78,7 +92,7 @@ interface ITemplateManager {
* @param string $templateId * @param string $templateId
* @param string $templateType * @param string $templateType
* @param array $templateFields Since 30.0.0 * @param array $templateFields Since 30.0.0
* @return array * @return FilesTemplateFile
* @throws GenericFileException * @throws GenericFileException
* @since 21.0.0 * @since 21.0.0
*/ */

@ -1908,7 +1908,8 @@
"mime", "mime",
"size", "size",
"type", "type",
"hasPreview" "hasPreview",
"permissions"
], ],
"properties": { "properties": {
"basename": { "basename": {
@ -1933,14 +1934,26 @@
"type": "string" "type": "string"
}, },
"size": { "size": {
"type": "integer", "anyOf": [
"format": "int64" {
"type": "integer",
"format": "int64"
},
{
"type": "number",
"format": "double"
}
]
}, },
"type": { "type": {
"type": "string" "type": "string"
}, },
"hasPreview": { "hasPreview": {
"type": "boolean" "type": "boolean"
},
"permissions": {
"type": "integer",
"format": "int64"
} }
} }
}, },

@ -25,6 +25,7 @@ use OCP\IPreview;
use OCP\IServerContainer; use OCP\IServerContainer;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\IUser;
use OCP\L10N\IFactory; use OCP\L10N\IFactory;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Test\TestCase; use Test\TestCase;
@ -63,8 +64,12 @@ class TemplateManagerTest extends TestCase {
$this->bootstrapCoordinator->method('getRegistrationContext') $this->bootstrapCoordinator->method('getRegistrationContext')
->willReturn(new RegistrationContext($logger)); ->willReturn(new RegistrationContext($logger));
$this->rootFolder = $this->createMock(IRootFolder::class); $this->rootFolder = $this->createMock(IRootFolder::class);
$userSession = $this->createMock(IUserSession::class); $user = $this->createMock(IUser::class);
$userManager = $this->createMock(IUserManager::class); $user->method('getUID')->willReturn('user1');
$userSession = $this->createMock(\OCP\IUserSession::class);
$userSession->method('getUser')
->willReturn($user);
$userManager = $this->createMock(\OCP\IUserManager::class);
$previewManager = $this->createMock(IPreview::class); $previewManager = $this->createMock(IPreview::class);
$this->templateManager = new TemplateManager( $this->templateManager = new TemplateManager(