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 | 47 | ||||
-rw-r--r-- | lib/private/Template/JSCombiner.php | 34 | ||||
-rw-r--r-- | lib/private/Template/JSConfigHelper.php | 181 | ||||
-rw-r--r-- | lib/private/Template/JSResourceLocator.php | 145 | ||||
-rwxr-xr-x | lib/private/Template/ResourceLocator.php | 32 | ||||
-rw-r--r-- | lib/private/Template/ResourceNotFoundException.php | 23 | ||||
-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 |
11 files changed, 858 insertions, 380 deletions
diff --git a/lib/private/Template/Base.php b/lib/private/Template/Base.php index 71ce2ed9d86..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 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 { @@ -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 5047b3e906f..b501fd69874 100644 --- a/lib/private/Template/CSSResourceLocator.php +++ b/lib/private/Template/CSSResourceLocator.php @@ -1,33 +1,9 @@ <?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; @@ -43,9 +19,8 @@ class CSSResourceLocator extends ResourceLocator { */ public function doFind($style) { $app = substr($style, 0, strpos($style, '/')); - if (strpos($style, '3rdparty') === 0 - && $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; } @@ -66,17 +41,17 @@ class CSSResourceLocator extends ResourceLocator { // turned into cwd. $app_path = realpath($app_path); - $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'); + $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/JSCombiner.php b/lib/private/Template/JSCombiner.php index b87829360d5..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; @@ -55,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; diff --git a/lib/private/Template/JSConfigHelper.php b/lib/private/Template/JSConfigHelper.php index 7b6d0a6a346..044fa8147a0 100644 --- a/lib/private/Template/JSConfigHelper.php +++ b/lib/private/Template/JSConfigHelper.php @@ -2,94 +2,65 @@ 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\ILogger; 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 { - protected IL10N $l; - protected Defaults $defaults; - protected IAppManager $appManager; - protected ISession $session; - protected ?IUser $currentUser; - protected IConfig $config; - protected IGroupManager $groupManager; - protected IniGetWrapper $iniWrapper; - protected IURLGenerator $urlGenerator; - protected CapabilitiesManager $capabilitiesManager; - protected IInitialStateService $initialStateService; /** @var array user back-ends excluded from password verification */ private $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true]; - public function __construct(IL10N $l, - Defaults $defaults, - IAppManager $appManager, - ISession $session, - ?IUser $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(): string { @@ -99,9 +70,11 @@ class JSConfigHelper { $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; @@ -111,7 +84,7 @@ class JSConfigHelper { $apps_paths = []; if ($this->currentUser === null) { - $apps = $this->appManager->getInstalledApps(); + $apps = $this->appManager->getEnabledApps(); } else { $apps = $this->appManager->getEnabledAppsForUser($this->currentUser); } @@ -124,8 +97,7 @@ class JSConfigHelper { } } - $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) { @@ -155,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; @@ -165,9 +141,16 @@ 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 = [ - 'auto_logout' => $this->config->getSystemValue('auto_logout', false), + /** @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), 'loglevel' => $this->config->getSystemValue('loglevel_frontend', $this->config->getSystemValue('loglevel', ILogger::WARN) ), @@ -178,21 +161,24 @@ class JSConfigHelper { '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), - 'version' => implode('.', Util::getVersion()), - 'versionstring' => \OC_Util::getVersionString() + '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'), @@ -201,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.'), @@ -210,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'), @@ -219,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'), @@ -233,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.'), @@ -247,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' => Util::isPublicLinkPasswordRequired(), 'enableLinkPasswordByDefault' => $enableLinkPasswordByDefault, - 'sharingDisabledForUser' => Util::isSharingDisabledForUser(), + '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, @@ -269,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(), @@ -304,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 7648c7953f3..a6d2d13a2ad 100644 --- a/lib/private/Template/JSResourceLocator.php +++ b/lib/private/Template/JSResourceLocator.php @@ -1,112 +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 { - /** @var JSCombiner */ - protected $jsCombiner; + protected JSCombiner $jsCombiner; + protected IAppManager $appManager; - public function __construct(LoggerInterface $logger, JSCombiner $JSCombiner) { + 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.'/'; + $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, $theme_dir . "dist/$app-$scriptName.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, $theme_dir . "dist/core-$scriptName.js") - || $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); - return; - } - - if ($app_path === false && $app_url === false) { - $this->logger->error('Could not find resource {resource} to load', [ - 'resource' => $app . '/' . $script . '.js', - 'app' => 'jsresourceloader', - ]); + // missing translations files will be ignored + if (str_contains($script, '/l10n/')) { 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', + ]); } /** @@ -115,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 9e6e2056e6b..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; @@ -97,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/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; +} |