You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

290 lines
8.2 KiB

  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2018 John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
  5. * @copyright Copyright (c) 2019 Janis Köhr <janiskoehr@icloud.com>
  6. *
  7. * @author Alexey Pyltsyn <lex61rus@gmail.com>
  8. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  9. * @author Janis Köhr <janis.koehr@novatec-gmbh.de>
  10. * @author Joas Schilling <coding@schilljs.com>
  11. * @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
  12. * @author Julius Härtl <jus@bitgrid.net>
  13. * @author Roeland Jago Douma <roeland@famdouma.nl>
  14. * @author Thomas Citharel <nextcloud@tcit.fr>
  15. *
  16. * @license GNU AGPL version 3 or any later version
  17. *
  18. * This program is free software: you can redistribute it and/or modify
  19. * it under the terms of the GNU Affero General Public License as
  20. * published by the Free Software Foundation, either version 3 of the
  21. * License, or (at your option) any later version.
  22. *
  23. * This program is distributed in the hope that it will be useful,
  24. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  25. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  26. * GNU Affero General Public License for more details.
  27. *
  28. * You should have received a copy of the GNU Affero General Public License
  29. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  30. *
  31. */
  32. namespace OCA\Accessibility\Controller;
  33. use OC\Template\IconsCacher;
  34. use OCP\App\IAppManager;
  35. use OCP\AppFramework\Controller;
  36. use OCP\AppFramework\Http;
  37. use OCP\AppFramework\Http\DataDisplayResponse;
  38. use OCP\AppFramework\Utility\ITimeFactory;
  39. use OCP\IConfig;
  40. use OCP\ILogger;
  41. use OCP\IRequest;
  42. use OCP\IURLGenerator;
  43. use OCP\IUserManager;
  44. use OCP\IUserSession;
  45. use ScssPhp\ScssPhp\Compiler;
  46. use ScssPhp\ScssPhp\Exception\ParserException;
  47. use ScssPhp\ScssPhp\Formatter\Crunched;
  48. class AccessibilityController extends Controller {
  49. /** @var string */
  50. protected $appName;
  51. /** @var string */
  52. protected $serverRoot;
  53. /** @var IConfig */
  54. private $config;
  55. /** @var IUserManager */
  56. private $userManager;
  57. /** @var ILogger */
  58. private $logger;
  59. /** @var IURLGenerator */
  60. private $urlGenerator;
  61. /** @var ITimeFactory */
  62. protected $timeFactory;
  63. /** @var IUserSession */
  64. private $userSession;
  65. /** @var IAppManager */
  66. private $appManager;
  67. /** @var IconsCacher */
  68. protected $iconsCacher;
  69. /** @var \OC_Defaults */
  70. private $defaults;
  71. /** @var null|string */
  72. private $injectedVariables;
  73. /**
  74. * Account constructor.
  75. *
  76. * @param string $appName
  77. * @param IRequest $request
  78. * @param IConfig $config
  79. * @param IUserManager $userManager
  80. * @param ILogger $logger
  81. * @param IURLGenerator $urlGenerator
  82. * @param ITimeFactory $timeFactory
  83. * @param IUserSession $userSession
  84. * @param IAppManager $appManager
  85. * @param \OC_Defaults $defaults
  86. */
  87. public function __construct(string $appName,
  88. IRequest $request,
  89. IConfig $config,
  90. IUserManager $userManager,
  91. ILogger $logger,
  92. IURLGenerator $urlGenerator,
  93. ITimeFactory $timeFactory,
  94. IUserSession $userSession,
  95. IAppManager $appManager,
  96. IconsCacher $iconsCacher,
  97. \OC_Defaults $defaults) {
  98. parent::__construct($appName, $request);
  99. $this->appName = $appName;
  100. $this->config = $config;
  101. $this->userManager = $userManager;
  102. $this->logger = $logger;
  103. $this->urlGenerator = $urlGenerator;
  104. $this->timeFactory = $timeFactory;
  105. $this->userSession = $userSession;
  106. $this->appManager = $appManager;
  107. $this->iconsCacher = $iconsCacher;
  108. $this->defaults = $defaults;
  109. $this->serverRoot = \OC::$SERVERROOT;
  110. $this->appRoot = $this->appManager->getAppPath($this->appName);
  111. }
  112. /**
  113. * @PublicPage
  114. * @NoCSRFRequired
  115. * @NoSameSiteCookieRequired
  116. *
  117. * @return DataDisplayResponse
  118. */
  119. public function getCss(): DataDisplayResponse {
  120. $css = '';
  121. $imports = '';
  122. if ($this->userSession->isLoggedIn()) {
  123. $userValues = $this->getUserValues();
  124. } else {
  125. $userValues = ['dark'];
  126. }
  127. foreach ($userValues as $key => $scssFile) {
  128. if ($scssFile !== false) {
  129. if ($scssFile === 'highcontrast' && in_array('dark', $userValues)) {
  130. $scssFile .= 'dark';
  131. }
  132. $imports .= '@import "' . $scssFile . '";';
  133. }
  134. }
  135. if ($imports !== '') {
  136. $scss = new Compiler();
  137. $scss->setImportPaths([
  138. $this->appRoot . '/css/',
  139. $this->serverRoot . '/core/css/'
  140. ]);
  141. // Continue after throw
  142. $scss->setIgnoreErrors(true);
  143. $scss->setFormatter(Crunched::class);
  144. // Import theme, variables and compile css4 variables
  145. try {
  146. $css .= $scss->compile(
  147. $imports .
  148. $this->getInjectedVariables() .
  149. '@import "variables.scss";' .
  150. '@import "css-variables.scss";'
  151. );
  152. } catch (ParserException $e) {
  153. $this->logger->error($e->getMessage(), ['app' => 'core']);
  154. }
  155. }
  156. // We don't want to override vars with url since path is different
  157. $css = $this->filterOutRule('/--[a-z-:]+url\([^;]+\)/mi', $css);
  158. // Rebase all urls
  159. $appWebRoot = substr($this->appRoot, strlen($this->serverRoot) - strlen(\OC::$WEBROOT));
  160. $css = $this->rebaseUrls($css, $appWebRoot . '/css');
  161. if (in_array('dark', $userValues) && $this->iconsCacher->getCachedList() && $this->iconsCacher->getCachedList()->getSize() > 0) {
  162. $iconsCss = $this->invertSvgIconsColor($this->iconsCacher->getCachedList()->getContent());
  163. $css = $css . $iconsCss;
  164. }
  165. $response = new DataDisplayResponse($css, Http::STATUS_OK, ['Content-Type' => 'text/css']);
  166. // Set cache control
  167. $ttl = 31536000;
  168. $response->addHeader('Cache-Control', 'max-age=' . $ttl . ', immutable');
  169. $expires = new \DateTime();
  170. $expires->setTimestamp($this->timeFactory->getTime());
  171. $expires->add(new \DateInterval('PT' . $ttl . 'S'));
  172. $response->addHeader('Expires', $expires->format(\DateTime::RFC1123));
  173. $response->addHeader('Pragma', 'cache');
  174. // store current cache hash
  175. if ($this->userSession->isLoggedIn()) {
  176. $this->config->setUserValue($this->userSession->getUser()->getUID(), $this->appName, 'icons-css', md5($css));
  177. }
  178. return $response;
  179. }
  180. /**
  181. * Return an array with the user theme & font settings
  182. *
  183. * @return array
  184. */
  185. private function getUserValues(): array {
  186. $userTheme = $this->config->getUserValue($this->userSession->getUser()->getUID(), $this->appName, 'theme', false);
  187. $userFont = $this->config->getUserValue($this->userSession->getUser()->getUID(), $this->appName, 'font', false);
  188. $userHighContrast = $this->config->getUserValue($this->userSession->getUser()->getUID(), $this->appName, 'highcontrast', false);
  189. return [$userTheme, $userHighContrast, $userFont];
  190. }
  191. /**
  192. * Remove all matches from the $rule regex
  193. *
  194. * @param string $rule regex to match
  195. * @param string $css string to parse
  196. * @return string
  197. */
  198. private function filterOutRule(string $rule, string $css): string {
  199. return preg_replace($rule, '', $css);
  200. }
  201. /**
  202. * Add the correct uri prefix to make uri valid again
  203. *
  204. * @param string $css
  205. * @param string $webDir
  206. * @return string
  207. */
  208. private function rebaseUrls(string $css, string $webDir): string {
  209. $re = '/url\([\'"]([^\/][\.\w?=\/-]*)[\'"]\)/x';
  210. $subst = 'url(\'' . $webDir . '/$1\')';
  211. return preg_replace($re, $subst, $css);
  212. }
  213. /**
  214. * Remove all matches from the $rule regex
  215. *
  216. * @param string $css string to parse
  217. * @return string
  218. */
  219. private function invertSvgIconsColor(string $css) {
  220. return str_replace(
  221. ['color=000&', 'color=fff&', 'color=***&'],
  222. ['color=***&', 'color=000&', 'color=fff&'],
  223. str_replace(
  224. ['color=000000&', 'color=ffffff&', 'color=******&'],
  225. ['color=******&', 'color=000000&', 'color=ffffff&'],
  226. $css
  227. )
  228. );
  229. }
  230. /**
  231. * @return string SCSS code for variables from OC_Defaults
  232. */
  233. private function getInjectedVariables(): string {
  234. if ($this->injectedVariables !== null) {
  235. return $this->injectedVariables;
  236. }
  237. $variables = '';
  238. foreach ($this->defaults->getScssVariables() as $key => $value) {
  239. $variables .= '$' . $key . ': ' . $value . ';';
  240. }
  241. // check for valid variables / otherwise fall back to defaults
  242. try {
  243. $scss = new Compiler();
  244. $scss->compile($variables);
  245. $this->injectedVariables = $variables;
  246. } catch (ParserException $e) {
  247. $this->logger->logException($e, ['app' => 'core']);
  248. }
  249. return $variables;
  250. }
  251. }