summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMorris Jobke <hey@morrisjobke.de>2016-11-18 15:35:34 +0100
committerGitHub <noreply@github.com>2016-11-18 15:35:34 +0100
commitfaee255ff47873ed2f8908c7d6b6e603ded11618 (patch)
tree87a46252211e9c0dbab91609e4115082401656ee
parente8511660f01faeebcc692b57cabdacd97845da92 (diff)
parent2ab4d1e0a3f15af2b8f04edcf18b7fe3fc0be262 (diff)
downloadnextcloud-server-faee255ff47873ed2f8908c7d6b6e603ded11618.tar.gz
nextcloud-server-faee255ff47873ed2f8908c7d6b6e603ded11618.zip
Merge pull request #840 from nextcloud/theming-icon-endpoint
Add dynamic icon creation
-rw-r--r--apps/theming/appinfo/routes.php18
-rw-r--r--apps/theming/lib/Controller/IconController.php174
-rw-r--r--apps/theming/lib/Controller/ThemingController.php11
-rw-r--r--apps/theming/lib/IconBuilder.php182
-rw-r--r--apps/theming/lib/ImageManager.php112
-rw-r--r--apps/theming/lib/ThemingDefaults.php31
-rw-r--r--apps/theming/lib/Util.php109
-rw-r--r--apps/theming/tests/Controller/IconControllerTest.php201
-rw-r--r--apps/theming/tests/Controller/ThemingControllerTest.php39
-rw-r--r--apps/theming/tests/IconBuilderTest.php192
-rw-r--r--apps/theming/tests/ImageManagerTest.php183
-rw-r--r--apps/theming/tests/ThemingDefaultsTest.php7
-rw-r--r--apps/theming/tests/UtilTest.php75
-rw-r--r--apps/theming/tests/data/favicon-original.icobin0 -> 1618 bytes
-rw-r--r--apps/theming/tests/data/touch-comments.pngbin0 -> 21814 bytes
-rw-r--r--apps/theming/tests/data/touch-core-red.pngbin0 -> 23415 bytes
-rw-r--r--apps/theming/tests/data/touch-original-png.pngbin0 -> 32785 bytes
-rw-r--r--apps/theming/tests/data/touch-original.pngbin0 -> 29078 bytes
-rw-r--r--apps/theming/tests/data/touch-testing-red.pngbin0 -> 23438 bytes
-rw-r--r--core/js/mimetype.js9
-rw-r--r--lib/private/Server.php3
-rw-r--r--lib/private/URLGenerator.php10
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
new file mode 100644
index 00000000000..fab2f7f0231
--- /dev/null
+++ b/apps/theming/tests/data/favicon-original.ico
Binary files differ
diff --git a/apps/theming/tests/data/touch-comments.png b/apps/theming/tests/data/touch-comments.png
new file mode 100644
index 00000000000..af0d2ca579b
--- /dev/null
+++ b/apps/theming/tests/data/touch-comments.png
Binary files differ
diff --git a/apps/theming/tests/data/touch-core-red.png b/apps/theming/tests/data/touch-core-red.png
new file mode 100644
index 00000000000..7a492f10e21
--- /dev/null
+++ b/apps/theming/tests/data/touch-core-red.png
Binary files differ
diff --git a/apps/theming/tests/data/touch-original-png.png b/apps/theming/tests/data/touch-original-png.png
new file mode 100644
index 00000000000..6997dfc6b88
--- /dev/null
+++ b/apps/theming/tests/data/touch-original-png.png
Binary files differ
diff --git a/apps/theming/tests/data/touch-original.png b/apps/theming/tests/data/touch-original.png
new file mode 100644
index 00000000000..ac39872aa6e
--- /dev/null
+++ b/apps/theming/tests/data/touch-original.png
Binary files differ
diff --git a/apps/theming/tests/data/touch-testing-red.png b/apps/theming/tests/data/touch-testing-red.png
new file mode 100644
index 00000000000..4cbdfbbb42c
--- /dev/null
+++ b/apps/theming/tests/data/touch-testing-red.png
Binary files differ
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")) {