diff options
Diffstat (limited to 'lib/private/Template')
-rw-r--r-- | lib/private/Template/Base.php | 92 | ||||
-rw-r--r-- | lib/private/Template/CSSResourceLocator.php | 92 | ||||
-rw-r--r-- | lib/private/Template/IconsCacher.php | 263 | ||||
-rw-r--r-- | lib/private/Template/JSCombiner.php | 37 | ||||
-rw-r--r-- | lib/private/Template/JSConfigHelper.php | 244 | ||||
-rw-r--r-- | lib/private/Template/JSResourceLocator.php | 150 | ||||
-rwxr-xr-x | lib/private/Template/ResourceLocator.php | 51 | ||||
-rw-r--r-- | lib/private/Template/ResourceNotFoundException.php | 23 | ||||
-rw-r--r-- | lib/private/Template/SCSSCacher.php | 528 | ||||
-rw-r--r-- | lib/private/Template/Template.php | 159 | ||||
-rw-r--r-- | lib/private/Template/TemplateFileLocator.php | 57 | ||||
-rw-r--r-- | lib/private/Template/TemplateManager.php | 169 | ||||
-rw-r--r-- | lib/private/Template/functions.php | 299 |
13 files changed, 891 insertions, 1273 deletions
diff --git a/lib/private/Template/Base.php b/lib/private/Template/Base.php index 2de8c7ad5b1..a13e6703960 100644 --- a/lib/private/Template/Base.php +++ b/lib/private/Template/Base.php @@ -1,40 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * 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 OCP\Defaults; -use Throwable; class Base { private $template; // The template - private $vars; // Vars + private array $vars = []; /** @var \OCP\IL10N */ private $l10n; @@ -46,11 +23,14 @@ class Base { * @param string $template * @param string $requestToken * @param \OCP\IL10N $l10n + * @param string $cspNonce * @param Defaults $theme */ - public function __construct($template, $requestToken, $l10n, $theme) { - $this->vars = []; - $this->vars['requesttoken'] = $requestToken; + public function __construct($template, $requestToken, $l10n, $theme, $cspNonce) { + $this->vars = [ + 'cspNonce' => $cspNonce, + 'requesttoken' => $requestToken, + ]; $this->l10n = $l10n; $this->template = $template; $this->theme = $theme; @@ -65,56 +45,48 @@ class Base { */ protected function getAppTemplateDirs($theme, $app, $serverRoot, $app_dir) { // Check if the app is in the app folder or in the root - if ($app_dir !== false && file_exists($app_dir.'/templates/')) { + if ($app_dir !== false && file_exists($app_dir . '/templates/')) { return [ - $serverRoot.'/themes/'.$theme.'/apps/'.$app.'/templates/', - $app_dir.'/templates/', + $serverRoot . '/themes/' . $theme . '/apps/' . $app . '/templates/', + $app_dir . '/templates/', ]; } return [ - $serverRoot.'/themes/'.$theme.'/'.$app.'/templates/', - $serverRoot.'/'.$app.'/templates/', + $serverRoot . '/themes/' . $theme . '/' . $app . '/templates/', + $serverRoot . '/' . $app . '/templates/', ]; } /** - * @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/', + $serverRoot . '/themes/' . $theme . '/core/templates/', + $serverRoot . '/core/templates/', ]; } /** * Assign variables - * @param string $key key - * @param 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 { @@ -124,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; @@ -177,6 +136,7 @@ class Base { // Include ob_start(); try { + require_once __DIR__ . '/functions.php'; include $file; $data = ob_get_contents(); } catch (\Exception $e) { diff --git a/lib/private/Template/CSSResourceLocator.php b/lib/private/Template/CSSResourceLocator.php index a038ba7ce9b..b501fd69874 100644 --- a/lib/private/Template/CSSResourceLocator.php +++ b/lib/private/Template/CSSResourceLocator.php @@ -1,53 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Axel Helmert <axel.helmert@luka.de> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Kyle Fazzari <kyrofa@ubuntu.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author tux-rampage <tux-rampage@users.noreply.github.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * 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 Psr\Log\LoggerInterface; class CSSResourceLocator extends ResourceLocator { - - /** @var SCSSCacher */ - protected $scssCacher; - - /** - * @param string $theme - * @param array $core_map - * @param array $party_map - * @param SCSSCacher $scssCacher - */ - public function __construct(LoggerInterface $logger, $theme, $core_map, $party_map, $scssCacher) { - $this->scssCacher = $scssCacher; - - parent::__construct($logger, $theme, $core_map, $party_map); + public function __construct(LoggerInterface $logger) { + parent::__construct($logger); } /** @@ -55,12 +19,8 @@ class CSSResourceLocator extends ResourceLocator { */ public function doFind($style) { $app = substr($style, 0, strpos($style, '/')); - if (strpos($style, '3rdparty') === 0 - && $this->appendIfExist($this->thirdpartyroot, $style.'.css') - || $this->cacheAndAppendScssIfExist($this->serverroot, $style.'.scss', $app) - || $this->cacheAndAppendScssIfExist($this->serverroot, 'core/'.$style.'.scss') - || $this->appendIfExist($this->serverroot, $style.'.css') - || $this->appendIfExist($this->serverroot, 'core/'.$style.'.css') + if ($this->appendIfExist($this->serverroot, $style . '.css') + || $this->appendIfExist($this->serverroot, 'core/' . $style . '.css') ) { return; } @@ -81,43 +41,17 @@ class CSSResourceLocator extends ResourceLocator { // turned into cwd. $app_path = realpath($app_path); - if (!$this->cacheAndAppendScssIfExist($app_path, $style.'.scss', $app)) { - $this->append($app_path, $style.'.css', $app_url); - } + $this->append($app_path, $style . '.css', $app_url); } /** * @param string $style */ public function doFindTheme($style) { - $theme_dir = 'themes/'.$this->theme.'/'; - $this->appendIfExist($this->serverroot, $theme_dir.'apps/'.$style.'.css') - || $this->appendIfExist($this->serverroot, $theme_dir.$style.'.css') - || $this->appendIfExist($this->serverroot, $theme_dir.'core/'.$style.'.css'); - } - - /** - * cache and append the scss $file if exist at $root - * - * @param string $root path to check - * @param string $file the filename - * @return bool True if the resource was found and cached, false otherwise - */ - protected function cacheAndAppendScssIfExist($root, $file, $app = 'core') { - if (is_file($root.'/'.$file)) { - if ($this->scssCacher !== null) { - if ($this->scssCacher->process($root, $file, $app)) { - $this->append($this->serverroot, $this->scssCacher->getCachedSCSS($app, $file), \OC::$WEBROOT, true, true); - return true; - } else { - $this->logger->warning('Failed to compile and/or save '.$root.'/'.$file, ['app' => 'core']); - return false; - } - } else { - return true; - } - } - return false; + $theme_dir = 'themes/' . $this->theme . '/'; + $this->appendIfExist($this->serverroot, $theme_dir . 'apps/' . $style . '.css') + || $this->appendIfExist($this->serverroot, $theme_dir . $style . '.css') + || $this->appendIfExist($this->serverroot, $theme_dir . 'core/' . $style . '.css'); } public function append($root, $file, $webRoot = null, $throw = true, $scss = false) { diff --git a/lib/private/Template/IconsCacher.php b/lib/private/Template/IconsCacher.php deleted file mode 100644 index c2956d5712a..00000000000 --- a/lib/private/Template/IconsCacher.php +++ /dev/null @@ -1,263 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv@protonmail.com) - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OC\Template; - -use OC\Files\AppData\Factory; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\Files\IAppData; -use OCP\Files\NotFoundException; -use OCP\Files\SimpleFS\ISimpleFile; -use OCP\Files\SimpleFS\ISimpleFolder; -use OCP\IURLGenerator; -use Psr\Log\LoggerInterface; - -class IconsCacher { - protected LoggerInterface $logger; - - /** @var IAppData */ - protected $appData; - - /** @var ISimpleFolder */ - private $folder; - - /** @var IURLGenerator */ - protected $urlGenerator; - - /** @var ITimeFactory */ - protected $timeFactory; - - /** @var string */ - private $iconVarRE = '/--(icon-[a-zA-Z0-9-]+):\s?url\(["\']?([a-zA-Z0-9-_\~\/\.\?\&\=\:\;\+\,]+)[^;]+;/m'; - - /** @var string */ - private $fileName = 'icons-vars.css'; - - private $iconList = 'icons-list.template'; - - private $cachedCss; - private $cachedList; - - /** - * @throws \OCP\Files\NotPermittedException - */ - public function __construct(LoggerInterface $logger, - Factory $appDataFactory, - IURLGenerator $urlGenerator, - ITimeFactory $timeFactory) { - $this->logger = $logger; - $this->appData = $appDataFactory->get('css'); - $this->urlGenerator = $urlGenerator; - $this->timeFactory = $timeFactory; - - try { - $this->folder = $this->appData->getFolder('icons'); - } catch (NotFoundException $e) { - $this->folder = $this->appData->newFolder('icons'); - } - } - - private function getIconsFromCss(string $css): array { - preg_match_all($this->iconVarRE, $css, $matches, PREG_SET_ORDER); - $icons = []; - foreach ($matches as $icon) { - $icons[$icon[1]] = $icon[2]; - } - - return $icons; - } - - /** - * @param string $css - * @return string - * @throws NotFoundException - * @throws \OCP\Files\NotPermittedException - */ - public function setIconsCss(string $css): string { - $cachedFile = $this->getCachedList(); - if (!$cachedFile) { - $currentData = ''; - $cachedFile = $this->folder->newFile($this->iconList); - } else { - $currentData = $cachedFile->getContent(); - } - - $cachedVarsCssFile = $this->getCachedCSS(); - if (!$cachedVarsCssFile) { - $cachedVarsCssFile = $this->folder->newFile($this->fileName); - } - - $icons = $this->getIconsFromCss($currentData . $css); - - $data = ''; - $list = ''; - foreach ($icons as $icon => $url) { - $list .= "--$icon: url('$url');"; - [$location,$color] = $this->parseUrl($url); - $svg = false; - if ($location !== '' && \file_exists($location)) { - $svg = \file_get_contents($location); - } - if ($svg === false) { - $this->logger->debug('Failed to get icon file ' . $location); - $data .= "--$icon: url('$url');"; - continue; - } - $encode = base64_encode($this->colorizeSvg($svg, $color)); - $data .= '--' . $icon . ': url(data:image/svg+xml;base64,' . $encode . ');'; - } - - if (\strlen($data) > 0 && \strlen($list) > 0) { - $data = ":root {\n$data\n}"; - $cachedVarsCssFile->putContent($data); - $list = ":root {\n$list\n}"; - $cachedFile->putContent($list); - $this->cachedList = null; - $this->cachedCss = null; - } - - return preg_replace($this->iconVarRE, '', $css); - } - - /** - * @param $url - * @return array - */ - private function parseUrl($url): array { - $location = ''; - $color = ''; - $base = $this->getRoutePrefix() . '/svg/'; - $cleanUrl = \substr($url, \strlen($base)); - if (\strpos($url, $base . 'core') === 0) { - $cleanUrl = \substr($cleanUrl, \strlen('core')); - if (\preg_match('/\/([a-zA-Z0-9-_\~\/\.\=\:\;\+\,]+)\?color=([0-9a-fA-F]{3,6})/', $cleanUrl, $matches)) { - [,$cleanUrl,$color] = $matches; - $location = \OC::$SERVERROOT . '/core/img/' . $cleanUrl . '.svg'; - } - } elseif (\strpos($url, $base) === 0) { - if (\preg_match('/([A-z0-9\_\-]+)\/([a-zA-Z0-9-_\~\/\.\=\:\;\+\,]+)\?color=([0-9a-fA-F]{3,6})/', $cleanUrl, $matches)) { - [,$app,$cleanUrl, $color] = $matches; - $appPath = \OC_App::getAppPath($app); - if ($appPath !== false) { - $location = $appPath . '/img/' . $cleanUrl . '.svg'; - } - if ($app === 'settings') { - $location = \OC::$SERVERROOT . '/settings/img/' . $cleanUrl . '.svg'; - } - } - } - return [ - $location, - $color - ]; - } - - /** - * @param $svg - * @param $color - * @return string - */ - public function colorizeSvg($svg, $color): string { - if (!preg_match('/^[0-9a-f]{3,6}$/i', $color)) { - // Prevent not-sane colors from being written into the SVG - $color = '000'; - } - - // add fill (fill is not present on black elements) - $fillRe = '/<((circle|rect|path)((?!fill)[a-z0-9 =".\-#():;,])+)\/>/mi'; - $svg = preg_replace($fillRe, '<$1 fill="#' . $color . '"/>', $svg); - - // replace any fill or stroke colors - $svg = preg_replace('/stroke="#([a-z0-9]{3,6})"/mi', 'stroke="#' . $color . '"', $svg); - $svg = preg_replace('/fill="#([a-z0-9]{3,6})"/mi', 'fill="#' . $color . '"', $svg); - return $svg; - } - - private function getRoutePrefix() { - $frontControllerActive = (\OC::$server->getConfig()->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true'); - $prefix = \OC::$WEBROOT . '/index.php'; - if ($frontControllerActive) { - $prefix = \OC::$WEBROOT; - } - return $prefix; - } - - /** - * Get icons css file - * @return ISimpleFile|boolean - */ - public function getCachedCSS() { - try { - if (!$this->cachedCss) { - $this->cachedCss = $this->folder->getFile($this->fileName); - } - return $this->cachedCss; - } catch (NotFoundException $e) { - return false; - } - } - - /** - * Get icon-vars list template - * @return ISimpleFile|boolean - */ - public function getCachedList() { - try { - if (!$this->cachedList) { - $this->cachedList = $this->folder->getFile($this->iconList); - } - return $this->cachedList; - } catch (NotFoundException $e) { - return false; - } - } - - /** - * Add the icons cache css into the header - */ - public function injectCss() { - $mtime = $this->timeFactory->getTime(); - $file = $this->getCachedList(); - if ($file) { - $mtime = $file->getMTime(); - } - // Only inject once - foreach (\OC_Util::$headers as $header) { - if ( - array_key_exists('attributes', $header) && - array_key_exists('href', $header['attributes']) && - strpos($header['attributes']['href'], $this->fileName) !== false) { - return; - } - } - $linkToCSS = $this->urlGenerator->linkToRoute('core.Css.getCss', ['appName' => 'icons', 'fileName' => $this->fileName, 'v' => $mtime]); - \OC_Util::addHeader('link', ['rel' => 'stylesheet', 'href' => $linkToCSS], null, true); - } -} diff --git a/lib/private/Template/JSCombiner.php b/lib/private/Template/JSCombiner.php index a6d9f0ee558..a94f822a448 100644 --- a/lib/private/Template/JSCombiner.php +++ b/lib/private/Template/JSCombiner.php @@ -1,28 +1,8 @@ <?php + /** - * @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Template; @@ -37,7 +17,6 @@ use OCP\IURLGenerator; use Psr\Log\LoggerInterface; class JSCombiner { - /** @var IAppData */ protected $appData; @@ -56,10 +35,10 @@ class JSCombiner { private $cacheFactory; public function __construct(IAppData $appData, - IURLGenerator $urlGenerator, - ICacheFactory $cacheFactory, - SystemConfig $config, - LoggerInterface $logger) { + IURLGenerator $urlGenerator, + ICacheFactory $cacheFactory, + SystemConfig $config, + LoggerInterface $logger) { $this->appData = $appData; $this->urlGenerator = $urlGenerator; $this->cacheFactory = $cacheFactory; @@ -198,7 +177,7 @@ class JSCombiner { $gzipFile->putContent(gzencode($res, 9)); $this->logger->debug('JSCombiner: successfully cached: ' . $fileName); return true; - } catch (NotPermittedException $e) { + } catch (NotPermittedException|NotFoundException $e) { $this->logger->error('JSCombiner: unable to cache: ' . $fileName); return false; } diff --git a/lib/private/Template/JSConfigHelper.php b/lib/private/Template/JSConfigHelper.php index 58f3106bafd..044fa8147a0 100644 --- a/lib/private/Template/JSConfigHelper.php +++ b/lib/private/Template/JSConfigHelper.php @@ -1,134 +1,80 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Abijeet <abijeetpatro@gmail.com> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Template; use bantu\IniGetWrapper\IniGetWrapper; +use OC\Authentication\Token\IProvider; use OC\CapabilitiesManager; +use OC\Core\AppInfo\ConfigLexicon; +use OC\Files\FilenameValidator; +use OC\Share\Share; +use OCA\Provisioning_API\Controller\AUserDataOCSController; +use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; +use OCP\Authentication\Exceptions\ExpiredTokenException; +use OCP\Authentication\Exceptions\InvalidTokenException; +use OCP\Authentication\Exceptions\WipeTokenException; +use OCP\Authentication\Token\IToken; use OCP\Constants; use OCP\Defaults; +use OCP\Files\FileInfo; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IGroupManager; use OCP\IInitialStateService; use OCP\IL10N; +use OCP\ILogger; use OCP\ISession; use OCP\IURLGenerator; use OCP\IUser; +use OCP\Server; +use OCP\ServerVersion; +use OCP\Session\Exceptions\SessionNotAvailableException; +use OCP\Share\IManager as IShareManager; use OCP\User\Backend\IPasswordConfirmationBackend; +use OCP\Util; class JSConfigHelper { - /** @var IL10N */ - private $l; - - /** @var Defaults */ - private $defaults; - - /** @var IAppManager */ - private $appManager; - - /** @var ISession */ - private $session; - - /** @var IUser|null */ - private $currentUser; - - /** @var IConfig */ - private $config; - - /** @var IGroupManager */ - private $groupManager; - - /** @var IniGetWrapper */ - private $iniWrapper; - - /** @var IURLGenerator */ - private $urlGenerator; - - /** @var CapabilitiesManager */ - private $capabilitiesManager; - - /** @var IInitialStateService */ - private $initialStateService; - /** @var array user back-ends excluded from password verification */ private $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true]; - /** - * @param IL10N $l - * @param Defaults $defaults - * @param IAppManager $appManager - * @param ISession $session - * @param IUser|null $currentUser - * @param IConfig $config - * @param IGroupManager $groupManager - * @param IniGetWrapper $iniWrapper - * @param IURLGenerator $urlGenerator - * @param CapabilitiesManager $capabilitiesManager - */ - public function __construct(IL10N $l, - Defaults $defaults, - IAppManager $appManager, - ISession $session, - $currentUser, - IConfig $config, - IGroupManager $groupManager, - IniGetWrapper $iniWrapper, - IURLGenerator $urlGenerator, - CapabilitiesManager $capabilitiesManager, - IInitialStateService $initialStateService) { - $this->l = $l; - $this->defaults = $defaults; - $this->appManager = $appManager; - $this->session = $session; - $this->currentUser = $currentUser; - $this->config = $config; - $this->groupManager = $groupManager; - $this->iniWrapper = $iniWrapper; - $this->urlGenerator = $urlGenerator; - $this->capabilitiesManager = $capabilitiesManager; - $this->initialStateService = $initialStateService; + public function __construct( + protected ServerVersion $serverVersion, + protected IL10N $l, + protected Defaults $defaults, + protected IAppManager $appManager, + protected ISession $session, + protected ?IUser $currentUser, + protected IConfig $config, + protected readonly IAppConfig $appConfig, + protected IGroupManager $groupManager, + protected IniGetWrapper $iniWrapper, + protected IURLGenerator $urlGenerator, + protected CapabilitiesManager $capabilitiesManager, + protected IInitialStateService $initialStateService, + protected IProvider $tokenProvider, + protected FilenameValidator $filenameValidator, + ) { } - public function getConfig() { + public function getConfig(): string { $userBackendAllowsPasswordConfirmation = true; if ($this->currentUser !== null) { $uid = $this->currentUser->getUID(); $backend = $this->currentUser->getBackend(); if ($backend instanceof IPasswordConfirmationBackend) { - $userBackendAllowsPasswordConfirmation = $backend->canConfirmPassword($uid); + $userBackendAllowsPasswordConfirmation = $backend->canConfirmPassword($uid) && $this->canUserValidatePassword(); } elseif (isset($this->excludedUserBackEnds[$this->currentUser->getBackendClassName()])) { $userBackendAllowsPasswordConfirmation = false; + } else { + $userBackendAllowsPasswordConfirmation = $this->canUserValidatePassword(); } } else { $uid = null; @@ -138,18 +84,20 @@ class JSConfigHelper { $apps_paths = []; if ($this->currentUser === null) { - $apps = $this->appManager->getInstalledApps(); + $apps = $this->appManager->getEnabledApps(); } else { $apps = $this->appManager->getEnabledAppsForUser($this->currentUser); } foreach ($apps as $app) { - $apps_paths[$app] = \OC_App::getAppWebPath($app); + try { + $apps_paths[$app] = $this->appManager->getAppWebPath($app); + } catch (AppPathNotFoundException $e) { + $apps_paths[$app] = false; + } } - - $enableLinkPasswordByDefault = $this->config->getAppValue('core', 'shareapi_enable_link_password_by_default', 'no'); - $enableLinkPasswordByDefault = $enableLinkPasswordByDefault === 'yes'; + $enableLinkPasswordByDefault = $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_PASSWORD_DEFAULT); $defaultExpireDateEnabled = $this->config->getAppValue('core', 'shareapi_default_expire_date', 'no') === 'yes'; $defaultExpireDate = $enforceDefaultExpireDate = null; if ($defaultExpireDateEnabled) { @@ -179,9 +127,13 @@ class JSConfigHelper { } if ($this->currentUser instanceof IUser) { - $lastConfirmTimestamp = $this->session->get('last-password-confirm'); - if (!is_int($lastConfirmTimestamp)) { - $lastConfirmTimestamp = 0; + if ($this->canUserValidatePassword()) { + $lastConfirmTimestamp = $this->session->get('last-password-confirm'); + if (!is_int($lastConfirmTimestamp)) { + $lastConfirmTimestamp = 0; + } + } else { + $lastConfirmTimestamp = PHP_INT_MAX; } } else { $lastConfirmTimestamp = 0; @@ -189,31 +141,44 @@ class JSConfigHelper { $capabilities = $this->capabilitiesManager->getCapabilities(false, true); + $userFirstDay = $this->config->getUserValue($uid, 'core', AUserDataOCSController::USER_FIELD_FIRST_DAY_OF_WEEK, null); + $firstDay = (int)($userFirstDay ?? $this->l->l('firstday', null)); + $config = [ - 'session_lifetime' => min($this->config->getSystemValue('session_lifetime', $this->iniWrapper->getNumeric('session.gc_maxlifetime')), $this->iniWrapper->getNumeric('session.gc_maxlifetime')), - 'session_keepalive' => $this->config->getSystemValue('session_keepalive', true), + /** @deprecated 30.0.0 - use files capabilities instead */ + 'blacklist_files_regex' => FileInfo::BLACKLIST_FILES_REGEX, + /** @deprecated 30.0.0 - use files capabilities instead */ + 'forbidden_filename_characters' => $this->filenameValidator->getForbiddenCharacters(), + 'auto_logout' => $this->config->getSystemValue('auto_logout', false), - 'version' => implode('.', \OCP\Util::getVersion()), - 'versionstring' => \OC_Util::getVersionString(), - 'enable_avatars' => true, // here for legacy reasons - to not crash existing code that relies on this value + 'loglevel' => $this->config->getSystemValue('loglevel_frontend', + $this->config->getSystemValue('loglevel', ILogger::WARN) + ), 'lost_password_link' => $this->config->getSystemValue('lost_password_link', null), 'modRewriteWorking' => $this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true', + 'no_unsupported_browser_warning' => $this->config->getSystemValue('no_unsupported_browser_warning', false), + 'session_keepalive' => $this->config->getSystemValue('session_keepalive', true), + 'session_lifetime' => min($this->config->getSystemValue('session_lifetime', $this->iniWrapper->getNumeric('session.gc_maxlifetime')), $this->iniWrapper->getNumeric('session.gc_maxlifetime')), 'sharing.maxAutocompleteResults' => max(0, $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT)), 'sharing.minSearchStringLength' => $this->config->getSystemValueInt('sharing.minSearchStringLength', 0), - 'blacklist_files_regex' => \OCP\Files\FileInfo::BLACKLIST_FILES_REGEX, + 'version' => implode('.', $this->serverVersion->getVersion()), + 'versionstring' => $this->serverVersion->getVersionString(), + 'enable_non-accessible_features' => $this->config->getSystemValueBool('enable_non-accessible_features', true), ]; + $shareManager = Server::get(IShareManager::class); + $array = [ - "_oc_debug" => $this->config->getSystemValue('debug', false) ? 'true' : 'false', - "_oc_isadmin" => $uid !== null && $this->groupManager->isAdmin($uid) ? 'true' : 'false', - "backendAllowsPasswordConfirmation" => $userBackendAllowsPasswordConfirmation ? 'true' : 'false', - "oc_dataURL" => is_string($dataLocation) ? "\"" . $dataLocation . "\"" : 'false', - "_oc_webroot" => "\"" . \OC::$WEBROOT . "\"", - "_oc_appswebroots" => str_replace('\\/', '/', json_encode($apps_paths)), // Ugly unescape slashes waiting for better solution - "datepickerFormatDate" => json_encode($this->l->l('jsdate', null)), + '_oc_debug' => $this->config->getSystemValue('debug', false) ? 'true' : 'false', + '_oc_isadmin' => $uid !== null && $this->groupManager->isAdmin($uid) ? 'true' : 'false', + 'backendAllowsPasswordConfirmation' => $userBackendAllowsPasswordConfirmation ? 'true' : 'false', + 'oc_dataURL' => is_string($dataLocation) ? '"' . $dataLocation . '"' : 'false', + '_oc_webroot' => '"' . \OC::$WEBROOT . '"', + '_oc_appswebroots' => str_replace('\\/', '/', json_encode($apps_paths)), // Ugly unescape slashes waiting for better solution + 'datepickerFormatDate' => json_encode($this->l->l('jsdate', null)), 'nc_lastLogin' => $lastConfirmTimestamp, 'nc_pageLoad' => time(), - "dayNames" => json_encode([ + 'dayNames' => json_encode([ $this->l->t('Sunday'), $this->l->t('Monday'), $this->l->t('Tuesday'), @@ -222,7 +187,7 @@ class JSConfigHelper { $this->l->t('Friday'), $this->l->t('Saturday') ]), - "dayNamesShort" => json_encode([ + 'dayNamesShort' => json_encode([ $this->l->t('Sun.'), $this->l->t('Mon.'), $this->l->t('Tue.'), @@ -231,7 +196,7 @@ class JSConfigHelper { $this->l->t('Fri.'), $this->l->t('Sat.') ]), - "dayNamesMin" => json_encode([ + 'dayNamesMin' => json_encode([ $this->l->t('Su'), $this->l->t('Mo'), $this->l->t('Tu'), @@ -240,7 +205,7 @@ class JSConfigHelper { $this->l->t('Fr'), $this->l->t('Sa') ]), - "monthNames" => json_encode([ + 'monthNames' => json_encode([ $this->l->t('January'), $this->l->t('February'), $this->l->t('March'), @@ -254,7 +219,7 @@ class JSConfigHelper { $this->l->t('November'), $this->l->t('December') ]), - "monthNamesShort" => json_encode([ + 'monthNamesShort' => json_encode([ $this->l->t('Jan.'), $this->l->t('Feb.'), $this->l->t('Mar.'), @@ -268,20 +233,20 @@ class JSConfigHelper { $this->l->t('Nov.'), $this->l->t('Dec.') ]), - "firstDay" => json_encode($this->l->l('firstday', null)), - "_oc_config" => json_encode($config), - "oc_appconfig" => json_encode([ + 'firstDay' => json_encode($firstDay), + '_oc_config' => json_encode($config), + 'oc_appconfig' => json_encode([ 'core' => [ 'defaultExpireDateEnabled' => $defaultExpireDateEnabled, 'defaultExpireDate' => $defaultExpireDate, 'defaultExpireDateEnforced' => $enforceDefaultExpireDate, - 'enforcePasswordForPublicLink' => \OCP\Util::isPublicLinkPasswordRequired(), + 'enforcePasswordForPublicLink' => Util::isPublicLinkPasswordRequired(), 'enableLinkPasswordByDefault' => $enableLinkPasswordByDefault, - 'sharingDisabledForUser' => \OCP\Util::isSharingDisabledForUser(), - 'resharingAllowed' => \OC\Share\Share::isResharingAllowed(), + 'sharingDisabledForUser' => $shareManager->sharingDisabledForUser($uid), + 'resharingAllowed' => Share::isResharingAllowed(), 'remoteShareAllowed' => $outgoingServer2serverShareEnabled, 'federatedCloudShareDoc' => $this->urlGenerator->linkToDocs('user-sharing-federated'), - 'allowGroupSharing' => \OC::$server->getShareManager()->allowGroupSharing(), + 'allowGroupSharing' => $shareManager->allowGroupSharing(), 'defaultInternalExpireDateEnabled' => $defaultInternalExpireDateEnabled, 'defaultInternalExpireDate' => $defaultInternalExpireDate, 'defaultInternalExpireDateEnforced' => $defaultInternalExpireDateEnforced, @@ -290,7 +255,7 @@ class JSConfigHelper { 'defaultRemoteExpireDateEnforced' => $defaultRemoteExpireDateEnforced, ] ]), - "_theme" => json_encode([ + '_theme' => json_encode([ 'entity' => $this->defaults->getEntity(), 'name' => $this->defaults->getName(), 'productName' => $this->defaults->getProductName(), @@ -314,6 +279,8 @@ class JSConfigHelper { ]); } + $this->initialStateService->provideInitialState('core', 'projects_enabled', $this->config->getSystemValueBool('projects.enabled', false)); + $this->initialStateService->provideInitialState('core', 'config', $config); $this->initialStateService->provideInitialState('core', 'capabilities', $capabilities); @@ -323,10 +290,21 @@ class JSConfigHelper { $result = ''; // Echo it - foreach ($array as $setting => $value) { - $result .= 'var '. $setting . '='. $value . ';' . PHP_EOL; + foreach ($array as $setting => $value) { + $result .= 'var ' . $setting . '=' . $value . ';' . PHP_EOL; } return $result; } + + protected function canUserValidatePassword(): bool { + try { + $token = $this->tokenProvider->getToken($this->session->getId()); + } catch (ExpiredTokenException|WipeTokenException|InvalidTokenException|SessionNotAvailableException) { + // actually we do not know, so we fall back to this statement + return true; + } + $scope = $token->getScopeAsArray(); + return !isset($scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION]) || $scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION] === false; + } } diff --git a/lib/private/Template/JSResourceLocator.php b/lib/private/Template/JSResourceLocator.php index 95ae0d3d832..a6d2d13a2ad 100644 --- a/lib/private/Template/JSResourceLocator.php +++ b/lib/private/Template/JSResourceLocator.php @@ -1,115 +1,93 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Kyle Fazzari <kyrofa@ubuntu.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * 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 OCP\App\AppPathNotFoundException; +use OCP\App\IAppManager; use Psr\Log\LoggerInterface; class JSResourceLocator extends ResourceLocator { + protected JSCombiner $jsCombiner; + protected IAppManager $appManager; - /** @var JSCombiner */ - protected $jsCombiner; - - public function __construct(LoggerInterface $logger, $theme, array $core_map, array $party_map, JSCombiner $JSCombiner) { - parent::__construct($logger, $theme, $core_map, $party_map); + public function __construct(LoggerInterface $logger, JSCombiner $JSCombiner, IAppManager $appManager) { + parent::__construct($logger); $this->jsCombiner = $JSCombiner; + $this->appManager = $appManager; } /** * @param string $script */ public function doFind($script) { - $theme_dir = 'themes/'.$this->theme.'/'; - if (strpos($script, '3rdparty') === 0 - && $this->appendIfExist($this->thirdpartyroot, $script.'.js')) { - return; - } + $theme_dir = 'themes/' . $this->theme . '/'; // Extracting the appId and the script file name $app = substr($script, 0, strpos($script, '/')); $scriptName = basename($script); + // Get the app root path + $appRoot = $this->serverroot . '/apps/'; + $appWebRoot = null; + try { + // We need the dir name as getAppPath appends the appid + $appRoot = dirname($this->appManager->getAppPath($app)); + // Only do this if $app_path is set, because an empty argument to realpath gets turned into cwd. + if ($appRoot) { + // Handle symlinks + $appRoot = realpath($appRoot); + } + // Get the app webroot + $appWebRoot = dirname($this->appManager->getAppWebPath($app)); + } catch (AppPathNotFoundException $e) { + // ignore + } - if (strpos($script, '/l10n/') !== false) { + if (str_contains($script, '/l10n/')) { // For language files we try to load them all, so themes can overwrite // single l10n strings without having to translate all of them. $found = 0; - $found += $this->appendIfExist($this->serverroot, 'core/'.$script.'.js'); - $found += $this->appendIfExist($this->serverroot, $theme_dir.'core/'.$script.'.js'); - $found += $this->appendIfExist($this->serverroot, $script.'.js'); - $found += $this->appendIfExist($this->serverroot, $theme_dir.$script.'.js'); - $found += $this->appendIfExist($this->serverroot, 'apps/'.$script.'.js'); - $found += $this->appendIfExist($this->serverroot, $theme_dir.'apps/'.$script.'.js'); + $found += $this->appendScriptIfExist($this->serverroot, 'core/' . $script); + $found += $this->appendScriptIfExist($this->serverroot, $theme_dir . 'core/' . $script); + $found += $this->appendScriptIfExist($this->serverroot, $script); + $found += $this->appendScriptIfExist($this->serverroot, $theme_dir . $script); + $found += $this->appendScriptIfExist($appRoot, $script, $appWebRoot); + $found += $this->appendScriptIfExist($this->serverroot, $theme_dir . 'apps/' . $script); if ($found) { return; } - } elseif ($this->appendIfExist($this->serverroot, $theme_dir.'apps/'.$script.'.js') - || $this->appendIfExist($this->serverroot, $theme_dir.$script.'.js') - || $this->appendIfExist($this->serverroot, $script.'.js') - || $this->appendIfExist($this->serverroot, "dist/$app-$scriptName.js") - || $this->appendIfExist($this->serverroot, 'apps/'.$script.'.js') - || $this->cacheAndAppendCombineJsonIfExist($this->serverroot, $script.'.json') - || $this->appendIfExist($this->serverroot, $theme_dir.'core/'.$script.'.js') - || $this->appendIfExist($this->serverroot, 'core/'.$script.'.js') - || (strpos($scriptName, '/') === -1 && $this->appendIfExist($this->serverroot, "dist/core-$scriptName.js")) - || $this->cacheAndAppendCombineJsonIfExist($this->serverroot, 'core/'.$script.'.json') + } elseif ($this->appendScriptIfExist($this->serverroot, $theme_dir . 'apps/' . $script) + || $this->appendScriptIfExist($this->serverroot, $theme_dir . $script) + || $this->appendScriptIfExist($this->serverroot, $script) + || $this->appendScriptIfExist($this->serverroot, $theme_dir . "dist/$app-$scriptName") + || $this->appendScriptIfExist($this->serverroot, "dist/$app-$scriptName") + || $this->appendScriptIfExist($appRoot, $script, $appWebRoot) + || $this->cacheAndAppendCombineJsonIfExist($this->serverroot, $script . '.json') + || $this->cacheAndAppendCombineJsonIfExist($appRoot, $script . '.json', $app) + || $this->appendScriptIfExist($this->serverroot, $theme_dir . 'core/' . $script) + || $this->appendScriptIfExist($this->serverroot, 'core/' . $script) + || (strpos($scriptName, '/') === -1 && ($this->appendScriptIfExist($this->serverroot, $theme_dir . "dist/core-$scriptName") + || $this->appendScriptIfExist($this->serverroot, "dist/core-$scriptName"))) + || $this->cacheAndAppendCombineJsonIfExist($this->serverroot, 'core/' . $script . '.json') ) { return; } - $script = substr($script, strpos($script, '/') + 1); - $app_path = \OC_App::getAppPath($app); - $app_url = \OC_App::getAppWebPath($app); - - if ($app_path !== false) { - // Account for the possibility of having symlinks in app path. Only - // do this if $app_path is set, because an empty argument to realpath - // gets turned into cwd. - $app_path = realpath($app_path); - } - - // missing translations files fill be ignored - if (strpos($script, 'l10n/') === 0) { - $this->appendIfExist($app_path, $script . '.js', $app_url); + // missing translations files will be ignored + if (str_contains($script, '/l10n/')) { return; } - if ($app_path === false && $app_url === false) { - $this->logger->error('Could not find resource {resource} to load', [ - 'resource' => $app . '/' . $script . '.js', - 'app' => 'jsresourceloader', - ]); - return; - } - - if (!$this->cacheAndAppendCombineJsonIfExist($app_path, $script.'.json', $app)) { - $this->append($app_path, $script . '.js', $app_url); - } + $this->logger->error('Could not find resource {resource} to load', [ + 'resource' => $script . '.js', + 'app' => 'jsresourceloader', + ]); } /** @@ -118,14 +96,30 @@ class JSResourceLocator extends ResourceLocator { public function doFindTheme($script) { } + /** + * Try to find ES6 script file (`.mjs`) with fallback to plain javascript (`.js`) + * @see appendIfExist() + */ + protected function appendScriptIfExist(string $root, string $file, ?string $webRoot = null) { + if (!$this->appendIfExist($root, $file . '.mjs', $webRoot)) { + return $this->appendIfExist($root, $file . '.js', $webRoot); + } + return true; + } + protected function cacheAndAppendCombineJsonIfExist($root, $file, $app = 'core') { - if (is_file($root.'/'.$file)) { + if (is_file($root . '/' . $file)) { if ($this->jsCombiner->process($root, $file, $app)) { $this->append($this->serverroot, $this->jsCombiner->getCachedJS($app, $file), false, false); } else { // Add all the files from the json $files = $this->jsCombiner->getContent($root, $file); - $app_url = \OC_App::getAppWebPath($app); + $app_url = null; + try { + $app_url = $this->appManager->getAppWebPath($app); + } catch (AppPathNotFoundException) { + // pass + } foreach ($files as $jsFile) { $this->append($root, $jsFile, $app_url); diff --git a/lib/private/Template/ResourceLocator.php b/lib/private/Template/ResourceLocator.php index 5a50cc6fd1b..fa52f8e5c0d 100755 --- a/lib/private/Template/ResourceLocator.php +++ b/lib/private/Template/ResourceLocator.php @@ -1,31 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author tux-rampage <tux-rampage@users.noreply.github.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Template; @@ -36,25 +14,20 @@ abstract class ResourceLocator { protected $mapping; protected $serverroot; - protected $thirdpartyroot; protected $webroot; protected $resources = []; protected LoggerInterface $logger; - /** - * @param string $theme - * @param array $core_map - * @param array $party_map - */ - public function __construct(LoggerInterface $logger, $theme, $core_map, $party_map) { + public function __construct(LoggerInterface $logger) { $this->logger = $logger; - $this->theme = $theme; - $this->mapping = $core_map + $party_map; - $this->serverroot = key($core_map); - $this->thirdpartyroot = key($party_map); - $this->webroot = $this->mapping[$this->serverroot]; + $this->mapping = [ + \OC::$SERVERROOT => \OC::$WEBROOT + ]; + $this->serverroot = \OC::$SERVERROOT; + $this->webroot = \OC::$WEBROOT; + $this->theme = \OC_Util::getTheme(); } /** @@ -102,7 +75,7 @@ abstract class ResourceLocator { * @return bool True if the resource was found, false otherwise */ protected function appendIfExist($root, $file, $webRoot = null) { - if ($root !== false && is_file($root.'/'.$file)) { + if ($root !== false && is_file($root . '/' . $file)) { $this->append($root, $file, $webRoot, false); return true; } diff --git a/lib/private/Template/ResourceNotFoundException.php b/lib/private/Template/ResourceNotFoundException.php index c8ed33f569c..e51dfb5cb89 100644 --- a/lib/private/Template/ResourceNotFoundException.php +++ b/lib/private/Template/ResourceNotFoundException.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Template; diff --git a/lib/private/Template/SCSSCacher.php b/lib/private/Template/SCSSCacher.php deleted file mode 100644 index a552122b358..00000000000 --- a/lib/private/Template/SCSSCacher.php +++ /dev/null @@ -1,528 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, John Molakvoæ (skjnldsv@protonmail.com) - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Haertl <jus@bitgrid.net> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Roland Tapken <roland@bitarbeiter.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OC\Template; - -use OC\AppConfig; -use OC\Files\AppData\Factory; -use OC\Memcache\NullCache; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\Files\IAppData; -use OCP\Files\NotFoundException; -use OCP\Files\NotPermittedException; -use OCP\Files\SimpleFS\ISimpleFile; -use OCP\Files\SimpleFS\ISimpleFolder; -use OCP\ICache; -use OCP\ICacheFactory; -use OCP\IConfig; -use OCP\IMemcache; -use OCP\IURLGenerator; -use Psr\Log\LoggerInterface; -use ScssPhp\ScssPhp\Compiler; -use ScssPhp\ScssPhp\OutputStyle; - -class SCSSCacher { - protected LoggerInterface $logger; - - /** @var IAppData */ - protected $appData; - - /** @var IURLGenerator */ - protected $urlGenerator; - - /** @var IConfig */ - protected $config; - - /** @var \OC_Defaults */ - private $defaults; - - /** @var string */ - protected $serverRoot; - - /** @var ICache */ - protected $depsCache; - - /** @var null|string */ - private $injectedVariables; - - /** @var ICacheFactory */ - private $cacheFactory; - - /** @var IconsCacher */ - private $iconsCacher; - - /** @var ICache */ - private $isCachedCache; - - /** @var ITimeFactory */ - private $timeFactory; - - /** @var IMemcache */ - private $lockingCache; - /** @var AppConfig */ - private $appConfig; - - /** - * @param string $serverRoot - */ - public function __construct(LoggerInterface $logger, - Factory $appDataFactory, - IURLGenerator $urlGenerator, - IConfig $config, - \OC_Defaults $defaults, - $serverRoot, - ICacheFactory $cacheFactory, - IconsCacher $iconsCacher, - ITimeFactory $timeFactory, - AppConfig $appConfig) { - $this->logger = $logger; - $this->appData = $appDataFactory->get('css'); - $this->urlGenerator = $urlGenerator; - $this->config = $config; - $this->defaults = $defaults; - $this->serverRoot = $serverRoot; - $this->cacheFactory = $cacheFactory; - $this->depsCache = $cacheFactory->createDistributed('SCSS-deps-' . md5($this->urlGenerator->getBaseUrl())); - $this->isCachedCache = $cacheFactory->createDistributed('SCSS-cached-' . md5($this->urlGenerator->getBaseUrl())); - $lockingCache = $cacheFactory->createDistributed('SCSS-locks-' . md5($this->urlGenerator->getBaseUrl())); - if (!($lockingCache instanceof IMemcache)) { - $lockingCache = new NullCache(); - } - $this->lockingCache = $lockingCache; - $this->iconsCacher = $iconsCacher; - $this->timeFactory = $timeFactory; - $this->appConfig = $appConfig; - } - - /** - * Process the caching process if needed - * - * @param string $root Root path to the nextcloud installation - * @param string $file - * @param string $app The app name - * @return boolean - * @throws NotPermittedException - */ - public function process(string $root, string $file, string $app): bool { - $path = explode('/', $root . '/' . $file); - - $fileNameSCSS = array_pop($path); - $fileNameCSS = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileNameSCSS)), $app); - - $path = implode('/', $path); - $webDir = $this->getWebDir($path, $app, $this->serverRoot, \OC::$WEBROOT); - - $this->logger->debug('SCSSCacher::process ordinary check follows', ['app' => 'scss_cacher']); - if (!$this->variablesChanged() && $this->isCached($fileNameCSS, $app)) { - // Inject icons vars css if any - return $this->injectCssVariablesIfAny(); - } - - try { - $folder = $this->appData->getFolder($app); - } catch (NotFoundException $e) { - // creating css appdata folder - $folder = $this->appData->newFolder($app); - } - - $lockKey = $webDir . '/' . $fileNameSCSS; - - if (!$this->lockingCache->add($lockKey, 'locked!', 120)) { - $this->logger->debug('SCSSCacher::process could not get lock for ' . $lockKey . ' and will wait 10 seconds for cached file to be available', ['app' => 'scss_cacher']); - $retry = 0; - sleep(1); - while ($retry < 10) { - $this->appConfig->clearCachedConfig(); - $this->logger->debug('SCSSCacher::process check in while loop follows', ['app' => 'scss_cacher']); - if (!$this->variablesChanged() && $this->isCached($fileNameCSS, $app)) { - // Inject icons vars css if any - $this->logger->debug("SCSSCacher::process cached file for app '$app' and file '$fileNameCSS' is now available after $retry s. Moving on...", ['app' => 'scss_cacher']); - return $this->injectCssVariablesIfAny(); - } - sleep(1); - $retry++; - } - $this->logger->debug('SCSSCacher::process Giving up scss caching for ' . $lockKey, ['app' => 'scss_cacher']); - return false; - } - - $this->logger->debug('SCSSCacher::process Lock acquired for ' . $lockKey, ['app' => 'scss_cacher']); - try { - $cached = $this->cache($path, $fileNameCSS, $fileNameSCSS, $folder, $webDir); - } catch (\Exception $e) { - $this->lockingCache->remove($lockKey); - throw $e; - } - - // Cleaning lock - $this->lockingCache->remove($lockKey); - $this->logger->debug('SCSSCacher::process Lock removed for ' . $lockKey, ['app' => 'scss_cacher']); - - // Inject icons vars css if any - if ($this->iconsCacher->getCachedCSS() && $this->iconsCacher->getCachedCSS()->getSize() > 0) { - $this->iconsCacher->injectCss(); - } - - return $cached; - } - - /** - * @param $appName - * @param $fileName - * @return ISimpleFile - */ - public function getCachedCSS(string $appName, string $fileName): ISimpleFile { - $folder = $this->appData->getFolder($appName); - $cachedFileName = $this->prependVersionPrefix($this->prependBaseurlPrefix($fileName), $appName); - - return $folder->getFile($cachedFileName); - } - - /** - * Check if the file is cached or not - * @param string $fileNameCSS - * @param string $app - * @return boolean - */ - private function isCached(string $fileNameCSS, string $app) { - $key = $this->config->getSystemValue('version') . '/' . $app . '/' . $fileNameCSS; - - // If the file mtime is more recent than our cached one, - // let's consider the file is properly cached - if ($cacheValue = $this->isCachedCache->get($key)) { - if ($cacheValue > $this->timeFactory->getTime()) { - return true; - } - } - $this->logger->debug("SCSSCacher::isCached $fileNameCSS isCachedCache is expired or unset", ['app' => 'scss_cacher']); - - // Creating file cache if none for further checks - try { - $folder = $this->appData->getFolder($app); - } catch (NotFoundException $e) { - $this->logger->debug("SCSSCacher::isCached app data folder for $app could not be fetched", ['app' => 'scss_cacher']); - return false; - } - - // Checking if file size is coherent - // and if one of the css dependency changed - try { - $cachedFile = $folder->getFile($fileNameCSS); - if ($cachedFile->getSize() > 0) { - $depFileName = $fileNameCSS . '.deps'; - $deps = $this->depsCache->get($folder->getName() . '-' . $depFileName); - if ($deps === null) { - $depFile = $folder->getFile($depFileName); - $deps = $depFile->getContent(); - // Set to memcache for next run - $this->depsCache->set($folder->getName() . '-' . $depFileName, $deps); - } - $deps = json_decode($deps, true); - - foreach ((array) $deps as $file => $mtime) { - if (!file_exists($file) || filemtime($file) > $mtime) { - $this->logger->debug("SCSSCacher::isCached $fileNameCSS is not considered as cached due to deps file $file", ['app' => 'scss_cacher']); - return false; - } - } - - $this->logger->debug("SCSSCacher::isCached $fileNameCSS dependencies successfully cached for 5 minutes", ['app' => 'scss_cacher']); - // It would probably make sense to adjust this timeout to something higher and see if that has some effect then - $this->isCachedCache->set($key, $this->timeFactory->getTime() + 5 * 60); - return true; - } - $this->logger->debug("SCSSCacher::isCached $fileNameCSS is not considered as cached cacheValue: $cacheValue", ['app' => 'scss_cacher']); - return false; - } catch (NotFoundException $e) { - $this->logger->debug("SCSSCacher::isCached NotFoundException " . $e->getMessage(), ['app' => 'scss_cacher']); - return false; - } - } - - /** - * Check if the variables file has changed - * @return bool - */ - private function variablesChanged(): bool { - $cachedVariables = $this->config->getAppValue('core', 'theming.variables', ''); - $injectedVariables = $this->getInjectedVariables($cachedVariables); - if ($cachedVariables !== md5($injectedVariables)) { - $this->logger->debug('SCSSCacher::variablesChanged storedVariables: ' . json_encode($this->config->getAppValue('core', 'theming.variables')) . ' currentInjectedVariables: ' . json_encode($injectedVariables), ['app' => 'scss_cacher']); - $this->config->setAppValue('core', 'theming.variables', md5($injectedVariables)); - $this->resetCache(); - return true; - } - return false; - } - - /** - * Cache the file with AppData - * - * @param string $path - * @param string $fileNameCSS - * @param string $fileNameSCSS - * @param ISimpleFolder $folder - * @param string $webDir - * @return boolean - * @throws NotPermittedException - */ - private function cache(string $path, string $fileNameCSS, string $fileNameSCSS, ISimpleFolder $folder, string $webDir) { - $scss = new Compiler(); - $scss->setImportPaths([ - $path, - $this->serverRoot . '/core/css/' - ]); - - // Continue after throw - if ($this->config->getSystemValue('debug')) { - // Debug mode - $scss->setOutputStyle(OutputStyle::EXPANDED); - } else { - // Compression - $scss->setOutputStyle(OutputStyle::COMPRESSED); - } - - try { - $cachedfile = $folder->getFile($fileNameCSS); - } catch (NotFoundException $e) { - $cachedfile = $folder->newFile($fileNameCSS); - } - - $depFileName = $fileNameCSS . '.deps'; - try { - $depFile = $folder->getFile($depFileName); - } catch (NotFoundException $e) { - $depFile = $folder->newFile($depFileName); - } - - // Compile - try { - $compiledScss = $scss->compile( - '$webroot: \'' . $this->getRoutePrefix() . '\';' . - $this->getInjectedVariables() . - '@import "variables.scss";' . - '@import "functions.scss";' . - '@import "' . $fileNameSCSS . '";'); - } catch (\Exception $e) { - $this->logger->error($e->getMessage(), ['app' => 'scss_cacher', 'exception' => $e]); - - return false; - } - - // Parse Icons and create related css variables - $compiledScss = $this->iconsCacher->setIconsCss($compiledScss); - - // Gzip file - try { - $gzipFile = $folder->getFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz - } catch (NotFoundException $e) { - $gzipFile = $folder->newFile($fileNameCSS . '.gzip'); # Safari doesn't like .gz - } - - try { - $data = $this->rebaseUrls($compiledScss, $webDir); - $cachedfile->putContent($data); - $deps = json_encode($scss->getParsedFiles()); - $depFile->putContent($deps); - $this->depsCache->set($folder->getName() . '-' . $depFileName, $deps); - $gzipFile->putContent(gzencode($data, 9)); - $this->logger->debug('SCSSCacher::cache ' . $webDir . '/' . $fileNameSCSS . ' compiled and successfully cached', ['app' => 'scss_cacher']); - - return true; - } catch (NotPermittedException $e) { - $this->logger->error('SCSSCacher::cache unable to cache: ' . $fileNameSCSS, ['app' => 'scss_cacher']); - - return false; - } - } - - /** - * Reset scss cache by deleting all generated css files - * We need to regenerate all files when variables change - */ - public function resetCache() { - $this->logger->debug('SCSSCacher::resetCache', ['app' => 'scss_cacher']); - if (!$this->lockingCache->add('resetCache', 'locked!', 120)) { - $this->logger->debug('SCSSCacher::resetCache Locked', ['app' => 'scss_cacher']); - return; - } - $this->logger->debug('SCSSCacher::resetCache Lock acquired', ['app' => 'scss_cacher']); - $this->injectedVariables = null; - - // do not clear locks - $this->depsCache->clear(); - $this->isCachedCache->clear(); - - $appDirectory = $this->appData->getDirectoryListing(); - foreach ($appDirectory as $folder) { - foreach ($folder->getDirectoryListing() as $file) { - try { - $file->delete(); - } catch (NotPermittedException $e) { - $this->logger->error('SCSSCacher::resetCache unable to delete file: ' . $file->getName(), ['exception' => $e, 'app' => 'scss_cacher']); - } - } - } - $this->logger->debug('SCSSCacher::resetCache css cache cleared!', ['app' => 'scss_cacher']); - $this->lockingCache->remove('resetCache'); - $this->logger->debug('SCSSCacher::resetCache Locking removed', ['app' => 'scss_cacher']); - } - - /** - * @return string SCSS code for variables from OC_Defaults - */ - private function getInjectedVariables(string $cache = ''): string { - if ($this->injectedVariables !== null) { - return $this->injectedVariables; - } - $variables = ''; - foreach ($this->defaults->getScssVariables() as $key => $value) { - $variables .= '$' . $key . ': ' . $value . ' !default;'; - } - - /* - * If we are trying to return the same variables as that are cached - * Then there is no need to do the compile step - */ - if ($cache === md5($variables)) { - $this->injectedVariables = $variables; - return $variables; - } - - // check for valid variables / otherwise fall back to defaults - try { - $scss = new Compiler(); - $scss->compile($variables); - $this->injectedVariables = $variables; - } catch (\Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'scss_cacher']); - } - - return $variables; - } - - /** - * Add the correct uri prefix to make uri valid again - * @param string $css - * @param string $webDir - * @return string - */ - private function rebaseUrls(string $css, string $webDir): string { - $re = '/url\([\'"]([^\/][\.\w?=\/-]*)[\'"]\)/x'; - $subst = 'url(\'' . $webDir . '/$1\')'; - - return preg_replace($re, $subst, $css); - } - - /** - * Return the cached css file uri - * @param string $appName the app name - * @param string $fileName - * @return string - */ - public function getCachedSCSS(string $appName, string $fileName): string { - $tmpfileLoc = explode('/', $fileName); - $fileName = array_pop($tmpfileLoc); - $fileName = $this->prependVersionPrefix($this->prependBaseurlPrefix(str_replace('.scss', '.css', $fileName)), $appName); - - return substr($this->urlGenerator->linkToRoute('core.Css.getCss', [ - 'fileName' => $fileName, - 'appName' => $appName, - 'v' => $this->config->getAppValue('core', 'theming.variables', '0') - ]), \strlen(\OC::$WEBROOT) + 1); - } - - /** - * Prepend hashed base url to the css file - * @param string $cssFile - * @return string - */ - private function prependBaseurlPrefix(string $cssFile): string { - return substr(md5($this->urlGenerator->getBaseUrl() . $this->getRoutePrefix()), 0, 4) . '-' . $cssFile; - } - - private function getRoutePrefix() { - $frontControllerActive = ($this->config->getSystemValue('htaccess.IgnoreFrontController', false) === true || getenv('front_controller_active') === 'true'); - $prefix = \OC::$WEBROOT . '/index.php'; - if ($frontControllerActive) { - $prefix = \OC::$WEBROOT; - } - return $prefix; - } - - /** - * Prepend hashed app version hash - * @param string $cssFile - * @param string $appId - * @return string - */ - private function prependVersionPrefix(string $cssFile, string $appId): string { - $appVersion = \OC_App::getAppVersion($appId); - if ($appVersion !== '0') { - return substr(md5($appVersion), 0, 4) . '-' . $cssFile; - } - $coreVersion = \OC_Util::getVersionString(); - - return substr(md5($coreVersion), 0, 4) . '-' . $cssFile; - } - - /** - * Get WebDir root - * @param string $path the css file path - * @param string $appName the app name - * @param string $serverRoot the server root path - * @param string $webRoot the nextcloud installation root path - * @return string the webDir - */ - private function getWebDir(string $path, string $appName, string $serverRoot, string $webRoot): string { - // Detect if path is within server root AND if path is within an app path - if (strpos($path, $serverRoot) === false && $appWebPath = \OC_App::getAppWebPath($appName)) { - // Get the file path within the app directory - $appDirectoryPath = explode($appName, $path)[1]; - // Remove the webroot - - return str_replace($webRoot, '', $appWebPath . $appDirectoryPath); - } - - return $webRoot . substr($path, strlen($serverRoot)); - } - - /** - * Add the icons css cache in the header if needed - * - * @return boolean true - */ - private function injectCssVariablesIfAny() { - // Inject icons vars css if any - if ($this->iconsCacher->getCachedCSS() && $this->iconsCacher->getCachedCSS()->getSize() > 0) { - $this->iconsCacher->injectCss(); - } - return true; - } -} diff --git a/lib/private/Template/Template.php b/lib/private/Template/Template.php new file mode 100644 index 00000000000..ee85562091f --- /dev/null +++ b/lib/private/Template/Template.php @@ -0,0 +1,159 @@ +<?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; + +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 027144e2f43..11a568b5b21 100644 --- a/lib/private/Template/TemplateFileLocator.php +++ b/lib/private/Template/TemplateFileLocator.php @@ -1,62 +1,41 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * 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'); } foreach ($this->dirs as $dir) { - $file = $dir.$template.'.php'; + $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/Template/functions.php b/lib/private/Template/functions.php new file mode 100644 index 00000000000..402a7491e03 --- /dev/null +++ b/lib/private/Template/functions.php @@ -0,0 +1,299 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +use OC\Security\CSP\ContentSecurityPolicyNonceManager; +use OCP\Files\IMimeTypeDetector; +use OCP\IDateTimeFormatter; +use OCP\IURLGenerator; +use OCP\Server; +use OCP\Util; + +/** + * @param string $string + */ +function p($string): void { + print(Util::sanitizeHTML($string)); +} + +/** + * Prints a <link> tag for loading css + * @param string $href the source URL, ignored when empty + * @param string $opts, additional optional options + */ +function emit_css_tag($href, $opts = ''): void { + $s = '<link rel="stylesheet"'; + if (!empty($href)) { + $s .= ' href="' . $href . '"'; + } + if (!empty($opts)) { + $s .= ' ' . $opts; + } + print_unescaped($s . ">\n"); +} + +/** + * Prints all tags for CSS loading + * @param array $obj all the script information from template + */ +function emit_css_loading_tags($obj): void { + foreach ($obj['cssfiles'] as $css) { + emit_css_tag($css); + } + foreach ($obj['printcssfiles'] as $css) { + emit_css_tag($css, 'media="print"'); + } +} + +/** + * Prints a <script> tag with nonce and defer depending on config + * @param string $src the source URL, ignored when empty + * @param string $script_content the inline script content, ignored when empty + * @param string $content_type the type of the source (e.g. 'module') + */ +function emit_script_tag(string $src, string $script_content = '', string $content_type = ''): void { + $nonceManager = Server::get(ContentSecurityPolicyNonceManager::class); + + $defer_str = ' defer'; + $type = $content_type !== '' ? ' type="' . $content_type . '"' : ''; + + $s = '<script nonce="' . $nonceManager->getNonce() . '"'; + if (!empty($src)) { + // emit script tag for deferred loading from $src + $s .= $defer_str . ' src="' . $src . '"' . $type . '>'; + } elseif ($script_content !== '') { + // emit script tag for inline script from $script_content without defer (see MDN) + $s .= ">\n" . $script_content . "\n"; + } else { + // no $src nor $src_content, really useless empty tag + $s .= '>'; + } + $s .= '</script>'; + print_unescaped($s . "\n"); +} + +/** + * Print all <script> tags for loading JS + * @param array $obj all the script information from template + */ +function emit_script_loading_tags($obj): void { + foreach ($obj['jsfiles'] as $jsfile) { + $fileName = explode('?', $jsfile, 2)[0]; + $type = str_ends_with($fileName, '.mjs') ? 'module' : ''; + emit_script_tag($jsfile, '', $type); + } + if (!empty($obj['inline_ocjs'])) { + emit_script_tag('', $obj['inline_ocjs']); + } +} + +/** + * Prints an unsanitized string - usage of this function may result into XSS. + * Consider using p() instead. + * @param string $string the string which will be printed as it is + */ +function print_unescaped($string): void { + print($string); +} + +/** + * Shortcut for adding scripts to a page + * All scripts are forced to be loaded after core since + * they are coming from a template registration. + * Please consider moving them into the relevant controller + * + * @deprecated 24.0.0 - Use \OCP\Util::addScript + * + * @param string $app the appname + * @param string|string[] $file the filename, + * if an array is given it will add all scripts + */ +function script($app, $file = null): void { + if (is_array($file)) { + foreach ($file as $script) { + Util::addScript($app, $script, 'core'); + } + } else { + Util::addScript($app, $file, 'core'); + } +} + +/** + * Shortcut for adding styles to a page + * @param string $app the appname + * @param string|string[] $file the filename, + * if an array is given it will add all styles + */ +function style($app, $file = null): void { + if (is_array($file)) { + foreach ($file as $f) { + Util::addStyle($app, $f); + } + } else { + Util::addStyle($app, $file); + } +} + +/** + * Shortcut for adding vendor styles to a page + * @param string $app the appname + * @param string|string[] $file the filename, + * if an array is given it will add all styles + * @deprecated 32.0.0 + */ +function vendor_style($app, $file = null): void { + if (is_array($file)) { + foreach ($file as $f) { + OC_Util::addVendorStyle($app, $f); + } + } else { + OC_Util::addVendorStyle($app, $file); + } +} + +/** + * Shortcut for adding translations to a page + * @param string $app the appname + * if an array is given it will add all styles + */ +function translation($app): void { + Util::addTranslations($app); +} + +/** + * make \OCP\IURLGenerator::linkTo available as a simple function + * @param string $app app + * @param string $file file + * @param array $args array with param=>value, will be appended to the returned url + * @return string link to the file + * + * For further information have a look at \OCP\IURLGenerator::linkTo + */ +function link_to($app, $file, $args = []) { + return Server::get(IURLGenerator::class)->linkTo($app, $file, $args); +} + +/** + * @param string $key + * @return string url to the online documentation + */ +function link_to_docs($key) { + return Server::get(IURLGenerator::class)->linkToDocs($key); +} + +/** + * make \OCP\IURLGenerator::imagePath available as a simple function + * @param string $app app + * @param string $image image + * @return string link to the image + * + * For further information have a look at \OCP\IURLGenerator::imagePath + */ +function image_path($app, $image) { + return Server::get(IURLGenerator::class)->imagePath($app, $image); +} + +/** + * make mimetypeIcon available as a simple function + * @param string $mimetype mimetype + * @return string link to the image + */ +function mimetype_icon($mimetype) { + return Server::get(IMimeTypeDetector::class)->mimeTypeIcon($mimetype); +} + +/** + * make preview_icon available as a simple function + * Returns the path to the preview of the image. + * @param string $path path of file + * @return string link to the preview + */ +function preview_icon($path) { + return Server::get(IURLGenerator::class)->linkToRoute('core.Preview.getPreview', ['x' => 32, 'y' => 32, 'file' => $path]); +} + +/** + * @param string $path + * @param string $token + * @return string + */ +function publicPreview_icon($path, $token) { + return Server::get(IURLGenerator::class)->linkToRoute('files_sharing.PublicPreview.getPreview', ['x' => 32, 'y' => 32, 'file' => $path, 'token' => $token]); +} + +/** + * make Util::humanFileSize available as a simple function + * @param int $bytes size in bytes + * @return string size as string + * @deprecated use Util::humanFileSize instead + * + * For further information have a look at Util::humanFileSize + */ +function human_file_size($bytes) { + return Util::humanFileSize($bytes); +} + +/** + * Strips the timestamp of its time value + * @param int $timestamp UNIX timestamp to strip + * @return int timestamp without time value + */ +function strip_time($timestamp) { + $date = new \DateTime("@{$timestamp}"); + $date->setTime(0, 0, 0); + return (int)$date->format('U'); +} + +/** + * Formats timestamp relatively to the current time using + * a human-friendly format like "x minutes ago" or "yesterday" + * @param int $timestamp timestamp to format + * @param int|null $fromTime timestamp to compare from, defaults to current time + * @param bool|null $dateOnly whether to strip time information + * @return string timestamp + */ +function relative_modified_date($timestamp, $fromTime = null, $dateOnly = false): string { + $formatter = Server::get(IDateTimeFormatter::class); + + if ($dateOnly) { + return $formatter->formatDateSpan($timestamp, $fromTime); + } + return $formatter->formatTimeSpan($timestamp, $fromTime); +} + +/** + * @param array $options + * @param string[]|string $selected + * @param array $params + */ +function html_select_options($options, $selected, $params = []): string { + if (!is_array($selected)) { + $selected = [$selected]; + } + if (isset($params['combine']) && $params['combine']) { + $options = array_combine($options, $options); + } + $value_name = $label_name = false; + if (isset($params['value'])) { + $value_name = $params['value']; + } + if (isset($params['label'])) { + $label_name = $params['label']; + } + $html = ''; + foreach ($options as $value => $label) { + if ($value_name && is_array($label)) { + $value = $label[$value_name]; + } + if ($label_name && is_array($label)) { + $label = $label[$label_name]; + } + $select = in_array($value, $selected) ? ' selected="selected"' : ''; + $html .= '<option value="' . Util::sanitizeHTML($value) . '"' . $select . '>' . Util::sanitizeHTML($label) . '</option>' . "\n"; + } + return $html; +} |