2021-01-12 05:28:04 -05:00
< ? php
2021-06-04 15:52:51 -04:00
declare ( strict_types = 1 );
2021-01-14 07:31:15 -05:00
/**
2024-05-23 03:26:56 -04:00
* SPDX - FileCopyrightText : 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2021-01-12 05:28:04 -05:00
*/
namespace OC\Files\Template ;
2021-01-28 05:50:40 -05:00
use OC\AppFramework\Bootstrap\Coordinator ;
2021-01-19 05:20:50 -05:00
use OC\Files\Cache\Scanner ;
2021-01-29 02:43:17 -05:00
use OC\Files\Filesystem ;
2025-11-17 09:32:54 -05:00
use OC\User\NoUserException ;
2025-11-06 11:47:32 -05:00
use OCA\Files\ResponseDefinitions ;
2021-01-12 05:28:04 -05:00
use OCP\EventDispatcher\IEventDispatcher ;
use OCP\Files\File ;
use OCP\Files\Folder ;
use OCP\Files\GenericFileException ;
2025-09-25 08:05:56 -04:00
use OCP\Files\IFilenameValidator ;
2025-11-17 09:32:54 -05:00
use OCP\Files\InvalidPathException ;
2021-01-12 05:28:04 -05:00
use OCP\Files\IRootFolder ;
use OCP\Files\NotFoundException ;
2025-11-17 09:32:54 -05:00
use OCP\Files\NotPermittedException ;
2024-07-05 12:47:01 -04:00
use OCP\Files\Template\BeforeGetTemplatesEvent ;
2021-01-28 05:50:40 -05:00
use OCP\Files\Template\FileCreatedFromTemplateEvent ;
2021-01-12 05:28:04 -05:00
use OCP\Files\Template\ICustomTemplateProvider ;
use OCP\Files\Template\ITemplateManager ;
2024-04-20 13:44:42 -04:00
use OCP\Files\Template\RegisterTemplateCreatorEvent ;
2021-01-12 05:28:04 -05:00
use OCP\Files\Template\Template ;
use OCP\Files\Template\TemplateFileCreator ;
use OCP\IConfig ;
2025-11-06 11:47:32 -05:00
use OCP\IL10N ;
2021-01-12 05:28:04 -05:00
use OCP\IPreview ;
2021-01-29 02:56:32 -05:00
use OCP\IUserManager ;
2021-01-12 05:28:04 -05:00
use OCP\IUserSession ;
use OCP\L10N\IFactory ;
2025-11-06 11:47:32 -05:00
use Override ;
use Psr\Container\ContainerInterface ;
2021-01-12 05:28:04 -05:00
use Psr\Log\LoggerInterface ;
2025-11-06 11:47:32 -05:00
/**
* @ psalm - import - type FilesTemplateFile from ResponseDefinitions
*/
2021-01-12 05:28:04 -05:00
class TemplateManager implements ITemplateManager {
2025-11-06 11:47:32 -05:00
/** @var list<callable(): TemplateFileCreator> */
private array $registeredTypes = [];
/** @var list<TemplateFileCreator> */
private array $types = [];
/** @var array<class-string<ICustomTemplateProvider>, ICustomTemplateProvider>|null */
private ? array $providers = null ;
private IL10n $l10n ;
private ? string $userId ;
2021-01-12 05:28:04 -05:00
public function __construct (
2025-11-06 11:47:32 -05:00
private readonly ContainerInterface $serverContainer ,
private readonly IEventDispatcher $eventDispatcher ,
private readonly Coordinator $bootstrapCoordinator ,
private readonly IRootFolder $rootFolder ,
2021-01-12 05:28:04 -05:00
IUserSession $userSession ,
2025-11-06 11:47:32 -05:00
private readonly IUserManager $userManager ,
private readonly IPreview $previewManager ,
private readonly IConfig $config ,
private readonly IFactory $l10nFactory ,
private readonly LoggerInterface $logger ,
private readonly IFilenameValidator $filenameValidator ,
2021-01-12 05:28:04 -05:00
) {
2021-01-19 05:20:50 -05:00
$this -> l10n = $l10nFactory -> get ( 'lib' );
2025-11-06 11:47:32 -05:00
$this -> userId = $userSession -> getUser () ? -> getUID ();
2021-01-12 05:28:04 -05:00
}
2025-11-06 11:47:32 -05:00
#[Override]
2021-01-28 05:50:40 -05:00
public function registerTemplateFileCreator ( callable $callback ) : void {
$this -> registeredTypes [] = $callback ;
2021-01-12 05:28:04 -05:00
}
2025-11-06 11:47:32 -05:00
/**
* @ return array < class - string < ICustomTemplateProvider > , ICustomTemplateProvider >
*/
private function getRegisteredProviders () : array {
2021-01-12 05:28:04 -05:00
if ( $this -> providers !== null ) {
return $this -> providers ;
}
2021-01-28 05:50:40 -05:00
$context = $this -> bootstrapCoordinator -> getRegistrationContext ();
2021-01-12 05:28:04 -05:00
$this -> providers = [];
2021-01-28 05:50:40 -05:00
foreach ( $context -> getTemplateProviders () as $provider ) {
2021-01-28 08:08:38 -05:00
$class = $provider -> getService ();
$this -> providers [ $class ] = $this -> serverContainer -> get ( $class );
2021-01-12 05:28:04 -05:00
}
return $this -> providers ;
}
2025-11-06 11:47:32 -05:00
/**
* @ return list < TemplateFileCreator >
*/
private function getTypes () : array {
2021-02-02 13:36:31 -05:00
if ( ! empty ( $this -> types )) {
return $this -> types ;
}
2024-04-20 13:44:42 -04:00
$this -> eventDispatcher -> dispatchTyped ( new RegisterTemplateCreatorEvent ( $this ));
2021-01-28 05:50:40 -05:00
foreach ( $this -> registeredTypes as $registeredType ) {
$this -> types [] = $registeredType ();
2021-01-19 10:38:51 -05:00
}
2021-01-28 05:50:40 -05:00
return $this -> types ;
}
2021-01-19 10:38:51 -05:00
2025-11-06 11:47:32 -05:00
#[Override]
2021-01-28 05:50:40 -05:00
public function listCreators () : array {
$types = $this -> getTypes ();
usort ( $types , function ( TemplateFileCreator $a , TemplateFileCreator $b ) {
2021-01-19 10:38:51 -05:00
return $a -> getOrder () - $b -> getOrder ();
});
2021-01-28 05:50:40 -05:00
return $types ;
}
2021-01-19 10:38:51 -05:00
2025-11-06 11:47:32 -05:00
#[Override]
2021-01-28 05:50:40 -05:00
public function listTemplates () : array {
2024-09-24 09:53:13 -04:00
return array_values ( array_map ( function ( TemplateFileCreator $entry ) {
2021-01-12 05:28:04 -05:00
return array_merge ( $entry -> jsonSerialize (), [
'templates' => $this -> getTemplateFiles ( $entry )
]);
2024-09-24 09:53:13 -04:00
}, $this -> listCreators ()));
2021-01-12 05:28:04 -05:00
}
2025-11-06 11:47:32 -05:00
#[Override]
2025-05-09 17:21:22 -04:00
public function listTemplateFields ( int $fileId ) : array {
foreach ( $this -> listCreators () as $creator ) {
$fields = $this -> getTemplateFields ( $creator , $fileId );
if ( empty ( $fields )) {
continue ;
}
return $fields ;
}
return [];
}
2025-11-06 11:47:32 -05:00
#[Override]
2024-07-24 15:59:37 -04:00
public function createFromTemplate ( string $filePath , string $templateId = '' , string $templateType = 'user' , array $templateFields = []) : array {
2021-01-12 05:28:04 -05:00
$userFolder = $this -> rootFolder -> getUserFolder ( $this -> userId );
try {
$userFolder -> get ( $filePath );
throw new GenericFileException ( $this -> l10n -> t ( 'File already exists' ));
} catch ( NotFoundException $e ) {
}
try {
2021-03-31 09:44:47 -04:00
if ( ! $userFolder -> nodeExists ( dirname ( $filePath ))) {
throw new GenericFileException ( $this -> l10n -> t ( 'Invalid path' ));
}
2025-11-06 11:47:32 -05:00
/** @var Folder $folder */
2021-03-31 09:44:47 -04:00
$folder = $userFolder -> get ( dirname ( $filePath ));
2021-05-12 10:07:54 -04:00
$template = null ;
2021-01-12 05:28:04 -05:00
if ( $templateType === 'user' && $templateId !== '' ) {
$template = $userFolder -> get ( $templateId );
} else {
$matchingProvider = array_filter ( $this -> getRegisteredProviders (), function ( ICustomTemplateProvider $provider ) use ( $templateType ) {
return $templateType === get_class ( $provider );
});
$provider = array_shift ( $matchingProvider );
if ( $provider ) {
$template = $provider -> getCustomTemplate ( $templateId );
}
}
2024-09-10 03:43:42 -04:00
2025-09-25 08:05:56 -04:00
$filename = basename ( $filePath );
$this -> filenameValidator -> validateFilename ( $filename );
$targetFile = $folder -> newFile ( $filename , ( $template instanceof File ? $template -> fopen ( 'rb' ) : null ));
2024-09-10 03:43:42 -04:00
2024-07-24 15:59:37 -04:00
$this -> eventDispatcher -> dispatchTyped ( new FileCreatedFromTemplateEvent ( $template , $targetFile , $templateFields ));
2025-11-06 11:47:32 -05:00
/** @var File $file */
$file = $userFolder -> get ( $filePath );
return $this -> formatFile ( $file );
2021-01-12 05:28:04 -05:00
} catch ( \Exception $e ) {
$this -> logger -> error ( $e -> getMessage (), [ 'exception' => $e ]);
throw new GenericFileException ( $this -> l10n -> t ( 'Failed to create file from template' ));
}
}
/**
2025-11-17 09:32:54 -05:00
* @ throws NotFoundException
* @ throws NotPermittedException
* @ throws NoUserException
2021-01-12 05:28:04 -05:00
*/
2024-11-23 02:07:31 -05:00
private function getTemplateFolder () : Folder {
2021-01-19 10:38:51 -05:00
if ( $this -> getTemplatePath () !== '' ) {
2024-11-23 02:07:31 -05:00
$path = $this -> rootFolder -> getUserFolder ( $this -> userId ) -> get ( $this -> getTemplatePath ());
if ( $path instanceof Folder ) {
return $path ;
}
2021-01-19 10:38:51 -05:00
}
throw new NotFoundException ();
2021-01-12 05:28:04 -05:00
}
2024-09-24 09:53:13 -04:00
/**
* @ return list < Template >
*/
2021-01-12 05:28:04 -05:00
private function getTemplateFiles ( TemplateFileCreator $type ) : array {
2025-05-09 17:21:22 -04:00
$templates = array_merge (
$this -> getProviderTemplates ( $type ),
$this -> getUserTemplates ( $type )
);
$this -> eventDispatcher -> dispatchTyped ( new BeforeGetTemplatesEvent ( $templates , false ));
return $templates ;
}
/**
* @ return list < Template >
*/
private function getProviderTemplates ( TemplateFileCreator $type ) : array {
2021-01-12 05:28:04 -05:00
$templates = [];
foreach ( $this -> getRegisteredProviders () as $provider ) {
foreach ( $type -> getMimetypes () as $mimetype ) {
foreach ( $provider -> getCustomTemplates ( $mimetype ) as $template ) {
2025-11-26 13:08:08 -05:00
$templateId = $template -> jsonSerialize ()[ 'templateId' ];
$templates [ $templateId ] = $template ;
2021-01-12 05:28:04 -05:00
}
}
}
2025-05-09 17:21:22 -04:00
2025-11-26 13:08:08 -05:00
return array_values ( $templates );
2025-05-09 17:21:22 -04:00
}
/**
* @ return list < Template >
*/
private function getUserTemplates ( TemplateFileCreator $type ) : array {
$templates = [];
2021-01-12 05:28:04 -05:00
try {
$userTemplateFolder = $this -> getTemplateFolder ();
} catch ( \Exception $e ) {
return $templates ;
}
2025-05-09 17:21:22 -04:00
2021-01-12 05:28:04 -05:00
foreach ( $type -> getMimetypes () as $mimetype ) {
foreach ( $userTemplateFolder -> searchByMime ( $mimetype ) as $templateFile ) {
2025-11-06 11:47:32 -05:00
if ( ! ( $templateFile instanceof File )) {
continue ;
}
2021-01-12 05:28:04 -05:00
$template = new Template (
'user' ,
$this -> rootFolder -> getUserFolder ( $this -> userId ) -> getRelativePath ( $templateFile -> getPath ()),
$templateFile
);
$template -> setHasPreview ( $this -> previewManager -> isAvailable ( $templateFile ));
$templates [] = $template ;
}
}
return $templates ;
}
2025-05-09 17:21:22 -04:00
/*
* @ return list < Field >
*/
private function getTemplateFields ( TemplateFileCreator $type , int $fileId ) : array {
$providerTemplates = $this -> getProviderTemplates ( $type );
$userTemplates = $this -> getUserTemplates ( $type );
$matchedTemplates = array_filter (
array_merge ( $providerTemplates , $userTemplates ),
2025-11-06 11:47:32 -05:00
fn ( Template $template ) : bool => $template -> jsonSerialize ()[ 'fileid' ] === $fileId );
2025-05-09 17:21:22 -04:00
if ( empty ( $matchedTemplates )) {
return [];
}
$this -> eventDispatcher -> dispatchTyped ( new BeforeGetTemplatesEvent ( $matchedTemplates , true ));
2025-11-06 11:47:32 -05:00
return array_values ( array_map ( static fn ( Template $template ) : array => $template -> jsonSerialize ()[ 'fields' ] ? ? [], $matchedTemplates ));
2025-05-09 17:21:22 -04:00
}
2021-01-12 05:28:04 -05:00
/**
2025-11-06 11:47:32 -05:00
* @ return FilesTemplateFile
2021-01-12 05:28:04 -05:00
* @ throws NotFoundException
2025-11-17 09:32:54 -05:00
* @ throws InvalidPathException
2021-01-12 05:28:04 -05:00
*/
2025-11-06 11:47:32 -05:00
private function formatFile ( File $file ) : array {
2021-01-12 05:28:04 -05:00
return [
'basename' => $file -> getName (),
'etag' => $file -> getEtag (),
2025-11-06 11:47:32 -05:00
'fileid' => $file -> getId () ? ? - 1 ,
2021-01-12 05:28:04 -05:00
'filename' => $this -> rootFolder -> getUserFolder ( $this -> userId ) -> getRelativePath ( $file -> getPath ()),
'lastmod' => $file -> getMTime (),
'mime' => $file -> getMimetype (),
'size' => $file -> getSize (),
'type' => $file -> getType (),
2023-09-29 05:10:51 -04:00
'hasPreview' => $this -> previewManager -> isAvailable ( $file ),
'permissions' => $file -> getPermissions (),
2021-01-12 05:28:04 -05:00
];
}
public function hasTemplateDirectory () : bool {
try {
$this -> getTemplateFolder ();
return true ;
} catch ( \Exception $e ) {
}
return false ;
}
2025-11-06 11:47:32 -05:00
#[Override]
2021-01-12 05:28:04 -05:00
public function setTemplatePath ( string $path ) : void {
$this -> config -> setUserValue ( $this -> userId , 'core' , 'templateDirectory' , $path );
}
2025-11-06 11:47:32 -05:00
#[Override]
2021-01-12 05:28:04 -05:00
public function getTemplatePath () : string {
2021-01-19 10:38:51 -05:00
return $this -> config -> getUserValue ( $this -> userId , 'core' , 'templateDirectory' , '' );
2021-01-12 05:28:04 -05:00
}
2025-11-06 11:47:32 -05:00
#[Override]
2024-03-28 11:13:19 -04:00
public function initializeTemplateDirectory ( ? string $path = null , ? string $userId = null , $copyTemplates = true ) : string {
2021-01-12 05:28:04 -05:00
if ( $userId !== null ) {
$this -> userId = $userId ;
}
2021-01-19 05:20:50 -05:00
$defaultSkeletonDirectory = \OC :: $SERVERROOT . '/core/skeleton' ;
$defaultTemplateDirectory = \OC :: $SERVERROOT . '/core/skeleton/Templates' ;
2023-04-05 06:50:08 -04:00
$skeletonPath = $this -> config -> getSystemValueString ( 'skeletondirectory' , $defaultSkeletonDirectory );
$skeletonTemplatePath = $this -> config -> getSystemValueString ( 'templatedirectory' , $defaultTemplateDirectory );
2021-01-19 10:38:51 -05:00
$isDefaultSkeleton = $skeletonPath === $defaultSkeletonDirectory ;
$isDefaultTemplates = $skeletonTemplatePath === $defaultTemplateDirectory ;
2021-01-29 02:56:32 -05:00
$userLang = $this -> l10nFactory -> getUserLanguage ( $this -> userManager -> get ( $this -> userId ));
2021-01-19 05:20:50 -05:00
2023-12-04 02:29:37 -05:00
if ( $skeletonTemplatePath === '' ) {
$this -> setTemplatePath ( '' );
return '' ;
}
2021-01-12 05:28:04 -05:00
try {
2021-01-19 05:20:50 -05:00
$l10n = $this -> l10nFactory -> get ( 'lib' , $userLang );
$userFolder = $this -> rootFolder -> getUserFolder ( $this -> userId );
2022-12-13 12:31:39 -05:00
$userTemplatePath = $path ? ? $this -> config -> getAppValue ( 'core' , 'defaultTemplateDirectory' , $l10n -> t ( 'Templates' )) . '/' ;
2021-01-19 05:20:50 -05:00
2021-01-19 10:38:51 -05:00
// Initial user setup without a provided path
if ( $path === null ) {
// All locations are default so we just need to rename the directory to the users language
2021-01-26 15:32:23 -05:00
if ( $isDefaultSkeleton && $isDefaultTemplates ) {
if ( ! $userFolder -> nodeExists ( 'Templates' )) {
return '' ;
}
2021-01-29 02:43:17 -05:00
$newPath = Filesystem :: normalizePath ( $userFolder -> getPath () . '/' . $userTemplatePath );
2021-01-19 10:38:51 -05:00
if ( $newPath !== $userFolder -> get ( 'Templates' ) -> getPath ()) {
$userFolder -> get ( 'Templates' ) -> move ( $newPath );
}
$this -> setTemplatePath ( $userTemplatePath );
return $userTemplatePath ;
2021-01-19 05:20:50 -05:00
}
2021-01-19 10:38:51 -05:00
if ( $isDefaultSkeleton && ! empty ( $skeletonTemplatePath ) && ! $isDefaultTemplates && $userFolder -> nodeExists ( 'Templates' )) {
2021-01-19 05:20:50 -05:00
$shippedSkeletonTemplates = $userFolder -> get ( 'Templates' );
$shippedSkeletonTemplates -> delete ();
}
2021-01-19 10:38:51 -05:00
}
2025-11-21 04:04:41 -05:00
$folder = $userFolder -> getOrCreateFolder ( $userTemplatePath );
2021-01-19 10:38:51 -05:00
$folderIsEmpty = count ( $folder -> getDirectoryListing ()) === 0 ;
if ( ! $copyTemplates ) {
2021-01-19 05:20:50 -05:00
$this -> setTemplatePath ( $userTemplatePath );
2021-01-19 10:38:51 -05:00
return $userTemplatePath ;
}
if ( ! $isDefaultTemplates && $folderIsEmpty ) {
$localizedSkeletonTemplatePath = $this -> getLocalizedTemplatePath ( $skeletonTemplatePath , $userLang );
if ( ! empty ( $localizedSkeletonTemplatePath ) && file_exists ( $localizedSkeletonTemplatePath )) {
\OC_Util :: copyr ( $localizedSkeletonTemplatePath , $folder );
2021-01-29 03:40:46 -05:00
$userFolder -> getStorage () -> getScanner () -> scan ( $folder -> getInternalPath (), Scanner :: SCAN_RECURSIVE );
2021-01-19 10:38:51 -05:00
$this -> setTemplatePath ( $userTemplatePath );
return $userTemplatePath ;
}
2021-01-19 05:20:50 -05:00
}
2021-01-19 10:38:51 -05:00
if ( $path !== null && $isDefaultSkeleton && $isDefaultTemplates && $folderIsEmpty ) {
$localizedSkeletonPath = $this -> getLocalizedTemplatePath ( $skeletonPath . '/Templates' , $userLang );
if ( ! empty ( $localizedSkeletonPath ) && file_exists ( $localizedSkeletonPath )) {
\OC_Util :: copyr ( $localizedSkeletonPath , $folder );
2021-01-29 03:40:46 -05:00
$userFolder -> getStorage () -> getScanner () -> scan ( $folder -> getInternalPath (), Scanner :: SCAN_RECURSIVE );
2021-01-19 10:38:51 -05:00
$this -> setTemplatePath ( $userTemplatePath );
return $userTemplatePath ;
}
}
$this -> setTemplatePath ( $path ? ? '' );
return $this -> getTemplatePath ();
2021-01-19 05:20:50 -05:00
} catch ( \Throwable $e ) {
2021-01-19 10:38:51 -05:00
$this -> logger -> error ( 'Failed to initialize templates directory to user language ' . $userLang . ' for ' . $userId , [ 'app' => 'files_templates' , 'exception' => $e ]);
2021-01-12 05:28:04 -05:00
}
2021-01-19 10:38:51 -05:00
$this -> setTemplatePath ( '' );
return $this -> getTemplatePath ();
2021-01-19 05:20:50 -05:00
}
2025-11-06 11:47:32 -05:00
private function getLocalizedTemplatePath ( string $skeletonTemplatePath , string $userLang ) : string {
2021-01-19 05:20:50 -05:00
$localizedSkeletonTemplatePath = str_replace ( '{lang}' , $userLang , $skeletonTemplatePath );
if ( ! file_exists ( $localizedSkeletonTemplatePath )) {
$dialectStart = strpos ( $userLang , '_' );
if ( $dialectStart !== false ) {
$localizedSkeletonTemplatePath = str_replace ( '{lang}' , substr ( $userLang , 0 , $dialectStart ), $skeletonTemplatePath );
}
if ( $dialectStart === false || ! file_exists ( $localizedSkeletonTemplatePath )) {
$localizedSkeletonTemplatePath = str_replace ( '{lang}' , 'default' , $skeletonTemplatePath );
}
}
return $localizedSkeletonTemplatePath ;
2021-01-12 05:28:04 -05:00
}
}