diff options
author | Côme Chilliet <91878298+come-nc@users.noreply.github.com> | 2025-03-10 16:20:45 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-03-10 16:20:45 +0100 |
commit | eb597917f6cf017bc9e5153eb5efb1308ccc5c68 (patch) | |
tree | a44c95000e0c7515f6becd096dca0f0a0c5b3897 | |
parent | e8b90c83daea0d3884cc66814f3114463386c3f9 (diff) | |
parent | b086f100284dd16154e0ff1819dc4d673fe42204 (diff) | |
download | nextcloud-server-eb597917f6cf017bc9e5153eb5efb1308ccc5c68.tar.gz nextcloud-server-eb597917f6cf017bc9e5153eb5efb1308ccc5c68.zip |
Merge pull request #51029 from nextcloud/fix/deprecate-oc-template-and-cleanup
fix: Deprecate OC_Template, add proper template manager instead
36 files changed, 813 insertions, 659 deletions
diff --git a/apps/dav/lib/Files/BrowserErrorPagePlugin.php b/apps/dav/lib/Files/BrowserErrorPagePlugin.php index 1491e1a89b1..de86c4995e2 100644 --- a/apps/dav/lib/Files/BrowserErrorPagePlugin.php +++ b/apps/dav/lib/Files/BrowserErrorPagePlugin.php @@ -8,9 +8,10 @@ namespace OCA\DAV\Files; use OC\AppFramework\Http\Request; -use OC_Template; use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\TemplateResponse; use OCP\IRequest; +use OCP\Template\ITemplateManager; use Sabre\DAV\Exception; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; @@ -84,7 +85,7 @@ class BrowserErrorPagePlugin extends ServerPlugin { $templateName = (string)$httpCode; } - $content = new OC_Template('core', $templateName, 'guest'); + $content = \OCP\Server::get(ITemplateManager::class)->getTemplate('core', $templateName, TemplateResponse::RENDER_AS_GUEST); $content->assign('title', $this->server->httpResponse->getStatusText()); $content->assign('remoteAddr', $request->getRemoteAddress()); $content->assign('requestID', $request->getId()); diff --git a/apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php b/apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php index 8bb044448c1..12959f57e9b 100644 --- a/apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php +++ b/apps/twofactor_backupcodes/lib/Provider/BackupCodesProvider.php @@ -17,7 +17,8 @@ use OCP\Authentication\TwoFactorAuth\IProvidesPersonalSettings; use OCP\IInitialStateService; use OCP\IL10N; use OCP\IUser; -use OCP\Template; +use OCP\Template\ITemplate; +use OCP\Template\ITemplateManager; class BackupCodesProvider implements IDeactivatableByAdmin, IProvidesPersonalSettings { @@ -36,6 +37,7 @@ class BackupCodesProvider implements IDeactivatableByAdmin, IProvidesPersonalSet private IL10N $l10n, AppManager $appManager, private IInitialStateService $initialStateService, + private ITemplateManager $templateManager, ) { $this->appManager = $appManager; } @@ -71,10 +73,10 @@ class BackupCodesProvider implements IDeactivatableByAdmin, IProvidesPersonalSet * Get the template for rending the 2FA provider view * * @param IUser $user - * @return Template + * @return ITemplate */ - public function getTemplate(IUser $user): Template { - return new Template('twofactor_backupcodes', 'challenge'); + public function getTemplate(IUser $user): ITemplate { + return $this->templateManager->getTemplate('twofactor_backupcodes', 'challenge'); } /** diff --git a/apps/twofactor_backupcodes/lib/Settings/Personal.php b/apps/twofactor_backupcodes/lib/Settings/Personal.php index 894597dd053..e03c3d303db 100644 --- a/apps/twofactor_backupcodes/lib/Settings/Personal.php +++ b/apps/twofactor_backupcodes/lib/Settings/Personal.php @@ -6,13 +6,16 @@ declare(strict_types=1); * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\TwoFactorBackupCodes\Settings; use OCP\Authentication\TwoFactorAuth\IPersonalProviderSettings; -use OCP\Template; +use OCP\Server; +use OCP\Template\ITemplate; +use OCP\Template\ITemplateManager; class Personal implements IPersonalProviderSettings { - public function getBody(): Template { - return new Template('twofactor_backupcodes', 'personal'); + public function getBody(): ITemplate { + return Server::get(ITemplateManager::class)->getTemplate('twofactor_backupcodes', 'personal'); } } diff --git a/apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php b/apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php index 24e986710c4..cef3e07a36e 100644 --- a/apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php +++ b/apps/twofactor_backupcodes/tests/Unit/Provider/BackupCodesProviderTest.php @@ -14,28 +14,21 @@ use OCA\TwoFactorBackupCodes\Service\BackupCodeStorage; use OCP\IInitialStateService; use OCP\IL10N; use OCP\IUser; -use OCP\Template; +use OCP\Server; +use OCP\Template\ITemplateManager; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class BackupCodesProviderTest extends TestCase { + private string $appName; - /** @var string */ - private $appName; + private BackupCodeStorage&MockObject $storage; + private IL10N&MockObject $l10n; + private AppManager&MockObject $appManager; + private IInitialStateService&MockObject $initialState; - /** @var BackupCodeStorage|\PHPUnit\Framework\MockObject\MockObject */ - private $storage; - - /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ - private $l10n; - - /** @var AppManager|\PHPUnit\Framework\MockObject\MockObject */ - private $appManager; - - /** @var IInitialStateService|\PHPUnit\Framework\MockObject\MockObject */ - private $initialState; - - /** @var BackupCodesProvider */ - private $provider; + private ITemplateManager $templateManager; + private BackupCodesProvider $provider; protected function setUp(): void { parent::setUp(); @@ -45,8 +38,16 @@ class BackupCodesProviderTest extends TestCase { $this->l10n = $this->createMock(IL10N::class); $this->appManager = $this->createMock(AppManager::class); $this->initialState = $this->createMock(IInitialStateService::class); - - $this->provider = new BackupCodesProvider($this->appName, $this->storage, $this->l10n, $this->appManager, $this->initialState); + $this->templateManager = Server::get(ITemplateManager::class); + + $this->provider = new BackupCodesProvider( + $this->appName, + $this->storage, + $this->l10n, + $this->appManager, + $this->initialState, + $this->templateManager, + ); } public function testGetId(): void { @@ -71,7 +72,7 @@ class BackupCodesProviderTest extends TestCase { public function testGetTempalte(): void { $user = $this->getMockBuilder(IUser::class)->getMock(); - $expected = new Template('twofactor_backupcodes', 'challenge'); + $expected = $this->templateManager->getTemplate('twofactor_backupcodes', 'challenge'); $this->assertEquals($expected, $this->provider->getTemplate($user)); } diff --git a/apps/user_ldap/lib/Settings/Admin.php b/apps/user_ldap/lib/Settings/Admin.php index bf75d3ce378..014210ca8f0 100644 --- a/apps/user_ldap/lib/Settings/Admin.php +++ b/apps/user_ldap/lib/Settings/Admin.php @@ -13,14 +13,12 @@ use OCP\IDBConnection; use OCP\IL10N; use OCP\Server; use OCP\Settings\IDelegatedSettings; -use OCP\Template; +use OCP\Template\ITemplateManager; class Admin implements IDelegatedSettings { - /** - * @param IL10N $l - */ public function __construct( private IL10N $l, + private ITemplateManager $templateManager, ) { } @@ -40,11 +38,12 @@ class Admin implements IDelegatedSettings { $hosts = $helper->getServerConfigurationHosts(); - $wControls = new Template('user_ldap', 'part.wizardcontrols'); + $wControls = $this->templateManager->getTemplate('user_ldap', 'part.wizardcontrols'); $wControls = $wControls->fetchPage(); - $sControls = new Template('user_ldap', 'part.settingcontrols'); + $sControls = $this->templateManager->getTemplate('user_ldap', 'part.settingcontrols'); $sControls = $sControls->fetchPage(); + $parameters = []; $parameters['serverConfigurationPrefixes'] = $prefixes; $parameters['serverConfigurationHosts'] = $hosts; $parameters['settingControls'] = $sControls; diff --git a/apps/user_ldap/tests/Settings/AdminTest.php b/apps/user_ldap/tests/Settings/AdminTest.php index 099cc09c068..05b9697e4c8 100644 --- a/apps/user_ldap/tests/Settings/AdminTest.php +++ b/apps/user_ldap/tests/Settings/AdminTest.php @@ -9,7 +9,9 @@ use OCA\User_LDAP\Configuration; use OCA\User_LDAP\Settings\Admin; use OCP\AppFramework\Http\TemplateResponse; use OCP\IL10N; -use OCP\Template; +use OCP\Server; +use OCP\Template\ITemplateManager; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; /** @@ -17,17 +19,19 @@ use Test\TestCase; * @package OCA\User_LDAP\Tests\Settings */ class AdminTest extends TestCase { - /** @var Admin */ - private $admin; - /** @var IL10N */ - private $l10n; + private IL10N&MockObject $l10n; + + private ITemplateManager $templateManager; + private Admin $admin; protected function setUp(): void { parent::setUp(); $this->l10n = $this->getMockBuilder(IL10N::class)->getMock(); + $this->templateManager = Server::get(ITemplateManager::class); $this->admin = new Admin( - $this->l10n + $this->l10n, + $this->templateManager, ); } @@ -38,11 +42,12 @@ class AdminTest extends TestCase { $prefixes = ['s01']; $hosts = ['s01' => '']; - $wControls = new Template('user_ldap', 'part.wizardcontrols'); + $wControls = $this->templateManager->getTemplate('user_ldap', 'part.wizardcontrols'); $wControls = $wControls->fetchPage(); - $sControls = new Template('user_ldap', 'part.settingcontrols'); + $sControls = $this->templateManager->getTemplate('user_ldap', 'part.settingcontrols'); $sControls = $sControls->fetchPage(); + $parameters = []; $parameters['serverConfigurationPrefixes'] = $prefixes; $parameters['serverConfigurationHosts'] = $hosts; $parameters['settingControls'] = $sControls; diff --git a/apps/workflowengine/lib/Listener/LoadAdditionalSettingsScriptsListener.php b/apps/workflowengine/lib/Listener/LoadAdditionalSettingsScriptsListener.php index 3b5f3f0ae11..e5a03fdcb2e 100644 --- a/apps/workflowengine/lib/Listener/LoadAdditionalSettingsScriptsListener.php +++ b/apps/workflowengine/lib/Listener/LoadAdditionalSettingsScriptsListener.php @@ -6,25 +6,18 @@ declare(strict_types=1); * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\WorkflowEngine\Listener; use OCA\WorkflowEngine\AppInfo\Application; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; -use OCP\Template; use OCP\Util; use OCP\WorkflowEngine\Events\LoadSettingsScriptsEvent; -use function class_exists; -use function function_exists; /** @template-implements IEventListener<LoadSettingsScriptsEvent> */ class LoadAdditionalSettingsScriptsListener implements IEventListener { public function handle(Event $event): void { - if (!function_exists('style')) { - // This is hacky, but we need to load the template class - class_exists(Template::class, true); - } - Util::addScript('core', 'files_fileinfo'); Util::addScript('core', 'files_client'); Util::addScript('core', 'systemtags'); diff --git a/build/psalm-baseline-ocp.xml b/build/psalm-baseline-ocp.xml index 9f69654ff3d..ce406cfde75 100644 --- a/build/psalm-baseline-ocp.xml +++ b/build/psalm-baseline-ocp.xml @@ -1,10 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> <files psalm-version="5.26.1@d747f6500b38ac4f7dfc5edbcae6e4b637d7add0"> - <file src="lib/private/legacy/OC_Template.php"> - <UndefinedClass> - <code><![CDATA[OC]]></code> - </UndefinedClass> - </file> <file src="lib/public/AppFramework/ApiController.php"> <NoInterfaceProperties> <code><![CDATA[$this->request->server]]></code> diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index cecea953d00..3e3b373ec34 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -2601,11 +2601,6 @@ <code><![CDATA[getQuota]]></code> </UndefinedInterfaceMethod> </file> - <file src="lib/private/legacy/OC_Template.php"> - <InvalidReturnType> - <code><![CDATA[bool|string]]></code> - </InvalidReturnType> - </file> <file src="lib/private/legacy/OC_User.php"> <UndefinedClass> <code><![CDATA[\Test\Util\User\Dummy]]></code> diff --git a/core/Controller/SetupController.php b/core/Controller/SetupController.php index bd3b8265c34..4b5902fdd3c 100644 --- a/core/Controller/SetupController.php +++ b/core/Controller/SetupController.php @@ -8,6 +8,7 @@ namespace OC\Core\Controller; use OC\Setup; +use OCP\Template\ITemplateManager; use OCP\Util; use Psr\Log\LoggerInterface; @@ -17,6 +18,7 @@ class SetupController { public function __construct( protected Setup $setupHelper, protected LoggerInterface $logger, + protected ITemplateManager $templateManager, ) { $this->autoConfigFile = \OC::$configDir . 'autoconfig.php'; } @@ -57,10 +59,10 @@ class SetupController { } private function displaySetupForbidden(): void { - \OC_Template::printGuestPage('', 'installation_forbidden'); + $this->templateManager->printGuestPage('', 'installation_forbidden'); } - public function display($post): void { + public function display(array $post): void { $defaults = [ 'adminlogin' => '', 'adminpass' => '', @@ -80,7 +82,7 @@ class SetupController { Util::addScript('core', 'main'); Util::addTranslations('core'); - \OC_Template::printGuestPage('', 'installation', $parameters); + $this->templateManager->printGuestPage('', 'installation', $parameters); } private function finishSetup(): void { @@ -90,7 +92,7 @@ class SetupController { \OC::$server->getIntegrityCodeChecker()->runInstanceVerification(); if ($this->setupHelper->shouldRemoveCanInstallFile()) { - \OC_Template::printGuestPage('', 'installation_incomplete'); + $this->templateManager->printGuestPage('', 'installation_incomplete'); } header('Location: ' . \OC::$server->getURLGenerator()->getAbsoluteURL('index.php/core/apps/recommended')); diff --git a/index.php b/index.php index f3ed07c3572..b368462371d 100644 --- a/index.php +++ b/index.php @@ -16,6 +16,7 @@ use OCP\HintException; use OCP\IRequest; use OCP\Security\Bruteforce\MaxDelayReached; use OCP\Server; +use OCP\Template\ITemplateManager; use Psr\Log\LoggerInterface; try { @@ -29,10 +30,10 @@ try { ]); //show the user a detailed error page - OC_Template::printExceptionErrorPage($ex, 503); + Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 503); } catch (HintException $ex) { try { - OC_Template::printErrorPage($ex->getMessage(), $ex->getHint(), 503); + Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getHint(), 503); } catch (Exception $ex2) { try { Server::get(LoggerInterface::class)->error($ex->getMessage(), [ @@ -48,7 +49,7 @@ try { } //show the user a detailed error page - OC_Template::printExceptionErrorPage($ex, 500); + Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500); } } catch (LoginException $ex) { $request = Server::get(IRequest::class); @@ -63,7 +64,7 @@ try { echo json_encode(['message' => $ex->getMessage()]); exit(); } - OC_Template::printErrorPage($ex->getMessage(), $ex->getMessage(), 401); + Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getMessage(), 401); } catch (MaxDelayReached $ex) { $request = Server::get(IRequest::class); /** @@ -78,7 +79,7 @@ try { exit(); } http_response_code(429); - OC_Template::printGuestPage('core', '429'); + Server::get(ITemplateManager::class)->printGuestPage('core', '429'); } catch (Exception $ex) { Server::get(LoggerInterface::class)->error($ex->getMessage(), [ 'app' => 'index', @@ -86,7 +87,7 @@ try { ]); //show the user a detailed error page - OC_Template::printExceptionErrorPage($ex, 500); + Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500); } catch (Error $ex) { try { Server::get(LoggerInterface::class)->error($ex->getMessage(), [ @@ -103,5 +104,5 @@ try { throw $ex; } - OC_Template::printExceptionErrorPage($ex, 500); + Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500); } diff --git a/lib/base.php b/lib/base.php index 7a7b9c64dd9..bda6b6e01a1 100644 --- a/lib/base.php +++ b/lib/base.php @@ -21,6 +21,7 @@ use OCP\IUserSession; use OCP\Security\Bruteforce\IThrottler; use OCP\Server; use OCP\Share; +use OCP\Template\ITemplateManager; use OCP\User\Events\UserChangedEvent; use OCP\User\Events\UserDeletedEvent; use OCP\Util; @@ -208,7 +209,7 @@ class OC { echo $l->t('See %s', [ $urlGenerator->linkToDocs('admin-config') ]) . "\n"; exit; } else { - OC_Template::printErrorPage( + Server::get(ITemplateManager::class)->printErrorPage( $l->t('Cannot write into "config" directory!'), $l->t('This can usually be fixed by giving the web server write access to the config directory.') . ' ' . $l->t('But, if you prefer to keep config.php file read only, set the option "config_is_read_only" to true in it.') . ' ' @@ -244,7 +245,7 @@ class OC { header('Retry-After: 120'); // render error page - $template = new OC_Template('', 'update.user', 'guest'); + $template = Server::get(ITemplateManager::class)->getTemplate('', 'update.user', 'guest'); \OCP\Util::addScript('core', 'maintenance'); \OCP\Util::addStyle('core', 'guest'); $template->printPage(); @@ -300,7 +301,7 @@ class OC { $serverVersion = \OCP\Server::get(\OCP\ServerVersion::class); // render error page - $template = new OC_Template('', 'update.use-cli', 'guest'); + $template = Server::get(ITemplateManager::class)->getTemplate('', 'update.use-cli', 'guest'); $template->assign('productName', 'nextcloud'); // for now $template->assign('version', $serverVersion->getVersionString()); $template->assign('tooBig', $tooBig); @@ -327,7 +328,7 @@ class OC { /** @var \OC\App\AppManager $appManager */ $appManager = Server::get(\OCP\App\IAppManager::class); - $tmpl = new OC_Template('', 'update.admin', 'guest'); + $tmpl = Server::get(ITemplateManager::class)->getTemplate('', 'update.admin', 'guest'); $tmpl->assign('version', \OCP\Server::get(\OCP\ServerVersion::class)->getVersionString()); $tmpl->assign('isAppsOnlyUpgrade', $isAppsOnlyUpgrade); @@ -420,7 +421,7 @@ class OC { } catch (Exception $e) { Server::get(LoggerInterface::class)->error($e->getMessage(), ['app' => 'base','exception' => $e]); //show the user a detailed error page - OC_Template::printExceptionErrorPage($e, 500); + Server::get(ITemplateManager::class)->printExceptionErrorPage($e, 500); die(); } @@ -662,7 +663,7 @@ class OC { if ($config->getSystemValueBool('debug', false)) { set_error_handler([$errorHandler, 'onAll'], E_ALL); if (\OC::$CLI) { - $exceptionHandler = ['OC_Template', 'printExceptionErrorPage']; + $exceptionHandler = [Server::get(ITemplateManager::class), 'printExceptionErrorPage']; } } else { set_error_handler([$errorHandler, 'onError']); @@ -705,7 +706,7 @@ class OC { http_response_code(503); OC_Util::addStyle('guest'); try { - OC_Template::printGuestPage('', 'error', ['errors' => $errors]); + Server::get(ITemplateManager::class)->printGuestPage('', 'error', ['errors' => $errors]); exit; } catch (\Exception $e) { // In case any error happens when showing the error page, we simply fall back to posting the text. @@ -784,7 +785,7 @@ class OC { // Check whether the sample configuration has been copied if ($systemConfig->getValue('copied_sample_config', false)) { $l = Server::get(\OCP\L10N\IFactory::class)->get('lib'); - OC_Template::printErrorPage( + Server::get(ITemplateManager::class)->printErrorPage( $l->t('Sample configuration detected'), $l->t('It has been detected that the sample configuration has been copied. This can break your installation and is unsupported. Please read the documentation before performing changes on config.php'), 503 @@ -826,7 +827,7 @@ class OC { ] ); - $tmpl = new OCP\Template('core', 'untrustedDomain', 'guest'); + $tmpl = Server::get(ITemplateManager::class)->getTemplate('core', 'untrustedDomain', 'guest'); $tmpl->assign('docUrl', Server::get(IURLGenerator::class)->linkToDocs('admin-trusted-domains')); $tmpl->printPage(); @@ -1097,7 +1098,7 @@ class OC { logger('core')->emergency($e->getMessage(), ['exception' => $e]); } $l = Server::get(\OCP\L10N\IFactory::class)->get('lib'); - OC_Template::printErrorPage( + Server::get(ITemplateManager::class)->printErrorPage( '404', $l->t('The page could not be found on the server.'), 404 diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index d900941fd1a..c353ff32c3a 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -850,6 +850,9 @@ return array( 'OCP\\Teams\\Team' => $baseDir . '/lib/public/Teams/Team.php', 'OCP\\Teams\\TeamResource' => $baseDir . '/lib/public/Teams/TeamResource.php', 'OCP\\Template' => $baseDir . '/lib/public/Template.php', + 'OCP\\Template\\ITemplate' => $baseDir . '/lib/public/Template/ITemplate.php', + 'OCP\\Template\\ITemplateManager' => $baseDir . '/lib/public/Template/ITemplateManager.php', + 'OCP\\Template\\TemplateNotFoundException' => $baseDir . '/lib/public/Template/TemplateNotFoundException.php', 'OCP\\TextProcessing\\Events\\AbstractTextProcessingEvent' => $baseDir . '/lib/public/TextProcessing/Events/AbstractTextProcessingEvent.php', 'OCP\\TextProcessing\\Events\\TaskFailedEvent' => $baseDir . '/lib/public/TextProcessing/Events/TaskFailedEvent.php', 'OCP\\TextProcessing\\Events\\TaskSuccessfulEvent' => $baseDir . '/lib/public/TextProcessing/Events/TaskSuccessfulEvent.php', @@ -2048,7 +2051,9 @@ return array( 'OC\\Template\\JSResourceLocator' => $baseDir . '/lib/private/Template/JSResourceLocator.php', 'OC\\Template\\ResourceLocator' => $baseDir . '/lib/private/Template/ResourceLocator.php', 'OC\\Template\\ResourceNotFoundException' => $baseDir . '/lib/private/Template/ResourceNotFoundException.php', + 'OC\\Template\\Template' => $baseDir . '/lib/private/Template/Template.php', 'OC\\Template\\TemplateFileLocator' => $baseDir . '/lib/private/Template/TemplateFileLocator.php', + 'OC\\Template\\TemplateManager' => $baseDir . '/lib/private/Template/TemplateManager.php', 'OC\\TextProcessing\\Db\\Task' => $baseDir . '/lib/private/TextProcessing/Db/Task.php', 'OC\\TextProcessing\\Db\\TaskMapper' => $baseDir . '/lib/private/TextProcessing/Db/TaskMapper.php', 'OC\\TextProcessing\\Manager' => $baseDir . '/lib/private/TextProcessing/Manager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 36886be53f3..dd4b0943d2f 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -899,6 +899,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Teams\\Team' => __DIR__ . '/../../..' . '/lib/public/Teams/Team.php', 'OCP\\Teams\\TeamResource' => __DIR__ . '/../../..' . '/lib/public/Teams/TeamResource.php', 'OCP\\Template' => __DIR__ . '/../../..' . '/lib/public/Template.php', + 'OCP\\Template\\ITemplate' => __DIR__ . '/../../..' . '/lib/public/Template/ITemplate.php', + 'OCP\\Template\\ITemplateManager' => __DIR__ . '/../../..' . '/lib/public/Template/ITemplateManager.php', + 'OCP\\Template\\TemplateNotFoundException' => __DIR__ . '/../../..' . '/lib/public/Template/TemplateNotFoundException.php', 'OCP\\TextProcessing\\Events\\AbstractTextProcessingEvent' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/Events/AbstractTextProcessingEvent.php', 'OCP\\TextProcessing\\Events\\TaskFailedEvent' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/Events/TaskFailedEvent.php', 'OCP\\TextProcessing\\Events\\TaskSuccessfulEvent' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/Events/TaskSuccessfulEvent.php', @@ -2097,7 +2100,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Template\\JSResourceLocator' => __DIR__ . '/../../..' . '/lib/private/Template/JSResourceLocator.php', 'OC\\Template\\ResourceLocator' => __DIR__ . '/../../..' . '/lib/private/Template/ResourceLocator.php', 'OC\\Template\\ResourceNotFoundException' => __DIR__ . '/../../..' . '/lib/private/Template/ResourceNotFoundException.php', + 'OC\\Template\\Template' => __DIR__ . '/../../..' . '/lib/private/Template/Template.php', 'OC\\Template\\TemplateFileLocator' => __DIR__ . '/../../..' . '/lib/private/Template/TemplateFileLocator.php', + 'OC\\Template\\TemplateManager' => __DIR__ . '/../../..' . '/lib/private/Template/TemplateManager.php', 'OC\\TextProcessing\\Db\\Task' => __DIR__ . '/../../..' . '/lib/private/TextProcessing/Db/Task.php', 'OC\\TextProcessing\\Db\\TaskMapper' => __DIR__ . '/../../..' . '/lib/private/TextProcessing/Db/TaskMapper.php', 'OC\\TextProcessing\\Manager' => __DIR__ . '/../../..' . '/lib/private/TextProcessing/Manager.php', diff --git a/lib/private/Server.php b/lib/private/Server.php index 8c5ec8ed252..523fc7b9914 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -286,6 +286,7 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(\OCP\DirectEditing\IManager::class, \OC\DirectEditing\Manager::class); $this->registerAlias(ITemplateManager::class, TemplateManager::class); + $this->registerAlias(\OCP\Template\ITemplateManager::class, \OC\Template\TemplateManager::class); $this->registerAlias(IActionFactory::class, ActionFactory::class); diff --git a/lib/private/Template/Base.php b/lib/private/Template/Base.php index 602c8e6257e..0dd7d92d6be 100644 --- a/lib/private/Template/Base.php +++ b/lib/private/Template/Base.php @@ -8,11 +8,10 @@ namespace OC\Template; use OCP\Defaults; -use Throwable; class Base { private $template; // The template - private $vars; // Vars + private array $vars = []; /** @var \OCP\IL10N */ private $l10n; @@ -59,11 +58,9 @@ class Base { } /** - * @param string $serverRoot - * @param string $theme * @return string[] */ - protected function getCoreTemplateDirs($theme, $serverRoot) { + protected function getCoreTemplateDirs(string $theme, string $serverRoot): array { return [ $serverRoot . '/themes/' . $theme . '/core/templates/', $serverRoot . '/core/templates/', @@ -72,30 +69,24 @@ class Base { /** * Assign variables - * @param string $key key - * @param float|array|bool|integer|string|Throwable $value value - * @return bool * * This function assigns a variable. It can be accessed via $_[$key] in * the template. * * If the key existed before, it will be overwritten */ - public function assign($key, $value) { + public function assign(string $key, mixed $value): void { $this->vars[$key] = $value; - return true; } /** * Appends a variable - * @param string $key key - * @param mixed $value value * * This function assigns a variable in an array context. If the key already * exists, the value will be appended. It can be accessed via * $_[$key][$position] in the template. */ - public function append($key, $value) { + public function append(string $key, mixed $value): void { if (array_key_exists($key, $this->vars)) { $this->vars[$key][] = $value; } else { @@ -105,42 +96,29 @@ class Base { /** * Prints the proceeded template - * @return bool * * This function proceeds the template and prints its output. */ - public function printPage() { + public function printPage(): void { $data = $this->fetchPage(); - if ($data === false) { - return false; - } else { - print $data; - return true; - } + print $data; } /** * Process the template * - * @param array|null $additionalParams - * @return string This function processes the template. - * * This function processes the template. */ - public function fetchPage($additionalParams = null) { + public function fetchPage(?array $additionalParams = null): string { return $this->load($this->template, $additionalParams); } /** * doing the actual work * - * @param string $file - * @param array|null $additionalParams - * @return string content - * * Includes the template file, fetches its output */ - protected function load($file, $additionalParams = null) { + protected function load(string $file, ?array $additionalParams = null): string { // Register the variables $_ = $this->vars; $l = $this->l10n; diff --git a/lib/private/Template/Template.php b/lib/private/Template/Template.php new file mode 100644 index 00000000000..b69d68b944f --- /dev/null +++ b/lib/private/Template/Template.php @@ -0,0 +1,161 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OC\Template; + +use OC\Security\CSP\ContentSecurityPolicyNonceManager; +use OC\TemplateLayout; +use OCP\App\AppPathNotFoundException; +use OCP\App\IAppManager; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\Defaults; +use OCP\Server; +use OCP\Template\ITemplate; +use OCP\Template\TemplateNotFoundException; +use OCP\Util; + +require_once __DIR__ . '/../legacy/template/functions.php'; + +class Template extends Base implements ITemplate { + private string $path; + private array $headers = []; + + /** + * @param string $app app providing the template + * @param string $name of the template file (without suffix) + * @param TemplateResponse::RENDER_AS_* $renderAs If $renderAs is set, will try to + * produce a full page in the according layout. + * @throws TemplateNotFoundException + */ + public function __construct( + protected string $app, + string $name, + private string $renderAs = TemplateResponse::RENDER_AS_BLANK, + bool $registerCall = true, + ) { + $theme = \OC_Util::getTheme(); + + $requestToken = ($registerCall ? Util::callRegister() : ''); + $cspNonce = Server::get(ContentSecurityPolicyNonceManager::class)->getNonce(); + + // fix translation when app is something like core/lostpassword + $parts = explode('/', $app); + $l10n = Util::getL10N($parts[0]); + + [$path, $template] = $this->findTemplate($theme, $app, $name); + + $this->path = $path; + + parent::__construct( + $template, + $requestToken, + $l10n, + Server::get(Defaults::class), + $cspNonce, + ); + } + + + /** + * find the template with the given name + * + * Will select the template file for the selected theme. + * Checking all the possible locations. + * + * @param string $name of the template file (without suffix) + * @return array{string,string} Directory path and filename + * @throws TemplateNotFoundException + */ + protected function findTemplate(string $theme, string $app, string $name): array { + // Check if it is a app template or not. + if ($app !== '') { + try { + $appDir = Server::get(IAppManager::class)->getAppPath($app); + } catch (AppPathNotFoundException) { + $appDir = false; + } + $dirs = $this->getAppTemplateDirs($theme, $app, \OC::$SERVERROOT, $appDir); + } else { + $dirs = $this->getCoreTemplateDirs($theme, \OC::$SERVERROOT); + } + $locator = new TemplateFileLocator($dirs); + return $locator->find($name); + } + + /** + * Add a custom element to the header + * @param string $tag tag name of the element + * @param array $attributes array of attributes for the element + * @param string $text the text content for the element. If $text is null then the + * element will be written as empty element. So use "" to get a closing tag. + */ + public function addHeader(string $tag, array $attributes, ?string $text = null): void { + $this->headers[] = [ + 'tag' => $tag, + 'attributes' => $attributes, + 'text' => $text + ]; + } + + /** + * Process the template + * + * This function process the template. If $this->renderAs is set, it + * will produce a full page. + */ + public function fetchPage(?array $additionalParams = null): string { + $data = parent::fetchPage($additionalParams); + + if ($this->renderAs) { + $page = Server::get(TemplateLayout::class)->getPageTemplate($this->renderAs, $this->app); + + if (is_array($additionalParams)) { + foreach ($additionalParams as $key => $value) { + $page->assign($key, $value); + } + } + + // Add custom headers + $headers = ''; + foreach (\OC_Util::$headers as $header) { + $headers .= '<' . Util::sanitizeHTML($header['tag']); + if (strcasecmp($header['tag'], 'script') === 0 && in_array('src', array_map('strtolower', array_keys($header['attributes'])))) { + $headers .= ' defer'; + } + foreach ($header['attributes'] as $name => $value) { + $headers .= ' ' . Util::sanitizeHTML($name) . '="' . Util::sanitizeHTML($value) . '"'; + } + if ($header['text'] !== null) { + $headers .= '>' . Util::sanitizeHTML($header['text']) . '</' . Util::sanitizeHTML($header['tag']) . '>'; + } else { + $headers .= '/>'; + } + } + + $page->assign('headers', $headers); + $page->assign('content', $data); + return $page->fetchPage($additionalParams); + } + + return $data; + } + + /** + * Include template + * + * @return string returns content of included template + * + * Includes another template. use <?php echo $this->inc('template'); ?> to + * do this. + */ + public function inc(string $file, ?array $additionalParams = null): string { + return $this->load($this->path . $file . '.php', $additionalParams); + } +} diff --git a/lib/private/Template/TemplateFileLocator.php b/lib/private/Template/TemplateFileLocator.php index 38583d158a3..11a568b5b21 100644 --- a/lib/private/Template/TemplateFileLocator.php +++ b/lib/private/Template/TemplateFileLocator.php @@ -1,29 +1,31 @@ <?php +declare(strict_types=1); + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OC\Template; -class TemplateFileLocator { - protected $dirs; - private $path; +use OCP\Template\TemplateNotFoundException; +class TemplateFileLocator { /** * @param string[] $dirs */ - public function __construct($dirs) { - $this->dirs = $dirs; + public function __construct( + private array $dirs, + ) { } /** - * @param string $template - * @return string - * @throws \Exception + * @return array{string,string} Directory path and filename + * @throws TemplateNotFoundException */ - public function find($template) { + public function find(string $template): array { if ($template === '') { throw new \InvalidArgumentException('Empty template name'); } @@ -31,14 +33,9 @@ class TemplateFileLocator { foreach ($this->dirs as $dir) { $file = $dir . $template . '.php'; if (is_file($file)) { - $this->path = $dir; - return $file; + return [$dir,$file]; } } - throw new \Exception('template file not found: template:' . $template); - } - - public function getPath() { - return $this->path; + throw new TemplateNotFoundException('template file not found: template:' . $template); } } diff --git a/lib/private/Template/TemplateManager.php b/lib/private/Template/TemplateManager.php new file mode 100644 index 00000000000..34da4deac72 --- /dev/null +++ b/lib/private/Template/TemplateManager.php @@ -0,0 +1,169 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Template; + +use OCP\App\IAppManager; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IRequest; +use OCP\Server; +use OCP\Template\ITemplate; +use OCP\Template\ITemplateManager; +use OCP\Template\TemplateNotFoundException; +use Psr\Log\LoggerInterface; + +class TemplateManager implements ITemplateManager { + public function __construct( + private IAppManager $appManager, + private IEventDispatcher $eventDispatcher, + ) { + } + + /** + * @param TemplateResponse::RENDER_AS_* $renderAs + * @throws TemplateNotFoundException if the template cannot be found + */ + public function getTemplate(string $app, string $name, string $renderAs = TemplateResponse::RENDER_AS_BLANK, bool $registerCall = true): ITemplate { + return new Template($app, $name, $renderAs, $registerCall); + } + + /** + * Shortcut to print a simple page for guests + * @param string $application The application we render the template for + * @param string $name Name of the template + * @param array $parameters Parameters for the template + */ + public function printGuestPage(string $application, string $name, array $parameters = []): void { + $content = $this->getTemplate($application, $name, $name === 'error' ? $name : 'guest'); + foreach ($parameters as $key => $value) { + $content->assign($key, $value); + } + $content->printPage(); + } + + /** + * Print a fatal error page and terminates the script + * @param string $error_msg The error message to show + * @param string $hint An optional hint message - needs to be properly escape + */ + public function printErrorPage(string $error_msg, string $hint = '', int $statusCode = 500): never { + if ($this->appManager->isEnabledForUser('theming') && !$this->appManager->isAppLoaded('theming')) { + $this->appManager->loadApp('theming'); + } + + if ($error_msg === $hint) { + // If the hint is the same as the message there is no need to display it twice. + $hint = ''; + } + $errors = [['error' => $error_msg, 'hint' => $hint]]; + + http_response_code($statusCode); + try { + // Try rendering themed html error page + $response = new TemplateResponse( + '', + 'error', + ['errors' => $errors], + TemplateResponse::RENDER_AS_ERROR, + $statusCode, + ); + $event = new BeforeTemplateRenderedEvent(false, $response); + $this->eventDispatcher->dispatchTyped($event); + print($response->render()); + } catch (\Throwable $e1) { + $logger = \OCP\Server::get(LoggerInterface::class); + $logger->error('Rendering themed error page failed. Falling back to un-themed error page.', [ + 'app' => 'core', + 'exception' => $e1, + ]); + + try { + // Try rendering unthemed html error page + $content = $this->getTemplate('', 'error', 'error', false); + $content->assign('errors', $errors); + $content->printPage(); + } catch (\Exception $e2) { + // If nothing else works, fall back to plain text error page + $logger->error("$error_msg $hint", ['app' => 'core']); + $logger->error('Rendering un-themed error page failed. Falling back to plain text error page.', [ + 'app' => 'core', + 'exception' => $e2, + ]); + + header('Content-Type: text/plain; charset=utf-8'); + print("$error_msg $hint"); + } + } + die(); + } + + /** + * print error page using Exception details + */ + public function printExceptionErrorPage(\Throwable $exception, int $statusCode = 503): never { + $debug = false; + http_response_code($statusCode); + try { + $debug = (bool)Server::get(\OC\SystemConfig::class)->getValue('debug', false); + $serverLogsDocumentation = Server::get(\OC\SystemConfig::class)->getValue('documentation_url.server_logs', ''); + $request = Server::get(IRequest::class); + $content = $this->getTemplate('', 'exception', 'error', false); + $content->assign('errorClass', get_class($exception)); + $content->assign('errorMsg', $exception->getMessage()); + $content->assign('errorCode', $exception->getCode()); + $content->assign('file', $exception->getFile()); + $content->assign('line', $exception->getLine()); + $content->assign('exception', $exception); + $content->assign('debugMode', $debug); + $content->assign('serverLogsDocumentation', $serverLogsDocumentation); + $content->assign('remoteAddr', $request->getRemoteAddress()); + $content->assign('requestID', $request->getId()); + $content->printPage(); + } catch (\Exception $e) { + try { + $logger = Server::get(LoggerInterface::class); + $logger->error($exception->getMessage(), ['app' => 'core', 'exception' => $exception]); + $logger->error($e->getMessage(), ['app' => 'core', 'exception' => $e]); + } catch (\Throwable $e) { + // no way to log it properly - but to avoid a white page of death we send some output + $this->printPlainErrorPage($e, $debug); + + // and then throw it again to log it at least to the web server error log + throw $e; + } + + $this->printPlainErrorPage($e, $debug); + } + die(); + } + + /** + * @psalm-taint-escape has_quotes + * @psalm-taint-escape html + */ + private function fakeEscapeForPlainText(string $str): string { + return $str; + } + + private function printPlainErrorPage(\Throwable $exception, bool $debug = false): void { + header('Content-Type: text/plain; charset=utf-8'); + print("Internal Server Error\n\n"); + print("The server encountered an internal error and was unable to complete your request.\n"); + print("Please contact the server administrator if this error reappears multiple times, please include the technical details below in your report.\n"); + print("More details can be found in the server log.\n"); + + if ($debug) { + print("\n"); + print($exception->getMessage() . ' ' . $exception->getFile() . ' at ' . $exception->getLine() . "\n"); + print($this->fakeEscapeForPlainText($exception->getTraceAsString())); + } + } +} diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index e52ef702ad3..9c1d7ca1d2c 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -25,36 +27,31 @@ use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserSession; use OCP\L10N\IFactory; +use OCP\Server; use OCP\ServerVersion; use OCP\Support\Subscription\IRegistry; +use OCP\Template\ITemplate; +use OCP\Template\ITemplateManager; use OCP\Util; -class TemplateLayout extends \OC_Template { - private static $versionHash = ''; +class TemplateLayout { + private static string $versionHash = ''; /** @var string[] */ - private static $cacheBusterCache = []; - - /** @var CSSResourceLocator|null */ - public static $cssLocator = null; - - /** @var JSResourceLocator|null */ - public static $jsLocator = null; - - private IConfig $config; - private IAppManager $appManager; - private InitialStateService $initialState; - private INavigationManager $navigationManager; - - /** - * @param string $renderAs - * @param string $appId application id - */ - public function __construct($renderAs, $appId = '') { - $this->config = \OCP\Server::get(IConfig::class); - $this->appManager = \OCP\Server::get(IAppManager::class); - $this->initialState = \OCP\Server::get(InitialStateService::class); - $this->navigationManager = \OCP\Server::get(INavigationManager::class); + private static array $cacheBusterCache = []; + + public static ?CSSResourceLocator $cssLocator = null; + public static ?JSResourceLocator $jsLocator = null; + + public function __construct( + private IConfig $config, + private IAppManager $appManager, + private InitialStateService $initialState, + private INavigationManager $navigationManager, + private ITemplateManager $templateManager, + ) { + } + public function getPageTemplate(string $renderAs, string $appId): ITemplate { // Add fallback theming variables if not rendered as user if ($renderAs !== TemplateResponse::RENDER_AS_USER) { // TODO cache generated default theme if enabled for fallback if server is erroring ? @@ -62,146 +59,148 @@ class TemplateLayout extends \OC_Template { } // Decide which page we show - if ($renderAs === TemplateResponse::RENDER_AS_USER) { - parent::__construct('core', 'layout.user'); - if (in_array(\OC_App::getCurrentApp(), ['settings','admin', 'help']) !== false) { - $this->assign('bodyid', 'body-settings'); - } else { - $this->assign('bodyid', 'body-user'); - } - - $this->initialState->provideInitialState('core', 'active-app', $this->navigationManager->getActiveEntry()); - $this->initialState->provideInitialState('core', 'apps', array_values($this->navigationManager->getAll())); - - if ($this->config->getSystemValueBool('unified_search.enabled', false) || !$this->config->getSystemValueBool('enable_non-accessible_features', true)) { - $this->initialState->provideInitialState('unified-search', 'limit-default', (int)$this->config->getAppValue('core', 'unified-search.limit-default', (string)SearchQuery::LIMIT_DEFAULT)); - $this->initialState->provideInitialState('unified-search', 'min-search-length', (int)$this->config->getAppValue('core', 'unified-search.min-search-length', (string)1)); - $this->initialState->provideInitialState('unified-search', 'live-search', $this->config->getAppValue('core', 'unified-search.live-search', 'yes') === 'yes'); - Util::addScript('core', 'legacy-unified-search', 'core'); - } else { - Util::addScript('core', 'unified-search', 'core'); - } - // Set body data-theme - $this->assign('enabledThemes', []); - if ($this->appManager->isEnabledForUser('theming') && class_exists('\OCA\Theming\Service\ThemesService')) { - /** @var \OCA\Theming\Service\ThemesService */ - $themesService = \OC::$server->get(\OCA\Theming\Service\ThemesService::class); - $this->assign('enabledThemes', $themesService->getEnabledThemes()); - } - - // Set logo link target - $logoUrl = $this->config->getSystemValueString('logo_url', ''); - $this->assign('logoUrl', $logoUrl); - - // Set default entry name - $defaultEntryId = $this->navigationManager->getDefaultEntryIdForUser(); - $defaultEntry = $this->navigationManager->get($defaultEntryId); - $this->assign('defaultAppName', $defaultEntry['name']); + switch ($renderAs) { + case TemplateResponse::RENDER_AS_USER: + $page = $this->templateManager->getTemplate('core', 'layout.user'); + if (in_array(\OC_App::getCurrentApp(), ['settings','admin', 'help']) !== false) { + $page->assign('bodyid', 'body-settings'); + } else { + $page->assign('bodyid', 'body-user'); + } - // Add navigation entry - $this->assign('application', ''); - $this->assign('appid', $appId); + $this->initialState->provideInitialState('core', 'active-app', $this->navigationManager->getActiveEntry()); + $this->initialState->provideInitialState('core', 'apps', array_values($this->navigationManager->getAll())); - $navigation = $this->navigationManager->getAll(); - $this->assign('navigation', $navigation); - $settingsNavigation = $this->navigationManager->getAll('settings'); - $this->initialState->provideInitialState('core', 'settingsNavEntries', $settingsNavigation); + if ($this->config->getSystemValueBool('unified_search.enabled', false) || !$this->config->getSystemValueBool('enable_non-accessible_features', true)) { + $this->initialState->provideInitialState('unified-search', 'limit-default', (int)$this->config->getAppValue('core', 'unified-search.limit-default', (string)SearchQuery::LIMIT_DEFAULT)); + $this->initialState->provideInitialState('unified-search', 'min-search-length', (int)$this->config->getAppValue('core', 'unified-search.min-search-length', (string)1)); + $this->initialState->provideInitialState('unified-search', 'live-search', $this->config->getAppValue('core', 'unified-search.live-search', 'yes') === 'yes'); + Util::addScript('core', 'legacy-unified-search', 'core'); + } else { + Util::addScript('core', 'unified-search', 'core'); + } + // Set body data-theme + $page->assign('enabledThemes', []); + if ($this->appManager->isEnabledForUser('theming') && class_exists('\OCA\Theming\Service\ThemesService')) { + $themesService = Server::get(\OCA\Theming\Service\ThemesService::class); + $page->assign('enabledThemes', $themesService->getEnabledThemes()); + } - foreach ($navigation as $entry) { - if ($entry['active']) { - $this->assign('application', $entry['name']); - break; + // Set logo link target + $logoUrl = $this->config->getSystemValueString('logo_url', ''); + $page->assign('logoUrl', $logoUrl); + + // Set default entry name + $defaultEntryId = $this->navigationManager->getDefaultEntryIdForUser(); + $defaultEntry = $this->navigationManager->get($defaultEntryId); + $page->assign('defaultAppName', $defaultEntry['name'] ?? ''); + + // Add navigation entry + $page->assign('application', ''); + $page->assign('appid', $appId); + + $navigation = $this->navigationManager->getAll(); + $page->assign('navigation', $navigation); + $settingsNavigation = $this->navigationManager->getAll('settings'); + $this->initialState->provideInitialState('core', 'settingsNavEntries', $settingsNavigation); + + foreach ($navigation as $entry) { + if ($entry['active']) { + $page->assign('application', $entry['name']); + break; + } } - } - foreach ($settingsNavigation as $entry) { - if ($entry['active']) { - $this->assign('application', $entry['name']); - break; + foreach ($settingsNavigation as $entry) { + if ($entry['active']) { + $page->assign('application', $entry['name']); + break; + } } - } - $userDisplayName = false; - $user = \OC::$server->get(IUserSession::class)->getUser(); - if ($user) { - $userDisplayName = $user->getDisplayName(); - } - $this->assign('user_displayname', $userDisplayName); - $this->assign('user_uid', \OC_User::getUser()); + $user = Server::get(IUserSession::class)->getUser(); - if ($user === null) { - $this->assign('userAvatarSet', false); - $this->assign('userStatus', false); - } else { - $this->assign('userAvatarSet', true); - $this->assign('userAvatarVersion', $this->config->getUserValue(\OC_User::getUser(), 'avatar', 'version', 0)); - } - } elseif ($renderAs === TemplateResponse::RENDER_AS_ERROR) { - parent::__construct('core', 'layout.guest', '', false); - $this->assign('bodyid', 'body-login'); - $this->assign('user_displayname', ''); - $this->assign('user_uid', ''); - } elseif ($renderAs === TemplateResponse::RENDER_AS_GUEST) { - parent::__construct('core', 'layout.guest'); - \OC_Util::addStyle('guest'); - $this->assign('bodyid', 'body-login'); - - $userDisplayName = false; - $user = \OC::$server->get(IUserSession::class)->getUser(); - if ($user) { - $userDisplayName = $user->getDisplayName(); - } - $this->assign('user_displayname', $userDisplayName); - $this->assign('user_uid', \OC_User::getUser()); - } elseif ($renderAs === TemplateResponse::RENDER_AS_PUBLIC) { - parent::__construct('core', 'layout.public'); - $this->assign('appid', $appId); - $this->assign('bodyid', 'body-public'); - - // Set body data-theme - $this->assign('enabledThemes', []); - if ($this->appManager->isEnabledForUser('theming') && class_exists('\OCA\Theming\Service\ThemesService')) { - /** @var \OCA\Theming\Service\ThemesService $themesService */ - $themesService = \OC::$server->get(\OCA\Theming\Service\ThemesService::class); - $this->assign('enabledThemes', $themesService->getEnabledThemes()); - } + if ($user === null) { + $page->assign('user_uid', false); + $page->assign('user_displayname', false); + $page->assign('userAvatarSet', false); + $page->assign('userStatus', false); + } else { + $page->assign('user_uid', $user->getUID()); + $page->assign('user_displayname', $user->getDisplayName()); + $page->assign('userAvatarSet', true); + $page->assign('userAvatarVersion', $this->config->getUserValue($user->getUID(), 'avatar', 'version', 0)); + } + break; + case TemplateResponse::RENDER_AS_ERROR: + $page = $this->templateManager->getTemplate('core', 'layout.guest', '', false); + $page->assign('bodyid', 'body-login'); + $page->assign('user_displayname', ''); + $page->assign('user_uid', ''); + break; + case TemplateResponse::RENDER_AS_GUEST: + $page = $this->templateManager->getTemplate('core', 'layout.guest'); + Util::addStyle('guest'); + $page->assign('bodyid', 'body-login'); + + $userDisplayName = false; + $user = Server::get(IUserSession::class)->getUser(); + if ($user) { + $userDisplayName = $user->getDisplayName(); + } + $page->assign('user_displayname', $userDisplayName); + $page->assign('user_uid', \OC_User::getUser()); + break; + case TemplateResponse::RENDER_AS_PUBLIC: + $page = $this->templateManager->getTemplate('core', 'layout.public'); + $page->assign('appid', $appId); + $page->assign('bodyid', 'body-public'); + + // Set body data-theme + $page->assign('enabledThemes', []); + if ($this->appManager->isEnabledForUser('theming') && class_exists('\OCA\Theming\Service\ThemesService')) { + $themesService = Server::get(\OCA\Theming\Service\ThemesService::class); + $page->assign('enabledThemes', $themesService->getEnabledThemes()); + } - // Set logo link target - $logoUrl = $this->config->getSystemValueString('logo_url', ''); - $this->assign('logoUrl', $logoUrl); + // Set logo link target + $logoUrl = $this->config->getSystemValueString('logo_url', ''); + $page->assign('logoUrl', $logoUrl); - /** @var IRegistry $subscription */ - $subscription = \OCP\Server::get(IRegistry::class); - $showSimpleSignup = $this->config->getSystemValueBool('simpleSignUpLink.shown', true); - if ($showSimpleSignup && $subscription->delegateHasValidSubscription()) { - $showSimpleSignup = false; - } + $subscription = Server::get(IRegistry::class); + $showSimpleSignup = $this->config->getSystemValueBool('simpleSignUpLink.shown', true); + if ($showSimpleSignup && $subscription->delegateHasValidSubscription()) { + $showSimpleSignup = false; + } - $defaultSignUpLink = 'https://nextcloud.com/signup/'; - $signUpLink = $this->config->getSystemValueString('registration_link', $defaultSignUpLink); - if ($signUpLink !== $defaultSignUpLink) { - $showSimpleSignup = true; - } + $defaultSignUpLink = 'https://nextcloud.com/signup/'; + $signUpLink = $this->config->getSystemValueString('registration_link', $defaultSignUpLink); + if ($signUpLink !== $defaultSignUpLink) { + $showSimpleSignup = true; + } - if ($this->appManager->isEnabledForUser('registration')) { - $urlGenerator = \OCP\Server::get(IURLGenerator::class); - $signUpLink = $urlGenerator->getAbsoluteURL('/index.php/apps/registration/'); - } + if ($this->appManager->isEnabledForUser('registration')) { + $urlGenerator = Server::get(IURLGenerator::class); + $signUpLink = $urlGenerator->getAbsoluteURL('/index.php/apps/registration/'); + } - $this->assign('showSimpleSignUpLink', $showSimpleSignup); - $this->assign('signUpLink', $signUpLink); - } else { - parent::__construct('core', 'layout.base'); + $page->assign('showSimpleSignUpLink', $showSimpleSignup); + $page->assign('signUpLink', $signUpLink); + break; + default: + $page = $this->templateManager->getTemplate('core', 'layout.base'); + break; } // Send the language, locale, and direction to our layouts - $lang = \OC::$server->get(IFactory::class)->findLanguage(); - $locale = \OC::$server->get(IFactory::class)->findLocale($lang); - $direction = \OC::$server->getL10NFactory()->getLanguageDirection($lang); + $l10nFactory = Server::get(IFactory::class); + $lang = $l10nFactory->findLanguage(); + $locale = $l10nFactory->findLocale($lang); + $direction = $l10nFactory->getLanguageDirection($lang); $lang = str_replace('_', '-', $lang); - $this->assign('language', $lang); - $this->assign('locale', $locale); - $this->assign('direction', $direction); + $page->assign('language', $lang); + $page->assign('locale', $locale); + $page->assign('direction', $direction); if ($this->config->getSystemValueBool('installed', false)) { if (empty(self::$versionHash)) { @@ -216,7 +215,7 @@ class TemplateLayout extends \OC_Template { // Add the js files // TODO: remove deprecated OC_Util injection $jsFiles = self::findJavascriptFiles(array_merge(\OC_Util::$scripts, Util::getScripts())); - $this->assign('jsfiles', []); + $page->assign('jsfiles', []); if ($this->config->getSystemValueBool('installed', false) && $renderAs != TemplateResponse::RENDER_AS_ERROR) { // this is on purpose outside of the if statement below so that the initial state is prefilled (done in the getConfig() call) // see https://github.com/nextcloud/server/pull/22636 for details @@ -238,26 +237,28 @@ class TemplateLayout extends \OC_Template { ); $config = $jsConfigHelper->getConfig(); if (\OC::$server->getContentSecurityPolicyNonceManager()->browserSupportsCspV3()) { - $this->assign('inline_ocjs', $config); + $page->assign('inline_ocjs', $config); } else { - $this->append('jsfiles', \OC::$server->getURLGenerator()->linkToRoute('core.OCJS.getConfig', ['v' => self::$versionHash])); + $page->append('jsfiles', \OC::$server->getURLGenerator()->linkToRoute('core.OCJS.getConfig', ['v' => self::$versionHash])); } } foreach ($jsFiles as $info) { $web = $info[1]; $file = $info[2]; - $this->append('jsfiles', $web . '/' . $file . $this->getVersionHashSuffix()); + $page->append('jsfiles', $web . '/' . $file . $this->getVersionHashSuffix()); } + $request = \OCP\Server::get(IRequest::class); + try { - $pathInfo = \OC::$server->getRequest()->getPathInfo(); + $pathInfo = $request->getPathInfo(); } catch (\Exception $e) { $pathInfo = ''; } // Do not initialise scss appdata until we have a fully installed instance // Do not load scss for update, errors, installation or login page - if (\OC::$server->getSystemConfig()->getValue('installed', false) + if ($this->config->getSystemValueBool('installed', false) && !\OCP\Util::needUpgrade() && $pathInfo !== '' && !preg_match('/^\/login/', $pathInfo) @@ -267,41 +268,42 @@ class TemplateLayout extends \OC_Template { } else { // If we ignore the scss compiler, // we need to load the guest css fallback - \OC_Util::addStyle('guest'); - $cssFiles = self::findStylesheetFiles(\OC_Util::$styles, false); + Util::addStyle('guest'); + $cssFiles = self::findStylesheetFiles(\OC_Util::$styles); } - $this->assign('cssfiles', []); - $this->assign('printcssfiles', []); + $page->assign('cssfiles', []); + $page->assign('printcssfiles', []); $this->initialState->provideInitialState('core', 'versionHash', self::$versionHash); foreach ($cssFiles as $info) { $web = $info[1]; $file = $info[2]; if (str_ends_with($file, 'print.css')) { - $this->append('printcssfiles', $web . '/' . $file . $this->getVersionHashSuffix()); + $page->append('printcssfiles', $web . '/' . $file . $this->getVersionHashSuffix()); } else { $suffix = $this->getVersionHashSuffix($web, $file); if (!str_contains($file, '?v=')) { - $this->append('cssfiles', $web . '/' . $file . $suffix); + $page->append('cssfiles', $web . '/' . $file . $suffix); } else { - $this->append('cssfiles', $web . '/' . $file . '-' . substr($suffix, 3)); + $page->append('cssfiles', $web . '/' . $file . '-' . substr($suffix, 3)); } } } - $request = \OCP\Server::get(IRequest::class); if ($request->isUserAgent([Request::USER_AGENT_CLIENT_IOS, Request::USER_AGENT_SAFARI, Request::USER_AGENT_SAFARI_MOBILE])) { // Prevent auto zoom with iOS but still allow user zoom // On chrome (and others) this does not work (will also disable user zoom) - $this->assign('viewport_maximum_scale', '1.0'); + $page->assign('viewport_maximum_scale', '1.0'); } - $this->assign('initialStates', $this->initialState->getInitialStates()); + $page->assign('initialStates', $this->initialState->getInitialStates()); + + $page->assign('id-app-content', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-content' : '#content'); + $page->assign('id-app-navigation', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-navigation' : null); - $this->assign('id-app-content', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-content' : '#content'); - $this->assign('id-app-navigation', $renderAs === TemplateResponse::RENDER_AS_USER ? '#app-navigation' : null); + return $page; } protected function getVersionHashSuffix(string $path = '', string $file = ''): string { @@ -339,7 +341,7 @@ class TemplateLayout extends \OC_Template { if (array_key_exists($path, self::$cacheBusterCache) === false) { // Not yet cached, so lets find the cache buster string $appId = $this->getAppNamefromPath($path); - if ($appId === false || $appId === null) { + if ($appId === false) { // No app Id could be guessed return false; } @@ -362,11 +364,7 @@ class TemplateLayout extends \OC_Template { return self::$cacheBusterCache[$path]; } - /** - * @param array $styles - * @return array - */ - public static function findStylesheetFiles($styles, $compileScss = true) { + public static function findStylesheetFiles(array $styles): array { if (!self::$cssLocator) { self::$cssLocator = \OCP\Server::get(CSSResourceLocator::class); } @@ -374,12 +372,8 @@ class TemplateLayout extends \OC_Template { return self::$cssLocator->getResources(); } - /** - * @param string $path - * @return string|false - */ - public function getAppNamefromPath($path) { - if ($path !== '' && is_string($path)) { + public function getAppNamefromPath(string $path): string|false { + if ($path !== '') { $pathParts = explode('/', $path); if ($pathParts[0] === 'css') { // This is a scss request @@ -392,11 +386,7 @@ class TemplateLayout extends \OC_Template { return false; } - /** - * @param array $scripts - * @return array - */ - public static function findJavascriptFiles($scripts) { + public static function findJavascriptFiles(array $scripts): array { if (!self::$jsLocator) { self::$jsLocator = \OCP\Server::get(JSResourceLocator::class); } @@ -410,7 +400,7 @@ class TemplateLayout extends \OC_Template { * @return string Relative path * @throws \Exception If $filePath is not under \OC::$SERVERROOT */ - public static function convertToRelativePath($filePath) { + public static function convertToRelativePath(string $filePath) { $relativePath = explode(\OC::$SERVERROOT, $filePath); if (count($relativePath) !== 2) { throw new \Exception('$filePath is not under the \OC::$SERVERROOT'); diff --git a/lib/private/legacy/OC_Template.php b/lib/private/legacy/OC_Template.php index af363e0a41e..77b25477d31 100644 --- a/lib/private/legacy/OC_Template.php +++ b/lib/private/legacy/OC_Template.php @@ -5,209 +5,27 @@ * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ -use OC\TemplateLayout; -use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; -use OCP\AppFramework\Http\TemplateResponse; -use OCP\EventDispatcher\IEventDispatcher; -use Psr\Log\LoggerInterface; +use OCP\Server; +use OCP\Template\ITemplateManager; require_once __DIR__ . '/template/functions.php'; /** * This class provides the templates for ownCloud. + * @deprecated 32.0.0 Use \OCP\Template\ITemplateManager instead */ -class OC_Template extends \OC\Template\Base { - /** @var string */ - private $renderAs; // Create a full page? - - /** @var string */ - private $path; // The path to the template - - /** @var array */ - private $headers = []; //custom headers - - /** @var string */ - protected $app; // app id - - /** - * Constructor - * - * @param string $app app providing the template - * @param string $name of the template file (without suffix) - * @param string $renderAs If $renderAs is set, OC_Template will try to - * produce a full page in the according layout. For - * now, $renderAs can be set to "guest", "user" or - * "admin". - * @param bool $registerCall = true - */ - public function __construct($app, $name, $renderAs = TemplateResponse::RENDER_AS_BLANK, $registerCall = true) { - $theme = OC_Util::getTheme(); - - $requestToken = (OC::$server->getSession() && $registerCall) ? \OCP\Util::callRegister() : ''; - $cspNonce = \OCP\Server::get(\OC\Security\CSP\ContentSecurityPolicyNonceManager::class)->getNonce(); - - $parts = explode('/', $app); // fix translation when app is something like core/lostpassword - $l10n = \OC::$server->getL10N($parts[0]); - /** @var \OCP\Defaults $themeDefaults */ - $themeDefaults = \OCP\Server::get(\OCP\Defaults::class); - - [$path, $template] = $this->findTemplate($theme, $app, $name); - - // Set the private data - $this->renderAs = $renderAs; - $this->path = $path; - $this->app = $app; - - parent::__construct( - $template, - $requestToken, - $l10n, - $themeDefaults, - $cspNonce, - ); - } - - - /** - * find the template with the given name - * @param string $name of the template file (without suffix) - * - * Will select the template file for the selected theme. - * Checking all the possible locations. - * @param string $theme - * @param string $app - * @return string[] - */ - protected function findTemplate($theme, $app, $name) { - // Check if it is a app template or not. - if ($app !== '') { - $dirs = $this->getAppTemplateDirs($theme, $app, OC::$SERVERROOT, OC_App::getAppPath($app)); - } else { - $dirs = $this->getCoreTemplateDirs($theme, OC::$SERVERROOT); - } - $locator = new \OC\Template\TemplateFileLocator($dirs); - $template = $locator->find($name); - $path = $locator->getPath(); - return [$path, $template]; - } - - /** - * Add a custom element to the header - * @param string $tag tag name of the element - * @param array $attributes array of attributes for the element - * @param string $text the text content for the element. If $text is null then the - * element will be written as empty element. So use "" to get a closing tag. - */ - public function addHeader($tag, $attributes, $text = null) { - $this->headers[] = [ - 'tag' => $tag, - 'attributes' => $attributes, - 'text' => $text - ]; - } - - /** - * Process the template - * @return string - * - * This function process the template. If $this->renderAs is set, it - * will produce a full page. - */ - public function fetchPage($additionalParams = null) { - $data = parent::fetchPage($additionalParams); - - if ($this->renderAs) { - $page = new TemplateLayout($this->renderAs, $this->app); - - if (is_array($additionalParams)) { - foreach ($additionalParams as $key => $value) { - $page->assign($key, $value); - } - } - - // Add custom headers - $headers = ''; - foreach (OC_Util::$headers as $header) { - $headers .= '<' . \OCP\Util::sanitizeHTML($header['tag']); - if (strcasecmp($header['tag'], 'script') === 0 && in_array('src', array_map('strtolower', array_keys($header['attributes'])))) { - $headers .= ' defer'; - } - foreach ($header['attributes'] as $name => $value) { - $headers .= ' ' . \OCP\Util::sanitizeHTML($name) . '="' . \OCP\Util::sanitizeHTML($value) . '"'; - } - if ($header['text'] !== null) { - $headers .= '>' . \OCP\Util::sanitizeHTML($header['text']) . '</' . \OCP\Util::sanitizeHTML($header['tag']) . '>'; - } else { - $headers .= '/>'; - } - } - - $page->assign('headers', $headers); - - $page->assign('content', $data); - return $page->fetchPage($additionalParams); - } - - return $data; - } - - /** - * Include template - * - * @param string $file - * @param array|null $additionalParams - * @return string returns content of included template - * - * Includes another template. use <?php echo $this->inc('template'); ?> to - * do this. - */ - public function inc($file, $additionalParams = null) { - return $this->load($this->path . $file . '.php', $additionalParams); - } - - /** - * Shortcut to print a simple page for users - * @param string $application The application we render the template for - * @param string $name Name of the template - * @param array $parameters Parameters for the template - * @return boolean|null - */ - public static function printUserPage($application, $name, $parameters = []) { - $content = new OC_Template($application, $name, 'user'); - foreach ($parameters as $key => $value) { - $content->assign($key, $value); - } - print $content->printPage(); - } - - /** - * Shortcut to print a simple page for admins - * @param string $application The application we render the template for - * @param string $name Name of the template - * @param array $parameters Parameters for the template - * @return bool - */ - public static function printAdminPage($application, $name, $parameters = []) { - $content = new OC_Template($application, $name, 'admin'); - foreach ($parameters as $key => $value) { - $content->assign($key, $value); - } - return $content->printPage(); - } - +class OC_Template extends \OC\Template\Template { /** * Shortcut to print a simple page for guests * @param string $application The application we render the template for * @param string $name Name of the template - * @param array|string $parameters Parameters for the template + * @param array $parameters Parameters for the template * @return bool + * @deprecated 32.0.0 Use \OCP\Template\ITemplateManager instead */ public static function printGuestPage($application, $name, $parameters = []) { - $content = new OC_Template($application, $name, $name === 'error' ? $name : 'guest'); - foreach ($parameters as $key => $value) { - $content->assign($key, $value); - } - return $content->printPage(); + Server::get(ITemplateManager::class)->printGuestPage($application, $name, $parameters); + return true; } /** @@ -215,123 +33,21 @@ class OC_Template extends \OC\Template\Base { * @param string $error_msg The error message to show * @param string $hint An optional hint message - needs to be properly escape * @param int $statusCode - * @suppress PhanAccessMethodInternal + * @return never + * @deprecated 32.0.0 Use \OCP\Template\ITemplateManager instead */ public static function printErrorPage($error_msg, $hint = '', $statusCode = 500) { - if (\OC::$server->getAppManager()->isEnabledForUser('theming') && !\OC_App::isAppLoaded('theming')) { - \OC_App::loadApp('theming'); - } - - - if ($error_msg === $hint) { - // If the hint is the same as the message there is no need to display it twice. - $hint = ''; - } - $errors = [['error' => $error_msg, 'hint' => $hint]]; - - http_response_code($statusCode); - try { - // Try rendering themed html error page - $response = new TemplateResponse( - '', - 'error', - ['errors' => $errors], - TemplateResponse::RENDER_AS_ERROR, - $statusCode, - ); - $event = new BeforeTemplateRenderedEvent(false, $response); - \OC::$server->get(IEventDispatcher::class)->dispatchTyped($event); - print($response->render()); - } catch (\Throwable $e1) { - $logger = \OCP\Server::get(LoggerInterface::class); - $logger->error('Rendering themed error page failed. Falling back to un-themed error page.', [ - 'app' => 'core', - 'exception' => $e1, - ]); - - try { - // Try rendering unthemed html error page - $content = new \OC_Template('', 'error', 'error', false); - $content->assign('errors', $errors); - $content->printPage(); - } catch (\Exception $e2) { - // If nothing else works, fall back to plain text error page - $logger->error("$error_msg $hint", ['app' => 'core']); - $logger->error('Rendering un-themed error page failed. Falling back to plain text error page.', [ - 'app' => 'core', - 'exception' => $e2, - ]); - - header('Content-Type: text/plain; charset=utf-8'); - print("$error_msg $hint"); - } - } - die(); + Server::get(ITemplateManager::class)->printErrorPage($error_msg, $hint, $statusCode); } /** * print error page using Exception details * @param Exception|Throwable $exception * @param int $statusCode - * @return bool|string - * @suppress PhanAccessMethodInternal + * @return never + * @deprecated 32.0.0 Use \OCP\Template\ITemplateManager instead */ public static function printExceptionErrorPage($exception, $statusCode = 503) { - $debug = false; - http_response_code($statusCode); - try { - $debug = \OC::$server->getSystemConfig()->getValue('debug', false); - $serverLogsDocumentation = \OC::$server->getSystemConfig()->getValue('documentation_url.server_logs', ''); - $request = \OC::$server->getRequest(); - $content = new \OC_Template('', 'exception', 'error', false); - $content->assign('errorClass', get_class($exception)); - $content->assign('errorMsg', $exception->getMessage()); - $content->assign('errorCode', $exception->getCode()); - $content->assign('file', $exception->getFile()); - $content->assign('line', $exception->getLine()); - $content->assign('exception', $exception); - $content->assign('debugMode', $debug); - $content->assign('serverLogsDocumentation', $serverLogsDocumentation); - $content->assign('remoteAddr', $request->getRemoteAddress()); - $content->assign('requestID', $request->getId()); - $content->printPage(); - } catch (\Exception $e) { - try { - $logger = \OCP\Server::get(LoggerInterface::class); - $logger->error($exception->getMessage(), ['app' => 'core', 'exception' => $exception]); - $logger->error($e->getMessage(), ['app' => 'core', 'exception' => $e]); - } catch (Throwable $e) { - // no way to log it properly - but to avoid a white page of death we send some output - self::printPlainErrorPage($e, $debug); - - // and then throw it again to log it at least to the web server error log - throw $e; - } - - self::printPlainErrorPage($e, $debug); - } - die(); - } - - /** - * @psalm-taint-escape has_quotes - * @psalm-taint-escape html - */ - private static function fakeEscapeForPlainText(string $str): string { - return $str; - } - - private static function printPlainErrorPage(\Throwable $exception, bool $debug = false): void { - header('Content-Type: text/plain; charset=utf-8'); - print("Internal Server Error\n\n"); - print("The server encountered an internal error and was unable to complete your request.\n"); - print("Please contact the server administrator if this error reappears multiple times, please include the technical details below in your report.\n"); - print("More details can be found in the server log.\n"); - - if ($debug) { - print("\n"); - print($exception->getMessage() . ' ' . $exception->getFile() . ' at ' . $exception->getLine() . "\n"); - print(self::fakeEscapeForPlainText($exception->getTraceAsString())); - } + Server::get(ITemplateManager::class)->printExceptionErrorPage($exception, $statusCode); } } diff --git a/lib/private/legacy/OC_Util.php b/lib/private/legacy/OC_Util.php index bf60fed0389..36a121094cc 100644 --- a/lib/private/legacy/OC_Util.php +++ b/lib/private/legacy/OC_Util.php @@ -725,11 +725,10 @@ class OC_Util { * string or array of strings before displaying it on a web page. * * @param string|string[] $value - * @return string|string[] an array of sanitized strings or a single sanitized string, depends on the input parameter. + * @return ($value is array ? string[] : string) */ public static function sanitizeHTML($value) { if (is_array($value)) { - /** @var string[] $value */ $value = array_map(function ($value) { return self::sanitizeHTML($value); }, $value); diff --git a/lib/public/AppFramework/Http/TemplateResponse.php b/lib/public/AppFramework/Http/TemplateResponse.php index 55b9f2b06af..af37a1a2313 100644 --- a/lib/public/AppFramework/Http/TemplateResponse.php +++ b/lib/public/AppFramework/Http/TemplateResponse.php @@ -1,13 +1,19 @@ <?php +declare(strict_types=1); + + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OCP\AppFramework\Http; use OCP\AppFramework\Http; +use OCP\Server; +use OCP\Template\ITemplateManager; /** * Response for a normal template @@ -180,7 +186,7 @@ class TemplateResponse extends Response { $renderAs = $this->renderAs; } - $template = new \OCP\Template($this->appName, $this->templateName, $renderAs); + $template = Server::get(ITemplateManager::class)->getTemplate($this->appName, $this->templateName, $renderAs); foreach ($this->params as $key => $value) { $template->assign($key, $value); diff --git a/lib/public/AppFramework/Http/TooManyRequestsResponse.php b/lib/public/AppFramework/Http/TooManyRequestsResponse.php index 6b2ef5b1b90..f7084ec768d 100644 --- a/lib/public/AppFramework/Http/TooManyRequestsResponse.php +++ b/lib/public/AppFramework/Http/TooManyRequestsResponse.php @@ -8,7 +8,8 @@ declare(strict_types=1); namespace OCP\AppFramework\Http; use OCP\AppFramework\Http; -use OCP\Template; +use OCP\Server; +use OCP\Template\ITemplateManager; /** * A generic 429 response showing an 404 error page as well to the end-user @@ -34,7 +35,7 @@ class TooManyRequestsResponse extends Response { * @since 19.0.0 */ public function render() { - $template = new Template('core', '429', 'blank'); + $template = Server::get(ITemplateManager::class)->getTemplate('core', '429', TemplateResponse::RENDER_AS_BLANK); return $template->fetchPage(); } } diff --git a/lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php b/lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php index 84b0a9066f2..32ede4f385c 100644 --- a/lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php +++ b/lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php @@ -8,16 +8,15 @@ declare(strict_types=1); */ namespace OCP\Authentication\TwoFactorAuth; -use OCP\Template; +use OCP\Template\ITemplate; /** * @since 17.0.0 */ interface ILoginSetupProvider { /** - * @return Template - * * @since 17.0.0 + * @since 32.0.0 Broader return type ITemplate instead of \OCP\Template */ - public function getBody(): Template; + public function getBody(): ITemplate; } diff --git a/lib/public/Authentication/TwoFactorAuth/IPersonalProviderSettings.php b/lib/public/Authentication/TwoFactorAuth/IPersonalProviderSettings.php index 610f299c526..3cf7946272e 100644 --- a/lib/public/Authentication/TwoFactorAuth/IPersonalProviderSettings.php +++ b/lib/public/Authentication/TwoFactorAuth/IPersonalProviderSettings.php @@ -6,9 +6,10 @@ declare(strict_types=1); * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCP\Authentication\TwoFactorAuth; -use OCP\Template; +use OCP\Template\ITemplate; /** * Interface IPersonalProviderSettings @@ -17,9 +18,8 @@ use OCP\Template; */ interface IPersonalProviderSettings { /** - * @return Template - * * @since 15.0.0 + * @since 32.0.0 Broader return type ITemplate instead of \OCP\Template */ - public function getBody(): Template; + public function getBody(): ITemplate; } diff --git a/lib/public/Authentication/TwoFactorAuth/IProvider.php b/lib/public/Authentication/TwoFactorAuth/IProvider.php index f1dd24ff0a2..27c4121f4ac 100644 --- a/lib/public/Authentication/TwoFactorAuth/IProvider.php +++ b/lib/public/Authentication/TwoFactorAuth/IProvider.php @@ -9,7 +9,7 @@ declare(strict_types=1); namespace OCP\Authentication\TwoFactorAuth; use OCP\IUser; -use OCP\Template; +use OCP\Template\ITemplate; /** * @since 9.1.0 @@ -50,11 +50,9 @@ interface IProvider { * Get the template for rending the 2FA provider view * * @since 9.1.0 - * - * @param IUser $user - * @return Template + * @since 32.0.0 Broader return type ITemplate instead of \OCP\Template. */ - public function getTemplate(IUser $user): Template; + public function getTemplate(IUser $user): ITemplate; /** * Verify the given challenge diff --git a/lib/public/Template.php b/lib/public/Template.php index 048697ffcc4..d9b3083319b 100644 --- a/lib/public/Template.php +++ b/lib/public/Template.php @@ -12,6 +12,7 @@ namespace OCP; * specific templates, add data and generate the html code * * @since 8.0.0 + * @deprecated 32.0.0 Use \OCP\Template\ITemplateManager instead */ class Template extends \OC_Template { /** diff --git a/lib/public/Template/ITemplate.php b/lib/public/Template/ITemplate.php new file mode 100644 index 00000000000..7131df4d17c --- /dev/null +++ b/lib/public/Template/ITemplate.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\Template; + +/** + * @since 32.0.0 + */ +interface ITemplate { + /** + * Process the template + * @since 32.0.0 + */ + public function fetchPage(?array $additionalParams = null): string; + + /** + * Proceed the template and print its output. + * @since 32.0.0 + */ + public function printPage(): void; + + /** + * Assign variables + * + * This function assigns a variable. It can be accessed via $_[$key] in + * the template. + * + * If the key existed before, it will be overwritten + * @since 32.0.0 + */ + public function assign(string $key, mixed $value): void; + + /** + * Appends a variable + * + * This function assigns a variable in an array context. If the key already + * exists, the value will be appended. It can be accessed via + * $_[$key][$position] in the template. + */ + public function append(string $key, mixed $value): void; +} diff --git a/lib/public/Template/ITemplateManager.php b/lib/public/Template/ITemplateManager.php new file mode 100644 index 00000000000..05549bbddfd --- /dev/null +++ b/lib/public/Template/ITemplateManager.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\Template; + +use OCP\AppFramework\Http\TemplateResponse; + +/** + * @since 32.0.0 + */ +interface ITemplateManager { + /** + * @param TemplateResponse::RENDER_AS_* $renderAs + * @throws TemplateNotFoundException if the template cannot be found + * @since 32.0.0 + */ + public function getTemplate(string $app, string $name, string $renderAs = TemplateResponse::RENDER_AS_BLANK, bool $registerCall = true): ITemplate; + + /** + * Shortcut to print a simple page for guests + * @since 32.0.0 + */ + public function printGuestPage(string $application, string $name, array $parameters = []): void; + + /** + * Print a fatal error page and terminates the script + * @since 32.0.0 + * @param string $error_msg The error message to show + * @param string $hint An optional hint message - needs to be properly escape + */ + public function printErrorPage(string $error_msg, string $hint = '', int $statusCode = 500): never; + + /** + * Print error page using Exception details + * @since 32.0.0 + */ + public function printExceptionErrorPage(\Throwable $exception, int $statusCode = 503): never; +} diff --git a/lib/public/Template/TemplateNotFoundException.php b/lib/public/Template/TemplateNotFoundException.php new file mode 100644 index 00000000000..e77fcd8646a --- /dev/null +++ b/lib/public/Template/TemplateNotFoundException.php @@ -0,0 +1,16 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\Template; + +/** + * @since 32.0.0 + */ +class TemplateNotFoundException extends \Exception { +} diff --git a/lib/public/Util.php b/lib/public/Util.php index 28da91c9a0f..65776bd15a7 100644 --- a/lib/public/Util.php +++ b/lib/public/Util.php @@ -107,7 +107,7 @@ class Util { * @param string $file * @since 4.0.0 */ - public static function addStyle($application, $file = null) { + public static function addStyle($application, $file = null): void { \OC_Util::addStyle($application, $file); } @@ -384,7 +384,7 @@ class Util { /** * Cached encrypted CSRF token. Some static unit-tests of ownCloud compare - * multiple OC_Template elements which invoke `callRegister`. If the value + * multiple Template elements which invoke `callRegister`. If the value * would not be cached these unit-tests would fail. * @var string */ @@ -393,6 +393,7 @@ class Util { /** * Register an get/post call. This is important to prevent CSRF attacks * @since 4.5.0 + * @deprecated 32.0.0 directly use CsrfTokenManager instead */ public static function callRegister() { if (self::$token === '') { @@ -408,7 +409,7 @@ class Util { * string or array of strings before displaying it on a web page. * * @param string|string[] $value - * @return string|string[] an array of sanitized strings or a single sanitized string, depends on the input parameter. + * @return ($value is array ? string[] : string) an array of sanitized strings or a single sanitized string, depends on the input parameter. * @since 4.5.0 */ public static function sanitizeHTML($value) { diff --git a/public.php b/public.php index 9443367eb12..682d5cb2538 100644 --- a/public.php +++ b/public.php @@ -14,6 +14,7 @@ use OCP\App\IAppManager; use OCP\IConfig; use OCP\IRequest; use OCP\Server; +use OCP\Template\ITemplateManager; use OCP\Util; use Psr\Log\LoggerInterface; @@ -92,9 +93,9 @@ try { } //show the user a detailed error page Server::get(LoggerInterface::class)->error($ex->getMessage(), ['app' => 'public', 'exception' => $ex]); - OC_Template::printExceptionErrorPage($ex, $status); + Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, $status); } catch (Error $ex) { //show the user a detailed error page Server::get(LoggerInterface::class)->error($ex->getMessage(), ['app' => 'public', 'exception' => $ex]); - OC_Template::printExceptionErrorPage($ex, 500); + Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500); } diff --git a/remote.php b/remote.php index f63fd49354a..7058e7aceb4 100644 --- a/remote.php +++ b/remote.php @@ -9,6 +9,8 @@ require_once __DIR__ . '/lib/versioncheck.php'; use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; use OCP\App\IAppManager; +use OCP\IRequest; +use OCP\Template\ITemplateManager; use Psr\Log\LoggerInterface; use Sabre\DAV\Exception\ServiceUnavailable; use Sabre\DAV\Server; @@ -23,7 +25,7 @@ class RemoteException extends \Exception { function handleException(Exception|Error $e): void { try { - $request = \OC::$server->getRequest(); + $request = \OCP\Server::get(IRequest::class); // in case the request content type is text/xml - we assume it's a WebDAV request $isXmlContentType = strpos($request->getHeader('Content-Type'), 'text/xml'); if ($isXmlContentType === 0) { @@ -31,7 +33,7 @@ function handleException(Exception|Error $e): void { $server = new Server(); if (!($e instanceof RemoteException)) { // we shall not log on RemoteException - $server->addPlugin(new ExceptionLoggerPlugin('webdav', \OC::$server->get(LoggerInterface::class))); + $server->addPlugin(new ExceptionLoggerPlugin('webdav', \OCP\Server::get(LoggerInterface::class))); } $server->on('beforeMethod:*', function () use ($e) { if ($e instanceof RemoteException) { @@ -54,19 +56,19 @@ function handleException(Exception|Error $e): void { } if ($e instanceof RemoteException) { // we shall not log on RemoteException - OC_Template::printErrorPage($e->getMessage(), '', $e->getCode()); + \OCP\Server::get(ITemplateManager::class)->printErrorPage($e->getMessage(), '', $e->getCode()); } else { - \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['app' => 'remote','exception' => $e]); - OC_Template::printExceptionErrorPage($e, $statusCode); + \OCP\Server::get(LoggerInterface::class)->error($e->getMessage(), ['app' => 'remote','exception' => $e]); + \OCP\Server::get(ITemplateManager::class)->printExceptionErrorPage($e, $statusCode); } } } catch (\Exception $e) { - OC_Template::printExceptionErrorPage($e, 500); + \OCP\Server::get(ITemplateManager::class)->printExceptionErrorPage($e, 500); } } /** - * @param $service + * @param string $service * @return string */ function resolveService($service) { diff --git a/tests/Core/Controller/TwoFactorChallengeControllerTest.php b/tests/Core/Controller/TwoFactorChallengeControllerTest.php index 11e18bd622a..7498abb9a9e 100644 --- a/tests/Core/Controller/TwoFactorChallengeControllerTest.php +++ b/tests/Core/Controller/TwoFactorChallengeControllerTest.php @@ -22,7 +22,7 @@ use OCP\ISession; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; -use OCP\Template; +use OCP\Template\ITemplate; use Psr\Log\LoggerInterface; use Test\TestCase; @@ -115,7 +115,7 @@ class TwoFactorChallengeControllerTest extends TestCase { $provider->method('getId')->willReturn('myprovider'); $backupProvider = $this->createMock(IProvider::class); $backupProvider->method('getId')->willReturn('backup_codes'); - $tmpl = $this->createMock(Template::class); + $tmpl = $this->createMock(ITemplate::class); $providerSet = new ProviderSet([$provider, $backupProvider], true); $this->userSession->expects($this->once()) @@ -399,7 +399,7 @@ class TwoFactorChallengeControllerTest extends TestCase { ->method('getLoginSetup') ->with($user) ->willReturn($loginSetup); - $tmpl = $this->createMock(Template::class); + $tmpl = $this->createMock(ITemplate::class); $loginSetup->expects($this->once()) ->method('getBody') ->willReturn($tmpl); diff --git a/tests/lib/TemplateLayoutTest.php b/tests/lib/TemplateLayoutTest.php index 405f1df7330..29e31b3f391 100644 --- a/tests/lib/TemplateLayoutTest.php +++ b/tests/lib/TemplateLayoutTest.php @@ -14,50 +14,69 @@ use OC\TemplateLayout; use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\IConfig; +use OCP\INavigationManager; +use OCP\Template\ITemplateManager; +use PHPUnit\Framework\MockObject\MockObject; class TemplateLayoutTest extends \Test\TestCase { - + private IConfig&MockObject $config; + private IAppManager&MockObject $appManager; + private InitialStateService&MockObject $initialState; + private INavigationManager&MockObject $navigationManager; + private ITemplateManager&MockObject $templateManager; + + private TemplateLayout $templateLayout; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->initialState = $this->createMock(InitialStateService::class); + $this->navigationManager = $this->createMock(INavigationManager::class); + $this->templateManager = $this->createMock(ITemplateManager::class); + } /** @dataProvider dataVersionHash */ public function testVersionHash($path, $file, $installed, $debug, $expected): void { - $appManager = $this->createMock(IAppManager::class); - $appManager->expects(self::any()) + $this->appManager->expects(self::any()) ->method('getAppVersion') ->willReturnCallback(fn ($appId) => match ($appId) { 'shippedApp' => 'shipped_1', 'otherApp' => 'other_2', default => "$appId", }); - $appManager->expects(self::any()) + $this->appManager->expects(self::any()) ->method('isShipped') ->willReturnCallback(fn (string $app) => $app === 'shippedApp'); - $config = $this->createMock(IConfig::class); - $config->expects(self::atLeastOnce()) + $this->config->expects(self::atLeastOnce()) ->method('getSystemValueBool') ->willReturnMap([ ['installed', false, $installed], ['debug', false, $debug], ]); - $config->expects(self::any()) + $this->config->expects(self::any()) ->method('getAppValue') ->with('theming', 'cachebuster', '0') ->willReturn('42'); - $initialState = $this->createMock(InitialStateService::class); - - $this->overwriteService(IConfig::class, $config); - $this->overwriteService(IAppManager::class, $appManager); - $this->overwriteService(InitialStateService::class, $initialState); - - $layout = $this->getMockBuilder(TemplateLayout::class) + $this->templateLayout = $this->getMockBuilder(TemplateLayout::class) ->onlyMethods(['getAppNamefromPath']) - ->setConstructorArgs([TemplateResponse::RENDER_AS_ERROR]) + ->setConstructorArgs([ + $this->config, + $this->appManager, + $this->initialState, + $this->navigationManager, + $this->templateManager, + ]) ->getMock(); + $layout = $this->templateLayout->getPageTemplate(TemplateResponse::RENDER_AS_ERROR, ''); + self::invokePrivate(TemplateLayout::class, 'versionHash', ['version_hash']); - $layout->expects(self::any()) + $this->templateLayout->expects(self::any()) ->method('getAppNamefromPath') ->willReturnCallback(fn ($appName) => match($appName) { 'apps/shipped' => 'shippedApp', @@ -65,7 +84,7 @@ class TemplateLayoutTest extends \Test\TestCase { default => false, }); - $hash = self::invokePrivate($layout, 'getVersionHashSuffix', [$path, $file]); + $hash = self::invokePrivate($this->templateLayout, 'getVersionHashSuffix', [$path, $file]); self::assertEquals($expected, $hash); } |