aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Template
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Template')
-rw-r--r--lib/private/Template/Base.php92
-rw-r--r--lib/private/Template/CSSResourceLocator.php92
-rw-r--r--lib/private/Template/IconsCacher.php263
-rw-r--r--lib/private/Template/JSCombiner.php37
-rw-r--r--lib/private/Template/JSConfigHelper.php244
-rw-r--r--lib/private/Template/JSResourceLocator.php150
-rwxr-xr-xlib/private/Template/ResourceLocator.php51
-rw-r--r--lib/private/Template/ResourceNotFoundException.php23
-rw-r--r--lib/private/Template/SCSSCacher.php528
-rw-r--r--lib/private/Template/Template.php159
-rw-r--r--lib/private/Template/TemplateFileLocator.php57
-rw-r--r--lib/private/Template/TemplateManager.php169
-rw-r--r--lib/private/Template/functions.php299
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;
+}