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.

457 lines
14 KiB

10 years ago
10 years ago
10 years ago
10 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org>
  4. * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
  5. *
  6. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  7. * @author Bjoern Schiessle <bjoern@schiessle.org>
  8. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  9. * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
  10. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  11. * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
  12. * @author Joas Schilling <coding@schilljs.com>
  13. * @author Julius Haertl <jus@bitgrid.net>
  14. * @author Julius Härtl <jus@bitgrid.net>
  15. * @author Kyle Fazzari <kyrofa@ubuntu.com>
  16. * @author Lukas Reschke <lukas@statuscode.ch>
  17. * @author Michael Weimann <mail@michael-weimann.eu>
  18. * @author rakekniven <mark.ziegler@rakekniven.de>
  19. * @author Robin Appelman <robin@icewind.nl>
  20. * @author Roeland Jago Douma <roeland@famdouma.nl>
  21. * @author Thomas Citharel <nextcloud@tcit.fr>
  22. *
  23. * @license GNU AGPL version 3 or any later version
  24. *
  25. * This program is free software: you can redistribute it and/or modify
  26. * it under the terms of the GNU Affero General Public License as
  27. * published by the Free Software Foundation, either version 3 of the
  28. * License, or (at your option) any later version.
  29. *
  30. * This program is distributed in the hope that it will be useful,
  31. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  32. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  33. * GNU Affero General Public License for more details.
  34. *
  35. * You should have received a copy of the GNU Affero General Public License
  36. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  37. *
  38. */
  39. namespace OCA\Theming\Controller;
  40. use OC\Template\SCSSCacher;
  41. use OCA\Theming\ImageManager;
  42. use OCA\Theming\ThemingDefaults;
  43. use OCP\App\IAppManager;
  44. use OCP\AppFramework\Controller;
  45. use OCP\AppFramework\Http;
  46. use OCP\AppFramework\Http\DataResponse;
  47. use OCP\AppFramework\Http\FileDisplayResponse;
  48. use OCP\AppFramework\Http\NotFoundResponse;
  49. use OCP\Files\IAppData;
  50. use OCP\Files\NotFoundException;
  51. use OCP\Files\NotPermittedException;
  52. use OCP\IConfig;
  53. use OCP\IL10N;
  54. use OCP\IRequest;
  55. use OCP\ITempManager;
  56. use OCP\IURLGenerator;
  57. /**
  58. * Class ThemingController
  59. *
  60. * handle ajax requests to update the theme
  61. *
  62. * @package OCA\Theming\Controller
  63. */
  64. class ThemingController extends Controller {
  65. /** @var ThemingDefaults */
  66. private $themingDefaults;
  67. /** @var IL10N */
  68. private $l10n;
  69. /** @var IConfig */
  70. private $config;
  71. /** @var ITempManager */
  72. private $tempManager;
  73. /** @var IAppData */
  74. private $appData;
  75. /** @var SCSSCacher */
  76. private $scssCacher;
  77. /** @var IURLGenerator */
  78. private $urlGenerator;
  79. /** @var IAppManager */
  80. private $appManager;
  81. /** @var ImageManager */
  82. private $imageManager;
  83. /**
  84. * ThemingController constructor.
  85. *
  86. * @param string $appName
  87. * @param IRequest $request
  88. * @param IConfig $config
  89. * @param ThemingDefaults $themingDefaults
  90. * @param IL10N $l
  91. * @param ITempManager $tempManager
  92. * @param IAppData $appData
  93. * @param SCSSCacher $scssCacher
  94. * @param IURLGenerator $urlGenerator
  95. * @param IAppManager $appManager
  96. * @param ImageManager $imageManager
  97. */
  98. public function __construct(
  99. $appName,
  100. IRequest $request,
  101. IConfig $config,
  102. ThemingDefaults $themingDefaults,
  103. IL10N $l,
  104. ITempManager $tempManager,
  105. IAppData $appData,
  106. SCSSCacher $scssCacher,
  107. IURLGenerator $urlGenerator,
  108. IAppManager $appManager,
  109. ImageManager $imageManager
  110. ) {
  111. parent::__construct($appName, $request);
  112. $this->themingDefaults = $themingDefaults;
  113. $this->l10n = $l;
  114. $this->config = $config;
  115. $this->tempManager = $tempManager;
  116. $this->appData = $appData;
  117. $this->scssCacher = $scssCacher;
  118. $this->urlGenerator = $urlGenerator;
  119. $this->appManager = $appManager;
  120. $this->imageManager = $imageManager;
  121. }
  122. /**
  123. * @param string $setting
  124. * @param string $value
  125. * @return DataResponse
  126. * @throws NotPermittedException
  127. */
  128. public function updateStylesheet($setting, $value) {
  129. $value = trim($value);
  130. $error = null;
  131. switch ($setting) {
  132. case 'name':
  133. if (strlen($value) > 250) {
  134. $error = $this->l10n->t('The given name is too long');
  135. }
  136. break;
  137. case 'url':
  138. if (strlen($value) > 500) {
  139. $error = $this->l10n->t('The given web address is too long');
  140. }
  141. if (!$this->isValidUrl($value)) {
  142. $error = $this->l10n->t('The given web address is not a valid URL');
  143. }
  144. break;
  145. case 'imprintUrl':
  146. if (strlen($value) > 500) {
  147. $error = $this->l10n->t('The given legal notice address is too long');
  148. }
  149. if (!$this->isValidUrl($value)) {
  150. $error = $this->l10n->t('The given legal notice address is not a valid URL');
  151. }
  152. break;
  153. case 'privacyUrl':
  154. if (strlen($value) > 500) {
  155. $error = $this->l10n->t('The given privacy policy address is too long');
  156. }
  157. if (!$this->isValidUrl($value)) {
  158. $error = $this->l10n->t('The given privacy policy address is not a valid URL');
  159. }
  160. break;
  161. case 'slogan':
  162. if (strlen($value) > 500) {
  163. $error = $this->l10n->t('The given slogan is too long');
  164. }
  165. break;
  166. case 'color':
  167. if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) {
  168. $error = $this->l10n->t('The given color is invalid');
  169. }
  170. break;
  171. }
  172. if ($error !== null) {
  173. return new DataResponse([
  174. 'data' => [
  175. 'message' => $error,
  176. ],
  177. 'status' => 'error'
  178. ], Http::STATUS_BAD_REQUEST);
  179. }
  180. $this->themingDefaults->set($setting, $value);
  181. // reprocess server scss for preview
  182. $cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core');
  183. return new DataResponse(
  184. [
  185. 'data' =>
  186. [
  187. 'message' => $this->l10n->t('Saved'),
  188. 'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/css-variables.scss'))
  189. ],
  190. 'status' => 'success'
  191. ]
  192. );
  193. }
  194. /**
  195. * Check that a string is a valid http/https url
  196. */
  197. private function isValidUrl(string $url): bool {
  198. return ((strpos($url, 'http://') === 0 || strpos($url, 'https://') === 0) &&
  199. filter_var($url, FILTER_VALIDATE_URL) !== false);
  200. }
  201. /**
  202. * @return DataResponse
  203. * @throws NotPermittedException
  204. */
  205. public function uploadImage(): DataResponse {
  206. // logo / background
  207. // new: favicon logo-header
  208. //
  209. $key = $this->request->getParam('key');
  210. $image = $this->request->getUploadedFile('image');
  211. $error = null;
  212. $phpFileUploadErrors = [
  213. UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
  214. UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
  215. UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
  216. UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
  217. UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
  218. UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
  219. UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
  220. UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
  221. ];
  222. if (empty($image)) {
  223. $error = $this->l10n->t('No file uploaded');
  224. }
  225. if (!empty($image) && array_key_exists('error', $image) && $image['error'] !== UPLOAD_ERR_OK) {
  226. $error = $phpFileUploadErrors[$image['error']];
  227. }
  228. if ($error !== null) {
  229. return new DataResponse(
  230. [
  231. 'data' => [
  232. 'message' => $error
  233. ],
  234. 'status' => 'failure',
  235. ],
  236. Http::STATUS_UNPROCESSABLE_ENTITY
  237. );
  238. }
  239. $name = '';
  240. try {
  241. $folder = $this->appData->getFolder('images');
  242. } catch (NotFoundException $e) {
  243. $folder = $this->appData->newFolder('images');
  244. }
  245. $this->imageManager->delete($key);
  246. $target = $folder->newFile($key);
  247. $supportedFormats = $this->getSupportedUploadImageFormats($key);
  248. $detectedMimeType = mime_content_type($image['tmp_name']);
  249. if (!in_array($image['type'], $supportedFormats) || !in_array($detectedMimeType, $supportedFormats)) {
  250. return new DataResponse(
  251. [
  252. 'data' => [
  253. 'message' => $this->l10n->t('Unsupported image type'),
  254. ],
  255. 'status' => 'failure',
  256. ],
  257. Http::STATUS_UNPROCESSABLE_ENTITY
  258. );
  259. }
  260. if ($key === 'background' && strpos($detectedMimeType, 'image/svg') === false) {
  261. // Optimize the image since some people may upload images that will be
  262. // either to big or are not progressive rendering.
  263. $newImage = @imagecreatefromstring(file_get_contents($image['tmp_name'], 'r'));
  264. $tmpFile = $this->tempManager->getTemporaryFile();
  265. $newWidth = imagesx($newImage) < 4096 ? imagesx($newImage) : 4096;
  266. $newHeight = imagesy($newImage) / (imagesx($newImage) / $newWidth);
  267. $outputImage = imagescale($newImage, $newWidth, $newHeight);
  268. imageinterlace($outputImage, 1);
  269. imagejpeg($outputImage, $tmpFile, 75);
  270. imagedestroy($outputImage);
  271. $target->putContent(file_get_contents($tmpFile, 'r'));
  272. } else {
  273. $target->putContent(file_get_contents($image['tmp_name'], 'r'));
  274. }
  275. $name = $image['name'];
  276. $this->themingDefaults->set($key.'Mime', $image['type']);
  277. $cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core');
  278. return new DataResponse(
  279. [
  280. 'data' =>
  281. [
  282. 'name' => $name,
  283. 'url' => $this->imageManager->getImageUrl($key),
  284. 'message' => $this->l10n->t('Saved'),
  285. 'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/css-variables.scss'))
  286. ],
  287. 'status' => 'success'
  288. ]
  289. );
  290. }
  291. /**
  292. * Returns a list of supported mime types for image uploads.
  293. * "favicon" images are only allowed to be SVG when imagemagick with SVG support is available.
  294. *
  295. * @param string $key The image key, e.g. "favicon"
  296. * @return array
  297. */
  298. private function getSupportedUploadImageFormats(string $key): array {
  299. $supportedFormats = ['image/jpeg', 'image/png', 'image/gif',];
  300. if ($key !== 'favicon' || $this->imageManager->shouldReplaceIcons() === true) {
  301. $supportedFormats[] = 'image/svg+xml';
  302. $supportedFormats[] = 'image/svg';
  303. }
  304. return $supportedFormats;
  305. }
  306. /**
  307. * Revert setting to default value
  308. *
  309. * @param string $setting setting which should be reverted
  310. * @return DataResponse
  311. * @throws NotPermittedException
  312. */
  313. public function undo(string $setting): DataResponse {
  314. $value = $this->themingDefaults->undo($setting);
  315. // reprocess server scss for preview
  316. $cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core');
  317. if (strpos($setting, 'Mime') !== -1) {
  318. $imageKey = str_replace('Mime', '', $setting);
  319. $this->imageManager->delete($imageKey);
  320. }
  321. return new DataResponse(
  322. [
  323. 'data' =>
  324. [
  325. 'value' => $value,
  326. 'message' => $this->l10n->t('Saved'),
  327. 'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/css-variables.scss'))
  328. ],
  329. 'status' => 'success'
  330. ]
  331. );
  332. }
  333. /**
  334. * @PublicPage
  335. * @NoCSRFRequired
  336. *
  337. * @param string $key
  338. * @param bool $useSvg
  339. * @return FileDisplayResponse|NotFoundResponse
  340. * @throws NotPermittedException
  341. */
  342. public function getImage(string $key, bool $useSvg = true) {
  343. try {
  344. $file = $this->imageManager->getImage($key, $useSvg);
  345. } catch (NotFoundException $e) {
  346. return new NotFoundResponse();
  347. }
  348. $response = new FileDisplayResponse($file);
  349. $csp = new Http\ContentSecurityPolicy();
  350. $csp->allowInlineStyle();
  351. $response->setContentSecurityPolicy($csp);
  352. $response->cacheFor(3600);
  353. $response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
  354. $response->addHeader('Content-Disposition', 'attachment; filename="' . $key . '"');
  355. if (!$useSvg) {
  356. $response->addHeader('Content-Type', 'image/png');
  357. } else {
  358. $response->addHeader('Content-Type', $this->config->getAppValue($this->appName, $key . 'Mime', ''));
  359. }
  360. return $response;
  361. }
  362. /**
  363. * @NoCSRFRequired
  364. * @PublicPage
  365. * @NoSameSiteCookieRequired
  366. *
  367. * @return FileDisplayResponse|NotFoundResponse
  368. * @throws NotPermittedException
  369. * @throws \Exception
  370. * @throws \OCP\App\AppPathNotFoundException
  371. */
  372. public function getStylesheet() {
  373. $appPath = $this->appManager->getAppPath('theming');
  374. /* SCSSCacher is required here
  375. * We cannot rely on automatic caching done by \OC_Util::addStyle,
  376. * since we need to add the cacheBuster value to the url
  377. */
  378. $cssCached = $this->scssCacher->process($appPath, 'css/theming.scss', 'theming');
  379. if (!$cssCached) {
  380. return new NotFoundResponse();
  381. }
  382. try {
  383. $cssFile = $this->scssCacher->getCachedCSS('theming', 'theming.css');
  384. $response = new FileDisplayResponse($cssFile, Http::STATUS_OK, ['Content-Type' => 'text/css']);
  385. $response->cacheFor(86400);
  386. return $response;
  387. } catch (NotFoundException $e) {
  388. return new NotFoundResponse();
  389. }
  390. }
  391. /**
  392. * @NoCSRFRequired
  393. * @PublicPage
  394. *
  395. * @return Http\JSONResponse
  396. */
  397. public function getManifest($app) {
  398. $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0');
  399. $responseJS = [
  400. 'name' => $this->themingDefaults->getName(),
  401. 'start_url' => $this->urlGenerator->getBaseUrl(),
  402. 'icons' =>
  403. [
  404. [
  405. 'src' => $this->urlGenerator->linkToRoute('theming.Icon.getTouchIcon',
  406. ['app' => $app]) . '?v=' . $cacheBusterValue,
  407. 'type'=> 'image/png',
  408. 'sizes'=> '128x128'
  409. ],
  410. [
  411. 'src' => $this->urlGenerator->linkToRoute('theming.Icon.getFavicon',
  412. ['app' => $app]) . '?v=' . $cacheBusterValue,
  413. 'type' => 'image/svg+xml',
  414. 'sizes' => '16x16'
  415. ]
  416. ],
  417. 'display' => 'standalone'
  418. ];
  419. $response = new Http\JSONResponse($responseJS);
  420. $response->cacheFor(3600);
  421. return $response;
  422. }
  423. }