diff options
author | Morris Jobke <hey@morrisjobke.de> | 2016-11-18 15:35:34 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-11-18 15:35:34 +0100 |
commit | faee255ff47873ed2f8908c7d6b6e603ded11618 (patch) | |
tree | 87a46252211e9c0dbab91609e4115082401656ee | |
parent | e8511660f01faeebcc692b57cabdacd97845da92 (diff) | |
parent | 2ab4d1e0a3f15af2b8f04edcf18b7fe3fc0be262 (diff) | |
download | nextcloud-server-faee255ff47873ed2f8908c7d6b6e603ded11618.tar.gz nextcloud-server-faee255ff47873ed2f8908c7d6b6e603ded11618.zip |
Merge pull request #840 from nextcloud/theming-icon-endpoint
Add dynamic icon creation
22 files changed, 1350 insertions, 6 deletions
diff --git a/apps/theming/appinfo/routes.php b/apps/theming/appinfo/routes.php index 4a8d4bac5bc..f4aa2f93162 100644 --- a/apps/theming/appinfo/routes.php +++ b/apps/theming/appinfo/routes.php @@ -60,5 +60,23 @@ return ['routes' => [ 'url' => '/js/theming', 'verb' => 'GET', ], + [ + 'name' => 'Icon#getFavicon', + 'url' => '/favicon/{app}', + 'verb' => 'GET', + 'defaults' => array('app' => 'core'), + ], + [ + 'name' => 'Icon#getTouchIcon', + 'url' => '/icon/{app}', + 'verb' => 'GET', + 'defaults' => array('app' => 'core'), + ], + [ + 'name' => 'Icon#getThemedIcon', + 'url' => '/img/{app}/{image}', + 'verb' => 'GET', + 'requirements' => array('image' => '.+') + ], ]]; diff --git a/apps/theming/lib/Controller/IconController.php b/apps/theming/lib/Controller/IconController.php new file mode 100644 index 00000000000..4c25d911e5e --- /dev/null +++ b/apps/theming/lib/Controller/IconController.php @@ -0,0 +1,174 @@ +<?php +/** + * @copyright Copyright (c) 2016 Julius Haertl <jus@bitgrid.net> + * + * @author Julius Haertl <jus@bitgrid.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 OCA\Theming\Controller; + +use OCA\Theming\IconBuilder; +use OCA\Theming\ImageManager; +use OCA\Theming\ThemingDefaults; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\NotFoundResponse; +use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\NotFoundException; +use OCP\IRequest; +use OCA\Theming\Util; +use OCP\IConfig; + +class IconController extends Controller { + /** @var ThemingDefaults */ + private $themingDefaults; + /** @var Util */ + private $util; + /** @var ITimeFactory */ + private $timeFactory; + /** @var IConfig */ + private $config; + /** @var IconBuilder */ + private $iconBuilder; + /** @var ImageManager */ + private $imageManager; + + /** + * IconController constructor. + * + * @param string $appName + * @param IRequest $request + * @param ThemingDefaults $themingDefaults + * @param Util $util + * @param ITimeFactory $timeFactory + * @param IConfig $config + * @param IconBuilder $iconBuilder + * @param ImageManager $imageManager + */ + public function __construct( + $appName, + IRequest $request, + ThemingDefaults $themingDefaults, + Util $util, + ITimeFactory $timeFactory, + IConfig $config, + IconBuilder $iconBuilder, + ImageManager $imageManager + ) { + parent::__construct($appName, $request); + + $this->themingDefaults = $themingDefaults; + $this->util = $util; + $this->timeFactory = $timeFactory; + $this->config = $config; + $this->iconBuilder = $iconBuilder; + $this->imageManager = $imageManager; + } + + /** + * @PublicPage + * @NoCSRFRequired + * + * @param $app string app name + * @param $image string image file name (svg required) + * @return FileDisplayResponse|NotFoundResponse + */ + public function getThemedIcon($app, $image) { + try { + $iconFile = $this->imageManager->getCachedImage("icon-" . $app . '-' . str_replace("/","_",$image)); + } catch (NotFoundException $exception) { + $icon = $this->iconBuilder->colorSvg($app, $image); + $iconFile = $this->imageManager->setCachedImage("icon-" . $app . '-' . str_replace("/","_",$image), $icon); + } + if ($iconFile !== false) { + $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']); + $response->cacheFor(86400); + $expires = new \DateTime(); + $expires->setTimestamp($this->timeFactory->getTime()); + $expires->add(new \DateInterval('PT24H')); + $response->addHeader('Expires', $expires->format(\DateTime::RFC2822)); + $response->addHeader('Pragma', 'cache'); + return $response; + } else { + return new NotFoundResponse(); + } + } + + /** + * Return a 32x32 favicon as png + * + * @PublicPage + * @NoCSRFRequired + * + * @param $app string app name + * @return FileDisplayResponse|NotFoundResponse + */ + public function getFavicon($app = "core") { + if ($this->themingDefaults->shouldReplaceIcons()) { + try { + $iconFile = $this->imageManager->getCachedImage('favIcon-' . $app); + } catch (NotFoundException $exception) { + $icon = $this->iconBuilder->getFavicon($app); + $iconFile = $this->imageManager->setCachedImage('favIcon-' . $app, $icon); + } + if ($iconFile !== false) { + $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); + $response->cacheFor(86400); + $expires = new \DateTime(); + $expires->setTimestamp($this->timeFactory->getTime()); + $expires->add(new \DateInterval('PT24H')); + $response->addHeader('Expires', $expires->format(\DateTime::RFC2822)); + $response->addHeader('Pragma', 'cache'); + return $response; + } + } + return new NotFoundResponse(); + } + + /** + * Return a 512x512 icon for touch devices + * + * @PublicPage + * @NoCSRFRequired + * + * @param $app string app name + * @return FileDisplayResponse|NotFoundResponse + */ + public function getTouchIcon($app = "core") { + if ($this->themingDefaults->shouldReplaceIcons()) { + try { + $iconFile = $this->imageManager->getCachedImage('touchIcon-' . $app); + } catch (NotFoundException $exception) { + $icon = $this->iconBuilder->getTouchIcon($app); + $iconFile = $this->imageManager->setCachedImage('touchIcon-' . $app, $icon); + } + if ($iconFile !== false) { + $response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/png']); + $response->cacheFor(86400); + $expires = new \DateTime(); + $expires->setTimestamp($this->timeFactory->getTime()); + $expires->add(new \DateInterval('PT24H')); + $response->addHeader('Expires', $expires->format(\DateTime::RFC2822)); + $response->addHeader('Pragma', 'cache'); + return $response; + } + } + return new NotFoundResponse(); + } +} diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php index 1eefb2e3dd7..58e843c5d9d 100644 --- a/apps/theming/lib/Controller/ThemingController.php +++ b/apps/theming/lib/Controller/ThemingController.php @@ -410,6 +410,15 @@ class ThemingController extends Controller { $responseCss .= '.nc-theming-contrast {color: #ffffff}' . "\n"; } + if($logo !== '' or $color !== '') { + $responseCss .= '.icon-file,.icon-filetype-text {' . + 'background-image: url(\'./img/core/filetypes/text.svg?v='.$cacheBusterValue.'\');' . "}\n" . + '.icon-folder, .icon-filetype-folder {' . + 'background-image: url(\'./img/core/filetypes/folder.svg?v='.$cacheBusterValue.'\');' . "}\n" . + '.icon-filetype-folder-drag-accept {' . + 'background-image: url(\'./img/core/filetypes/folder-drag-accept.svg?v='.$cacheBusterValue.'\')!important;' . "}\n"; + } + $response = new DataDownloadResponse($responseCss, 'style', 'text/css'); $response->addHeader('Expires', date(\DateTime::RFC2822, $this->timeFactory->getTime())); $response->addHeader('Pragma', 'cache'); @@ -423,6 +432,7 @@ class ThemingController extends Controller { * @return DataDownloadResponse */ public function getJavascript() { + $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0'); $responseJS = '(function() { OCA.Theming = { name: ' . json_encode($this->template->getName()) . ', @@ -430,6 +440,7 @@ class ThemingController extends Controller { slogan: ' . json_encode($this->template->getSlogan()) . ', color: ' . json_encode($this->template->getMailHeaderColor()) . ', inverted: ' . json_encode($this->util->invertTextColor($this->template->getMailHeaderColor())) . ', + cacheBuster: ' . json_encode($cacheBusterValue). ' }; })();'; $response = new Http\DataDisplayResponse($responseJS); diff --git a/apps/theming/lib/IconBuilder.php b/apps/theming/lib/IconBuilder.php new file mode 100644 index 00000000000..d8161051ebb --- /dev/null +++ b/apps/theming/lib/IconBuilder.php @@ -0,0 +1,182 @@ +<?php +/** + * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.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 OCA\Theming; + +use Imagick; +use ImagickPixel; +use OCP\App\AppPathNotFoundException; + +class IconBuilder { + /** @var ThemingDefaults */ + private $themingDefaults; + /** @var Util */ + private $util; + + /** + * IconBuilder constructor. + * + * @param ThemingDefaults $themingDefaults + * @param Util $util + */ + public function __construct( + ThemingDefaults $themingDefaults, + Util $util + ) { + $this->themingDefaults = $themingDefaults; + $this->util = $util; + } + + /** + * @param $app string app name + * @return string|false image blob + */ + public function getFavicon($app) { + $icon = $this->renderAppIcon($app); + if($icon === false) { + return false; + } + $icon->resizeImage(32, 32, Imagick::FILTER_LANCZOS, 1); + $icon->setImageFormat("png24"); + $data = $icon->getImageBlob(); + $icon->destroy(); + return $data; + } + + /** + * @param $app string app name + * @return string|false image blob + */ + public function getTouchIcon($app) { + $icon = $this->renderAppIcon($app); + if($icon === false) { + return false; + } + $icon->setImageFormat("png24"); + $data = $icon->getImageBlob(); + $icon->destroy(); + return $data; + } + + /** + * Render app icon on themed background color + * fallback to logo + * + * @param $app string app name + * @return Imagick|false + */ + public function renderAppIcon($app) { + try { + $appIcon = $this->util->getAppIcon($app); + $appIconContent = file_get_contents($appIcon); + } catch (AppPathNotFoundException $e) { + return false; + } + + if($appIconContent === false) { + return false; + } + + $color = $this->themingDefaults->getMailHeaderColor(); + $mime = mime_content_type($appIcon); + + // generate background image with rounded corners + $background = '<?xml version="1.0" encoding="UTF-8"?>' . + '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:cc="http://creativecommons.org/ns#" width="512" height="512" xmlns:xlink="http://www.w3.org/1999/xlink">' . + '<rect x="0" y="0" rx="75" ry="75" width="512" height="512" style="fill:' . $color . ';" />' . + '</svg>'; + // resize svg magic as this seems broken in Imagemagick + if($mime === "image/svg+xml" || substr($appIconContent, 0, 4) === "<svg") { + if(substr($appIconContent, 0, 5) !== "<?xml") { + $svg = "<?xml version=\"1.0\"?>".$appIconContent; + } else { + $svg = $appIconContent; + } + $tmp = new Imagick(); + $tmp->readImageBlob($svg); + $x = $tmp->getImageWidth(); + $y = $tmp->getImageHeight(); + $res = $tmp->getImageResolution(); + $tmp->destroy(); + + if($x>$y) { + $max = $x; + } else { + $max = $y; + } + + // convert svg to resized image + $appIconFile = new Imagick(); + $resX = (int)(512 * $res['x'] / $max * 2.53); + $resY = (int)(512 * $res['y'] / $max * 2.53); + $appIconFile->setResolution($resX, $resY); + $appIconFile->setBackgroundColor(new ImagickPixel('transparent')); + $appIconFile->readImageBlob($svg); + $appIconFile->scaleImage(512, 512, true); + } else { + $appIconFile = new Imagick(); + $appIconFile->setBackgroundColor(new ImagickPixel('transparent')); + $appIconFile->readImageBlob(file_get_contents($appIcon)); + $appIconFile->scaleImage(512, 512, true); + } + + // offset for icon positioning + $border_w = (int)($appIconFile->getImageWidth() * 0.05); + $border_h = (int)($appIconFile->getImageHeight() * 0.05); + $innerWidth = (int)($appIconFile->getImageWidth() - $border_w * 2); + $innerHeight = (int)($appIconFile->getImageHeight() - $border_h * 2); + $appIconFile->adaptiveResizeImage($innerWidth, $innerHeight); + // center icon + $offset_w = 512 / 2 - $innerWidth / 2; + $offset_h = 512 / 2 - $innerHeight / 2; + + $appIconFile->setImageFormat("png24"); + + $finalIconFile = new Imagick(); + $finalIconFile->readImageBlob($background); + $finalIconFile->setImageVirtualPixelMethod(Imagick::VIRTUALPIXELMETHOD_TRANSPARENT); + $finalIconFile->setImageArtifact('compose:args', "1,0,-0.5,0.5"); + $finalIconFile->compositeImage($appIconFile, Imagick::COMPOSITE_ATOP, $offset_w, $offset_h); + $finalIconFile->resizeImage(512, 512, Imagick::FILTER_LANCZOS, 1); + + $appIconFile->destroy(); + return $finalIconFile; + } + + public function colorSvg($app, $image) { + try { + $imageFile = $this->util->getAppImage($app, $image); + } catch (AppPathNotFoundException $e) { + return false; + } + $svg = file_get_contents($imageFile); + if ($svg !== false) { + $color = $this->util->elementColor($this->themingDefaults->getMailHeaderColor()); + $svg = $this->util->colorizeSvg($svg, $color); + return $svg; + } else { + return false; + } + } + +} diff --git a/apps/theming/lib/ImageManager.php b/apps/theming/lib/ImageManager.php new file mode 100644 index 00000000000..4cd43e02054 --- /dev/null +++ b/apps/theming/lib/ImageManager.php @@ -0,0 +1,112 @@ +<?php +/** + * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.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 OCA\Theming; + +use OCP\IConfig; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; + +class ImageManager { + + /** @var IConfig */ + private $config; + /** @var IAppData */ + private $appData; + + /** + * ImageManager constructor. + * + * @param IConfig $config + * @param IAppData $appData + */ + public function __construct(IConfig $config, + IAppData $appData + ) { + $this->config = $config; + $this->appData = $appData; + } + + /** + * Get folder for current theming files + * + * @return \OCP\Files\SimpleFS\ISimpleFolder + * @throws NotPermittedException + * @throws \RuntimeException + */ + public function getCacheFolder() { + $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0'); + try { + $folder = $this->appData->getFolder($cacheBusterValue); + } catch (NotFoundException $e) { + $folder = $this->appData->newFolder($cacheBusterValue); + $this->cleanup(); + } + return $folder; + } + + /** + * Get a file from AppData + * + * @param string $filename + * @throws NotFoundException + * @return \OCP\Files\SimpleFS\ISimpleFile + */ + public function getCachedImage($filename) { + $currentFolder = $this->getCacheFolder(); + return $currentFolder->getFile($filename); + } + + /** + * Store a file for theming in AppData + * + * @param string $filename + * @param string $data + * @return \OCP\Files\SimpleFS\ISimpleFile + */ + public function setCachedImage($filename, $data) { + $currentFolder = $this->getCacheFolder(); + if ($currentFolder->fileExists($filename)) { + $file = $currentFolder->getFile($filename); + } else { + $file = $currentFolder->newFile($filename); + } + $file->putContent($data); + return $file; + } + + /** + * remove cached files that are not required any longer + */ + public function cleanup() { + $currentFolder = $this->getCacheFolder(); + $folders = $this->appData->getDirectoryListing(); + foreach ($folders as $folder) { + if ($folder->getName() !== $currentFolder->getName()) { + $folder->delete(); + } + } + } +} diff --git a/apps/theming/lib/ThemingDefaults.php b/apps/theming/lib/ThemingDefaults.php index 9139dd56247..36f19157637 100644 --- a/apps/theming/lib/ThemingDefaults.php +++ b/apps/theming/lib/ThemingDefaults.php @@ -23,6 +23,7 @@ namespace OCA\Theming; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; @@ -38,6 +39,8 @@ class ThemingDefaults extends \OC_Defaults { private $urlGenerator; /** @var IRootFolder */ private $rootFolder; + /** @var ICacheFactory */ + private $cacheFactory; /** @var string */ private $name; /** @var string */ @@ -55,18 +58,21 @@ class ThemingDefaults extends \OC_Defaults { * @param IURLGenerator $urlGenerator * @param \OC_Defaults $defaults * @param IRootFolder $rootFolder + * @param ICacheFactory $cacheFactory */ public function __construct(IConfig $config, IL10N $l, IURLGenerator $urlGenerator, \OC_Defaults $defaults, - IRootFolder $rootFolder + IRootFolder $rootFolder, + ICacheFactory $cacheFactory ) { parent::__construct(); $this->config = $config; $this->l = $l; $this->urlGenerator = $urlGenerator; $this->rootFolder = $rootFolder; + $this->cacheFactory = $cacheFactory; $this->name = $defaults->getName(); $this->url = $defaults->getBaseUrl(); @@ -145,6 +151,29 @@ class ThemingDefaults extends \OC_Defaults { } /** + * Check if Imagemagick is enabled and if SVG is supported + * otherwise we can't render custom icons + * + * @return bool + */ + public function shouldReplaceIcons() { + $cache = $this->cacheFactory->create('theming'); + if($value = $cache->get('shouldReplaceIcons')) { + return (bool)$value; + } + $value = false; + if(extension_loaded('imagick')) { + $checkImagick = new \Imagick(); + if (count($checkImagick->queryFormats('SVG')) >= 1) { + $value = true; + } + $checkImagick->clear(); + } + $cache->set('shouldReplaceIcons', $value); + return $value; + } + + /** * Increases the cache buster key */ private function increaseCacheBuster() { diff --git a/apps/theming/lib/Util.php b/apps/theming/lib/Util.php index 71ed0958e42..9fea56838ad 100644 --- a/apps/theming/lib/Util.php +++ b/apps/theming/lib/Util.php @@ -23,8 +23,35 @@ namespace OCA\Theming; +use OCP\App\AppPathNotFoundException; +use OCP\App\IAppManager; +use OCP\IConfig; +use OCP\Files\IRootFolder; + class Util { + /** @var IConfig */ + private $config; + + /** @var IRootFolder */ + private $rootFolder; + + /** @var IAppManager */ + private $appManager; + + /** + * Util constructor. + * + * @param IConfig $config + * @param IRootFolder $rootFolder + * @param IAppManager $appManager + */ + public function __construct(IConfig $config, IRootFolder $rootFolder, IAppManager $appManager) { + $this->config = $config; + $this->rootFolder = $rootFolder; + $this->appManager = $appManager; + } + /** * @param string $color rgb color value * @return bool @@ -81,4 +108,86 @@ class Util { return base64_encode($radioButtonIcon); } + + /** + * @param $app string app name + * @return string path to app icon / logo + */ + public function getAppIcon($app) { + $app = str_replace(array('\0', '/', '\\', '..'), '', $app); + try { + $appPath = $this->appManager->getAppPath($app); + $icon = $appPath . '/img/' . $app . '.svg'; + if (file_exists($icon)) { + return $icon; + } + $icon = $appPath . '/img/app.svg'; + if (file_exists($icon)) { + return $icon; + } + } catch (AppPathNotFoundException $e) {} + + if($this->config->getAppValue('theming', 'logoMime', '') !== '' && $this->rootFolder->nodeExists('/themedinstancelogo')) { + return $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/') . '/themedinstancelogo'; + } + return \OC::$SERVERROOT . '/core/img/logo.svg'; + } + + /** + * @param $app string app name + * @param $image string relative path to image in app folder + * @return string|false absolute path to image + */ + public function getAppImage($app, $image) { + $app = str_replace(array('\0', '/', '\\', '..'), '', $app); + $image = str_replace(array('\0', '\\', '..'), '', $image); + if ($app === "core") { + $icon = \OC::$SERVERROOT . '/core/img/' . $image; + if (file_exists($icon)) { + return $icon; + } + } + + try { + $appPath = $this->appManager->getAppPath($app); + } catch (AppPathNotFoundException $e) { + return false; + } + + $icon = $appPath . '/img/' . $image; + if (file_exists($icon)) { + return $icon; + } + $icon = $appPath . '/img/' . $image . '.svg'; + if (file_exists($icon)) { + return $icon; + } + $icon = $appPath . '/img/' . $image . '.png'; + if (file_exists($icon)) { + return $icon; + } + $icon = $appPath . '/img/' . $image . '.gif'; + if (file_exists($icon)) { + return $icon; + } + $icon = $appPath . '/img/' . $image . '.jpg'; + if (file_exists($icon)) { + return $icon; + } + + return false; + } + + /** + * replace default color with a custom one + * + * @param $svg string content of a svg file + * @param $color string color to match + * @return string + */ + public function colorizeSvg($svg, $color) { + $svg = preg_replace('/#0082c9/i', $color, $svg); + return $svg; + } + } diff --git a/apps/theming/tests/Controller/IconControllerTest.php b/apps/theming/tests/Controller/IconControllerTest.php new file mode 100644 index 00000000000..591c1075492 --- /dev/null +++ b/apps/theming/tests/Controller/IconControllerTest.php @@ -0,0 +1,201 @@ +<?php +/** + * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.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 OCA\Theming\Tests\Controller; + + +use OC\Files\SimpleFS\SimpleFile; +use OCA\Theming\ImageManager; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataDisplayResponse; +use OCP\AppFramework\Http\NotFoundResponse; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\IConfig; +use OCP\IRequest; +use Test\TestCase; +use OCA\Theming\Util; +use OCA\Theming\Controller\IconController; +use OCP\AppFramework\Http\FileDisplayResponse; + + +class IconControllerTest extends TestCase { + /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */ + private $request; + /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ + private $themingDefaults; + /** @var Util */ + private $util; + /** @var \OCP\AppFramework\Utility\ITimeFactory */ + private $timeFactory; + /** @var IconController|\PHPUnit_Framework_MockObject_MockObject */ + private $iconController; + /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ + private $config; + /** @var IRootFolder|\PHPUnit_Framework_MockObject_MockObject */ + private $iconBuilder; + /** @var ImageManager */ + private $imageManager; + + public function setUp() { + $this->request = $this->getMockBuilder('OCP\IRequest')->getMock(); + $this->themingDefaults = $this->getMockBuilder('OCA\Theming\ThemingDefaults') + ->disableOriginalConstructor()->getMock(); + $this->util = $this->getMockBuilder('\OCA\Theming\Util')->disableOriginalConstructor() + ->setMethods(['getAppImage', 'getAppIcon', 'elementColor'])->getMock(); + $this->timeFactory = $this->getMockBuilder('OCP\AppFramework\Utility\ITimeFactory') + ->disableOriginalConstructor() + ->getMock(); + $this->config = $this->getMockBuilder('OCP\IConfig')->getMock(); + $this->iconBuilder = $this->getMockBuilder('OCA\Theming\IconBuilder') + ->disableOriginalConstructor()->getMock(); + $this->imageManager = $this->getMockBuilder('OCA\Theming\ImageManager')->disableOriginalConstructor()->getMock(); + $this->timeFactory->expects($this->any()) + ->method('getTime') + ->willReturn(123); + + $this->iconController = new IconController( + 'theming', + $this->request, + $this->themingDefaults, + $this->util, + $this->timeFactory, + $this->config, + $this->iconBuilder, + $this->imageManager + ); + + parent::setUp(); + } + + private function iconFileMock($filename, $data) { + $icon = $this->getMockBuilder('OCP\Files\File')->getMock(); + $icon->expects($this->any())->method('getContent')->willReturn($data); + $icon->expects($this->any())->method('getMimeType')->willReturn('image type'); + $icon->expects($this->any())->method('getEtag')->willReturn('my etag'); + $icon->method('getName')->willReturn($filename); + return new SimpleFile($icon); + } + + public function testGetThemedIcon() { + $file = $this->iconFileMock('icon-core-filetypes_folder.svg', 'filecontent'); + $this->imageManager->expects($this->once()) + ->method('getCachedImage') + ->with('icon-core-filetypes_folder.svg') + ->willReturn($file); + $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/svg+xml']); + $expected->cacheFor(86400); + $expires = new \DateTime(); + $expires->setTimestamp($this->timeFactory->getTime()); + $expires->add(new \DateInterval('PT24H')); + $expected->addHeader('Expires', $expires->format(\DateTime::RFC2822)); + $expected->addHeader('Pragma', 'cache'); + @$this->assertEquals($expected, $this->iconController->getThemedIcon('core', 'filetypes/folder.svg')); + } + + public function testGetFaviconDefault() { + if (!extension_loaded('imagick')) { + $this->markTestSkipped('Imagemagick is required for dynamic icon generation.'); + } + $checkImagick = new \Imagick(); + if (count($checkImagick->queryFormats('SVG')) < 1) { + $this->markTestSkipped('No SVG provider present.'); + } + $this->themingDefaults->expects($this->any()) + ->method('shouldReplaceIcons') + ->willReturn(true); + + $this->iconBuilder->expects($this->once()) + ->method('getFavicon') + ->with('core') + ->willReturn('filecontent'); + $file = $this->iconFileMock('filename', 'filecontent'); + $this->imageManager->expects($this->once()) + ->method('getCachedImage') + ->will($this->throwException(new NotFoundException())); + $this->imageManager->expects($this->once()) + ->method('setCachedImage') + ->willReturn($file); + + $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']); + $expected->cacheFor(86400); + $expires = new \DateTime(); + $expires->setTimestamp($this->timeFactory->getTime()); + $expires->add(new \DateInterval('PT24H')); + $expected->addHeader('Expires', $expires->format(\DateTime::RFC2822)); + $expected->addHeader('Pragma', 'cache'); + $this->assertEquals($expected, $this->iconController->getFavicon()); + } + + public function testGetFaviconFail() { + $this->themingDefaults->expects($this->any()) + ->method('shouldReplaceIcons') + ->willReturn(false); + $expected = new Http\Response(); + $expected->setStatus(Http::STATUS_NOT_FOUND); + $expected->cacheFor(0); + $expected->setLastModified(new \DateTime('now', new \DateTimeZone('GMT'))); + $this->assertInstanceOf(NotFoundResponse::class, $this->iconController->getFavicon()); + } + + public function testGetTouchIconDefault() { + if (!extension_loaded('imagick')) { + $this->markTestSkipped('Imagemagick is required for dynamic icon generation.'); + } + $checkImagick = new \Imagick(); + if (count($checkImagick->queryFormats('SVG')) < 1) { + $this->markTestSkipped('No SVG provider present.'); + } + $this->themingDefaults->expects($this->any()) + ->method('shouldReplaceIcons') + ->willReturn(true); + + $this->iconBuilder->expects($this->once()) + ->method('getTouchIcon') + ->with('core') + ->willReturn('filecontent'); + $file = $this->iconFileMock('filename', 'filecontent'); + $this->imageManager->expects($this->once()) + ->method('getCachedImage') + ->will($this->throwException(new NotFoundException())); + $this->imageManager->expects($this->once()) + ->method('setCachedImage') + ->willReturn($file); + + $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/png']); + $expected->cacheFor(86400); + $expires = new \DateTime(); + $expires->setTimestamp($this->timeFactory->getTime()); + $expires->add(new \DateInterval('PT24H')); + $expected->addHeader('Expires', $expires->format(\DateTime::RFC2822)); + $expected->addHeader('Pragma', 'cache'); + $this->assertEquals($expected, $this->iconController->getTouchIcon()); + } + + public function testGetTouchIconFail() { + $this->themingDefaults->expects($this->any()) + ->method('shouldReplaceIcons') + ->willReturn(false); + $this->assertInstanceOf(NotFoundResponse::class, $this->iconController->getTouchIcon()); + } + +} diff --git a/apps/theming/tests/Controller/ThemingControllerTest.php b/apps/theming/tests/Controller/ThemingControllerTest.php index c9f6ff8a885..d42e5afb245 100644 --- a/apps/theming/tests/Controller/ThemingControllerTest.php +++ b/apps/theming/tests/Controller/ThemingControllerTest.php @@ -26,6 +26,7 @@ namespace OCA\Theming\Tests\Controller; use OCA\Theming\Controller\ThemingController; use OCA\Theming\Util; +use OCP\App\IAppManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\Files\File; @@ -57,18 +58,21 @@ class ThemingControllerTest extends TestCase { private $rootFolder; /** @var ITempManager */ private $tempManager; + /** @var IAppManager */ + private $appManager; public function setUp() { $this->request = $this->getMockBuilder('OCP\IRequest')->getMock(); $this->config = $this->getMockBuilder('OCP\IConfig')->getMock(); $this->template = $this->getMockBuilder('OCA\Theming\ThemingDefaults') ->disableOriginalConstructor()->getMock(); - $this->util = new Util(); $this->timeFactory = $this->getMockBuilder('OCP\AppFramework\Utility\ITimeFactory') ->disableOriginalConstructor() ->getMock(); $this->l10n = $this->getMockBuilder('OCP\IL10N')->getMock(); $this->rootFolder = $this->getMockBuilder('OCP\Files\IRootFolder')->getMock(); + $this->appManager = $this->getMockBuilder('OCP\App\IAppManager')->getMock(); + $this->util = new Util($this->config, $this->rootFolder, $this->appManager); $this->timeFactory->expects($this->any()) ->method('getTime') ->willReturn(123); @@ -484,6 +488,12 @@ class ThemingControllerTest extends TestCase { $expectedData .= sprintf('.nc-theming-main-background {background-color: %s}' . "\n", $color); $expectedData .= sprintf('.nc-theming-main-text {color: %s}' . "\n", $color); $expectedData .= '.nc-theming-contrast {color: #ffffff}' . "\n"; + $expectedData .= '.icon-file,.icon-filetype-text {' . + 'background-image: url(\'./img/core/filetypes/text.svg?v=0\');' . "}\n" . + '.icon-folder, .icon-filetype-folder {' . + 'background-image: url(\'./img/core/filetypes/folder.svg?v=0\');' . "}\n" . + '.icon-filetype-folder-drag-accept {' . + 'background-image: url(\'./img/core/filetypes/folder-drag-accept.svg?v=0\')!important;' . "}\n"; $expected = new Http\DataDownloadResponse($expectedData, 'style', 'text/css'); @@ -578,6 +588,12 @@ class ThemingControllerTest extends TestCase { $expectedData .= '#body-login input.login { background-image: url(\'' . \OC::$WEBROOT . '/core/img/actions/confirm.svg?v=2\'); }' . "\n"; $expectedData .= '.nc-theming-contrast {color: #000000}' . "\n"; $expectedData .= '.ui-widget-header { color: #000000; }' . "\n"; + $expectedData .= '.icon-file,.icon-filetype-text {' . + 'background-image: url(\'./img/core/filetypes/text.svg?v=0\');' . "}\n" . + '.icon-folder, .icon-filetype-folder {' . + 'background-image: url(\'./img/core/filetypes/folder.svg?v=0\');' . "}\n" . + '.icon-filetype-folder-drag-accept {' . + 'background-image: url(\'./img/core/filetypes/folder-drag-accept.svg?v=0\')!important;' . "}\n"; $expected = new Http\DataDownloadResponse($expectedData, 'style', 'text/css'); @@ -623,6 +639,12 @@ class ThemingControllerTest extends TestCase { 'background-size: contain;' . '}' . "\n"; $expectedData .= '.nc-theming-contrast {color: #ffffff}' . "\n"; + $expectedData .= '.icon-file,.icon-filetype-text {' . + 'background-image: url(\'./img/core/filetypes/text.svg?v=0\');' . "}\n" . + '.icon-folder, .icon-filetype-folder {' . + 'background-image: url(\'./img/core/filetypes/folder.svg?v=0\');' . "}\n" . + '.icon-filetype-folder-drag-accept {' . + 'background-image: url(\'./img/core/filetypes/folder-drag-accept.svg?v=0\')!important;' . "}\n"; $expected = new Http\DataDownloadResponse($expectedData, 'style', 'text/css'); @@ -765,6 +787,12 @@ class ThemingControllerTest extends TestCase { 'background-image: url(\'./loginbackground?v=0\');' . '}' . "\n"; $expectedData .= '.nc-theming-contrast {color: #ffffff}' . "\n"; + $expectedData .= '.icon-file,.icon-filetype-text {' . + 'background-image: url(\'./img/core/filetypes/text.svg?v=0\');' . "}\n" . + '.icon-folder, .icon-filetype-folder {' . + 'background-image: url(\'./img/core/filetypes/folder.svg?v=0\');' . "}\n" . + '.icon-filetype-folder-drag-accept {' . + 'background-image: url(\'./img/core/filetypes/folder-drag-accept.svg?v=0\')!important;' . "}\n"; $expected = new Http\DataDownloadResponse($expectedData, 'style', 'text/css'); $expected->cacheFor(3600); @@ -876,6 +904,13 @@ class ThemingControllerTest extends TestCase { $expectedData .= '#body-login input.login { background-image: url(\'' . \OC::$WEBROOT . '/core/img/actions/confirm.svg?v=2\'); }' . "\n"; $expectedData .= '.nc-theming-contrast {color: #000000}' . "\n"; $expectedData .= '.ui-widget-header { color: #000000; }' . "\n"; + $expectedData .= '.icon-file,.icon-filetype-text {' . + 'background-image: url(\'./img/core/filetypes/text.svg?v=0\');' . "}\n" . + '.icon-folder, .icon-filetype-folder {' . + 'background-image: url(\'./img/core/filetypes/folder.svg?v=0\');' . "}\n" . + '.icon-filetype-folder-drag-accept {' . + 'background-image: url(\'./img/core/filetypes/folder-drag-accept.svg?v=0\')!important;' . "}\n"; + $expected = new Http\DataDownloadResponse($expectedData, 'style', 'text/css'); $expected = new Http\DataDownloadResponse($expectedData, 'style', 'text/css'); $expected->cacheFor(3600); @@ -910,6 +945,7 @@ class ThemingControllerTest extends TestCase { slogan: "", color: "#000", inverted: false, + cacheBuster: null }; })();'; $expected = new Http\DataDisplayResponse($expectedResponse); @@ -944,6 +980,7 @@ class ThemingControllerTest extends TestCase { slogan: "awesome", color: "#ffffff", inverted: true, + cacheBuster: null }; })();'; $expected = new Http\DataDisplayResponse($expectedResponse); diff --git a/apps/theming/tests/IconBuilderTest.php b/apps/theming/tests/IconBuilderTest.php new file mode 100644 index 00000000000..54850c8f3c2 --- /dev/null +++ b/apps/theming/tests/IconBuilderTest.php @@ -0,0 +1,192 @@ +<?php +/** + * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.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 OCA\Theming\Tests; + +use OCA\Theming\IconBuilder; +use OCA\Theming\ThemingDefaults; +use OCA\Theming\Util; +use OCP\App\IAppManager; +use OCP\AppFramework\Http\NotFoundResponse; +use OCP\Files\IRootFolder; +use OCP\IConfig; +use Test\TestCase; + +class IconBuilderTest extends TestCase { + + /** @var IConfig */ + protected $config; + /** @var IRootFolder */ + protected $rootFolder; + /** @var ThemingDefaults */ + protected $themingDefaults; + /** @var Util */ + protected $util; + /** @var IconBuilder */ + protected $iconBuilder; + /** @var IAppManager */ + protected $appManager; + + protected function setUp() { + parent::setUp(); + + $this->config = $this->getMockBuilder('\OCP\IConfig')->getMock(); + $this->rootFolder = $this->getMockBuilder('OCP\Files\IRootFolder')->getMock(); + $this->themingDefaults = $this->getMockBuilder('OCA\Theming\ThemingDefaults') + ->disableOriginalConstructor()->getMock(); + $this->appManager = $this->getMockBuilder('OCP\App\IAppManager')->getMock(); + $this->util = new Util($this->config, $this->rootFolder, $this->appManager); + $this->iconBuilder = new IconBuilder($this->themingDefaults, $this->util); + } + + private function checkImagick() { + if(!extension_loaded('imagick')) { + $this->markTestSkipped('Imagemagick is required for dynamic icon generation.'); + } + $checkImagick = new \Imagick(); + if (count($checkImagick->queryFormats('SVG')) < 1) { + $this->markTestSkipped('No SVG provider present.'); + } + } + + public function dataRenderAppIcon() { + return [ + ['core', '#0082c9', 'touch-original.png'], + ['core', '#FF0000', 'touch-core-red.png'], + ['testing', '#FF0000', 'touch-testing-red.png'], + ['comments', '#0082c9', 'touch-comments.png'], + ['core', '#0082c9', 'touch-original-png.png'], + ]; + } + + /** + * @dataProvider dataRenderAppIcon + * @param $app + * @param $color + * @param $file + */ + public function testRenderAppIcon($app, $color, $file) { + $this->checkImagick(); + $this->themingDefaults->expects($this->once()) + ->method('getMailHeaderColor') + ->willReturn($color); + + $expectedIcon = new \Imagick(realpath(dirname(__FILE__)). "/data/" . $file); + $icon = $this->iconBuilder->renderAppIcon($app); + + $this->assertEquals(true, $icon->valid()); + $this->assertEquals(512, $icon->getImageWidth()); + $this->assertEquals(512, $icon->getImageHeight()); + $this->assertEquals($icon, $expectedIcon); + $icon->destroy(); + $expectedIcon->destroy(); + // FIXME: We may need some comparison of the generated and the test images + // cloud be something like $expectedIcon->compareImages($icon, Imagick::METRIC_MEANABSOLUTEERROR)[1]) + } + + /** + * @dataProvider dataRenderAppIcon + * @param $app + * @param $color + * @param $file + */ + public function testGetTouchIcon($app, $color, $file) { + $this->checkImagick(); + $this->themingDefaults->expects($this->once()) + ->method('getMailHeaderColor') + ->willReturn($color); + + $expectedIcon = new \Imagick(realpath(dirname(__FILE__)). "/data/" . $file); + $icon = new \Imagick(); + $icon->readImageBlob($this->iconBuilder->getTouchIcon($app)); + + $this->assertEquals(true, $icon->valid()); + $this->assertEquals(512, $icon->getImageWidth()); + $this->assertEquals(512, $icon->getImageHeight()); + $this->assertEquals($icon, $expectedIcon); + $icon->destroy(); + $expectedIcon->destroy(); + // FIXME: We may need some comparison of the generated and the test images + // cloud be something like $expectedIcon->compareImages($icon, Imagick::METRIC_MEANABSOLUTEERROR)[1]) + } + + /** + * @dataProvider dataRenderAppIcon + * @param $app + * @param $color + * @param $file + */ + public function testGetFavicon($app, $color, $file) { + $this->checkImagick(); + $this->themingDefaults->expects($this->once()) + ->method('getMailHeaderColor') + ->willReturn($color); + + $expectedIcon = new \Imagick(realpath(dirname(__FILE__)). "/data/" . $file); + $icon = new \Imagick(); + $icon->readImageBlob($this->iconBuilder->getFavicon($app)); + + $this->assertEquals(true, $icon->valid()); + $this->assertEquals(32, $icon->getImageWidth()); + $this->assertEquals(32, $icon->getImageHeight()); + $icon->destroy(); + $expectedIcon->destroy(); + // FIXME: We may need some comparison of the generated and the test images + // cloud be something like $expectedIcon->compareImages($icon, Imagick::METRIC_MEANABSOLUTEERROR)[1]) + } + + /** + * @expectedException \PHPUnit_Framework_Error_Warning + */ + public function testGetFaviconNotFound() { + $util = $this->getMockBuilder(Util::class)->disableOriginalConstructor()->getMock(); + $iconBuilder = new IconBuilder($this->themingDefaults, $util); + $util->expects($this->once()) + ->method('getAppIcon') + ->willReturn('notexistingfile'); + $this->assertFalse($iconBuilder->getFavicon('noapp')); + } + + /** + * @expectedException \PHPUnit_Framework_Error_Warning + */ + public function testGetTouchIconNotFound() { + $util = $this->getMockBuilder(Util::class)->disableOriginalConstructor()->getMock(); + $iconBuilder = new IconBuilder($this->themingDefaults, $util); + $util->expects($this->once()) + ->method('getAppIcon') + ->willReturn('notexistingfile'); + $this->assertFalse($iconBuilder->getTouchIcon('noapp')); + } + + /** + * @expectedException \PHPUnit_Framework_Error_Warning + */ + public function testColorSvgNotFound() { + $util = $this->getMockBuilder(Util::class)->disableOriginalConstructor()->getMock(); + $iconBuilder = new IconBuilder($this->themingDefaults, $util); + $util->expects($this->once()) + ->method('getAppImage') + ->willReturn('notexistingfile'); + $this->assertFalse($iconBuilder->colorSvg('noapp','noimage')); + } +} diff --git a/apps/theming/tests/ImageManagerTest.php b/apps/theming/tests/ImageManagerTest.php new file mode 100644 index 00000000000..4df49633d80 --- /dev/null +++ b/apps/theming/tests/ImageManagerTest.php @@ -0,0 +1,183 @@ +<?php +/** + * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.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 OCA\Theming\Tests; + +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\IConfig; +use Test\TestCase; +use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; + +class ImageManager extends TestCase { + + /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ + protected $config; + /** @var IAppData|\PHPUnit_Framework_MockObject_MockObject */ + protected $appData; + /** @var ImageManager */ + protected $imageManager; + + protected function setUp() { + parent::setUp(); + $this->config = $this->getMockBuilder('\OCP\IConfig')->getMock(); + $this->appData = $this->getMockBuilder('OCP\Files\IAppData')->getMock(); + $this->imageManager = new \OCA\Theming\ImageManager( + $this->config, + $this->appData + ); + } + + public function testGetCacheFolder() { + $folder = $this->createMock(ISimpleFolder::class); + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('theming', 'cachebuster', '0') + ->willReturn('0'); + $this->appData->expects($this->at(0)) + ->method('getFolder') + ->with('0') + ->willReturn($folder); + $this->assertEquals($folder, $this->imageManager->getCacheFolder()); + } + public function testGetCacheFolderCreate() { + $folder = $this->createMock(ISimpleFolder::class); + $this->config->expects($this->exactly(2)) + ->method('getAppValue') + ->with('theming', 'cachebuster', '0') + ->willReturn('0'); + $this->appData->expects($this->at(0)) + ->method('getFolder') + ->willThrowException(new NotFoundException()); + $this->appData->expects($this->at(1)) + ->method('newFolder') + ->with('0') + ->willReturn($folder); + $this->appData->expects($this->at(2)) + ->method('getFolder') + ->with('0') + ->willReturn($folder); + $this->appData->expects($this->once()) + ->method('getDirectoryListing') + ->willReturn([]); + $this->assertEquals($folder, $this->imageManager->getCacheFolder()); + } + + public function testGetCachedImage() { + $folder = $this->setupCacheFolder(); + $folder->expects($this->once()) + ->method('getFile') + ->with('filename') + ->willReturn('filecontent'); + $expected = 'filecontent'; + $this->assertEquals($expected, $this->imageManager->getCachedImage('filename')); + } + + /** + * @expectedException \OCP\Files\NotFoundException + */ + public function testGetCachedImageNotFound() { + $folder = $this->setupCacheFolder(); + $folder->expects($this->once()) + ->method('getFile') + ->with('filename') + ->will($this->throwException(new \OCP\Files\NotFoundException())); + $image = $this->imageManager->getCachedImage('filename'); + } + + public function testSetCachedImage() { + $folder = $this->setupCacheFolder(); + $file = $this->createMock(ISimpleFile::class); + $folder->expects($this->once()) + ->method('fileExists') + ->with('filename') + ->willReturn(true); + $folder->expects($this->once()) + ->method('getFile') + ->with('filename') + ->willReturn($file); + $file->expects($this->once()) + ->method('putContent') + ->with('filecontent'); + $this->assertEquals($file, $this->imageManager->setCachedImage('filename', 'filecontent')); + } + + public function testSetCachedImageCreate() { + $folder = $this->setupCacheFolder(); + $file = $this->createMock(ISimpleFile::class); + $folder->expects($this->once()) + ->method('fileExists') + ->with('filename') + ->willReturn(false); + $folder->expects($this->once()) + ->method('newFile') + ->with('filename') + ->willReturn($file); + $file->expects($this->once()) + ->method('putContent') + ->with('filecontent'); + $this->assertEquals($file, $this->imageManager->setCachedImage('filename', 'filecontent')); + } + + private function setupCacheFolder() { + $folder = $this->createMock(ISimpleFolder::class); + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('theming', 'cachebuster', '0') + ->willReturn('0'); + $this->appData->expects($this->at(0)) + ->method('getFolder') + ->with('0') + ->willReturn($folder); + return $folder; + } + + public function testCleanup() { + $folders = [ + $this->createMock(ISimpleFolder::class), + $this->createMock(ISimpleFolder::class), + $this->createMock(ISimpleFolder::class) + ]; + foreach ($folders as $index=>$folder) { + $folder->expects($this->any()) + ->method('getName') + ->willReturn($index); + } + $folders[0]->expects($this->once())->method('delete'); + $folders[1]->expects($this->once())->method('delete'); + $folders[2]->expects($this->never())->method('delete'); + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('theming','cachebuster','0') + ->willReturn('2'); + $this->appData->expects($this->once()) + ->method('getDirectoryListing') + ->willReturn($folders); + $this->appData->expects($this->once()) + ->method('getFolder') + ->with('2') + ->willReturn($folders[2]); + $this->imageManager->cleanup(); + } + +} diff --git a/apps/theming/tests/ThemingDefaultsTest.php b/apps/theming/tests/ThemingDefaultsTest.php index 204c96d86d5..cd3a90e760a 100644 --- a/apps/theming/tests/ThemingDefaultsTest.php +++ b/apps/theming/tests/ThemingDefaultsTest.php @@ -24,6 +24,7 @@ namespace OCA\Theming\Tests; use OCA\Theming\ThemingDefaults; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; @@ -43,6 +44,8 @@ class ThemingDefaultsTest extends TestCase { private $template; /** @var IRootFolder */ private $rootFolder; + /** @var ICacheFactory */ + private $cacheFactory; public function setUp() { parent::setUp(); @@ -52,6 +55,7 @@ class ThemingDefaultsTest extends TestCase { $this->rootFolder = $this->getMockBuilder(IRootFolder::class) ->disableOriginalConstructor() ->getMock(); + $this->cacheFactory = $this->getMockBuilder(ICacheFactory::class)->getMock(); $this->defaults = $this->getMockBuilder(\OC_Defaults::class) ->disableOriginalConstructor() ->getMock(); @@ -76,7 +80,8 @@ class ThemingDefaultsTest extends TestCase { $this->l10n, $this->urlGenerator, $this->defaults, - $this->rootFolder + $this->rootFolder, + $this->cacheFactory ); } diff --git a/apps/theming/tests/UtilTest.php b/apps/theming/tests/UtilTest.php index c7fc385d25d..83895208fea 100644 --- a/apps/theming/tests/UtilTest.php +++ b/apps/theming/tests/UtilTest.php @@ -23,16 +23,28 @@ namespace OCA\Theming\Tests; use OCA\Theming\Util; +use OCP\App\IAppManager; +use OCP\IConfig; +use OCP\Files\IRootFolder; use Test\TestCase; class UtilTest extends TestCase { /** @var Util */ protected $util; + /** @var IConfig */ + protected $config; + /** @var IRootFolder */ + protected $rootFolder; + /** @var IAppManager */ + protected $appManager; protected function setUp() { parent::setUp(); - $this->util = new Util(); + $this->config = $this->getMockBuilder('\OCP\IConfig')->getMock(); + $this->rootFolder = $this->getMockBuilder('OCP\Files\IRootFolder')->getMock(); + $this->appManager = $this->getMockBuilder('OCP\App\IAppManager')->getMock(); + $this->util = new Util($this->config, $this->rootFolder, $this->appManager); } public function testInvertTextColorLight() { @@ -89,9 +101,70 @@ class UtilTest extends TestCase { $expected = 'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTYiIHdpZHRoPSIxNiI+PHBhdGggZD0iTTggMWE3IDcgMCAwIDAtNyA3IDcgNyAwIDAgMCA3IDcgNyA3IDAgMCAwIDctNyA3IDcgMCAwIDAtNy03em0wIDFhNiA2IDAgMCAxIDYgNiA2IDYgMCAwIDEtNiA2IDYgNiAwIDAgMS02LTYgNiA2IDAgMCAxIDYtNnptMCAyYTQgNCAwIDEgMCAwIDggNCA0IDAgMCAwIDAtOHoiIGZpbGw9IiNmZmZmZmYiLz48L3N2Zz4='; $this->assertEquals($expected, $button); } + public function testGenerateRadioButtonBlack() { $button = $this->util->generateRadioButton('#000000'); $expected = 'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTYiIHdpZHRoPSIxNiI+PHBhdGggZD0iTTggMWE3IDcgMCAwIDAtNyA3IDcgNyAwIDAgMCA3IDcgNyA3IDAgMCAwIDctNyA3IDcgMCAwIDAtNy03em0wIDFhNiA2IDAgMCAxIDYgNiA2IDYgMCAwIDEtNiA2IDYgNiAwIDAgMS02LTYgNiA2IDAgMCAxIDYtNnptMCAyYTQgNCAwIDEgMCAwIDggNCA0IDAgMCAwIDAtOHoiIGZpbGw9IiMwMDAwMDAiLz48L3N2Zz4='; $this->assertEquals($expected, $button); } + + /** + * @dataProvider dataGetAppIcon + */ + public function testGetAppIcon($app, $expected) { + $this->appManager->expects($this->once()) + ->method('getAppPath') + ->with($app) + ->willReturn(\OC_App::getAppPath($app)); + $icon = $this->util->getAppIcon($app); + $this->assertEquals($expected, $icon); + } + + public function dataGetAppIcon() { + return [ + ['user_ldap', \OC_App::getAppPath('user_ldap') . '/img/app.svg'], + ['noapplikethis', \OC::$SERVERROOT . '/core/img/logo.svg'], + ['comments', \OC_App::getAppPath('comments') . '/img/comments.svg'], + ]; + } + + public function testGetAppIconThemed() { + $this->rootFolder->expects($this->once()) + ->method('nodeExists') + ->with('/themedinstancelogo') + ->willReturn(true); + $expected = '/themedinstancelogo'; + $icon = $this->util->getAppIcon('noapplikethis'); + $this->assertEquals($expected, $icon); + } + + /** + * @dataProvider dataGetAppImage + */ + public function testGetAppImage($app, $image, $expected) { + if($app !== 'core') { + $this->appManager->expects($this->once()) + ->method('getAppPath') + ->with($app) + ->willReturn(\OC_App::getAppPath($app)); + } + $this->assertEquals($expected, $this->util->getAppImage($app, $image)); + } + + public function dataGetAppImage() { + return [ + ['core', 'logo.svg', \OC::$SERVERROOT . '/core/img/logo.svg'], + ['files', 'external', \OC::$SERVERROOT . '/apps/files/img/external.svg'], + ['files', 'external.svg', \OC::$SERVERROOT . '/apps/files/img/external.svg'], + ['noapplikethis', 'foobar.svg', false], + ]; + } + + public function testColorizeSvg() { + $input = "#0082c9 #0082C9 #000000 #FFFFFF"; + $expected = "#AAAAAA #AAAAAA #000000 #FFFFFF"; + $result = $this->util->colorizeSvg($input, '#AAAAAA'); + $this->assertEquals($expected, $result); + } + } diff --git a/apps/theming/tests/data/favicon-original.ico b/apps/theming/tests/data/favicon-original.ico Binary files differnew file mode 100644 index 00000000000..fab2f7f0231 --- /dev/null +++ b/apps/theming/tests/data/favicon-original.ico diff --git a/apps/theming/tests/data/touch-comments.png b/apps/theming/tests/data/touch-comments.png Binary files differnew file mode 100644 index 00000000000..af0d2ca579b --- /dev/null +++ b/apps/theming/tests/data/touch-comments.png diff --git a/apps/theming/tests/data/touch-core-red.png b/apps/theming/tests/data/touch-core-red.png Binary files differnew file mode 100644 index 00000000000..7a492f10e21 --- /dev/null +++ b/apps/theming/tests/data/touch-core-red.png diff --git a/apps/theming/tests/data/touch-original-png.png b/apps/theming/tests/data/touch-original-png.png Binary files differnew file mode 100644 index 00000000000..6997dfc6b88 --- /dev/null +++ b/apps/theming/tests/data/touch-original-png.png diff --git a/apps/theming/tests/data/touch-original.png b/apps/theming/tests/data/touch-original.png Binary files differnew file mode 100644 index 00000000000..ac39872aa6e --- /dev/null +++ b/apps/theming/tests/data/touch-original.png diff --git a/apps/theming/tests/data/touch-testing-red.png b/apps/theming/tests/data/touch-testing-red.png Binary files differnew file mode 100644 index 00000000000..4cbdfbbb42c --- /dev/null +++ b/apps/theming/tests/data/touch-testing-red.png diff --git a/core/js/mimetype.js b/core/js/mimetype.js index 0d30da26c26..8920fe09a7e 100644 --- a/core/js/mimetype.js +++ b/core/js/mimetype.js @@ -91,6 +91,11 @@ OC.MimeType = { path += icon; } } + if(OCA.Theming) { + path = OC.generateUrl('/apps/theming/img/core/filetypes/'); + path += OC.MimeType._getFile(mimeType, OC.MimeTypeList.files); + gotIcon = true; + } // If we do not yet have an icon fall back to the default if (gotIcon === null) { @@ -100,6 +105,10 @@ OC.MimeType = { path += '.svg'; + if(OCA.Theming) { + path += "?v=" + OCA.Theming.cacheBuster; + } + // Cache the result OC.MimeType._mimeTypeIcons[mimeType] = path; return path; diff --git a/lib/private/Server.php b/lib/private/Server.php index c6755357a1d..c6cfa018be5 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -703,7 +703,8 @@ class Server extends ServerContainer implements IServerContainer { $c->getL10N('theming'), $c->getURLGenerator(), new \OC_Defaults(), - $c->getLazyRootFolder() + $c->getLazyRootFolder(), + $c->getMemCacheFactory() ); } return new \OC_Defaults(); diff --git a/lib/private/URLGenerator.php b/lib/private/URLGenerator.php index 93517dc9f7e..bdf7bdafae3 100644 --- a/lib/private/URLGenerator.php +++ b/lib/private/URLGenerator.php @@ -157,7 +157,15 @@ class URLGenerator implements IURLGenerator { // Check if the app is in the app folder $path = ''; - if (file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$app/img/$image")) { + if(\OCP\App::isEnabled('theming') && $image === "favicon.ico" && \OC::$server->getThemingDefaults()->shouldReplaceIcons()) { + $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0'); + if($app==="") { $app = "core"; } + $path = $this->linkToRoute('theming.Icon.getFavicon', [ 'app' => $app ]) . '?v='. $cacheBusterValue; + } elseif(\OCP\App::isEnabled('theming') && $image === "favicon-touch.png" && \OC::$server->getThemingDefaults()->shouldReplaceIcons()) { + $cacheBusterValue = $this->config->getAppValue('theming', 'cachebuster', '0'); + if($app==="") { $app = "core"; } + $path = $this->linkToRoute('theming.Icon.getTouchIcon', [ 'app' => $app ]) . '?v='. $cacheBusterValue; + } elseif (file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$app/img/$image")) { $path = \OC::$WEBROOT . "/themes/$theme/apps/$app/img/$image"; } elseif (!file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$app/img/$basename.svg") && file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$app/img/$basename.png")) { |