diff --git a/application/Config/ContentSecurityPolicy.php b/application/Config/ContentSecurityPolicy.php index ca46c513f7..2cd5029258 100644 --- a/application/Config/ContentSecurityPolicy.php +++ b/application/Config/ContentSecurityPolicy.php @@ -38,6 +38,8 @@ class ContentSecurityPolicy extends BaseConfig public $mediaSrc = null; public $objectSrc = null; + + public $manifestSrc = null; public $pluginTypes = null; diff --git a/system/Files/Exceptions/FileException.php b/system/Files/Exceptions/FileException.php index e8950abc16..f16827f713 100644 --- a/system/Files/Exceptions/FileException.php +++ b/system/Files/Exceptions/FileException.php @@ -10,4 +10,14 @@ class FileException extends \RuntimeException implements ExceptionInterface return new static(lang('Files.cannotMove', [$from, $to, $error])); } + public static function forInvalidFilename(string $to = null) + { + return new self(lang('Files.invalidFilename', [$to])); + } + + public static function forCopyError(string $to = null) + { + return new self(lang('Files.cannotCopy', [$to])); + } + } diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index 61f74d9721..33cc6325e8 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -140,6 +140,12 @@ class ContentSecurityPolicy * @var array */ protected $styleSrc = []; + + /** + * Used for security enforcement + * @var array + */ + protected $manifestSrc = []; /** * Used for security enforcement @@ -432,6 +438,26 @@ class ContentSecurityPolicy return $this; } + + //-------------------------------------------------------------------- + + /** + * Adds a new valid endpoint for manifest sources. Can be either + * a URI class or simple string. + * + * @see https://www.w3.org/TR/CSP/#directive-manifest-src + * + * @param $uri + * @param bool $reportOnly + * + * @return $this + */ + public function addManifestSrc($uri, bool $reportOnly = false) + { + $this->addOption($uri, 'manifestSrc', $reportOnly); + + return $this; + } //-------------------------------------------------------------------- @@ -688,6 +714,7 @@ class ContentSecurityPolicy 'plugin-types' => 'pluginTypes', 'script-src' => 'scriptSrc', 'style-src' => 'styleSrc', + 'manifest-src' => 'manifestSrc', 'sandbox' => 'sandbox', 'report-uri' => 'reportURI' ]; diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 28ef8832bd..1607d16c85 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -52,9 +52,9 @@ abstract class BaseHandler implements ImageHandlerInterface * d * @var \CodeIgniter\Images\Image */ - protected $image; - protected $width; - protected $height; + protected $image = null; + protected $width = 0; + protected $height = 0; protected $filePermissions = 0644; protected $xAxis = 0; protected $yAxis = 0; @@ -112,13 +112,41 @@ abstract class BaseHandler implements ImageHandlerInterface $this->image = new Image($path, true); - $this->image->getProperties(); + $this->image->getProperties(false); + $this->width = $this->image->origWidth; + $this->height = $this->image->origHeight; return $this; } //-------------------------------------------------------------------- + /** + * Make the image resource object if needed + */ + protected function ensureResource() + { + if ($this->resource == null) + { + $path = $this->image->getPathname(); + // if valid image type, make corresponding image resource + switch ($this->image->imageType) + { + case IMAGETYPE_GIF: + $this->resource = imagecreatefromgif($path); + break; + case IMAGETYPE_JPEG: + $this->resource = imagecreatefromjpeg($path); + break; + case IMAGETYPE_PNG: + $this->resource = imagecreatefrompng($path); + break; + } + } + } + + //-------------------------------------------------------------------- + /** * Returns the image instance. * @@ -140,6 +168,7 @@ abstract class BaseHandler implements ImageHandlerInterface */ public function getResource() { + $this->ensureResource(); return $this->resource; } @@ -231,16 +260,15 @@ abstract class BaseHandler implements ImageHandlerInterface throw ImageException::forMissingAngle(); } + // cast angle as an int, for our use + $angle = (int) $angle; + // Reassign the width and height if ($angle === 90 || $angle === 270) { - $this->width = $this->image->origHeight; - $this->height = $this->image->origWidth; - } - else - { - $this->width = $this->image->origWidth; - $this->height = $this->image->origHeight; + $temp = $this->height; + $this->width = $this->height; + $this->height = $temp; } // Call the Handler-specific version. @@ -303,13 +331,13 @@ abstract class BaseHandler implements ImageHandlerInterface * * @return $this */ - public function flip(string $dir) + public function flip(string $dir = 'vertical') { $dir = strtolower($dir); if ($dir !== 'vertical' && $dir !== 'horizontal') { - throw new ImageException(lang('images.invalidDirection')); + throw ImageException::forInvalidDirection($dir); } return $this->_flip($dir); @@ -347,7 +375,7 @@ abstract class BaseHandler implements ImageHandlerInterface * @param string $text * @param array $options * - * @return BaseHandler + * @return $this */ public function text(string $text, array $options = []) { @@ -437,12 +465,9 @@ abstract class BaseHandler implements ImageHandlerInterface { return null; } - - throw ImageException::forEXIFUnsupported(); } $exif = exif_read_data($this->image->getPathname()); - if ( ! is_null($key) && is_array($exif)) { $exif = array_key_exists($key, $exif) ? $exif[$key] : false; @@ -662,7 +687,12 @@ abstract class BaseHandler implements ImageHandlerInterface */ protected function reproportion() { - if (($this->width === 0 && $this->height === 0) || $this->image->origWidth === 0 || $this->image->origHeight === 0 || ( ! ctype_digit((string) $this->width) && ! ctype_digit((string) $this->height)) || ! ctype_digit((string) $this->image->origWidth) || ! ctype_digit((string) $this->image->origHeight) + if (($this->width === 0 && $this->height === 0) || + $this->image->origWidth === 0 || + $this->image->origHeight === 0 || + ( ! ctype_digit((string) $this->width) && ! ctype_digit((string) $this->height)) || + ! ctype_digit((string) $this->image->origWidth) || + ! ctype_digit((string) $this->image->origHeight) ) { return; @@ -700,4 +730,16 @@ abstract class BaseHandler implements ImageHandlerInterface } //-------------------------------------------------------------------- + // accessor for testing; not part of interface + public function getWidth() + { + return ($this->resource != null) ? $this->_getWidth() : $this->width; + } + + // accessor for testing; not part of interface + public function getHeight() + { + return ($this->resource != null) ? $this->_getHeight() : $this->height; + } + } diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 1ce4e6c15f..6719b560c5 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -42,21 +42,17 @@ class GDHandler extends BaseHandler public $version; - /** - * Stores image resource in memory. - * - * @var - */ - protected $resource; - public function __construct($config = null) { parent::__construct($config); + // We should never see this, so can't test it + // @codeCoverageIgnoreStart if ( ! extension_loaded('gd')) { throw ImageException::forMissingExtension('GD'); } + // @codeCoverageIgnoreEnd } //-------------------------------------------------------------------- @@ -72,10 +68,7 @@ class GDHandler extends BaseHandler protected function _rotate(int $angle) { // Create the image handle - if ( ! ($srcImg = $this->createImage())) - { - return false; - } + $srcImg = $this->createImage(); // Set the background color // This won't work with transparent PNG files so we are @@ -85,7 +78,7 @@ class GDHandler extends BaseHandler $white = imagecolorallocate($srcImg, 255, 255, 255); // Rotate it! - $destImg = imagerotate($this->resource, $angle, $white); + $destImg = imagerotate($srcImg, $angle, $white); // Kill the file handles imagedestroy($srcImg); @@ -106,12 +99,9 @@ class GDHandler extends BaseHandler * * @return $this */ - public function _flatten(int $red = 255, int $green = 255, int $blue = 255) { - - if ( ! ($src = $this->createImage())) - { - return false; - } + public function _flatten(int $red = 255, int $green = 255, int $blue = 255) + { + $srcImg = $this->createImage(); if (function_exists('imagecreatetruecolor')) { @@ -128,15 +118,14 @@ class GDHandler extends BaseHandler $matte = imagecolorallocate($dest, $red, $green, $blue); imagefilledrectangle($dest, 0, 0, $this->width, $this->height, $matte); - imagecopy($dest, $src, 0, 0, 0, 0, $this->width, $this->height); + imagecopy($dest, $srcImg, 0, 0, 0, 0, $this->width, $this->height); // Kill the file handles - imagedestroy($src); + imagedestroy($srcImg); $this->resource = $dest; return $this; - } //-------------------------------------------------------------------- @@ -157,7 +146,7 @@ class GDHandler extends BaseHandler if ($direction === 'horizontal') { - for ($i = 0; $i < $height; $i ++ ) + for ($i = 0; $i < $height; $i ++) { $left = 0; $right = $width - 1; @@ -177,7 +166,7 @@ class GDHandler extends BaseHandler } else { - for ($i = 0; $i < $width; $i ++ ) + for ($i = 0; $i < $width; $i ++) { $top = 0; $bottom = $height - 1; @@ -422,7 +411,7 @@ class GDHandler extends BaseHandler return imagecreatefrompng($path); default: - throw ImageException::forInvalidImageCreate(); + throw ImageException::forInvalidImageCreate('Ima'); } } @@ -560,4 +549,15 @@ class GDHandler extends BaseHandler } //-------------------------------------------------------------------- + + public function _getWidth() + { + return imagesx($this->resource); + } + + public function _getHeight() + { + return imagesy($this->resource); + } + } diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index d9988a1fcb..a720730909 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -44,6 +44,10 @@ use CodeIgniter\Images\Image; * To make this library as compatible as possible with the broadest * number of installations, we do not use the Imagick extension, * but simply use the command line version. + * + * hmm - the width & height accessors at the end use the imagick extension. + * + * FIXME - This needs conversion & unit testing, to use the imagick extension * * @package CodeIgniter\Images\Handlers */ @@ -61,6 +65,13 @@ class ImageMagickHandler extends BaseHandler //-------------------------------------------------------------------- + public function __construct($config = null) + { + parent::__construct($config); + } + + //-------------------------------------------------------------------- + /** * Handles the actual resizing of the image. * @@ -76,7 +87,8 @@ class ImageMagickHandler extends BaseHandler //todo FIX THIS HANDLER PROPERLY $escape = "\\"; - if (strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN') { + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') + { $escape = ""; } @@ -141,9 +153,10 @@ class ImageMagickHandler extends BaseHandler * * @return $this */ - public function _flatten(int $red = 255, int $green = 255, int $blue = 255){ + public function _flatten(int $red = 255, int $green = 255, int $blue = 255) + { - $flatten = "-background RGB({$red},{$green},{$blue}) -flatten"; + $flatten = "-background RGB({$red},{$green},{$blue}) -flatten"; $source = ! empty($this->resource) ? $this->resource : $this->image->getPathname(); $destination = $this->getResourcePath(); @@ -408,4 +421,18 @@ class ImageMagickHandler extends BaseHandler } //-------------------------------------------------------------------- + + //-------------------------------------------------------------------- + + public function _getWidth() + { + return imagesx($this->resource); + } + + public function _getHeight() + { + return imagesy($this->resource); + } + + } diff --git a/system/Images/Image.php b/system/Images/Image.php index e57c5a7865..7c7d5c87f4 100644 --- a/system/Images/Image.php +++ b/system/Images/Image.php @@ -107,7 +107,7 @@ class Image extends File if ( ! copy($this->getPathname(), "{$targetPath}{$targetName}")) { - throw ImageException::forCopyError(); + throw ImageException::forCopyError($targetPath); } chmod("{$targetPath}/{$targetName}", $perms); @@ -132,6 +132,7 @@ class Image extends File $vals = getimagesize($path); $types = [1 => 'gif', 2 => 'jpeg', 3 => 'png']; + $mime = 'image/' . ($types[$vals[2]] ?? 'jpg'); if ($return === true) diff --git a/system/Images/ImageHandlerInterface.php b/system/Images/ImageHandlerInterface.php index 87ec21362b..f33774353e 100644 --- a/system/Images/ImageHandlerInterface.php +++ b/system/Images/ImageHandlerInterface.php @@ -44,8 +44,9 @@ interface ImageHandlerInterface * @param int $width * @param int $height * @param bool $maintainRatio If true, will get the closest match possible while keeping aspect ratio true. + * @param string $masterDim */ - public function resize(int $width, int $height, bool $maintainRatio = false); + public function resize(int $width, int $height, bool $maintainRatio = false, string $masterDim = 'auto'); //-------------------------------------------------------------------- @@ -58,10 +59,12 @@ interface ImageHandlerInterface * @param int|null $height * @param int|null $x X-axis coord to start cropping from the left of image * @param int|null $y Y-axis coord to start cropping from the top of image + * @param bool $maintainRatio + * @param string $masterDim * * @return mixed */ - public function crop(int $width = null, int $height = null, int $x = null, int $y = null); + public function crop(int $width = null, int $height = null, int $x = null, int $y = null, bool $maintainRatio = false, string $masterDim = 'auto'); //-------------------------------------------------------------------- @@ -110,6 +113,17 @@ interface ImageHandlerInterface //-------------------------------------------------------------------- + /** + * Flip an image horizontally or vertically + * + * @param string $dir Direction to flip, either 'vertical' or 'horizontal' + * + * @return mixed + */ + public function flip(string $dir = 'vertical'); + + //-------------------------------------------------------------------- + /** * Combine cropping and resizing into a single command. * @@ -133,4 +147,42 @@ interface ImageHandlerInterface public function fit(int $width, int $height, string $position); //-------------------------------------------------------------------- + + /** + * Overlays a string of text over the image. + * + * Valid options: + * + * - color Text Color (hex number) + * - shadowColor Color of the shadow (hex number) + * - hAlign Horizontal alignment: left, center, right + * - vAlign Vertical alignment: top, middle, bottom + * - hOffset + * - vOffset + * - fontPath + * - fontSize + * - shadowOffset + * + * @param string $text + * @param array $options + * + * @return $this + */ + public function text(string $text, array $options = []); + + //-------------------------------------------------------------------- + + /** + * Saves any changes that have been made to file. + * + * Example: + * $image->resize(100, 200, true) + * ->save($target); + * + * @param string $target + * @param int $quality + * + * @return mixed + */ + public function save(string $target = null, int $quality = 90); } diff --git a/system/Language/en/Files.php b/system/Language/en/Files.php index 2b1610f44c..6e1eb5f319 100644 --- a/system/Language/en/Files.php +++ b/system/Language/en/Files.php @@ -1,5 +1,4 @@ 'File not found: {0}', - 'cannotMove' => 'Could not move file {0} to {1} ({2})', + 'fileNotFound' => 'File not found: {0}', + 'cannotMove' => 'Could not move file {0} to {1} ({2})', + 'invalidFilename' => 'Target filename missing or invalid: {0}', + 'cannotCopy' => 'Could not copy to {0} - make sure the folder is writeable', ]; diff --git a/tests/_support/Images/EXIFsamples/down-mirrored.jpg b/tests/_support/Images/EXIFsamples/down-mirrored.jpg new file mode 100644 index 0000000000..34a7b1d395 Binary files /dev/null and b/tests/_support/Images/EXIFsamples/down-mirrored.jpg differ diff --git a/tests/_support/Images/EXIFsamples/down.jpg b/tests/_support/Images/EXIFsamples/down.jpg new file mode 100644 index 0000000000..9077a7c92b Binary files /dev/null and b/tests/_support/Images/EXIFsamples/down.jpg differ diff --git a/tests/_support/Images/EXIFsamples/left-mirrored.jpg b/tests/_support/Images/EXIFsamples/left-mirrored.jpg new file mode 100644 index 0000000000..1832702492 Binary files /dev/null and b/tests/_support/Images/EXIFsamples/left-mirrored.jpg differ diff --git a/tests/_support/Images/EXIFsamples/left.jpg b/tests/_support/Images/EXIFsamples/left.jpg new file mode 100644 index 0000000000..ad1f89850f Binary files /dev/null and b/tests/_support/Images/EXIFsamples/left.jpg differ diff --git a/tests/_support/Images/EXIFsamples/right-mirrored.jpg b/tests/_support/Images/EXIFsamples/right-mirrored.jpg new file mode 100644 index 0000000000..cc8a29aebe Binary files /dev/null and b/tests/_support/Images/EXIFsamples/right-mirrored.jpg differ diff --git a/tests/_support/Images/EXIFsamples/right.jpg b/tests/_support/Images/EXIFsamples/right.jpg new file mode 100644 index 0000000000..183ffebb8e Binary files /dev/null and b/tests/_support/Images/EXIFsamples/right.jpg differ diff --git a/tests/_support/Images/EXIFsamples/up-mirrored.jpg b/tests/_support/Images/EXIFsamples/up-mirrored.jpg new file mode 100644 index 0000000000..e1865a5f0e Binary files /dev/null and b/tests/_support/Images/EXIFsamples/up-mirrored.jpg differ diff --git a/tests/_support/Images/EXIFsamples/up.jpg b/tests/_support/Images/EXIFsamples/up.jpg new file mode 100644 index 0000000000..70fc26ff2d Binary files /dev/null and b/tests/_support/Images/EXIFsamples/up.jpg differ diff --git a/tests/_support/Images/Steveston_dusk.JPG b/tests/_support/Images/Steveston_dusk.JPG new file mode 100644 index 0000000000..c3b9b121b7 Binary files /dev/null and b/tests/_support/Images/Steveston_dusk.JPG differ diff --git a/tests/_support/Images/ci-logo.gif b/tests/_support/Images/ci-logo.gif new file mode 100644 index 0000000000..3001b2f752 Binary files /dev/null and b/tests/_support/Images/ci-logo.gif differ diff --git a/tests/_support/Images/ci-logo.jpeg b/tests/_support/Images/ci-logo.jpeg new file mode 100644 index 0000000000..1b0178bba3 Binary files /dev/null and b/tests/_support/Images/ci-logo.jpeg differ diff --git a/tests/_support/ci-logo.png b/tests/_support/Images/ci-logo.png similarity index 100% rename from tests/_support/ci-logo.png rename to tests/_support/Images/ci-logo.png diff --git a/tests/system/Images/BaseHandlerTest.php b/tests/system/Images/BaseHandlerTest.php new file mode 100644 index 0000000000..a58003dd93 --- /dev/null +++ b/tests/system/Images/BaseHandlerTest.php @@ -0,0 +1,96 @@ +markTestSkipped('The GD extension is not available.'); + return; + } + + // create virtual file system + $this->root = vfsStream::setup(); + // copy our support files + $this->origin = SUPPORTPATH . 'Images/'; + vfsStream::copyFromFileSystem($this->origin, $this->root); + // make subfolders + $structure = ['work' => [], 'wontwork' => []]; + vfsStream::create($structure); + // with one of them read only + $wont = $this->root->getChild('wontwork')->chmod(0400); + + // for VFS tests + $this->start = $this->root->url() . '/'; + $this->path = $this->start . 'ci-logo.png'; + } + + //-------------------------------------------------------------------- + + public function testNew() + { + $handler = Services::image('gd', null, false); + $this->assertTrue($handler instanceof Handlers\BaseHandler); + } + + public function testWithFile() + { + $path = $this->origin . 'ci-logo.png'; + $handler = Services::image('gd', null, false); + $handler->withFile($path); + + $image = $handler->getFile(); + $this->assertTrue($image instanceof Image); + $this->assertEquals(155, $image->origWidth); + $this->assertEquals($path, $image->getPathname()); + } + + public function testMissingFile() + { + $this->expectException(\CodeIgniter\Files\Exceptions\FileNotFoundException::class); + $handler = Services::image('gd', null, false); + $handler->withFile($this->start . 'No_such_file.jpg'); + } + + public function testFileTypes() + { + $handler = Services::image('gd', null, false); + $handler->withFile($this->start . 'ci-logo.png'); + $image = $handler->getFile(); + $this->assertTrue($image instanceof Image); + + $handler->withFile($this->start . 'ci-logo.jpeg'); + $image = $handler->getFile(); + $this->assertTrue($image instanceof Image); + + $handler->withFile($this->start . 'ci-logo.gif'); + $image = $handler->getFile(); + $this->assertTrue($image instanceof Image); + } + + //-------------------------------------------------------------------- + // Something handled by our Image + public function testImageHandled() + { + $handler = Services::image('gd', null, false); + $handler->withFile($this->path); + $this->assertEquals($this->path, $handler->getPathname()); + } + +} diff --git a/tests/system/Images/GDHandlerTest.php b/tests/system/Images/GDHandlerTest.php index 739c117e8d..20ad4b484e 100644 --- a/tests/system/Images/GDHandlerTest.php +++ b/tests/system/Images/GDHandlerTest.php @@ -1,14 +1,326 @@ path); + if ( ! extension_loaded('gd')) + { + $this->markTestSkipped('The GD extension is not available.'); + return; + } - $this->assertInternalType('array', $image->getProperties(true)); + // create virtual file system + $this->root = vfsStream::setup(); + // copy our support files + $this->origin = SUPPORTPATH . 'Images/'; + // make subfolders + $structure = ['work' => [], 'wontwork' => []]; + vfsStream::create($structure); + // with one of them read only + $wont = $this->root->getChild('wontwork')->chmod(0400); + + $this->start = $this->root->url() . '/'; + + $this->path = $this->origin . 'ci-logo.png'; + $this->handler = Services::image('gd', null, false); + } + + public function testGetVersion() + { + $version = $this->handler->getVersion(); + // make sure that the call worked + $this->assertNotFalse($version); + // we should have a numeric version, with 3 digits + $this->assertGreaterThan(100, $version); + $this->assertLessThan(999, $version); + } + + public function testImageProperties() + { + $this->handler->withFile($this->path); + $file = $this->handler->getFile(); + $props = $file->getProperties(true); + + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(155, $props['width']); + $this->assertEquals(155, $file->origWidth); + + $this->assertEquals(200, $this->handler->getHeight()); + $this->assertEquals(200, $props['height']); + $this->assertEquals(200, $file->origHeight); + + $this->assertEquals('width="155" height="200"', $props['size_str']); + } + + public function testImageTypeProperties() + { + $this->handler->withFile($this->path); + $file = $this->handler->getFile(); + $props = $file->getProperties(true); + + $this->assertEquals(IMAGETYPE_PNG, $props['image_type']); + $this->assertEquals('image/png', $props['mime_type']); + } + +//-------------------------------------------------------------------- + + public function testResizeIgnored() + { + $this->handler->withFile($this->path); + $this->handler->resize(155, 200); // 155x200 result + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + + public function testResizeAbsolute() + { + $this->handler->withFile($this->path); + $this->handler->resize(123, 456, false); // 123x456 result + $this->assertEquals(123, $this->handler->getWidth()); + $this->assertEquals(456, $this->handler->getHeight()); + } + + public function testResizeAspect() + { + $this->handler->withFile($this->path); + $this->handler->resize(123, 456, true); // 123x159 result + $this->assertEquals(123, $this->handler->getWidth()); + $this->assertEquals(159, $this->handler->getHeight()); + } + + public function testResizeAspectWidth() + { + $this->handler->withFile($this->path); + $this->handler->resize(123, 0, true); // 123x159 result + $this->assertEquals(123, $this->handler->getWidth()); + $this->assertEquals(159, $this->handler->getHeight()); + } + + public function testResizeAspectHeight() + { + $this->handler->withFile($this->path); + $this->handler->resize(0, 456, true); // 354x456 result + $this->assertEquals(354, $this->handler->getWidth()); + $this->assertEquals(456, $this->handler->getHeight()); + } + +//-------------------------------------------------------------------- + + public function testCropTopLeft() + { + $this->handler->withFile($this->path); + $this->handler->crop(100, 100); // 100x100 result + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + + public function testCropMiddle() + { + $this->handler->withFile($this->path); + $this->handler->crop(100, 100, 50, 50, false); // 100x100 result + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + + public function testCropMiddlePreserved() + { + $this->handler->withFile($this->path); + $this->handler->crop(100, 100, 50, 50, true); // 78x100 result + $this->assertEquals(78, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + + public function testCropTopLeftPreserveAspect() + { + $this->handler->withFile($this->path); + $this->handler->crop(100, 100); // 100x100 result + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + + public function testCropNothing() + { + $this->handler->withFile($this->path); + $this->handler->crop(155, 200); // 155x200 result + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + + public function testCropOutOfBounds() + { + $this->handler->withFile($this->path); + $this->handler->crop(100, 100, 100); // 55x100 result in 100x100 + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + +//-------------------------------------------------------------------- + + public function testRotate() + { + $this->handler->withFile($this->path); // 155x200 + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + + // first rotation + $this->handler->rotate(90); // 200x155 + $this->assertEquals(200, $this->handler->getWidth()); + + // check image size again after another rotation + $this->handler->rotate(180); // 200x155 + $this->assertEquals(200, $this->handler->getWidth()); + } + + public function testRotateBadAngle() + { + $this->handler->withFile($this->path); + $this->expectException(ImageException::class); + $this->handler->rotate(77); + } + +//-------------------------------------------------------------------- + + public function testFlatten() + { + $this->handler->withFile($this->path); + $this->handler->flatten(); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + +//-------------------------------------------------------------------- + + public function testFlip() + { + $this->handler->withFile($this->path); + $this->handler->flip(); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + + public function testHorizontal() + { + $this->handler->withFile($this->path); + $this->handler->flip('horizontal'); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + + public function testFlipVertical() + { + $this->handler->withFile($this->path); + $this->handler->flip('vertical'); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + + public function testFlipUnknown() + { + $this->handler->withFile($this->path); + $this->expectException(ImageException::class); + $this->handler->flip('bogus'); + } + +//-------------------------------------------------------------------- + public function testFit() + { + $this->handler->withFile($this->path); + $this->handler->fit(100, 100); + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + + public function testFitTaller() + { + $this->handler->withFile($this->path); + $this->handler->fit(100, 400); + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(400, $this->handler->getHeight()); + } + + public function testFitAutoHeight() + { + $this->handler->withFile($this->path); + $this->handler->fit(100); + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(129, $this->handler->getHeight()); + } + + public function testFitPositions() + { + $choices = ['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right']; + $this->handler->withFile($this->path); + foreach ($choices as $position) + { + $this->handler->fit(100, 100, $position); + $this->assertEquals(100, $this->handler->getWidth(), 'Position ' . $position . ' failed'); + $this->assertEquals(100, $this->handler->getHeight(), 'Position ' . $position . ' failed'); + } + } + +//-------------------------------------------------------------------- + + public function testText() + { + $this->handler->withFile($this->path); + $this->handler->text('vertical', ['hAlign' => 'right', 'vAlign' => 'bottom']); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + +//-------------------------------------------------------------------- + + public function testMoreText() + { + $this->handler->withFile($this->path); + $this->handler->text('vertical', ['vAlign' => 'middle', 'withShadow' => 'sure', 'shadowOffset' => 3]); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + +//-------------------------------------------------------------------- + + public function testImageCreation() + { + foreach (['gif', 'jpeg', 'png'] as $type) + { + $this->handler->withFile($this->origin . 'ci-logo.' . $type); + $this->handler->text('vertical'); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + } + +//-------------------------------------------------------------------- + + public function testImageSave() + { + foreach (['gif', 'jpeg', 'png'] as $type) + { + $this->handler->withFile($this->origin . 'ci-logo.' . $type); + $this->handler->getResource(); // make sure resource is loaded + $this->handler->save($this->start . 'work/ci-logo.' . $type); + $this->assertTrue($this->root->hasChild('work/ci-logo.' . $type)); + } } } diff --git a/tests/system/Images/ImageTest.php b/tests/system/Images/ImageTest.php index bb8f76a354..5005718af9 100644 --- a/tests/system/Images/ImageTest.php +++ b/tests/system/Images/ImageTest.php @@ -1,56 +1,84 @@ root = vfsStream::setup(); + // copy our support files + $this->origin = '_support/Images/'; + vfsStream::copyFromFileSystem(TESTPATH . $this->origin, $this->root); + // make subfolders + $structure = ['work' => [], 'wontwork' => []]; + vfsStream::create($structure); + // with one of them read only + $wont = $this->root->getChild('wontwork')->chmod(0400); + + $this->start = $this->root->url() . '/'; + + $this->image = new Image($this->start . 'ci-logo.png'); + } + public function testBasicPropertiesInherited() { - $image = new Image(ROOTPATH.$this->path); - - $this->assertEquals('ci-logo.png', $image->getFilename()); - $this->assertEquals(ROOTPATH.$this->path, $image->getPathname()); - $this->assertEquals(ROOTPATH.'tests/_support', $image->getPath()); - $this->assertEquals('ci-logo.png', $image->getBasename()); + $this->assertEquals('ci-logo.png', $this->image->getFilename()); + $this->assertEquals($this->start . 'ci-logo.png', $this->image->getPathname()); + $this->assertEquals($this->root->url(), $this->image->getPath()); + $this->assertEquals('ci-logo.png', $this->image->getBasename()); } - public function testGetProperties() { - $image = new Image(ROOTPATH.$this->path); - $expected = [ - 'width' => 155, - 'height' => 200, + 'width' => 155, + 'height' => 200, 'image_type' => IMAGETYPE_PNG, - 'size_str' => 'width="155" height="200"', - 'mime_type' => "image/png", + 'size_str' => 'width="155" height="200"', + 'mime_type' => "image/png", ]; - $this->assertEquals($expected, $image->getProperties(true)); + $this->assertEquals($expected, $this->image->getProperties(true)); } - - public function testCanCopyDefaultName() + public function testExtractProperties() { - $image = new Image(ROOTPATH.$this->path); + // extract properties from the image + $this->assertTrue($this->image->getProperties(false)); - $image->copy(WRITEPATH); - - $this->assertFileExists(WRITEPATH.'ci-logo.png'); - - unlink(WRITEPATH.'ci-logo.png'); + $this->assertEquals(155, $this->image->origWidth); + $this->assertEquals(200, $this->image->origHeight); + $this->assertEquals(IMAGETYPE_PNG, $this->image->imageType); + $this->assertEquals('width="155" height="200"', $this->image->sizeStr); + $this->assertEquals("image/png", $this->image->mime); } - public function testCanCopyNewName() + public function testCopyDefaultName() { - $image = new Image(ROOTPATH.$this->path); + $targetPath = $this->start . 'work'; + $this->image->copy($targetPath); + $this->assertTrue($this->root->hasChild('work/ci-logo.png')); + } - $image->copy(WRITEPATH, 'new-logo.png'); + public function testCopyNewName() + { + $this->image->copy($this->root->url(), 'new-logo.png'); + $this->assertTrue($this->root->hasChild('new-logo.png')); + } - $this->assertFileExists(WRITEPATH.'new-logo.png'); - - unlink(WRITEPATH.'new-logo.png'); + public function testCopyNowhere() + { + $this->expectException(ImageException::class); + $targetPath = $this->start . 'work'; + $this->image->copy($targetPath, ''); } } diff --git a/user_guide_src/source/libraries/response.rst b/user_guide_src/source/libraries/response.rst index 7c36a0c6bc..8a53b02e6c 100644 --- a/user_guide_src/source/libraries/response.rst +++ b/user_guide_src/source/libraries/response.rst @@ -179,6 +179,7 @@ class holds a number of methods that map pretty clearly to the appropriate heade $response->CSP->addFrameAncestor('none', $reportOnly); $response->CSP->addImageSrc('cdn.example.com', $reportOnly); $response->CSP->addMediaSrc('cdn.example.com', $reportOnly); + $response->CSP->addManifestSrc('cdn.example.com', $reportOnly); $response->CSP->addObjectSrc('cdn.example.com', $reportOnly); $response->CSP->addPluginType('application/pdf', $reportOnly); $response->CSP->addScriptSrc('scripts.example.com', $reportOnly);