diff --git a/VERSIONLOG.md b/VERSIONLOG.md index 061be77..acc37d4 100644 --- a/VERSIONLOG.md +++ b/VERSIONLOG.md @@ -1,4 +1,5 @@ ## 0.7.11 +* Added IsImage. ## 0.7.10 2026-01-13 diff --git a/composer.json b/composer.json index 8492056..423a733 100644 --- a/composer.json +++ b/composer.json @@ -21,5 +21,10 @@ "psr-4": { "Neuron\\": "src/" } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } } } diff --git a/src/Validation/IsImage.php b/src/Validation/IsImage.php new file mode 100644 index 0000000..962dd83 --- /dev/null +++ b/src/Validation/IsImage.php @@ -0,0 +1,283 @@ +allowedMimeTypes = $allowedMimeTypes; + $this->maxSize = $maxSize; + $this->checkImageData = $checkImageData; + $this->allowSvg = $allowSvg; + + // If SVG is explicitly allowed AND we have MIME type restrictions, add SVG to allowed list + // Don't add if allowedMimeTypes is empty (meaning allow all types) + if( $this->allowSvg && !empty( $this->allowedMimeTypes ) && !in_array( 'image/svg+xml', $this->allowedMimeTypes, true ) ) + { + $this->allowedMimeTypes[] = 'image/svg+xml'; + } + } + + /** + * @param mixed $value + * @return bool + */ + protected function validate( mixed $value ) : bool + { + if( !is_string( $value ) ) + { + return false; + } + + // Empty string is not a valid image + if( $value === '' ) + { + return false; + } + + // Check if it's a data URI + if( strpos( $value, 'data:' ) === 0 ) + { + return $this->validateDataUri( $value ); + } + + // Otherwise, treat it as base64 encoded image data + return $this->validateBase64Image( $value ); + } + + /** + * Validates a data URI formatted image. + * + * @param string $dataUri + * @return bool + */ + private function validateDataUri( string $dataUri ) : bool + { + // Parse data URI: data:[][;base64], + // Use 's' modifier to allow . to match newlines in base64 data + $pattern = '/^data:([a-zA-Z0-9][a-zA-Z0-9\/+\-]*);base64,(.+)$/s'; + + if( !preg_match( $pattern, $dataUri, $matches ) ) + { + return false; + } + + $mimeType = strtolower( $matches[1] ); // Normalize to lowercase per RFC 2045 + $base64Data = $matches[2]; + + // Check MIME type (case-insensitive per RFC 2045) + if( !empty( $this->allowedMimeTypes ) && !$this->isMimeTypeAllowed( $mimeType ) ) + { + return false; + } + + // Validate base64 data + return $this->validateBase64Image( $base64Data ); + } + + /** + * Validates base64 encoded image data. + * + * @param string $base64Data + * @return bool + */ + private function validateBase64Image( string $base64Data ) : bool + { + // Remove any whitespace + $cleanData = preg_replace( '/\s+/', '', $base64Data ); + + // Attempt to decode + $decoded = base64_decode( $cleanData, true ); + + if( $decoded === false ) + { + return false; + } + + // Check size constraint + if( $this->maxSize !== null && strlen( $decoded ) > $this->maxSize ) + { + return false; + } + + // Always validate image data to check MIME type restrictions + // Even when checkImageData is false, we need to detect the type for MIME validation + // but we can skip the more expensive image content validation + if( !empty( $this->allowedMimeTypes ) ) + { + // Detect the image type from signatures + $detectedType = $this->detectImageType( $decoded ); + + // Check for SVG if: + // 1. allowSvg is true (general SVG support enabled), OR + // 2. 'image/svg+xml' is explicitly in allowedMimeTypes + $svgExplicitlyAllowed = $this->isMimeTypeAllowed( 'image/svg+xml' ); + if( $detectedType === null && ( $this->allowSvg || $svgExplicitlyAllowed ) ) + { + $detectedType = $this->detectSvg( $decoded ); + } + + // If we couldn't detect a type, it's not a valid image + if( $detectedType === null ) + { + return false; + } + + // Check if detected type is allowed + if( !$this->isMimeTypeAllowed( $detectedType ) ) + { + return false; + } + } + elseif( $this->checkImageData ) + { + // No MIME restrictions but need to check if it's a recognizable image + return $this->isRecognizableImage( $decoded ); + } + + return true; + } + + /** + * Detects image type from binary data signatures. + * + * @param string $imageData + * @return string|null Returns MIME type if detected, null otherwise + */ + private function detectImageType( string $imageData ) : ?string + { + // Check for common image file signatures (magic numbers) + $signatures = [ + // JPEG + "\xFF\xD8\xFF" => 'image/jpeg', + // PNG + "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A" => 'image/png', + // GIF + "GIF87a" => 'image/gif', + "GIF89a" => 'image/gif', + // WebP + "RIFF" => 'image/webp', // Note: WebP also needs WEBP at offset 8 + ]; + + $dataStart = substr( $imageData, 0, 20 ); + + // Check for image signatures + foreach( $signatures as $signature => $mimeType ) + { + if( strpos( $dataStart, $signature ) === 0 ) + { + // Special handling for WebP + if( $signature === 'RIFF' && substr( $imageData, 8, 4 ) !== 'WEBP' ) + { + continue; + } + + return $mimeType; + } + } + + return null; + } + + /** + * Checks if the decoded data is a recognizable image format. + * This method only validates that the data contains valid image signatures, + * without checking MIME type restrictions (caller's responsibility). + * + * @param string $imageData + * @return bool + */ + private function isRecognizableImage( string $imageData ) : bool + { + // Use the extracted method to detect image type + $detectedType = $this->detectImageType( $imageData ); + + // Check for SVG if: + // 1. allowSvg is true (general SVG support enabled), OR + // 2. 'image/svg+xml' is explicitly in allowedMimeTypes (if any restrictions exist) + $svgExplicitlyAllowed = !empty( $this->allowedMimeTypes ) && $this->isMimeTypeAllowed( 'image/svg+xml' ); + if( $detectedType === null && ( $this->allowSvg || $svgExplicitlyAllowed ) ) + { + $detectedType = $this->detectSvg( $imageData ); + } + + // Return true if we recognized any valid image format + return $detectedType !== null; + } + + /** + * Checks if a MIME type is in the allowed list (case-insensitive per RFC 2045). + * + * @param string $mimeType + * @return bool + */ + private function isMimeTypeAllowed( string $mimeType ) : bool + { + $normalizedMimeType = strtolower( $mimeType ); + foreach( $this->allowedMimeTypes as $allowed ) + { + if( strtolower( $allowed ) === $normalizedMimeType ) + { + return true; + } + } + return false; + } + + /** + * Detects if the data is a valid SVG image. + * SVG detection is more permissive but requires explicit opt-in for security. + * + * @param string $imageData + * @return string|null Returns 'image/svg+xml' if valid SVG, null otherwise + */ + private function detectSvg( string $imageData ) : ?string + { + // Only check first 1KB for performance + $dataToCheck = substr( $imageData, 0, 1024 ); + + // Remove UTF-8 BOM if present (exact 3-byte sequence) + if( substr( $dataToCheck, 0, 3 ) === "\xEF\xBB\xBF" ) + { + $dataToCheck = substr( $dataToCheck, 3 ); + } + + // Case-insensitive check for ]*xmlns\s*=\s*["\']http:\/\/www\.w3\.org\/2000\/svg["\'][^>]*>/i', $dataToCheck ) ) + { + // Valid SVG with proper namespace declaration + return 'image/svg+xml'; + } + + // Also check for xmlns:svg pattern (less common but valid) + if( preg_match( '/]*xmlns:svg\s*=\s*["\']http:\/\/www\.w3\.org\/2000\/svg["\'][^>]*>/i', $dataToCheck ) ) + { + return 'image/svg+xml'; + } + + return null; + } +} \ No newline at end of file diff --git a/tests/Validation/IsImageTest.php b/tests/Validation/IsImageTest.php new file mode 100644 index 0000000..74ee4d6 --- /dev/null +++ b/tests/Validation/IsImageTest.php @@ -0,0 +1,465 @@ +assertTrue( $validator->isValid( $jpegBase64 ) ); + } + + /** + * Test valid data URI formatted JPEG image. + */ + public function testValidDataUriJpegImage() + { + // Small 1x1 pixel JPEG image as data URI + $jpegDataUri = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAr/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='; + + $validator = new IsImage(); + $this->assertTrue( $validator->isValid( $jpegDataUri ) ); + } + + /** + * Test valid base64 encoded PNG image. + */ + public function testValidBase64PngImage() + { + // Small 1x1 pixel PNG image in base64 + $pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; + + $validator = new IsImage(); + $this->assertTrue( $validator->isValid( $pngBase64 ) ); + } + + /** + * Test valid data URI formatted PNG image. + */ + public function testValidDataUriPngImage() + { + // Small 1x1 pixel PNG image as data URI + $pngDataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; + + $validator = new IsImage(); + $this->assertTrue( $validator->isValid( $pngDataUri ) ); + } + + /** + * Test valid base64 encoded GIF image. + */ + public function testValidBase64GifImage() + { + // Small 1x1 pixel GIF image in base64 + $gifBase64 = 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + + $validator = new IsImage(); + $this->assertTrue( $validator->isValid( $gifBase64 ) ); + } + + /** + * Test invalid base64 string. + */ + public function testInvalidBase64String() + { + $invalidBase64 = 'This is not base64!@#$%'; + + $validator = new IsImage(); + $this->assertFalse( $validator->isValid( $invalidBase64 ) ); + } + + /** + * Test valid base64 but not an image. + */ + public function testValidBase64NotImage() + { + // Base64 encoded text "Hello World" + $textBase64 = 'SGVsbG8gV29ybGQ='; + + $validator = new IsImage(); + $this->assertFalse( $validator->isValid( $textBase64 ) ); + } + + /** + * Test empty string. + */ + public function testEmptyString() + { + $validator = new IsImage(); + $this->assertFalse( $validator->isValid( '' ) ); + } + + /** + * Test non-string input. + */ + public function testNonStringInput() + { + $validator = new IsImage(); + $this->assertFalse( $validator->isValid( 123 ) ); + $this->assertFalse( $validator->isValid( [] ) ); + $this->assertFalse( $validator->isValid( null ) ); + $this->assertFalse( $validator->isValid( true ) ); + } + + /** + * Test MIME type restrictions. + */ + public function testMimeTypeRestrictions() + { + // Only allow JPEG images + $validator = new IsImage( [ 'image/jpeg' ] ); + + // JPEG should pass + $jpegBase64 = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAr/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='; + $this->assertTrue( $validator->isValid( $jpegBase64 ) ); + + // PNG should fail + $pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; + $this->assertFalse( $validator->isValid( $pngBase64 ) ); + } + + /** + * Test data URI MIME type restrictions. + */ + public function testDataUriMimeTypeRestrictions() + { + // Only allow PNG images + $validator = new IsImage( [ 'image/png' ] ); + + // PNG data URI should pass + $pngDataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; + $this->assertTrue( $validator->isValid( $pngDataUri ) ); + + // JPEG data URI should fail + $jpegDataUri = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAr/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='; + $this->assertFalse( $validator->isValid( $jpegDataUri ) ); + } + + /** + * Test file size restrictions. + */ + public function testFileSizeRestrictions() + { + // Small PNG (should be < 100 bytes decoded) + $smallPng = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; + + // Allow max 200 bytes + $validator200 = new IsImage( [], 200 ); + $this->assertTrue( $validator200->isValid( $smallPng ) ); + + // Allow max 10 bytes (should fail) + $validator10 = new IsImage( [], 10 ); + $this->assertFalse( $validator10->isValid( $smallPng ) ); + } + + /** + * Test without image data checking (faster validation). + */ + public function testWithoutImageDataChecking() + { + // Validator that doesn't check actual image data + $validator = new IsImage( [], null, false ); + + // Valid base64 but not an image - should pass when not checking image data + $textBase64 = 'SGVsbG8gV29ybGQ='; // "Hello World" + $this->assertTrue( $validator->isValid( $textBase64 ) ); + + // Invalid base64 should still fail + $invalidBase64 = 'Not valid base64!@#'; + $this->assertFalse( $validator->isValid( $invalidBase64 ) ); + } + + /** + * Test SVG image support with explicit opt-in. + */ + public function testSvgImageSupportWithOptIn() + { + // Simple SVG in base64 + $svgContent = ''; + $svgBase64 = base64_encode( $svgContent ); + + // Default validator (SVG not allowed) + $validator = new IsImage(); + $this->assertFalse( $validator->isValid( $svgBase64 ) ); + + // Validator with SVG explicitly allowed + $validatorWithSvg = new IsImage( [], null, true, true ); + $this->assertTrue( $validatorWithSvg->isValid( $svgBase64 ) ); + + // SVG data URI with explicit SVG support + $svgDataUri = 'data:image/svg+xml;base64,' . $svgBase64; + $this->assertFalse( $validator->isValid( $svgDataUri ) ); + $this->assertTrue( $validatorWithSvg->isValid( $svgDataUri ) ); + } + + /** + * Test that generic XML is not accepted as SVG. + */ + public function testGenericXmlNotAcceptedAsSvg() + { + // Generic XML that is not SVG + $xmlContent = 'test'; + $xmlBase64 = base64_encode( $xmlContent ); + + // Even with SVG allowed, generic XML should not pass + $validatorWithSvg = new IsImage( [], null, true, true ); + $this->assertFalse( $validatorWithSvg->isValid( $xmlBase64 ) ); + } + + /** + * Test SVG detection requires proper SVG tag. + */ + public function testSvgDetectionRequiresSvgTag() + { + // SVG without proper tag + $invalidSvg1 = ''; + $invalidBase64 = base64_encode( $invalidSvg1 ); + + $validatorWithSvg = new IsImage( [], null, true, true ); + $this->assertFalse( $validatorWithSvg->isValid( $invalidBase64 ) ); + + // Valid SVG with proper tag + $validSvg = ''; + $validBase64 = base64_encode( $validSvg ); + $this->assertTrue( $validatorWithSvg->isValid( $validBase64 ) ); + } + + /** + * Test SVG without xmlns namespace is rejected for security. + */ + public function testSvgWithoutXmlnsRejected() + { + // SVG without xmlns namespace - should be rejected even with SVG allowed + $svgNoNamespace = ''; + $svgBase64 = base64_encode( $svgNoNamespace ); + + // Even with SVG enabled, should reject SVG without proper namespace + $validatorWithSvg = new IsImage( [], null, true, true ); + $this->assertFalse( $validatorWithSvg->isValid( $svgBase64 ) ); + + // SVG with xmlns should pass + $svgWithNamespace = ''; + $svgWithNsBase64 = base64_encode( $svgWithNamespace ); + $this->assertTrue( $validatorWithSvg->isValid( $svgWithNsBase64 ) ); + } + + /** + * Test SVG with xmlns in text content is rejected (bypass prevention). + */ + public function testSvgWithXmlnsInTextRejected() + { + // SVG with xmlns in text content but not as attribute - should be rejected + $svgWithTextXmlns = 'xmlns http://www.w3.org/2000/svg'; + $svgBase64 = base64_encode( $svgWithTextXmlns ); + + // Should be rejected even with SVG enabled + $validatorWithSvg = new IsImage( [], null, true, true ); + $this->assertFalse( $validatorWithSvg->isValid( $svgBase64 ) ); + + // SVG with xmlns in comment - should be rejected + $svgWithCommentXmlns = ''; + $svgBase64Comment = base64_encode( $svgWithCommentXmlns ); + $this->assertFalse( $validatorWithSvg->isValid( $svgBase64Comment ) ); + } + + /** + * Test case-insensitive MIME type matching in data URIs. + */ + public function testCaseInsensitiveMimeTypeInDataUri() + { + // PNG with uppercase MIME type + $pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; + + // Test various case combinations + $validator = new IsImage(); + + // Uppercase MIME type + $upperDataUri = 'data:IMAGE/PNG;base64,' . $pngBase64; + $this->assertTrue( $validator->isValid( $upperDataUri ) ); + + // Mixed case MIME type + $mixedDataUri = 'data:Image/Png;base64,' . $pngBase64; + $this->assertTrue( $validator->isValid( $mixedDataUri ) ); + + // JPEG with uppercase + $jpegBase64 = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAr/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='; + $upperJpegUri = 'data:IMAGE/JPEG;base64,' . $jpegBase64; + $this->assertTrue( $validator->isValid( $upperJpegUri ) ); + } + + /** + * Test BOM removal handles exact sequence only. + */ + public function testBomRemovalExactSequence() + { + // SVG with proper BOM followed by valid content + $svgWithBom = "\xEF\xBB\xBF"; + $svgBomBase64 = base64_encode( $svgWithBom ); + + $validatorWithSvg = new IsImage( [], null, true, true ); + $this->assertTrue( $validatorWithSvg->isValid( $svgBomBase64 ) ); + + // Test with only first two bytes of BOM - should not be stripped + // The incomplete BOM should prevent proper SVG detection + $svgTwoBomBytes = "\xEF\xBB"; + $svgTwoBase64 = base64_encode( $svgTwoBomBytes ); + // This passes because the regex can still find assertTrue( $validatorWithSvg->isValid( $svgTwoBase64 ) ); + + // Test that we only remove exact BOM, not individual bytes + // This demonstrates the fix - ltrim would have stripped all \xEF, \xBB, \xBF anywhere + $svgWithEFAfterBom = "\xEF\xBB\xBF\xEF"; + $svgEfBase64 = base64_encode( $svgWithEFAfterBom ); + $this->assertTrue( $validatorWithSvg->isValid( $svgEfBase64 ) ); + } + + /** + * Test data URI with line-wrapped base64 (common in email/MIME). + */ + public function testDataUriWithLineWrappedBase64() + { + // PNG image with base64 wrapped at 76 characters (common MIME format) + $pngBase64Wrapped = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChw\nGA60e6kgAAAABJRU5ErkJggg=="; + + // Test with newline in base64 data + $dataUriWithNewline = 'data:image/png;base64,' . $pngBase64Wrapped; + + $validator = new IsImage(); + // Should pass - newlines are valid in base64 data URIs + $this->assertTrue( $validator->isValid( $dataUriWithNewline ) ); + + // Test with multiple newlines and spaces (also valid) + $pngBase64MultiLine = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ\n" . + "AAAADUlEQVR42mNkYPhfDwAChw\n" . + "GA60e6kgAAAABJRU5ErkJggg=="; + + $dataUriMultiLine = 'data:image/png;base64,' . $pngBase64MultiLine; + $this->assertTrue( $validator->isValid( $dataUriMultiLine ) ); + + // Test with carriage return + newline (Windows style) + $pngBase64CRLF = str_replace("\n", "\r\n", $pngBase64Wrapped); + $dataUriCRLF = 'data:image/png;base64,' . $pngBase64CRLF; + $this->assertTrue( $validator->isValid( $dataUriCRLF ) ); + } + + /** + * Test MIME type restrictions work for raw base64 input. + */ + public function testMimeTypeRestrictionsForRawBase64() + { + // PNG image as raw base64 + $pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; + + // JPEG image as raw base64 + $jpegBase64 = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAr/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='; + + // Validator that only allows JPEG, even with checkImageData=false + $jpegOnlyValidator = new IsImage( [ 'image/jpeg' ], null, false ); + + // JPEG should pass + $this->assertTrue( $jpegOnlyValidator->isValid( $jpegBase64 ) ); + + // PNG should fail even with checkImageData=false + $this->assertFalse( $jpegOnlyValidator->isValid( $pngBase64 ) ); + + // Validator that only allows PNG + $pngOnlyValidator = new IsImage( [ 'image/png' ], null, false ); + + // PNG should pass + $this->assertTrue( $pngOnlyValidator->isValid( $pngBase64 ) ); + + // JPEG should fail + $this->assertFalse( $pngOnlyValidator->isValid( $jpegBase64 ) ); + } + + /** + * Test empty allowedMimeTypes with SVG doesn't restrict to SVG only. + */ + public function testEmptyAllowedMimeTypesWithSvgAllowsAll() + { + // Empty array means allow all types + $validatorAllowAll = new IsImage( [], null, true, true ); + + // All image types should pass + $jpegBase64 = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAr/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='; + $this->assertTrue( $validatorAllowAll->isValid( $jpegBase64 ) ); + + $pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; + $this->assertTrue( $validatorAllowAll->isValid( $pngBase64 ) ); + + $gifBase64 = 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + $this->assertTrue( $validatorAllowAll->isValid( $gifBase64 ) ); + + // SVG should also pass + $svgBase64 = base64_encode( '' ); + $this->assertTrue( $validatorAllowAll->isValid( $svgBase64 ) ); + + // Now test with allowSvg=false but empty allowedMimeTypes + $validatorNoSvg = new IsImage( [], null, true, false ); + + // Non-SVG images should still pass + $this->assertTrue( $validatorNoSvg->isValid( $jpegBase64 ) ); + $this->assertTrue( $validatorNoSvg->isValid( $pngBase64 ) ); + + // SVG should fail + $this->assertFalse( $validatorNoSvg->isValid( $svgBase64 ) ); + } + + /** + * Test SVG accepted when explicitly in allowedMimeTypes even with allowSvg=false. + */ + public function testSvgAcceptedWhenExplicitlyAllowed() + { + // SVG content + $svgContent = ''; + $svgBase64 = base64_encode( $svgContent ); + + // Validator with SVG explicitly in allowedMimeTypes but allowSvg=false + $validatorExplicitSvg = new IsImage( [ 'image/jpeg', 'image/svg+xml' ], null, true, false ); + + // SVG should pass because it's explicitly allowed in MIME types + $this->assertTrue( $validatorExplicitSvg->isValid( $svgBase64 ) ); + + // Data URI with SVG should also work + $svgDataUri = 'data:image/svg+xml;base64,' . $svgBase64; + $this->assertTrue( $validatorExplicitSvg->isValid( $svgDataUri ) ); + + // Validator without SVG in allowedMimeTypes and allowSvg=false + $validatorNoSvg = new IsImage( [ 'image/jpeg', 'image/png' ], null, true, false ); + + // SVG should fail + $this->assertFalse( $validatorNoSvg->isValid( $svgBase64 ) ); + $this->assertFalse( $validatorNoSvg->isValid( $svgDataUri ) ); + } + + /** + * Test malformed data URI. + */ + public function testMalformedDataUri() + { + $validator = new IsImage(); + + // Missing base64 declaration + $malformed1 = 'data:image/png,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB'; + $this->assertFalse( $validator->isValid( $malformed1 ) ); + + // Missing MIME type + $malformed2 = 'data:;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB'; + $this->assertFalse( $validator->isValid( $malformed2 ) ); + + // Completely invalid format + $malformed3 = 'data:not-a-valid-uri'; + $this->assertFalse( $validator->isValid( $malformed3 ) ); + } +} \ No newline at end of file