Merge pull request #840 from nextcloud/theming-icon-endpoint
Add dynamic icon creationpull/2133/head
commit
faee255ff4
@ -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(); |
||||
} |
||||
} |
||||
@ -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; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -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(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -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()); |
||||
} |
||||
|
||||
} |
||||
@ -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')); |
||||
} |
||||
} |
||||
@ -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(); |
||||
} |
||||
|
||||
} |
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 23 KiB |
Loading…
Reference in new issue