Autoría | Ultima modificación | Ver Log |
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_files\redactor\services;
/**
* Tests for the EXIF remover service.
*
* If you wish to use these unit tests all you need to do is add the following definition to
* your config.php file:
*
* define('TEST_PATH_TO_EXIFTOOL', '/usr/bin/exiftool');
*
* @package core_files
* @copyright Meirza <meirza.arson@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @covers \core_files\redactor\services\exifremover_service
*/
final class exifremover_service_test extends \advanced_testcase {
/**
* Tests the `exifremover_service` functionality using PHP GD.
*
* This test verifies the ability of the `exifremover_service` to remove all EXIF
* tags from an image file when using PHP GD. It ensures that all tags, including
* GPSLatitude, GPSLongitude, and Orientation, are removed from the EXIF data.
*
* @return void
*/
public function test_exifremover_service_with_gd(): void {
$this->resetAfterTest(true);
// Ensure that the exif remover tool path is not set.
set_config('exifremovertoolpath', null, 'core_files');
$sourcepath = self::get_fixture_path('core_files', 'redactor/dummy.jpg');
// Get the EXIF data from the original file.
$currentexif = $this->get_exif_data_from_file($sourcepath);
$this->assertStringContainsString('GPSLatitude', $currentexif);
$this->assertStringContainsString('GPSLongitude', $currentexif);
$this->assertStringContainsString('Orientation', $currentexif);
// Redact the file.
$service = new exifremover_service();
$newfile = $service->redact_file_by_path('image/jpeg', $sourcepath);
// Get the EXIF data from the new file.
$newexif = $this->get_exif_data_from_file($newfile);
// Removing the "all" tags will result in removing all existing tags.
$this->assertStringNotContainsString('GPSLatitude', $newexif);
$this->assertStringNotContainsString('GPSLongitude', $newexif);
$this->assertStringNotContainsString('Orientation', $newexif);
}
/**
* Tests the `exifremover_service` functionality to flip orientation.
*
* @dataProvider exifremover_service_flip_orientation_provider
* @param string $sourcepath the path to the source image.
* @param string $expectedpath the path to the expected image.
* @param bool $expectedresult the expected result of the comparison.
*/
public function test_exifremover_service_flip_orientation_with_gd(
string $sourcepath,
string $expectedpath,
bool $expectedresult
): void {
$this->resetAfterTest(true);
// Ensure that the exif remover tool path is not set.
set_config('exifremovertoolpath', null, 'core_files');
// Flip the orientation.
$service = new exifremover_service();
$newfile = $service->redact_file_by_path('image/jpeg', $sourcepath);
// Compare the actual and expected images.
$this->assertEquals($expectedresult, $this->compare_images($newfile, $expectedpath));
}
/**
* Data provider for test_exifremover_service_flip_orientation().
*
* @return array
*/
public static function exifremover_service_flip_orientation_provider(): array {
return [
'Flip right-top' => [
'sourcepath' => self::get_fixture_path('core_files', 'redactor/righttop.jpg'),
'expectedpath' => self::get_fixture_path('core_files', 'redactor/topleft.jpg'),
'expectedresult' => true,
],
'The image will not be the same after the flip process' => [
'sourcepath' => self::get_fixture_path('core_files', 'redactor/righttop.jpg'),
'expectedpath' => self::get_fixture_path('core_files', 'redactor/righttop.jpg'),
'expectedresult' => false,
],
];
}
/**
* Compares two images pixel by pixel.
*
* @param string $image1path the path to the first image.
* @param string $image2path the path to the second image.
* @return bool True if the images are identical, false otherwise.
*/
private function compare_images(string $image1path, string $image2path): bool {
$image1 = imagecreatefromjpeg($image1path);
$image2 = imagecreatefromjpeg($image2path);
if (!$image1 || !$image2) {
return false;
}
$width1 = imagesx($image1);
$height1 = imagesy($image1);
$width2 = imagesx($image2);
$height2 = imagesy($image2);
if ($width1 !== $width2 || $height1 !== $height2) {
return false;
}
for ($x = 0; $x < $width1; $x++) {
for ($y = 0; $y < $height1; $y++) {
if (imagecolorat($image1, $x, $y) !== imagecolorat($image2, $x, $y)) {
return false;
}
}
}
return true;
}
/**
* Tests the `exifremover_service` functionality using ExifTool.
*
* This test verifies the ability of the `exifremover_service` to remove specific
* EXIF tags from an image file when configured to use ExifTool. The test includes
* scenarios for removing all EXIF tags and for removing only GPS tags.
*/
public function test_exifremover_service_with_exiftool(): void {
$this->require_exiftool();
$this->resetAfterTest(true);
$sourcepath = self::get_fixture_path('core_files', 'redactor/dummy.jpg');
set_config('file_redactor_exifremovertoolpath', TEST_PATH_TO_EXIFTOOL);
// Remove All tags.
set_config('file_redactor_exifremoverremovetags', 'all');
$service = new exifremover_service();
$newfile = $service->redact_file_by_path('image/jpeg', $sourcepath);
// Get the EXIF data from the new file.
$newexif = $this->get_exif_data_from_file($newfile);
// Removing the "all" tags will result in removing all existing tags.
$this->assertStringNotContainsString('GPSLatitude', $newexif);
$this->assertStringNotContainsString('GPSLongitude', $newexif);
$this->assertStringNotContainsString('Aperture', $newexif);
// Orientation is a preserve tag. Ensure it always exists.
$this->assertStringContainsString('Orientation', $newexif);
// Remove the GPS tag only.
set_config('file_redactor_exifremoverremovetags', 'gps');
$service = new exifremover_service();
$newfile = $service->redact_file_by_path('image/jpeg', $sourcepath);
// Get the EXIF data from the new file.
$newexif = $this->get_exif_data_from_file($newfile);
// The GPS tag only removal will remove the tag containing "GPS" keyword.
$this->assertStringNotContainsString('GPSLatitude', $newexif);
$this->assertStringNotContainsString('GPSLongitude', $newexif);
// And keep the other tags remaining.
$this->assertStringContainsString('Aperture', $newexif);
// Orientation is a preserve tag. Ensure it always exists.
$this->assertStringContainsString('Orientation', $newexif);
}
/**
* Tests the `is_mimetype_supported` method.
*
* This test initializes the `exifremover_service` and verifies if the given
* MIME types are supported for EXIF removal using both PHP GD and ExifTool.
*/
public function test_exifremover_service_is_mimetype_supported_generic(): void {
$service = new exifremover_service();
// Ensure that an unsupported mimetype is not accepted.
$this->assertFalse($service->is_mimetype_supported('application/binary'));
// An unsupported mimetype will just return null.
$sourcepath = self::get_fixture_path('core_files', 'redactor/dummy.jpg');
$this->assertNull($service->redact_file_by_path('application/binary', $sourcepath));
$this->assertNull($service->redact_file_by_content('application/binary', $sourcepath));
}
/**
* Tests the `is_mimetype_supported` method.
*
* This test initializes the `exifremover_service` and verifies if the given
* MIME types are supported for EXIF removal using both PHP GD and ExifTool.
*/
public function test_exifremover_service_is_mimetype_supported_gd(): void {
$this->resetAfterTest(true);
// Ensure that the exif remover tool path is not set.
set_config('file_redactor_exifremovertoolpath', null);
$service = new exifremover_service();
// The default MIME type is supported.
$this->assertTrue($service->is_mimetype_supported(exifremover_service::DEFAULT_MIMETYPE));
// Other than the default, the function will returns false.
$this->assertFalse($service->is_mimetype_supported('image/tiff'));
}
/**
* Tests the `is_mimetype_supported` method.
*
* This test initializes the `exifremover_service` and verifies if the given
* MIME types are supported for EXIF removal using both PHP GD and ExifTool.
*/
public function test_exifremover_service_is_mimetype_supported_exiftool(): void {
$this->require_exiftool();
$this->resetAfterTest(true);
set_config('file_redactor_exifremovertoolpath', TEST_PATH_TO_EXIFTOOL);
// Set the supported mime types to only redact image/tiff.
set_config('file_redactor_exifremovermimetype', 'image/tiff');
$service = new exifremover_service();
$this->assertTrue($service->is_mimetype_supported('image/tiff'));
// Other image formats are not supported.
$this->assertFalse($service->is_mimetype_supported('image/png'));
}
/**
* Tests the EXIF remover service with an unknown filename and an invalid EXIF tool path.
*/
public function test_exiftool_notfound_filename_unknown(): void {
$this->resetAfterTest(true);
set_config('file_redactor_exifremovertoolpath', 'fakeexiftool');
$service = new exifremover_service();
$this->expectException(\core\exception\moodle_exception::class);
$this->expectExceptionMessage(get_string('redactor:exifremover:failedprocessgd', 'core_files'));
$service->redact_file_by_content('image/jpeg', 'content');
}
/**
* Retrieves the EXIF metadata of a file.
*
* @param string $content the content of file.
* @return string The EXIF metadata as a string.
*/
private function get_exif_data_from_content(string $content): string {
$logpath = make_request_directory() . '/temp.jpg';
file_put_contents($logpath, $content);
return $this->get_exif_data_from_file($logpath);
}
/**
* Retrieves the EXIF metadata of a file.
*
* @param string $filepath the path to the file.
* @return string The EXIF metadata as a string.
*/
private function get_exif_data_from_file(string $filepath): string {
$exif = exif_read_data($filepath);
$string = "";
foreach ($exif as $key => $value) {
if (is_array($value)) {
foreach ($value as $subkey => $subvalue) {
$string .= "$subkey: $subvalue\n";
}
} else {
$string .= "$key: $value\n";
}
}
return $string;
}
/**
* Data provider for test_exifremover_service_clean_filename().
*
* @return array
*/
public static function exifremover_service_clean_filename_provider(): array {
return [
'Hyphen minus -' => [
'filename' => '-if \'$LensModel eq "18-35mm"\'',
'expected' => 'if $LensModel eq 18-35mm',
],
'Minus −' => [
'filename' => '−filename.jpg',
'expected' => 'filename.jpg',
],
];
}
/**
* Helper to require valid testing exiftool configuration.
*/
private function require_exiftool(): void {
if (!defined('TEST_PATH_TO_EXIFTOOL')) {
$this->markTestSkipped('Could not test the EXIF remover service, missing configuration.');
}
if (!TEST_PATH_TO_EXIFTOOL) {
$this->markTestSkipped('Could not test the EXIF remover service, configuration invalid.');
}
if (!is_executable(TEST_PATH_TO_EXIFTOOL)) {
$this->markTestSkipped('Could not test the EXIF remover service, exiftool not executable.');
}
}
}