diff options
Diffstat (limited to 'lib/private/Template')
-rw-r--r-- | lib/private/Template/Base.php | 38 | ||||
-rw-r--r-- | lib/private/Template/Template.php | 161 | ||||
-rw-r--r-- | lib/private/Template/TemplateFileLocator.php | 29 | ||||
-rw-r--r-- | lib/private/Template/TemplateManager.php | 169 |
4 files changed, 351 insertions, 46 deletions
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())); + } + } +} |