1441 |
ariadna |
1 |
<?php
|
|
|
2 |
// This file is part of Moodle - http://moodle.org/
|
|
|
3 |
//
|
|
|
4 |
// Moodle is free software: you can redistribute it and/or modify
|
|
|
5 |
// it under the terms of the GNU General Public License as published by
|
|
|
6 |
// the Free Software Foundation, either version 3 of the License, or
|
|
|
7 |
// (at your option) any later version.
|
|
|
8 |
//
|
|
|
9 |
// Moodle is distributed in the hope that it will be useful,
|
|
|
10 |
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
11 |
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
12 |
// GNU General Public License for more details.
|
|
|
13 |
//
|
|
|
14 |
// You should have received a copy of the GNU General Public License
|
|
|
15 |
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
16 |
|
|
|
17 |
namespace core_files\redactor\services;
|
|
|
18 |
|
|
|
19 |
/**
|
|
|
20 |
* Tests for the EXIF remover service.
|
|
|
21 |
*
|
|
|
22 |
* If you wish to use these unit tests all you need to do is add the following definition to
|
|
|
23 |
* your config.php file:
|
|
|
24 |
*
|
|
|
25 |
* define('TEST_PATH_TO_EXIFTOOL', '/usr/bin/exiftool');
|
|
|
26 |
*
|
|
|
27 |
* @package core_files
|
|
|
28 |
* @copyright Meirza <meirza.arson@moodle.com>
|
|
|
29 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
30 |
*
|
|
|
31 |
* @covers \core_files\redactor\services\exifremover_service
|
|
|
32 |
*/
|
|
|
33 |
final class exifremover_service_test extends \advanced_testcase {
|
|
|
34 |
/**
|
|
|
35 |
* Tests the `exifremover_service` functionality using PHP GD.
|
|
|
36 |
*
|
|
|
37 |
* This test verifies the ability of the `exifremover_service` to remove all EXIF
|
|
|
38 |
* tags from an image file when using PHP GD. It ensures that all tags, including
|
|
|
39 |
* GPSLatitude, GPSLongitude, and Orientation, are removed from the EXIF data.
|
|
|
40 |
*
|
|
|
41 |
* @return void
|
|
|
42 |
*/
|
|
|
43 |
public function test_exifremover_service_with_gd(): void {
|
|
|
44 |
$this->resetAfterTest(true);
|
|
|
45 |
|
|
|
46 |
// Ensure that the exif remover tool path is not set.
|
|
|
47 |
set_config('exifremovertoolpath', null, 'core_files');
|
|
|
48 |
|
|
|
49 |
$sourcepath = self::get_fixture_path('core_files', 'redactor/dummy.jpg');
|
|
|
50 |
|
|
|
51 |
// Get the EXIF data from the original file.
|
|
|
52 |
$currentexif = $this->get_exif_data_from_file($sourcepath);
|
|
|
53 |
$this->assertStringContainsString('GPSLatitude', $currentexif);
|
|
|
54 |
$this->assertStringContainsString('GPSLongitude', $currentexif);
|
|
|
55 |
$this->assertStringContainsString('Orientation', $currentexif);
|
|
|
56 |
|
|
|
57 |
// Redact the file.
|
|
|
58 |
$service = new exifremover_service();
|
|
|
59 |
$newfile = $service->redact_file_by_path('image/jpeg', $sourcepath);
|
|
|
60 |
|
|
|
61 |
// Get the EXIF data from the new file.
|
|
|
62 |
$newexif = $this->get_exif_data_from_file($newfile);
|
|
|
63 |
|
|
|
64 |
// Removing the "all" tags will result in removing all existing tags.
|
|
|
65 |
$this->assertStringNotContainsString('GPSLatitude', $newexif);
|
|
|
66 |
$this->assertStringNotContainsString('GPSLongitude', $newexif);
|
|
|
67 |
$this->assertStringNotContainsString('Orientation', $newexif);
|
|
|
68 |
}
|
|
|
69 |
|
|
|
70 |
/**
|
|
|
71 |
* Tests the `exifremover_service` functionality to flip orientation.
|
|
|
72 |
*
|
|
|
73 |
* @dataProvider exifremover_service_flip_orientation_provider
|
|
|
74 |
* @param string $sourcepath the path to the source image.
|
|
|
75 |
* @param string $expectedpath the path to the expected image.
|
|
|
76 |
* @param bool $expectedresult the expected result of the comparison.
|
|
|
77 |
*/
|
|
|
78 |
public function test_exifremover_service_flip_orientation_with_gd(
|
|
|
79 |
string $sourcepath,
|
|
|
80 |
string $expectedpath,
|
|
|
81 |
bool $expectedresult
|
|
|
82 |
): void {
|
|
|
83 |
$this->resetAfterTest(true);
|
|
|
84 |
|
|
|
85 |
// Ensure that the exif remover tool path is not set.
|
|
|
86 |
set_config('exifremovertoolpath', null, 'core_files');
|
|
|
87 |
|
|
|
88 |
// Flip the orientation.
|
|
|
89 |
$service = new exifremover_service();
|
|
|
90 |
$newfile = $service->redact_file_by_path('image/jpeg', $sourcepath);
|
|
|
91 |
|
|
|
92 |
// Compare the actual and expected images.
|
|
|
93 |
$this->assertEquals($expectedresult, $this->compare_images($newfile, $expectedpath));
|
|
|
94 |
}
|
|
|
95 |
|
|
|
96 |
/**
|
|
|
97 |
* Data provider for test_exifremover_service_flip_orientation().
|
|
|
98 |
*
|
|
|
99 |
* @return array
|
|
|
100 |
*/
|
|
|
101 |
public static function exifremover_service_flip_orientation_provider(): array {
|
|
|
102 |
return [
|
|
|
103 |
'Flip right-top' => [
|
|
|
104 |
'sourcepath' => self::get_fixture_path('core_files', 'redactor/righttop.jpg'),
|
|
|
105 |
'expectedpath' => self::get_fixture_path('core_files', 'redactor/topleft.jpg'),
|
|
|
106 |
'expectedresult' => true,
|
|
|
107 |
],
|
|
|
108 |
'The image will not be the same after the flip process' => [
|
|
|
109 |
'sourcepath' => self::get_fixture_path('core_files', 'redactor/righttop.jpg'),
|
|
|
110 |
'expectedpath' => self::get_fixture_path('core_files', 'redactor/righttop.jpg'),
|
|
|
111 |
'expectedresult' => false,
|
|
|
112 |
],
|
|
|
113 |
];
|
|
|
114 |
}
|
|
|
115 |
|
|
|
116 |
/**
|
|
|
117 |
* Compares two images pixel by pixel.
|
|
|
118 |
*
|
|
|
119 |
* @param string $image1path the path to the first image.
|
|
|
120 |
* @param string $image2path the path to the second image.
|
|
|
121 |
* @return bool True if the images are identical, false otherwise.
|
|
|
122 |
*/
|
|
|
123 |
private function compare_images(string $image1path, string $image2path): bool {
|
|
|
124 |
$image1 = imagecreatefromjpeg($image1path);
|
|
|
125 |
$image2 = imagecreatefromjpeg($image2path);
|
|
|
126 |
|
|
|
127 |
if (!$image1 || !$image2) {
|
|
|
128 |
return false;
|
|
|
129 |
}
|
|
|
130 |
|
|
|
131 |
$width1 = imagesx($image1);
|
|
|
132 |
$height1 = imagesy($image1);
|
|
|
133 |
$width2 = imagesx($image2);
|
|
|
134 |
$height2 = imagesy($image2);
|
|
|
135 |
|
|
|
136 |
if ($width1 !== $width2 || $height1 !== $height2) {
|
|
|
137 |
return false;
|
|
|
138 |
}
|
|
|
139 |
|
|
|
140 |
for ($x = 0; $x < $width1; $x++) {
|
|
|
141 |
for ($y = 0; $y < $height1; $y++) {
|
|
|
142 |
if (imagecolorat($image1, $x, $y) !== imagecolorat($image2, $x, $y)) {
|
|
|
143 |
return false;
|
|
|
144 |
}
|
|
|
145 |
}
|
|
|
146 |
}
|
|
|
147 |
|
|
|
148 |
return true;
|
|
|
149 |
}
|
|
|
150 |
|
|
|
151 |
/**
|
|
|
152 |
* Tests the `exifremover_service` functionality using ExifTool.
|
|
|
153 |
*
|
|
|
154 |
* This test verifies the ability of the `exifremover_service` to remove specific
|
|
|
155 |
* EXIF tags from an image file when configured to use ExifTool. The test includes
|
|
|
156 |
* scenarios for removing all EXIF tags and for removing only GPS tags.
|
|
|
157 |
*/
|
|
|
158 |
public function test_exifremover_service_with_exiftool(): void {
|
|
|
159 |
$this->require_exiftool();
|
|
|
160 |
$this->resetAfterTest(true);
|
|
|
161 |
|
|
|
162 |
$sourcepath = self::get_fixture_path('core_files', 'redactor/dummy.jpg');
|
|
|
163 |
set_config('file_redactor_exifremovertoolpath', TEST_PATH_TO_EXIFTOOL);
|
|
|
164 |
|
|
|
165 |
// Remove All tags.
|
|
|
166 |
set_config('file_redactor_exifremoverremovetags', 'all');
|
|
|
167 |
$service = new exifremover_service();
|
|
|
168 |
$newfile = $service->redact_file_by_path('image/jpeg', $sourcepath);
|
|
|
169 |
|
|
|
170 |
// Get the EXIF data from the new file.
|
|
|
171 |
$newexif = $this->get_exif_data_from_file($newfile);
|
|
|
172 |
|
|
|
173 |
// Removing the "all" tags will result in removing all existing tags.
|
|
|
174 |
$this->assertStringNotContainsString('GPSLatitude', $newexif);
|
|
|
175 |
$this->assertStringNotContainsString('GPSLongitude', $newexif);
|
|
|
176 |
$this->assertStringNotContainsString('Aperture', $newexif);
|
|
|
177 |
|
|
|
178 |
// Orientation is a preserve tag. Ensure it always exists.
|
|
|
179 |
$this->assertStringContainsString('Orientation', $newexif);
|
|
|
180 |
|
|
|
181 |
// Remove the GPS tag only.
|
|
|
182 |
set_config('file_redactor_exifremoverremovetags', 'gps');
|
|
|
183 |
|
|
|
184 |
$service = new exifremover_service();
|
|
|
185 |
$newfile = $service->redact_file_by_path('image/jpeg', $sourcepath);
|
|
|
186 |
|
|
|
187 |
// Get the EXIF data from the new file.
|
|
|
188 |
$newexif = $this->get_exif_data_from_file($newfile);
|
|
|
189 |
|
|
|
190 |
// The GPS tag only removal will remove the tag containing "GPS" keyword.
|
|
|
191 |
$this->assertStringNotContainsString('GPSLatitude', $newexif);
|
|
|
192 |
$this->assertStringNotContainsString('GPSLongitude', $newexif);
|
|
|
193 |
|
|
|
194 |
// And keep the other tags remaining.
|
|
|
195 |
$this->assertStringContainsString('Aperture', $newexif);
|
|
|
196 |
|
|
|
197 |
// Orientation is a preserve tag. Ensure it always exists.
|
|
|
198 |
$this->assertStringContainsString('Orientation', $newexif);
|
|
|
199 |
}
|
|
|
200 |
|
|
|
201 |
/**
|
|
|
202 |
* Tests the `is_mimetype_supported` method.
|
|
|
203 |
*
|
|
|
204 |
* This test initializes the `exifremover_service` and verifies if the given
|
|
|
205 |
* MIME types are supported for EXIF removal using both PHP GD and ExifTool.
|
|
|
206 |
*/
|
|
|
207 |
public function test_exifremover_service_is_mimetype_supported_generic(): void {
|
|
|
208 |
$service = new exifremover_service();
|
|
|
209 |
|
|
|
210 |
// Ensure that an unsupported mimetype is not accepted.
|
|
|
211 |
$this->assertFalse($service->is_mimetype_supported('application/binary'));
|
|
|
212 |
|
|
|
213 |
// An unsupported mimetype will just return null.
|
|
|
214 |
$sourcepath = self::get_fixture_path('core_files', 'redactor/dummy.jpg');
|
|
|
215 |
$this->assertNull($service->redact_file_by_path('application/binary', $sourcepath));
|
|
|
216 |
$this->assertNull($service->redact_file_by_content('application/binary', $sourcepath));
|
|
|
217 |
}
|
|
|
218 |
|
|
|
219 |
/**
|
|
|
220 |
* Tests the `is_mimetype_supported` method.
|
|
|
221 |
*
|
|
|
222 |
* This test initializes the `exifremover_service` and verifies if the given
|
|
|
223 |
* MIME types are supported for EXIF removal using both PHP GD and ExifTool.
|
|
|
224 |
*/
|
|
|
225 |
public function test_exifremover_service_is_mimetype_supported_gd(): void {
|
|
|
226 |
$this->resetAfterTest(true);
|
|
|
227 |
|
|
|
228 |
// Ensure that the exif remover tool path is not set.
|
|
|
229 |
set_config('file_redactor_exifremovertoolpath', null);
|
|
|
230 |
|
|
|
231 |
$service = new exifremover_service();
|
|
|
232 |
|
|
|
233 |
// The default MIME type is supported.
|
|
|
234 |
$this->assertTrue($service->is_mimetype_supported(exifremover_service::DEFAULT_MIMETYPE));
|
|
|
235 |
|
|
|
236 |
// Other than the default, the function will returns false.
|
|
|
237 |
$this->assertFalse($service->is_mimetype_supported('image/tiff'));
|
|
|
238 |
}
|
|
|
239 |
|
|
|
240 |
/**
|
|
|
241 |
* Tests the `is_mimetype_supported` method.
|
|
|
242 |
*
|
|
|
243 |
* This test initializes the `exifremover_service` and verifies if the given
|
|
|
244 |
* MIME types are supported for EXIF removal using both PHP GD and ExifTool.
|
|
|
245 |
*/
|
|
|
246 |
public function test_exifremover_service_is_mimetype_supported_exiftool(): void {
|
|
|
247 |
$this->require_exiftool();
|
|
|
248 |
$this->resetAfterTest(true);
|
|
|
249 |
|
|
|
250 |
set_config('file_redactor_exifremovertoolpath', TEST_PATH_TO_EXIFTOOL);
|
|
|
251 |
|
|
|
252 |
// Set the supported mime types to only redact image/tiff.
|
|
|
253 |
set_config('file_redactor_exifremovermimetype', 'image/tiff');
|
|
|
254 |
$service = new exifremover_service();
|
|
|
255 |
|
|
|
256 |
$this->assertTrue($service->is_mimetype_supported('image/tiff'));
|
|
|
257 |
|
|
|
258 |
// Other image formats are not supported.
|
|
|
259 |
$this->assertFalse($service->is_mimetype_supported('image/png'));
|
|
|
260 |
}
|
|
|
261 |
|
|
|
262 |
/**
|
|
|
263 |
* Tests the EXIF remover service with an unknown filename and an invalid EXIF tool path.
|
|
|
264 |
*/
|
|
|
265 |
public function test_exiftool_notfound_filename_unknown(): void {
|
|
|
266 |
$this->resetAfterTest(true);
|
|
|
267 |
set_config('file_redactor_exifremovertoolpath', 'fakeexiftool');
|
|
|
268 |
|
|
|
269 |
$service = new exifremover_service();
|
|
|
270 |
$this->expectException(\core\exception\moodle_exception::class);
|
|
|
271 |
$this->expectExceptionMessage(get_string('redactor:exifremover:failedprocessgd', 'core_files'));
|
|
|
272 |
$service->redact_file_by_content('image/jpeg', 'content');
|
|
|
273 |
}
|
|
|
274 |
|
|
|
275 |
/**
|
|
|
276 |
* Retrieves the EXIF metadata of a file.
|
|
|
277 |
*
|
|
|
278 |
* @param string $content the content of file.
|
|
|
279 |
* @return string The EXIF metadata as a string.
|
|
|
280 |
*/
|
|
|
281 |
private function get_exif_data_from_content(string $content): string {
|
|
|
282 |
$logpath = make_request_directory() . '/temp.jpg';
|
|
|
283 |
file_put_contents($logpath, $content);
|
|
|
284 |
|
|
|
285 |
return $this->get_exif_data_from_file($logpath);
|
|
|
286 |
}
|
|
|
287 |
|
|
|
288 |
/**
|
|
|
289 |
* Retrieves the EXIF metadata of a file.
|
|
|
290 |
*
|
|
|
291 |
* @param string $filepath the path to the file.
|
|
|
292 |
* @return string The EXIF metadata as a string.
|
|
|
293 |
*/
|
|
|
294 |
private function get_exif_data_from_file(string $filepath): string {
|
|
|
295 |
$exif = exif_read_data($filepath);
|
|
|
296 |
|
|
|
297 |
$string = "";
|
|
|
298 |
foreach ($exif as $key => $value) {
|
|
|
299 |
if (is_array($value)) {
|
|
|
300 |
foreach ($value as $subkey => $subvalue) {
|
|
|
301 |
$string .= "$subkey: $subvalue\n";
|
|
|
302 |
}
|
|
|
303 |
} else {
|
|
|
304 |
$string .= "$key: $value\n";
|
|
|
305 |
}
|
|
|
306 |
}
|
|
|
307 |
return $string;
|
|
|
308 |
}
|
|
|
309 |
|
|
|
310 |
/**
|
|
|
311 |
* Data provider for test_exifremover_service_clean_filename().
|
|
|
312 |
*
|
|
|
313 |
* @return array
|
|
|
314 |
*/
|
|
|
315 |
public static function exifremover_service_clean_filename_provider(): array {
|
|
|
316 |
return [
|
|
|
317 |
'Hyphen minus -' => [
|
|
|
318 |
'filename' => '-if \'$LensModel eq "18-35mm"\'',
|
|
|
319 |
'expected' => 'if $LensModel eq 18-35mm',
|
|
|
320 |
],
|
|
|
321 |
'Minus −' => [
|
|
|
322 |
'filename' => '−filename.jpg',
|
|
|
323 |
'expected' => 'filename.jpg',
|
|
|
324 |
],
|
|
|
325 |
];
|
|
|
326 |
}
|
|
|
327 |
|
|
|
328 |
/**
|
|
|
329 |
* Helper to require valid testing exiftool configuration.
|
|
|
330 |
*/
|
|
|
331 |
private function require_exiftool(): void {
|
|
|
332 |
if (!defined('TEST_PATH_TO_EXIFTOOL')) {
|
|
|
333 |
$this->markTestSkipped('Could not test the EXIF remover service, missing configuration.');
|
|
|
334 |
}
|
|
|
335 |
|
|
|
336 |
if (!TEST_PATH_TO_EXIFTOOL) {
|
|
|
337 |
$this->markTestSkipped('Could not test the EXIF remover service, configuration invalid.');
|
|
|
338 |
}
|
|
|
339 |
|
|
|
340 |
if (!is_executable(TEST_PATH_TO_EXIFTOOL)) {
|
|
|
341 |
$this->markTestSkipped('Could not test the EXIF remover service, exiftool not executable.');
|
|
|
342 |
}
|
|
|
343 |
}
|
|
|
344 |
}
|