mirror of
https://github.com/moodle/moodle.git
synced 2025-01-17 21:49:15 +01:00
MDL-83245 core_files: Update file redaction API
This change: - Moves the API to use the `before_file_created` hook - Remove the newly created `$notify` param for `after_file_created` hook - Stop persisting redactable content - Update manager to not deal with `stored_file` instances - Correct namespace from `\core\filereact` to `\core_files\redactor` - Add `redactor` as a valid L2 namespace within the `core_files` API - Correct config setting names - Adds missing unit tests - Disables the service for PHPUnit tests AMOS BEGIN MOV [fileredact,core_files],[redactor,core_files] MOV [fileredact,core_files],[redactor:exifremover,core_files] MOV [fileredact,core_files],[redactor:exifremover:emptyremovetags,core_files] MOV [fileredact,core_files],[redactor:exifremover:enabled,core_files] MOV [fileredact,core_files],[redactor:exifremover:enabled_desc,core_files] MOV [fileredact,core_files],[redactor:exifremover:failedprocessexiftool,core_files] MOV [fileredact,core_files],[redactor:exifremover:failedprocessgd,core_files] MOV [fileredact,core_files],[redactor:exifremover:heading,core_files] MOV [fileredact,core_files],[redactor:exifremover:mimetype,core_files] MOV [fileredact,core_files],[redactor:exifremover:mimetype_desc,core_files] MOV [fileredact,core_files],[redactor:exifremover:removetags,core_files] MOV [fileredact,core_files],[redactor:exifremover:removetags_desc,core_files] MOV [fileredact,core_files],[redactor:exifremover:tag:all,core_files] MOV [fileredact,core_files],[redactor:exifremover:tag:gps,core_files] MOV [fileredact,core_files],[redactor:exifremover:tooldoesnotexist,core_files] MOV [fileredact,core_files],[redactor:exifremover:toolpath,core_files] MOV [fileredact,core_files],[redactor:exifremover:toolpath_desc,core_files] AMOS END
This commit is contained in:
parent
525fc81cf6
commit
b9a5ed7737
3
.github/workflows/push.yml
vendored
3
.github/workflows/push.yml
vendored
@ -122,3 +122,6 @@ jobs:
|
||||
dbtype: ${{ matrix.db }}
|
||||
phpunit_options: ${{ secrets.phpunit_options }}
|
||||
run: vendor/bin/phpunit $phpunit_options ${{ inputs.phpunit_extra_options }}
|
||||
|
||||
- name: Git status
|
||||
run: git status
|
||||
|
@ -2,12 +2,5 @@ issueNumber: MDL-75850
|
||||
notes:
|
||||
core_files:
|
||||
- message: |
|
||||
The following are the changes made:
|
||||
- New hook after_file_created
|
||||
- In the \core_files\file_storage, new additional param $notify (default is true) added to:
|
||||
- ::create_file_from_storedfile()
|
||||
- ::create_file_from_pathname()
|
||||
- ::create_file_from_string()
|
||||
- ::create_file()
|
||||
If true, it will trigger the after_file_created hook to re-create the image.
|
||||
A new hook, `\core_files\hook\after_file_created`, has been created to allow the inspection of files after they have been saved in the filesystem.
|
||||
type: improved
|
||||
|
8
.upgradenotes/MDL-83245-2024092406514231.yml
Normal file
8
.upgradenotes/MDL-83245-2024092406514231.yml
Normal file
@ -0,0 +1,8 @@
|
||||
issueNumber: MDL-83245
|
||||
notes:
|
||||
core_files:
|
||||
- message: >-
|
||||
A new hook, `\core_files\hook\before_file_created`, has been created to
|
||||
allow modification of a file immediately before it is stored in the file
|
||||
system.
|
||||
type: improved
|
@ -15,7 +15,7 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Configure the settings for fileredact.
|
||||
* Configure the settings for file redaction service.
|
||||
*
|
||||
* @package core_admin
|
||||
* @copyright Meirza <meirza.arson@moodle.com>
|
||||
@ -25,19 +25,20 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
if ($hassiteconfig) {
|
||||
if (!$ADMIN->locate('fileredact')) {
|
||||
$ADMIN->add('server', new admin_category('fileredact', get_string('fileredact', 'core_files')));
|
||||
if (!$ADMIN->locate('file_redactor')) {
|
||||
$ADMIN->add('server', new admin_category('file_redactor', get_string('redactor', 'core_files')));
|
||||
}
|
||||
|
||||
$manager = \core\di::get(\core_files\redactor\manager::class);
|
||||
|
||||
// Get settings from each service.
|
||||
$servicesdir = "{$CFG->libdir}/classes/fileredact/services/";
|
||||
$servicefiles = glob("{$servicesdir}*_service.php");
|
||||
foreach ($servicefiles as $servicefile) {
|
||||
$servicename = basename($servicefile, '_service.php');
|
||||
$classname = "\\core\\fileredact\\services\\{$servicename}_service";
|
||||
if (class_exists($classname)) {
|
||||
$fileredactsettings = new admin_settingpage($servicename, new lang_string("fileredact:$servicename", 'core_files'));
|
||||
call_user_func("{$classname}::add_settings", $fileredactsettings);
|
||||
$ADMIN->add('fileredact', $fileredactsettings);
|
||||
}
|
||||
foreach ($manager->get_service_classnames() as $servicename => $service) {
|
||||
$servicesettings = new admin_settingpage(
|
||||
$servicename,
|
||||
new lang_string("redactor:{$servicename}", 'core_files'),
|
||||
);
|
||||
$service::add_settings($servicesettings);
|
||||
|
||||
$ADMIN->add('file_redactor', $servicesettings);
|
||||
}
|
||||
}
|
||||
|
@ -14,19 +14,19 @@
|
||||
// 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\hook\filestorage;
|
||||
namespace core_files\hook;
|
||||
|
||||
use core\attribute;
|
||||
use core\hook\stoppable_trait;
|
||||
|
||||
/**
|
||||
* Class after_file_created
|
||||
* A hook which is fired after a file is created in the file storage API.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2024 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
#[attribute\label('Allows subscribers to modify file after it is created')]
|
||||
#[attribute\label('Allows subscribers to inspect a file after it is created in the file pool')]
|
||||
#[attribute\tags('file')]
|
||||
#[attribute\hook\replaces_callbacks('after_file_created')]
|
||||
final class after_file_created {
|
160
files/classes/hook/before_file_created.php
Normal file
160
files/classes/hook/before_file_created.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?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\hook;
|
||||
|
||||
use core\exception\coding_exception;
|
||||
use core\attribute;
|
||||
|
||||
/**
|
||||
* A hook which is fired before a file is created in the file storage API.
|
||||
*
|
||||
* @package core_files
|
||||
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
#[attribute\label('Allows subscribers to modify file content before it is stored in the file pool')]
|
||||
#[attribute\tags('file')]
|
||||
#[attribute\hook\replaces_callbacks('before_file_created')]
|
||||
final class before_file_created {
|
||||
use \core\hook\stoppable_trait;
|
||||
|
||||
/** @var bool Whether the content has been updated at all */
|
||||
public bool $contentupdated = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \stdClass|null $filerecord
|
||||
* @param string|null $filepath The path to the file on disk
|
||||
* @param string|null $filecontent The content of the file
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var \stdClass The file record */
|
||||
protected ?\stdClass $filerecord = null,
|
||||
/** @var string|null $filepath The source file on disk */
|
||||
protected ?string $filepath = null,
|
||||
/** @var string|null $filecontent The content of the file if it is not stored on disk */
|
||||
protected ?string $filecontent = null,
|
||||
) {
|
||||
if ($filepath === null && $filecontent === null) {
|
||||
throw new \InvalidArgumentException('Either $filepath or $filecontent must be set');
|
||||
}
|
||||
|
||||
if ($filepath !== null && $filecontent !== null) {
|
||||
throw new \InvalidArgumentException('Only one of $filepath or $filecontent can be set');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the file path was specified.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_filepath(): bool {
|
||||
return $this->filepath !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the file content was specified.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_filecontent(): bool {
|
||||
return $this->filecontent !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file path to the file that will be stored.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_filepath(): ?string {
|
||||
return $this->filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file content that will be stored.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_filecontent(): ?string {
|
||||
return $this->filecontent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file record.
|
||||
*
|
||||
* @return \stdClass|null
|
||||
*/
|
||||
public function get_filerecord(): ?\stdClass {
|
||||
return $this->filerecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the file path to a new value.
|
||||
*
|
||||
* @param string $filepath
|
||||
*/
|
||||
public function update_filepath(string $filepath): void {
|
||||
if ($this->filepath === null) {
|
||||
throw new coding_exception('Cannot update file path when the file path is not set');
|
||||
}
|
||||
|
||||
if ($filepath !== $this->filepath) {
|
||||
$this->contentupdated = true;
|
||||
$this->filepath = $filepath;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the file content to a new value.
|
||||
*
|
||||
* @param string $filecontent
|
||||
*/
|
||||
public function update_filecontent(string $filecontent): void {
|
||||
if ($this->filecontent === null) {
|
||||
throw new coding_exception('Cannot update file content when the file content is not set');
|
||||
}
|
||||
|
||||
if ($filecontent !== $this->filecontent) {
|
||||
$this->contentupdated = true;
|
||||
$this->filecontent = $filecontent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the file path or file content has been changed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_changed(): bool {
|
||||
return $this->contentupdated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process legacy callbacks.
|
||||
*/
|
||||
public function process_legacy_callbacks(): void {
|
||||
if ($pluginsfunction = get_plugins_with_function(function: 'before_file_created', migratedtohook: true)) {
|
||||
foreach ($pluginsfunction as $plugintype => $plugins) {
|
||||
foreach ($plugins as $pluginfunction) {
|
||||
$pluginfunction($this->filerecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,9 +14,9 @@
|
||||
// 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\fileredact;
|
||||
namespace core_files\redactor;
|
||||
|
||||
use core\hook\filestorage\after_file_created;
|
||||
use core_files\hook\before_file_created;
|
||||
|
||||
/**
|
||||
* Allow the plugin to call as soon as possible before the file is created.
|
||||
@ -27,20 +27,37 @@ use core\hook\filestorage\after_file_created;
|
||||
*/
|
||||
class hook_listener {
|
||||
/**
|
||||
* Execute the available services after creating the file.
|
||||
* Execute the before_file_created hook listener for file redaction.
|
||||
*
|
||||
* @param after_file_created $hook
|
||||
* @param before_file_created $hook
|
||||
*/
|
||||
public static function redact_after_file_created(after_file_created $hook): void {
|
||||
$storedfile = $hook->storedfile;
|
||||
|
||||
public static function file_redaction_handler(before_file_created $hook): void {
|
||||
// The file mime-type must be present. Otherwise, bypass the process.
|
||||
if (empty($storedfile->get_mimetype())) {
|
||||
if (empty($hook->get_filerecord()) || empty($hook->get_filerecord()->mimetype)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$manager = new manager($storedfile);
|
||||
$manager->execute();
|
||||
$manager = \core\di::get(manager::class);
|
||||
|
||||
if ($hook->has_filepath()) {
|
||||
$file = $manager->redact_file(
|
||||
$hook->get_filerecord()->mimetype,
|
||||
$hook->get_filepath(),
|
||||
);
|
||||
|
||||
if ($file !== null) {
|
||||
$hook->update_filepath($file);
|
||||
}
|
||||
} else {
|
||||
$data = $manager->redact_file_content(
|
||||
$hook->get_filerecord()->mimetype,
|
||||
$hook->get_filecontent(),
|
||||
);
|
||||
|
||||
if ($data !== null) {
|
||||
$hook->update_filecontent($data);
|
||||
}
|
||||
}
|
||||
|
||||
// Iterates through the errors returned by the manager and outputs each error message.
|
||||
foreach ($manager->get_errors() as $e) {
|
144
files/classes/redactor/manager.php
Normal file
144
files/classes/redactor/manager.php
Normal file
@ -0,0 +1,144 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Fileredact manager.
|
||||
*
|
||||
* Manages and executes redaction services.
|
||||
*
|
||||
* @package core_files
|
||||
* @copyright Meirza <meirza.arson@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class manager {
|
||||
/** @var array Holds an array of error messages. */
|
||||
private array $errors = [];
|
||||
|
||||
/**
|
||||
* Redacts the given file.
|
||||
*
|
||||
* @param string $mimetype The mime-type of the file
|
||||
* @param string $filepath The path to the file to redact
|
||||
* @return string|null The path to the redacted file or null if no redaction services are available.
|
||||
*/
|
||||
public function redact_file(
|
||||
string $mimetype,
|
||||
string $filepath,
|
||||
): ?string {
|
||||
// Get the file redact services.
|
||||
$services = $this->get_service_classnames();
|
||||
$serviceinstances = array_filter(
|
||||
array_map(fn($serviceclass) => new $serviceclass(), $services),
|
||||
fn($service) => $service->is_enabled() && $service->is_mimetype_supported($mimetype)
|
||||
);
|
||||
|
||||
if (count($serviceinstances) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($serviceinstances as $servicename => $service) {
|
||||
try {
|
||||
return $service->redact_file_by_path($mimetype, $filepath);
|
||||
} catch (\Throwable $e) {
|
||||
$this->errors[] = $e;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redacts the given file content.
|
||||
*
|
||||
* @param string $mimetype The mime-type of the file
|
||||
* @param string $filecontent The file content to redact
|
||||
* @return string|null The content of the redacted file
|
||||
*/
|
||||
public function redact_file_content(
|
||||
string $mimetype,
|
||||
string $filecontent,
|
||||
): ?string {
|
||||
// Get the file redact services.
|
||||
$services = $this->get_file_services_for_mimetype($mimetype);
|
||||
|
||||
foreach ($services as $servicename => $service) {
|
||||
try {
|
||||
return $service->redact_file_by_content($mimetype, $filecontent);
|
||||
} catch (\Throwable $e) {
|
||||
$this->errors[] = $e;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of applicable redaction services.
|
||||
*
|
||||
* @return string[] list of service classnames.
|
||||
*/
|
||||
public function get_service_classnames(): array {
|
||||
global $CFG;
|
||||
$servicesdir = "{$CFG->dirroot}/files/classes/redactor/services/";
|
||||
$servicefiles = glob("{$servicesdir}*_service.php");
|
||||
$services = [];
|
||||
foreach ($servicefiles as $servicefile) {
|
||||
$servicename = basename($servicefile, '_service.php');
|
||||
$serviceclass = services::class . "\\{$servicename}_service";
|
||||
|
||||
if (!is_a($serviceclass, services\service::class, true)) {
|
||||
continue;
|
||||
}
|
||||
$services[$servicename] = $serviceclass;
|
||||
}
|
||||
return $services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of file redaction services that support the given mime-type.
|
||||
*
|
||||
* @param string $mimetype The mime-type to filter by
|
||||
* @return services\file_redactor_service_interface[] An array of file redaction services that support the given mime-type.
|
||||
*/
|
||||
protected function get_file_services_for_mimetype(string $mimetype): array {
|
||||
return array_filter(array_map(
|
||||
function(string $serviceclass) use ($mimetype): ?services\file_redactor_service_interface {
|
||||
if (!is_a($serviceclass, services\file_redactor_service_interface::class, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$service = new $serviceclass();
|
||||
if ($service->is_mimetype_supported($mimetype)) {
|
||||
return $service;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
$this->get_service_classnames(),
|
||||
), fn ($service) => $service !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an array of error messages.
|
||||
*
|
||||
* @return array An array of error messages.
|
||||
*/
|
||||
public function get_errors(): array {
|
||||
return $this->errors;
|
||||
}
|
||||
}
|
@ -14,9 +14,15 @@
|
||||
// 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\fileredact\services;
|
||||
namespace core_files\redactor\services;
|
||||
|
||||
use stored_file;
|
||||
use admin_setting_configcheckbox;
|
||||
use admin_setting_configexecutable;
|
||||
use admin_setting_configselect;
|
||||
use admin_setting_configtextarea;
|
||||
use admin_setting_heading;
|
||||
use core\exception\moodle_exception;
|
||||
use core\output\html_writer;
|
||||
|
||||
/**
|
||||
* Remove EXIF data from supported image files using PHP GD, or ExifTool if it is configured.
|
||||
@ -24,12 +30,11 @@ use stored_file;
|
||||
* The PHP GD stripping has minimal configuration and removes all EXIF data.
|
||||
* More stripping is made available when using ExifTool.
|
||||
*
|
||||
* @package core
|
||||
* @package core_files
|
||||
* @copyright Meirza <meirza.arson@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class exifremover_service extends service {
|
||||
|
||||
class exifremover_service extends service implements file_redactor_service_interface {
|
||||
/** @var array REMOVE_TAGS Tags to remove and their corresponding values. */
|
||||
const REMOVE_TAGS = [
|
||||
"gps" => '"-gps*="',
|
||||
@ -59,85 +64,136 @@ class exifremover_service extends service {
|
||||
private bool $useexiftool = false;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param stored_file $storedfile The file record.
|
||||
* Initialise the EXIF remover service.
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var stored_file The file record. */
|
||||
private readonly stored_file $storedfile,
|
||||
) {
|
||||
parent::__construct($storedfile);
|
||||
|
||||
public function __construct() {
|
||||
// To decide whether to use ExifTool or PHP GD, check the ExifTool path.
|
||||
if (!empty($this->get_exiftool_path())) {
|
||||
$this->useexiftool = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs redaction on the specified file.
|
||||
*/
|
||||
public function execute(): void {
|
||||
#[\Override]
|
||||
public function redact_file_by_path(
|
||||
string $mimetype,
|
||||
string $filepath,
|
||||
): ?string {
|
||||
if (!$this->is_mimetype_supported($mimetype)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->useexiftool) {
|
||||
// Use the ExifTool executable to remove the desired EXIF tags.
|
||||
$this->execute_exiftool();
|
||||
return $this->execute_exiftool($filepath);
|
||||
} else {
|
||||
// Use PHP GD lib to remove all EXIF tags.
|
||||
$this->execute_gd();
|
||||
return $this->execute_gd($filepath);
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function redact_file_by_content(
|
||||
string $mimetype,
|
||||
string $filecontent,
|
||||
): ?string {
|
||||
if (!$this->is_mimetype_supported($mimetype)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->useexiftool) {
|
||||
// Use the ExifTool executable to remove the desired EXIF tags.
|
||||
return $this->execute_exiftool_on_content($filecontent);
|
||||
} else {
|
||||
// Use PHP GD lib to remove all EXIF tags.
|
||||
return $this->execute_gd_on_content($filecontent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes ExifTool to remove metadata from the original file.
|
||||
*
|
||||
* @throws \moodle_exception If the ExifTool process fails or the destination file is not created.
|
||||
* @param string $sourcefile The file path of the file to redact
|
||||
* @return string The destination path of the recreated content
|
||||
* @throws moodle_exception If the ExifTool process fails or the destination file is not created.
|
||||
*/
|
||||
private function execute_exiftool(): void {
|
||||
$tmpfilepath = make_request_directory();
|
||||
$filerecordname = $this->clean_filename($this->storedfile->get_filename());
|
||||
$neworiginalfile = $tmpfilepath . DIRECTORY_SEPARATOR . 'new_' . $filerecordname;
|
||||
$destinationfile = $tmpfilepath . DIRECTORY_SEPARATOR . $filerecordname;
|
||||
|
||||
// Copy the original file to a new file.
|
||||
try {
|
||||
$this->storedfile->copy_content_to($neworiginalfile);
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
}
|
||||
private function execute_exiftool(string $sourcefile): string {
|
||||
$destinationfile = make_request_directory() . '/' . basename($sourcefile);
|
||||
|
||||
// Prepare the ExifTool command.
|
||||
$command = $this->get_exiftool_command($neworiginalfile, $destinationfile);
|
||||
$command = $this->get_exiftool_command($sourcefile, $destinationfile);
|
||||
|
||||
// Run the command.
|
||||
exec($command, $output, $resultcode);
|
||||
|
||||
// If the return code was not zero or the destination file was not successfully created.
|
||||
if ($resultcode !== 0 || !file_exists($destinationfile)) {
|
||||
throw new \moodle_exception(
|
||||
errorcode: 'fileredact:exifremover:failedprocessexiftool',
|
||||
throw new moodle_exception(
|
||||
errorcode: 'redactor:exifremover:failedprocessexiftool',
|
||||
module: 'core_files',
|
||||
a: get_class($this),
|
||||
debuginfo: implode($output),
|
||||
);
|
||||
}
|
||||
// Replacing the EXIF processed file to the original file.
|
||||
$this->persist_redacted_file(file_get_contents($destinationfile));
|
||||
|
||||
return $destinationfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes ExifTool to remove metadata from the original file content.
|
||||
*
|
||||
* @param string $filecontent The file content to redact.
|
||||
* @return string The redacted updated content
|
||||
* @throws moodle_exception If the ExifTool process fails or the destination file is not created.
|
||||
*/
|
||||
private function execute_exiftool_on_content(string $filecontent): string {
|
||||
$sourcefile = make_request_directory() . '/input';
|
||||
file_put_contents($sourcefile, $filecontent);
|
||||
|
||||
$destinationfile = $this->execute_exiftool($sourcefile);
|
||||
return file_get_contents($destinationfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes GD library to remove metadata from the original file.
|
||||
*
|
||||
* @param string $sourcefile The source file to redact.
|
||||
* @return string The destination path of the recreated content
|
||||
* @throws moodle_exception If the image data is not successfully recreated.
|
||||
*/
|
||||
private function execute_gd(): void {
|
||||
$imagedata = $this->recreate_image_gd();
|
||||
if (!$imagedata) {
|
||||
throw new \moodle_exception(
|
||||
errorcode: 'fileredact:exifremover:failedprocessgd',
|
||||
private function execute_gd(string $sourcefile): string {
|
||||
$filecontent = file_get_contents($sourcefile);
|
||||
$destinationfile = $this->recreate_image_gd($filecontent);
|
||||
if (!$destinationfile) {
|
||||
throw new moodle_exception(
|
||||
errorcode: 'redactor:exifremover:failedprocessgd',
|
||||
module: 'core_files',
|
||||
a: get_class($this),
|
||||
);
|
||||
}
|
||||
// Put the image string object data to the original file.
|
||||
$this->persist_redacted_file($imagedata);
|
||||
|
||||
return $destinationfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes GD library to remove metadata from the original file.
|
||||
*
|
||||
* @param string $filecontent The source file content to redact.
|
||||
* @return string The redacted file content
|
||||
* @throws moodle_exception If the image data is not successfully recreated.
|
||||
*/
|
||||
private function execute_gd_on_content(string $filecontent): string {
|
||||
$destinationfile = $this->recreate_image_gd($filecontent);
|
||||
if (!$destinationfile) {
|
||||
throw new moodle_exception(
|
||||
errorcode: 'redactor:exifremover:failedprocessgd',
|
||||
module: 'core_files',
|
||||
a: get_class($this),
|
||||
);
|
||||
}
|
||||
|
||||
return file_get_contents($destinationfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ExifTool command to strip the file of EXIF data.
|
||||
*
|
||||
@ -162,7 +218,7 @@ class exifremover_service extends service {
|
||||
* @return string The remove tag options.
|
||||
*/
|
||||
private function get_remove_tags(): string {
|
||||
$removetags = get_config('core_fileredact', 'exifremoverremovetags');
|
||||
$removetags = get_config('core', 'file_redactor_exifremoverremovetags');
|
||||
// If the remove tags value is empty or not empty but does not exist in the array, then set the default.
|
||||
if (!$removetags || ($removetags && !array_key_exists($removetags, self::REMOVE_TAGS))) {
|
||||
$removetags = self::DEFAULT_REMOVE_TAGS;
|
||||
@ -176,7 +232,7 @@ class exifremover_service extends service {
|
||||
* @return string The path to the ExifTool executable.
|
||||
*/
|
||||
private function get_exiftool_path(): string {
|
||||
$toolpathconfig = get_config('core_fileredact', 'exifremovertoolpath');
|
||||
$toolpathconfig = get_config('core', 'file_redactor_exifremovertoolpath');
|
||||
if (!empty($toolpathconfig) && is_executable($toolpathconfig)) {
|
||||
return $toolpathconfig;
|
||||
}
|
||||
@ -186,84 +242,36 @@ class exifremover_service extends service {
|
||||
/**
|
||||
* Recreate the image using PHP GD library to strip all EXIF data.
|
||||
*
|
||||
* @return string|false The recreated image data as a string if successful, false otherwise.
|
||||
* @param string $content The source file content
|
||||
* @return null|string The path to the recreated image, or null on failure.
|
||||
*/
|
||||
private function recreate_image_gd(): string|false {
|
||||
$content = $this->storedfile->get_content();
|
||||
private function recreate_image_gd(
|
||||
string $content,
|
||||
): ?string {
|
||||
// Fetch the image information for this image.
|
||||
$imageinfo = @getimagesizefromstring($content);
|
||||
if (empty($imageinfo)) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
// Create a new image from the file.
|
||||
$image = @imagecreatefromstring($content);
|
||||
|
||||
$destinationfile = make_request_directory() . '/output';
|
||||
|
||||
// Capture the image as a string object, rather than straight to file.
|
||||
ob_start();
|
||||
if (!imagejpeg(
|
||||
image: $image,
|
||||
quality: self::DEFAULT_JPEG_COMPRESSION,
|
||||
)
|
||||
) {
|
||||
ob_end_clean();
|
||||
return false;
|
||||
}
|
||||
$data = ob_get_clean();
|
||||
imagedestroy($image);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the redacted file to the file storage.
|
||||
*
|
||||
* @param string $content File content.
|
||||
*/
|
||||
private function persist_redacted_file(string $content): void {
|
||||
$filerecord = (object) [
|
||||
'id' => $this->storedfile->get_id(),
|
||||
'mimetype' => $this->storedfile->get_mimetype(),
|
||||
'userid' => $this->storedfile->get_userid(),
|
||||
'source' => $this->storedfile->get_source(),
|
||||
'contextid' => $this->storedfile->get_contextid(),
|
||||
'component' => $this->storedfile->get_component(),
|
||||
'filearea' => $this->storedfile->get_filearea(),
|
||||
'itemid' => $this->storedfile->get_itemid(),
|
||||
'filepath' => $this->storedfile->get_filepath(),
|
||||
'filename' => $this->storedfile->get_filename(),
|
||||
'repositoryid' => $this->storedfile->get_repository_id(),
|
||||
'reference' => $this->storedfile->get_reference(),
|
||||
];
|
||||
$fs = get_file_storage();
|
||||
$existingfile = $fs->get_file(
|
||||
$filerecord->contextid,
|
||||
$filerecord->component,
|
||||
$filerecord->filearea,
|
||||
$filerecord->itemid,
|
||||
$filerecord->filepath,
|
||||
$filerecord->filename,
|
||||
$result = imagejpeg(
|
||||
image: $image,
|
||||
file: $destinationfile,
|
||||
quality: self::DEFAULT_JPEG_COMPRESSION,
|
||||
);
|
||||
if ($existingfile) {
|
||||
$existingfile->delete();
|
||||
}
|
||||
$redactedfile = $fs->create_file_from_string($filerecord, $content, false);
|
||||
$this->storedfile->replace_file_with($redactedfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up a file name if it starts with a dash (U+002D) or a Unicode minus sign (U+2212).
|
||||
*
|
||||
* According to https://exiftool.org/#security, ensure that input file names do not start with
|
||||
* a dash (U+002D) or a Unicode minus sign (U+2212). If found, remove the leading dash or Unicode minus sign.
|
||||
*
|
||||
* @param string $filename The file name to clean.
|
||||
* @return string The cleaned file name.
|
||||
*/
|
||||
private function clean_filename(string $filename): string {
|
||||
$pattern = '/^[\x{002D}\x{2212}]/u';
|
||||
if (preg_match($pattern, $filename)) {
|
||||
$filename = preg_replace($pattern, '', $filename);
|
||||
imagedestroy($image);
|
||||
|
||||
if ($result) {
|
||||
return $destinationfile;
|
||||
}
|
||||
return clean_param($filename, PARAM_PATH);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -272,7 +280,7 @@ class exifremover_service extends service {
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool {
|
||||
return (bool) get_config('core_fileredact', 'exifremoverenabled');
|
||||
return (bool) get_config('core', 'file_redactor_exifremoverenabled');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -289,7 +297,7 @@ class exifremover_service extends service {
|
||||
|
||||
if ($this->useexiftool) {
|
||||
// Get the supported MIME types from the config if using ExifTool.
|
||||
$supportedmimetypesconfig = get_config('core_fileredact', 'exifremovermimetype');
|
||||
$supportedmimetypesconfig = get_config('core', 'file_redactor_exifremovermimetype');
|
||||
$supportedmimetypes = array_filter(array_map('trim', explode("\n", $supportedmimetypesconfig)));
|
||||
return in_array($mimetype, $supportedmimetypes) ?? false;
|
||||
}
|
||||
@ -307,66 +315,70 @@ class exifremover_service extends service {
|
||||
|
||||
// Enabled for a fresh install, disabled for an upgrade.
|
||||
$defaultenabled = 1;
|
||||
if (!during_initial_install() && empty(get_config('core_fileredact', 'exifremoverenabled'))) {
|
||||
$defaultenabled = 0;
|
||||
|
||||
if (empty(get_config('core', 'file_redactor_exifremoverenabled'))) {
|
||||
if (PHPUNIT_TEST || !during_initial_install()) {
|
||||
$defaultenabled = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$icon = $OUTPUT->pix_icon('i/externallink', get_string('opensinnewwindow'));
|
||||
$a = new \stdClass;
|
||||
$a->link = \html_writer::link(
|
||||
url: 'https://exiftool.sourceforge.net/install.html',
|
||||
text: "https://exiftool.sourceforge.net/install.html $icon",
|
||||
attributes: ['role' => 'opener', 'rel' => 'noreferrer', 'target' => '_blank'],
|
||||
);
|
||||
$a = (object) [
|
||||
'link' => html_writer::link(
|
||||
url: 'https://exiftool.sourceforge.net/install.html',
|
||||
text: "https://exiftool.sourceforge.net/install.html $icon",
|
||||
attributes: ['role' => 'opener', 'rel' => 'noreferrer', 'target' => '_blank'],
|
||||
),
|
||||
];
|
||||
|
||||
$settings->add(
|
||||
new \admin_setting_configcheckbox(
|
||||
name: 'core_fileredact/exifremoverenabled',
|
||||
visiblename: get_string('fileredact:exifremover:enabled', 'core_files'),
|
||||
description: get_string('fileredact:exifremover:enabled_desc', 'core_files', $a),
|
||||
new admin_setting_configcheckbox(
|
||||
name: 'file_redactor_exifremoverenabled',
|
||||
visiblename: get_string('redactor:exifremover:enabled', 'core_files'),
|
||||
description: get_string('redactor:exifremover:enabled_desc', 'core_files', $a),
|
||||
defaultsetting: $defaultenabled,
|
||||
),
|
||||
);
|
||||
|
||||
$settings->add(
|
||||
new \admin_setting_heading(
|
||||
new admin_setting_heading(
|
||||
name: 'exifremoverheading',
|
||||
heading: get_string('fileredact:exifremover:heading', 'core_files'),
|
||||
heading: get_string('redactor:exifremover:heading', 'core_files'),
|
||||
information: '',
|
||||
)
|
||||
);
|
||||
|
||||
$settings->add(
|
||||
new \admin_setting_configexecutable(
|
||||
name: 'core_fileredact/exifremovertoolpath',
|
||||
visiblename: get_string('fileredact:exifremover:toolpath', 'core_files'),
|
||||
description: get_string('fileredact:exifremover:toolpath_desc', 'core_files'),
|
||||
new admin_setting_configexecutable(
|
||||
name: 'file_redactor_exifremovertoolpath',
|
||||
visiblename: get_string('redactor:exifremover:toolpath', 'core_files'),
|
||||
description: get_string('redactor:exifremover:toolpath_desc', 'core_files'),
|
||||
defaultdirectory: '',
|
||||
)
|
||||
);
|
||||
|
||||
foreach (array_keys(self::REMOVE_TAGS) as $key) {
|
||||
$removedtagchoices[$key] = get_string("fileredact:exifremover:tag:$key", 'core_files');
|
||||
$removedtagchoices[$key] = get_string("redactor:exifremover:tag:$key", 'core_files');
|
||||
}
|
||||
$settings->add(
|
||||
new \admin_setting_configselect(
|
||||
name: 'core_fileredact/exifremoverremovetags',
|
||||
visiblename: get_string('fileredact:exifremover:removetags', 'core_files'),
|
||||
description: get_string('fileredact:exifremover:removetags_desc', 'core_files'),
|
||||
new admin_setting_configselect(
|
||||
name: 'files_redactor_exifremoverremovetags',
|
||||
visiblename: get_string('redactor:exifremover:removetags', 'core_files'),
|
||||
description: get_string('redactor:exifremover:removetags_desc', 'core_files'),
|
||||
defaultsetting: self::DEFAULT_REMOVE_TAGS,
|
||||
choices: $removedtagchoices,
|
||||
),
|
||||
);
|
||||
|
||||
$mimetypedefault = <<<EOF
|
||||
image/jpeg
|
||||
image/tiff
|
||||
EOF;
|
||||
image/jpeg
|
||||
image/tiff
|
||||
EOF;
|
||||
$settings->add(
|
||||
new \admin_setting_configtextarea(
|
||||
name: 'core_fileredact/exifremovermimetype',
|
||||
visiblename: get_string('fileredact:exifremover:mimetype', 'core_files'),
|
||||
description: get_string('fileredact:exifremover:mimetype_desc', 'core_files'),
|
||||
new admin_setting_configtextarea(
|
||||
name: 'file_redactor_exifremovermimetype',
|
||||
visiblename: get_string('redactor:exifremover:mimetype', 'core_files'),
|
||||
description: get_string('redactor:exifremover:mimetype_desc', 'core_files'),
|
||||
defaultsetting: $mimetypedefault,
|
||||
),
|
||||
);
|
@ -0,0 +1,58 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Class file_redactor_service_interface
|
||||
*
|
||||
* @package core_files
|
||||
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
interface file_redactor_service_interface {
|
||||
/**
|
||||
* Performs redaction on the specified stored_file.
|
||||
*
|
||||
* @param string $mimetype The mime-type of the file
|
||||
* @param string $filepath The path of the file to redact
|
||||
* @return string|null The path to the redacted file, or null if redaction was not attempted
|
||||
*/
|
||||
public function redact_file_by_path(
|
||||
string $mimetype,
|
||||
string $filepath,
|
||||
): ?string;
|
||||
|
||||
/**
|
||||
* Performs redaction on the specified stored_file.
|
||||
*
|
||||
* @param string $mimetype The mime-type of the file
|
||||
* @param string $filecontent The content of the file to redact
|
||||
* @return string|null The redacted content, or null if redaction was not attempted
|
||||
*/
|
||||
public function redact_file_by_content(
|
||||
string $mimetype,
|
||||
string $filecontent,
|
||||
): ?string;
|
||||
|
||||
/**
|
||||
* Determines whether a certain mime-type is supported by the service.
|
||||
*
|
||||
* @param string $mimetype
|
||||
* @return bool
|
||||
*/
|
||||
public function is_mimetype_supported(string $mimetype): bool;
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
// 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\fileredact\services;
|
||||
namespace core_files\redactor\services;
|
||||
|
||||
use stored_file;
|
||||
/**
|
||||
@ -25,22 +25,6 @@ use stored_file;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class service {
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param stored_file $storedfile The file record object.
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var stored_file $storedfile The file record object. */
|
||||
private readonly stored_file $storedfile,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs redaction on the specified file.
|
||||
*/
|
||||
abstract public function execute(): void;
|
||||
|
||||
/**
|
||||
* Returns true if the service is enabled, and false if it is not.
|
||||
*
|
||||
@ -48,17 +32,6 @@ abstract class service {
|
||||
*/
|
||||
abstract public function is_enabled(): bool;
|
||||
|
||||
/**
|
||||
* Determines whether a certain mime-type is supported by the service.
|
||||
* It will return true if the mime-type is supported, and false if it is not.
|
||||
*
|
||||
* @param string $mimetype
|
||||
* @return bool
|
||||
*/
|
||||
public function is_mimetype_supported(string $mimetype): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds settings to the provided admin settings page.
|
||||
*
|
41
files/tests/classes/hook/before_file_created_callbacks.php
Normal file
41
files/tests/classes/hook/before_file_created_callbacks.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?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\tests\hook;
|
||||
|
||||
/**
|
||||
* Helper class for before_file_created hooks.
|
||||
*
|
||||
* @package core_files
|
||||
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class before_file_created_callbacks {
|
||||
/**
|
||||
* Before file created hook callback for testing.
|
||||
*
|
||||
* @param \core_files\hook\before_file_created $hook
|
||||
*/
|
||||
public static function before_file_created(\core_files\hook\before_file_created $hook): void {
|
||||
// phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
|
||||
global $TESTCALLBACK;
|
||||
|
||||
if (!isset($TESTCALLBACK)) {
|
||||
return;
|
||||
}
|
||||
call_user_func($TESTCALLBACK, $hook);
|
||||
}
|
||||
}
|
@ -14,7 +14,10 @@
|
||||
// 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\fileredact\services;
|
||||
namespace core_files\tests\redactor\services;
|
||||
|
||||
use core_files\redactor\services\file_redactor_service_interface;
|
||||
use core_files\redactor\services\service;
|
||||
|
||||
/**
|
||||
* Dummy service for testing only.
|
||||
@ -23,13 +26,15 @@ namespace core\fileredact\services;
|
||||
* @copyright Meirza <meirza.arson@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class dummy_service extends service {
|
||||
class dummy_file_service extends service implements file_redactor_service_interface {
|
||||
#[\Override]
|
||||
public function redact_file_by_content(string $mimetype, string $filecontent): string {
|
||||
return "redacted:{$filecontent}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs redaction on the specified file.
|
||||
*/
|
||||
public function execute(): void {
|
||||
// The function body.
|
||||
#[\Override]
|
||||
public function redact_file_by_path(string $mimetype, string $filepath): string {
|
||||
return "/redacted{$filepath}";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,7 +54,11 @@ class dummy_service extends service {
|
||||
* @return bool
|
||||
*/
|
||||
public function is_mimetype_supported(string $mimetype): bool {
|
||||
return true;
|
||||
if (str_starts_with($mimetype, 'image/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
35
files/tests/fixtures/hook/before_file_created_hooks.php
vendored
Normal file
35
files/tests/fixtures/hook/before_file_created_hooks.php
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* A hook listener for tests realting to the before_file_created hook.
|
||||
*
|
||||
* @package core_files
|
||||
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'hook' => \core_files\hook\before_file_created::class,
|
||||
'callback' => [
|
||||
\core_files\tests\hook\before_file_created_callbacks::class,
|
||||
'before_file_created',
|
||||
],
|
||||
],
|
||||
];
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
95
files/tests/hook/before_file_created_test.php
Normal file
95
files/tests/hook/before_file_created_test.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?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\hook;
|
||||
use coding_exception;
|
||||
|
||||
/**
|
||||
* Tests for before_file_created hook.
|
||||
*
|
||||
* @package core_files
|
||||
* @category test
|
||||
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_files\hook\before_file_created
|
||||
*/
|
||||
final class before_file_created_test extends \advanced_testcase {
|
||||
public function test_init_with_file_and_content_throws_exception(): void {
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Only one of $filepath or $filecontent can be set');
|
||||
new before_file_created(new \stdClass(), 'path', 'content');
|
||||
}
|
||||
|
||||
public function test_init_with_no_file_and_no_content_throws_exception(): void {
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Either $filepath or $filecontent must be set');
|
||||
new before_file_created(new \stdClass());
|
||||
}
|
||||
|
||||
public function test_content_updated(): void {
|
||||
$hook = new before_file_created(new \stdClass(), filecontent: 'data');
|
||||
$this->assertFalse($hook->has_changed());
|
||||
$this->assertTrue($hook->has_filecontent());
|
||||
$this->assertFalse($hook->has_filepath());
|
||||
|
||||
$hook->update_filecontent('data');
|
||||
$this->assertEquals('data', $hook->get_filecontent());
|
||||
$this->assertFalse($hook->has_changed());
|
||||
$this->assertNull($hook->get_filepath());
|
||||
|
||||
$hook->update_filecontent('new data');
|
||||
$this->assertEquals('new data', $hook->get_filecontent());
|
||||
$this->assertTrue($hook->has_changed());
|
||||
$this->assertNull($hook->get_filepath());
|
||||
}
|
||||
|
||||
public function test_file_updated(): void {
|
||||
$initialdata = self::get_fixture_path('core_files', 'hook/before_file_created_hooks.php');
|
||||
$newdata = __FILE__;
|
||||
|
||||
$hook = new before_file_created(
|
||||
new \stdClass(),
|
||||
filepath: $initialdata,
|
||||
);
|
||||
$this->assertFalse($hook->has_changed());
|
||||
$this->assertFalse($hook->has_filecontent());
|
||||
$this->assertTrue($hook->has_filepath());
|
||||
|
||||
$hook->update_filepath($initialdata);
|
||||
$this->assertNull($hook->get_filecontent());
|
||||
$this->assertEquals($initialdata, $hook->get_filepath());
|
||||
$this->assertFalse($hook->has_changed());
|
||||
|
||||
$hook->update_filepath($newdata);
|
||||
$this->assertNull($hook->get_filecontent());
|
||||
$this->assertEquals($newdata, $hook->get_filepath());
|
||||
$this->assertTrue($hook->has_changed());
|
||||
}
|
||||
|
||||
public function test_cannot_update_file_when_content_set(): void {
|
||||
$hook = new before_file_created(new \stdClass(), filecontent: 'data');
|
||||
$this->expectException(coding_exception::class);
|
||||
$this->expectExceptionMessage('Cannot update file path when the file path is not set');
|
||||
$hook->update_filepath('new path');
|
||||
}
|
||||
|
||||
public function test_cannot_update_content_when_file_ste(): void {
|
||||
$hook = new before_file_created(new \stdClass(), filepath: __FILE__);
|
||||
$this->expectException(coding_exception::class);
|
||||
$this->expectExceptionMessage('Cannot update file content when the file content is not set');
|
||||
$hook->update_filecontent('new path');
|
||||
}
|
||||
}
|
63
files/tests/redactor/manager_test.php
Normal file
63
files/tests/redactor/manager_test.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Tests for file redactor manager class.
|
||||
*
|
||||
* @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\manager
|
||||
*/
|
||||
final class manager_test extends \advanced_testcase {
|
||||
/**
|
||||
* Helper to get a manager with a dummy file service.
|
||||
*
|
||||
* @return \core_files\redactor\manager
|
||||
*/
|
||||
private function get_manager_with_dummy_file_service(): manager {
|
||||
$manager = $this->getMockBuilder(\core_files\redactor\manager::class)
|
||||
->onlyMethods(['get_service_classnames'])
|
||||
->getMock();
|
||||
|
||||
$manager->method('get_service_classnames')
|
||||
->willReturn([\core_files\tests\redactor\services\dummy_file_service::class]);
|
||||
|
||||
return $manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test file redaction by path.
|
||||
*/
|
||||
public function test_redact_file(): void {
|
||||
|
||||
$manager = $this->get_manager_with_dummy_file_service();
|
||||
|
||||
// Test redaction for a binary (not supported).
|
||||
$redactedfile = $manager->redact_file("application/binary", "/path/to/binary");
|
||||
$this->assertNull($redactedfile);
|
||||
$redactedfile = $manager->redact_file_content("application/binary", "Binary content here");
|
||||
$this->assertNull($redactedfile);
|
||||
|
||||
// Test redaction for an image.
|
||||
$redactedfile = $manager->redact_file("image/jpeg", "/path/to/image.jpg");
|
||||
$this->assertEquals('/redacted/path/to/image.jpg', $redactedfile);
|
||||
$redactedfile = $manager->redact_file_content("image/jpeg", "Example picture goes here");
|
||||
$this->assertEquals('redacted:Example picture goes here', $redactedfile);
|
||||
}
|
||||
}
|
263
files/tests/redactor/services/exifremover_service_test.php
Normal file
263
files/tests/redactor/services/exifremover_service_test.php
Normal file
@ -0,0 +1,263 @@
|
||||
<?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 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.');
|
||||
}
|
||||
}
|
||||
}
|
@ -28,28 +28,28 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['contenthash'] = 'Content hash';
|
||||
$string['eventfileaddedtodraftarea'] = 'File added to draft area';
|
||||
$string['eventfiledeletedfromdraftarea'] = 'File deleted from draft area';
|
||||
$string['fileredact'] = 'File redact';
|
||||
$string['fileredact:exifremover'] = 'EXIF remover';
|
||||
$string['fileredact:exifremover:emptyremovetags'] = 'Remove tags can not be empty!';
|
||||
$string['fileredact:exifremover:enabled'] = 'Enable EXIF remover';
|
||||
$string['fileredact:exifremover:enabled_desc'] = 'By default, EXIF Remover only supports JPG files using PHP GD, or ExifTool if it is configured.
|
||||
$string['redactor'] = 'File redaction';
|
||||
$string['redactor:exifremover'] = 'EXIF remover';
|
||||
$string['redactor:exifremover:emptyremovetags'] = 'Remove tags can not be empty!';
|
||||
$string['redactor:exifremover:enabled'] = 'Enable EXIF remover';
|
||||
$string['redactor:exifremover:enabled_desc'] = 'By default, EXIF Remover only supports JPG files using PHP GD, or ExifTool if it is configured.
|
||||
This degrades the quality of the image and removes the orientation tag.
|
||||
|
||||
To enhance the performance of EXIF Remover, please configure the ExifTool settings below.
|
||||
|
||||
More information about installing ExifTool can be found at {$a->link}';
|
||||
$string['fileredact:exifremover:failedprocessexiftool'] = 'Redaction failed: failed to process file with ExifTool!';
|
||||
$string['fileredact:exifremover:failedprocessgd'] = 'Redaction failed: failed to process file with PHP gd!';
|
||||
$string['fileredact:exifremover:heading'] = 'ExifTool';
|
||||
$string['fileredact:exifremover:mimetype'] = 'Supported MIME types';
|
||||
$string['fileredact:exifremover:mimetype_desc'] = 'To add new MIME types, ensure they\'re included in the <a href="./tool/filetypes/index.php">File Types</a>.';
|
||||
$string['fileredact:exifremover:removetags'] = 'The EXIF tags that will be removed.';
|
||||
$string['fileredact:exifremover:removetags_desc'] = 'The EXIF tags that need to be removed.';
|
||||
$string['fileredact:exifremover:tag:all'] = 'All';
|
||||
$string['fileredact:exifremover:tag:gps'] = 'GPS only';
|
||||
$string['fileredact:exifremover:tooldoesnotexist'] = 'Redaction failed: ExifTool does not exist!';
|
||||
$string['fileredact:exifremover:toolpath'] = 'Path to ExifTool';
|
||||
$string['fileredact:exifremover:toolpath_desc'] = 'To use the ExifTool, please provide the path to the ExifTool executable.
|
||||
$string['redactor:exifremover:failedprocessexiftool'] = 'Redaction failed: failed to process file with ExifTool!';
|
||||
$string['redactor:exifremover:failedprocessgd'] = 'Redaction failed: failed to process file with PHP gd!';
|
||||
$string['redactor:exifremover:heading'] = 'ExifTool';
|
||||
$string['redactor:exifremover:mimetype'] = 'Supported MIME types';
|
||||
$string['redactor:exifremover:mimetype_desc'] = 'To add new MIME types, ensure they\'re included in the <a href="./tool/filetypes/index.php">File Types</a>.';
|
||||
$string['redactor:exifremover:removetags'] = 'The EXIF tags that will be removed.';
|
||||
$string['redactor:exifremover:removetags_desc'] = 'The EXIF tags that need to be removed.';
|
||||
$string['redactor:exifremover:tag:all'] = 'All';
|
||||
$string['redactor:exifremover:tag:gps'] = 'GPS only';
|
||||
$string['redactor:exifremover:tooldoesnotexist'] = 'Redaction failed: ExifTool does not exist!';
|
||||
$string['redactor:exifremover:toolpath'] = 'Path to ExifTool';
|
||||
$string['redactor:exifremover:toolpath_desc'] = 'To use the ExifTool, please provide the path to the ExifTool executable.
|
||||
Typically, on Unix/Linux systems, the path is /usr/bin/exiftool.';
|
||||
$string['privacy:metadata:file_conversions'] = 'A record of the file conversions performed by a user.';
|
||||
$string['privacy:metadata:file_conversion:usermodified'] = 'The user who started the file conversion.';
|
||||
|
@ -234,6 +234,11 @@
|
||||
"allowedlevel2": false,
|
||||
"allowedspread": false
|
||||
},
|
||||
"redactor": {
|
||||
"component": "core_files",
|
||||
"allowedlevel2": true,
|
||||
"allowedspread": false
|
||||
},
|
||||
"reportbuilder": {
|
||||
"component": "core_reportbuilder",
|
||||
"allowedlevel2": true,
|
||||
|
@ -1,93 +0,0 @@
|
||||
<?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\fileredact;
|
||||
|
||||
use stored_file;
|
||||
|
||||
/**
|
||||
* Fileredact manager.
|
||||
*
|
||||
* Manages and executes redaction services.
|
||||
*
|
||||
* @package core
|
||||
* @copyright Meirza <meirza.arson@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class manager {
|
||||
|
||||
/** @var array Holds an array of error messages. */
|
||||
private $errors = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param stored_file $filerecord The file record as a stdClass object, or null if not available.
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var stored_file $filerecord File record. */
|
||||
private readonly stored_file $filerecord
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes redaction services.
|
||||
*/
|
||||
public function execute(): void {
|
||||
// Get the file redact services.
|
||||
$services = $this->get_services();
|
||||
foreach ($services as $serviceclass) {
|
||||
try {
|
||||
if (class_exists($serviceclass)) {
|
||||
$service = new $serviceclass($this->filerecord);
|
||||
// For the given service, execute them if they are enabled, and the given mime type is supported.
|
||||
if ($service->is_enabled() && $service->is_mimetype_supported($this->filerecord->get_mimetype())) {
|
||||
$service->execute();
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->errors[] = $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of applicable redaction services.
|
||||
*
|
||||
* @return string[] return list of services.
|
||||
*/
|
||||
protected function get_services(): array {
|
||||
global $CFG;
|
||||
$servicesdir = "{$CFG->libdir}/classes/fileredact/services/";
|
||||
$servicefiles = glob("{$servicesdir}*_service.php");
|
||||
$services = [];
|
||||
foreach ($servicefiles as $servicefile) {
|
||||
$servicename = basename($servicefile, '_service.php');
|
||||
$serviceclass = "\\core\\fileredact\\services\\{$servicename}_service";
|
||||
$services[] = $serviceclass;
|
||||
}
|
||||
return $services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an array of error messages.
|
||||
*
|
||||
* @return array An array of error messages.
|
||||
*/
|
||||
public function get_errors(): array {
|
||||
return $this->errors;
|
||||
}
|
||||
}
|
@ -111,7 +111,7 @@ $callbacks = [
|
||||
'callback' => [\core\router\hook_callbacks::class, 'provide_di_configuration'],
|
||||
],
|
||||
[
|
||||
'hook' => \core\hook\filestorage\after_file_created::class,
|
||||
'callback' => \core\fileredact\hook_listener::class . '::redact_after_file_created',
|
||||
'hook' => \core_files\hook\before_file_created::class,
|
||||
'callback' => [\core_files\redactor\hook_listener::class, 'file_redaction_handler'],
|
||||
],
|
||||
];
|
||||
|
@ -14,7 +14,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
/**
|
||||
* Core file storage class definition.
|
||||
*
|
||||
@ -25,6 +24,8 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_files\hook\before_file_created;
|
||||
|
||||
require_once("$CFG->libdir/filestorage/stored_file.php");
|
||||
|
||||
/**
|
||||
@ -1069,9 +1070,8 @@ class file_storage {
|
||||
* Add new file record to database and handle callbacks.
|
||||
*
|
||||
* @param stdClass $newrecord
|
||||
* @param bool $notify Notify the hook about the new file or not
|
||||
*/
|
||||
protected function create_file($newrecord, bool $notify = true) {
|
||||
protected function create_file($newrecord) {
|
||||
global $DB;
|
||||
$newrecord->id = $DB->insert_record('files', $newrecord);
|
||||
|
||||
@ -1079,14 +1079,13 @@ class file_storage {
|
||||
if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
|
||||
return;
|
||||
}
|
||||
if ($notify) {
|
||||
// The $fileinstance is needed for the legacy callback.
|
||||
$fileinstance = $this->get_file_instance($newrecord);
|
||||
// Dispatch the new Hook implementation immediately after the legacy callback.
|
||||
$hook = new \core\hook\filestorage\after_file_created($fileinstance, $newrecord);
|
||||
\core\di::get(\core\hook\manager::class)->dispatch($hook);
|
||||
$hook->process_legacy_callbacks();
|
||||
}
|
||||
|
||||
// The $fileinstance is needed for the legacy callback.
|
||||
$fileinstance = $this->get_file_instance($newrecord);
|
||||
// Dispatch the new Hook implementation immediately after the legacy callback.
|
||||
$hook = new \core_files\hook\after_file_created($fileinstance, $newrecord);
|
||||
$hook->process_legacy_callbacks();
|
||||
\core\di::get(\core\hook\manager::class)->dispatch($hook);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1095,10 +1094,9 @@ class file_storage {
|
||||
*
|
||||
* @param stdClass|array $filerecord object or array describing changes
|
||||
* @param stored_file|int $fileorid id or stored_file instance of the existing local file
|
||||
* @param bool $notify Notify the hook about the new file or not
|
||||
* @return stored_file instance of newly created file
|
||||
*/
|
||||
public function create_file_from_storedfile($filerecord, $fileorid, bool $notify = true) {
|
||||
public function create_file_from_storedfile($filerecord, $fileorid) {
|
||||
global $DB;
|
||||
|
||||
if ($fileorid instanceof stored_file) {
|
||||
@ -1205,7 +1203,7 @@ class file_storage {
|
||||
}
|
||||
|
||||
try {
|
||||
$this->create_file($newrecord, $notify);
|
||||
$this->create_file($newrecord);
|
||||
} catch (dml_exception $e) {
|
||||
throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
|
||||
$newrecord->filepath, $newrecord->filename, $e->debuginfo);
|
||||
@ -1277,10 +1275,9 @@ class file_storage {
|
||||
*
|
||||
* @param stdClass|array $filerecord object or array describing file
|
||||
* @param string $pathname path to file or content of file
|
||||
* @param bool $notify Notify the hook about the new file or not.
|
||||
* @return stored_file
|
||||
*/
|
||||
public function create_file_from_pathname($filerecord, $pathname, bool $notify = true) {
|
||||
public function create_file_from_pathname($filerecord, $pathname) {
|
||||
global $DB;
|
||||
|
||||
$filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
|
||||
@ -1374,7 +1371,7 @@ class file_storage {
|
||||
$newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
|
||||
|
||||
try {
|
||||
$this->create_file($newrecord, $notify);
|
||||
$this->create_file($newrecord);
|
||||
} catch (dml_exception $e) {
|
||||
if ($newfile) {
|
||||
$this->filesystem->remove_file($newrecord->contenthash);
|
||||
@ -1393,10 +1390,9 @@ class file_storage {
|
||||
*
|
||||
* @param stdClass|array $filerecord object or array describing file
|
||||
* @param string $content content of file
|
||||
* @param bool $notify Notify the hook about the new file or not.
|
||||
* @return stored_file
|
||||
*/
|
||||
public function create_file_from_string($filerecord, $content, bool $notify = true) {
|
||||
public function create_file_from_string($filerecord, $content) {
|
||||
global $DB;
|
||||
|
||||
$filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
|
||||
@ -1498,7 +1494,7 @@ class file_storage {
|
||||
}
|
||||
|
||||
try {
|
||||
$this->create_file($newrecord, $notify);
|
||||
$this->create_file($newrecord);
|
||||
} catch (dml_exception $e) {
|
||||
if ($newfile) {
|
||||
$this->filesystem->remove_file($newrecord->contenthash);
|
||||
@ -1844,7 +1840,19 @@ class file_storage {
|
||||
* @return array (contenthash, filesize, newfile)
|
||||
*/
|
||||
public function add_file_to_pool($pathname, $contenthash = null, $newrecord = null) {
|
||||
$this->call_before_file_created_plugin_functions($newrecord, $pathname);
|
||||
$hook = new before_file_created(
|
||||
filerecord: $newrecord,
|
||||
filepath: $pathname,
|
||||
);
|
||||
|
||||
$hook->process_legacy_callbacks();
|
||||
\core\di::get(\core\hook\manager::class)->dispatch($hook);
|
||||
|
||||
if ($hook->has_changed()) {
|
||||
$contenthash = null;
|
||||
$pathname = $hook->get_filepath();
|
||||
}
|
||||
|
||||
return $this->filesystem->add_file_from_path($pathname, $contenthash);
|
||||
}
|
||||
|
||||
@ -1855,24 +1863,22 @@ class file_storage {
|
||||
* @return array (contenthash, filesize, newfile)
|
||||
*/
|
||||
public function add_string_to_pool($content, $newrecord = null) {
|
||||
$this->call_before_file_created_plugin_functions($newrecord, null, $content);
|
||||
return $this->filesystem->add_file_from_string($content);
|
||||
}
|
||||
if ($content !== null) {
|
||||
// This is a directory and there is no record information.
|
||||
$hook = new before_file_created(
|
||||
filerecord: $newrecord,
|
||||
filecontent: $content,
|
||||
);
|
||||
|
||||
/**
|
||||
* before_file_created hook.
|
||||
*
|
||||
* @param stdClass|null $newrecord New file record.
|
||||
* @param string|null $pathname Path to file.
|
||||
* @param string|null $content File content.
|
||||
*/
|
||||
protected function call_before_file_created_plugin_functions($newrecord, $pathname = null, $content = null) {
|
||||
$pluginsfunction = get_plugins_with_function('before_file_created');
|
||||
foreach ($pluginsfunction as $plugintype => $plugins) {
|
||||
foreach ($plugins as $pluginfunction) {
|
||||
$pluginfunction($newrecord, ['pathname' => $pathname, 'content' => $content]);
|
||||
$hook->process_legacy_callbacks();
|
||||
\core\di::get(\core\hook\manager::class)->dispatch($hook);
|
||||
|
||||
if ($hook->has_changed()) {
|
||||
$content = $hook->get_filecontent();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->filesystem->add_file_from_string($content);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,6 +27,8 @@ namespace core;
|
||||
|
||||
use file_exception;
|
||||
use file_reference_exception;
|
||||
use file_storage;
|
||||
use file_system;
|
||||
use repository;
|
||||
use stored_file;
|
||||
use stored_file_creation_exception;
|
||||
@ -2213,6 +2215,158 @@ class file_storage_test extends \advanced_testcase {
|
||||
$this->assertEquals($hash2, $hash3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the before_file_created hook has no impact if not called.
|
||||
*
|
||||
* @covers \core_files\hook\before_file_created
|
||||
*/
|
||||
public function test_before_file_created_hook_executed_nochange(): void {
|
||||
global $TESTCALLBACK; // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$testdata = self::get_fixture_path('core_files', 'hook/before_file_created_hooks.php');
|
||||
|
||||
\core\di::set(
|
||||
\core\hook\manager::class,
|
||||
\core\hook\manager::phpunit_get_instance([]),
|
||||
);
|
||||
|
||||
// Create a file.
|
||||
$fs = get_file_storage();
|
||||
$file = $fs->create_file_from_pathname(
|
||||
(object) [
|
||||
'contextid' => 1,
|
||||
'component' => 'core',
|
||||
'filearea' => 'phpunit',
|
||||
'itemid' => 0,
|
||||
'filepath' => '/',
|
||||
'filename' => 'testfile.csv',
|
||||
],
|
||||
$testdata,
|
||||
);
|
||||
|
||||
// The content should have been updated.
|
||||
$this->assertEquals(
|
||||
file_get_contents($testdata),
|
||||
$file->get_content(),
|
||||
);
|
||||
|
||||
// The content hash should match the new content.
|
||||
$this->assertEquals(
|
||||
file_storage::hash_from_path($testdata),
|
||||
$file->get_contenthash(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the before_file_created hook is called before a file is created.
|
||||
*
|
||||
* @covers \core_files\hook\before_file_created
|
||||
*/
|
||||
public function test_before_file_created_hook_executed_filepath(): void {
|
||||
global $TESTCALLBACK; // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$testdata = self::get_fixture_path('core', 'tabfile.csv');
|
||||
|
||||
// The before_file_created test hook calls a callback function at TESTCALLBACK.
|
||||
$TESTCALLBACK = function( // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
|
||||
\core_files\hook\before_file_created $hook,
|
||||
) use ($testdata) {
|
||||
if ($hook->get_filecontent() === '') {
|
||||
return;
|
||||
}
|
||||
$hook->update_filepath($testdata);
|
||||
};
|
||||
|
||||
\core\di::set(
|
||||
\core\hook\manager::class,
|
||||
\core\hook\manager::phpunit_get_instance([
|
||||
'example' => self::get_fixture_path('core_files', 'hook/before_file_created_hooks.php'),
|
||||
]),
|
||||
);
|
||||
|
||||
// Create a file.
|
||||
$fs = get_file_storage();
|
||||
$file = $fs->create_file_from_pathname(
|
||||
(object) [
|
||||
'contextid' => 1,
|
||||
'component' => 'core',
|
||||
'filearea' => 'phpunit',
|
||||
'itemid' => 0,
|
||||
'filepath' => '/',
|
||||
'filename' => 'testfile.csv',
|
||||
],
|
||||
self::get_fixture_path('core_files', 'hook/before_file_created_hooks.php'),
|
||||
);
|
||||
|
||||
// The content should have been updated.
|
||||
$this->assertEquals(
|
||||
file_get_contents($testdata),
|
||||
$file->get_content(),
|
||||
);
|
||||
|
||||
// The content hash should match the new content.
|
||||
$this->assertEquals(
|
||||
file_storage::hash_from_path($testdata),
|
||||
$file->get_contenthash(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the before_file_created hook is called before a file is created with content.
|
||||
*
|
||||
* @covers \core_files\hook\before_file_created
|
||||
*/
|
||||
public function test_before_file_created_hook_executed_filecontent(): void {
|
||||
global $TESTCALLBACK; // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$testdata = 'New content';
|
||||
|
||||
// The before_file_created test hook calls a callback function at TESTCALLBACK.
|
||||
$TESTCALLBACK = function( // phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
|
||||
\core_files\hook\before_file_created $hook,
|
||||
) use ($testdata) {
|
||||
if ($hook->get_filecontent() === '') {
|
||||
return;
|
||||
}
|
||||
$hook->update_filecontent($testdata);
|
||||
};
|
||||
|
||||
\core\di::set(
|
||||
\core\hook\manager::class,
|
||||
\core\hook\manager::phpunit_get_instance([
|
||||
'example' => self::get_fixture_path('core_files', 'hook/before_file_created_hooks.php'),
|
||||
]),
|
||||
);
|
||||
|
||||
// Create a file.
|
||||
$fs = get_file_storage();
|
||||
$file = $fs->create_file_from_string(
|
||||
(object) [
|
||||
'contextid' => 1,
|
||||
'component' => 'core',
|
||||
'filearea' => 'phpunit',
|
||||
'itemid' => 0,
|
||||
'filepath' => '/',
|
||||
'filename' => 'testfile.csv',
|
||||
],
|
||||
'Original content',
|
||||
);
|
||||
|
||||
// The content should have been updated.
|
||||
$this->assertEquals(
|
||||
$testdata,
|
||||
$file->get_content(),
|
||||
);
|
||||
|
||||
// The content hash should match the new content.
|
||||
$this->assertEquals(
|
||||
file_storage::hash_from_string($testdata),
|
||||
$file->get_contenthash(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class test_stored_file_inspection extends stored_file {
|
||||
|
@ -1,310 +0,0 @@
|
||||
<?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\fileredact;
|
||||
|
||||
use file_storage;
|
||||
use stored_file;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @copyright Meirza <meirza.arson@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*
|
||||
* @covers \core\fileredact\services\exifremover_service
|
||||
*/
|
||||
final class exifremover_service_test extends \advanced_testcase {
|
||||
|
||||
/** @var file_storage File storage. */
|
||||
private file_storage $fs;
|
||||
|
||||
/**
|
||||
* Set up the test environment.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->resetAfterTest();
|
||||
$this->fs = get_file_storage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary file for testing purposes.
|
||||
*
|
||||
* @return stored_file The stored file.
|
||||
*/
|
||||
private function create_test_file(): stored_file {
|
||||
$filename = 'dummy.jpg';
|
||||
$path = __DIR__ . '/../fixtures/fileredact/' . $filename;
|
||||
$filerecord = (object) [
|
||||
'contextid' => \context_user::instance(get_admin()->id)->id,
|
||||
'component' => 'user',
|
||||
'filearea' => 'unittest',
|
||||
'itemid' => 0,
|
||||
'filepath' => '/',
|
||||
'filename' => $filename,
|
||||
];
|
||||
$file = $this->fs->get_file($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
|
||||
$filerecord->filepath, $filerecord->filename);
|
||||
if ($file) {
|
||||
$file->delete();
|
||||
}
|
||||
|
||||
return $this->fs->create_file_from_pathname($filerecord, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary invalid file for testing purposes.
|
||||
*
|
||||
* @return stored_file The stored file.
|
||||
*/
|
||||
private function create_invalid_test_file(): stored_file {
|
||||
$filename = 'dummy_invalid.jpg';
|
||||
$filerecord = (object) [
|
||||
'contextid' => \context_user::instance(get_admin()->id)->id,
|
||||
'component' => 'user',
|
||||
'filearea' => 'unittest',
|
||||
'itemid' => 0,
|
||||
'filepath' => '/',
|
||||
'filename' => $filename,
|
||||
];
|
||||
$file = $this->fs->get_file($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
|
||||
$filerecord->filepath, $filerecord->filename);
|
||||
if ($file) {
|
||||
$file->delete();
|
||||
}
|
||||
|
||||
return $this->fs->create_file_from_string($filerecord, 'Dummy content');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
$file = $this->create_test_file();
|
||||
// Get the EXIF data from the new file.
|
||||
$currentexif = $this->get_new_exif($file->get_content());
|
||||
$this->assertStringContainsString('GPSLatitude', $currentexif);
|
||||
$this->assertStringContainsString('GPSLongitude', $currentexif);
|
||||
$this->assertStringContainsString('Orientation', $currentexif);
|
||||
|
||||
$exifremoverservice = new services\exifremover_service($file);
|
||||
$exifremoverservice->execute();
|
||||
// Get the EXIF data from the new file.
|
||||
$newexif = $this->get_new_exif($file->get_content());
|
||||
|
||||
// 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 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 {
|
||||
if ( (defined('TEST_PATH_TO_EXIFTOOL') && TEST_PATH_TO_EXIFTOOL && !is_executable(TEST_PATH_TO_EXIFTOOL))
|
||||
|| (!defined('TEST_PATH_TO_EXIFTOOL'))) {
|
||||
$this->markTestSkipped('Could not test the EXIF remover service, missing configuration. ' .
|
||||
"Example: define('TEST_PATH_TO_EXIFTOOL', '/usr/bin/exiftool');");
|
||||
}
|
||||
|
||||
set_config('exifremovertoolpath', TEST_PATH_TO_EXIFTOOL, 'core_fileredact');
|
||||
|
||||
// Remove All tags.
|
||||
set_config('exifremoverremovetags', 'all', 'core_fileredact');
|
||||
$file1 = $this->create_test_file();
|
||||
$exifremoverservice = new services\exifremover_service($file1);
|
||||
$exifremoverservice->execute();
|
||||
// Get the EXIF data from the new file.
|
||||
$newexif = $this->get_new_exif($file1->get_content());
|
||||
// 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('exifremoverremovetags', 'gps', 'core_fileredact');
|
||||
$file2 = $this->create_test_file();
|
||||
$exifremoverservice = new services\exifremover_service($file2);
|
||||
$exifremoverservice->execute();
|
||||
// Get the EXIF data from the new file.
|
||||
$newexif = $this->get_new_exif($file2->get_content());
|
||||
// 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(): void {
|
||||
$file = $this->create_test_file();
|
||||
// Init uals(false, $resultthe service.
|
||||
$exifremoverservice = new services\exifremover_service($file);
|
||||
|
||||
// Test using PHP GD.
|
||||
$rc = new \ReflectionClass(services\exifremover_service::class);
|
||||
$rcexifremover = $rc->getMethod('is_mimetype_supported');
|
||||
// As default, the exif remover only accepts the default mime type.
|
||||
$result = $rcexifremover->invokeArgs($exifremoverservice, [services\exifremover_service::DEFAULT_MIMETYPE]);
|
||||
$this->assertEquals(true, $result);
|
||||
// Other than the default, the function will returns false.
|
||||
$result = $rcexifremover->invokeArgs($exifremoverservice, ['image/tiff']);
|
||||
$this->assertEquals(false, $result);
|
||||
|
||||
// Test using ExifTool.
|
||||
$useexiftool = $rc->getProperty('useexiftool');
|
||||
$useexiftool->setValue($exifremoverservice, true);
|
||||
// Set the supported mime types.
|
||||
set_config('exifremovermimetype', 'image/tiff', 'core_fileredact');
|
||||
// Other than the `image/tiff`, the function will returns false.
|
||||
$result = $rcexifremover->invokeArgs($exifremoverservice, ['image/png']);
|
||||
$this->assertEquals(false, $result);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the `clean_filename` method.
|
||||
*
|
||||
* This test initializes the `exifremover_service` with a mock file record and
|
||||
* invokes the `clean_filename` method via reflection to ensure it correctly
|
||||
* processes the given filename.
|
||||
*
|
||||
* @dataProvider exifremover_service_clean_filename_provider
|
||||
*
|
||||
* @param string $filename The filename to be cleaned by the `clean_filename` method.
|
||||
* @param string $expected The expected result after cleaning the filename.
|
||||
*/
|
||||
public function test_exifremover_service_clean_filename($filename, $expected): void {
|
||||
$file = $this->create_test_file();
|
||||
// Init the service.
|
||||
$exifremoverservice = new services\exifremover_service($file);
|
||||
|
||||
$rc = new \ReflectionClass(services\exifremover_service::class);
|
||||
$rccleanfilename = $rc->getMethod('clean_filename');
|
||||
|
||||
$result = $rccleanfilename->invokeArgs($exifremoverservice, [$filename]);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the EXIF remover service alters the content hash of a file
|
||||
* when a new file is created from the original.
|
||||
*/
|
||||
public function test_exifremover_contenthash(): void {
|
||||
$file = $this->create_test_file();
|
||||
$beforehash = $file->get_contenthash();
|
||||
$exifremoverservice = new services\exifremover_service($file);
|
||||
$exifremoverservice->execute();
|
||||
$afterhash = $file->get_contenthash();
|
||||
$this->assertNotSame($beforehash, $afterhash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the EXIF remover service with an unknown filename and a valid EXIF tool path.
|
||||
*/
|
||||
public function test_exiftool_filename_unknown(): void {
|
||||
if ( (defined('TEST_PATH_TO_EXIFTOOL') && TEST_PATH_TO_EXIFTOOL && !is_executable(TEST_PATH_TO_EXIFTOOL))
|
||||
|| (!defined('TEST_PATH_TO_EXIFTOOL'))) {
|
||||
$this->markTestSkipped('Could not test the EXIF remover service, missing configuration. ' .
|
||||
"Example: define('TEST_PATH_TO_EXIFTOOL', '/usr/bin/exiftool');");
|
||||
}
|
||||
set_config('exifremovertoolpath', TEST_PATH_TO_EXIFTOOL, 'core_fileredact');
|
||||
$invalidfile = $this->create_invalid_test_file();
|
||||
$exifremoverservice = new services\exifremover_service($invalidfile);
|
||||
$this->expectException(\Exception::class);
|
||||
$exifremoverservice->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the EXIF remover service with an unknown filename and an invalid EXIF tool path.
|
||||
*/
|
||||
public function test_exiftool_notfound_filename_unknown(): void {
|
||||
set_config('exifremovertoolpath', 'fakeexiftool', 'core_fileredact');
|
||||
$invalidfile = $this->create_invalid_test_file();
|
||||
$exifremoverservice = new services\exifremover_service($invalidfile);
|
||||
$this->expectException(\moodle_exception::class);
|
||||
$this->expectExceptionMessage(get_string('fileredact:exifremover:failedprocessgd', 'core_files'));
|
||||
$exifremoverservice->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_new_exif(string $content): string {
|
||||
$logpath = make_request_directory() . '/temp.jpg';
|
||||
file_put_contents($logpath, $content);
|
||||
$exif = exif_read_data($logpath);
|
||||
$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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
<?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\fileredact;
|
||||
|
||||
use stored_file;
|
||||
|
||||
/**
|
||||
* Tests for fileredact manager class.
|
||||
*
|
||||
* @package core
|
||||
* @copyright Meirza <meirza.arson@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*
|
||||
* @covers \core\fileredact\manager
|
||||
*/
|
||||
final class manager_test extends \advanced_testcase {
|
||||
|
||||
/** @var stored_file Stored file object. */
|
||||
private stored_file $storedfile;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->resetAfterTest();
|
||||
|
||||
$file = new \stdClass;
|
||||
$file->contextid = \context_user::instance(get_admin()->id)->id;
|
||||
$file->component = 'user';
|
||||
$file->filearea = 'private';
|
||||
$file->itemid = 0;
|
||||
$file->filepath = '/';
|
||||
$file->filename = 'test.jpg';
|
||||
$file->source = 'test';
|
||||
|
||||
$fs = get_file_storage();
|
||||
$this->storedfile = $fs->create_file_from_string($file, 'file1 content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the `get_services` method.
|
||||
*
|
||||
* This test initializes the `manager` and verifies that the `get_services` method.
|
||||
*/
|
||||
public function test_get_services(): void {
|
||||
// Init the manager.
|
||||
$manager = new \core\fileredact\manager($this->storedfile);
|
||||
|
||||
$rc = new \ReflectionClass(\core\fileredact\manager::class);
|
||||
$rcm = $rc->getMethod('get_services');
|
||||
$services = $rcm->invoke($manager);
|
||||
|
||||
$this->assertGreaterThan(0, count($services));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the `execute` method and error handling.
|
||||
*
|
||||
* This test mocks the `manager` class to return a dummy service for `get_services`
|
||||
* and verifies that the `execute` method runs without errors.
|
||||
*/
|
||||
public function test_execute(): void {
|
||||
$managermock = $this->getMockBuilder(\core\fileredact\manager::class)
|
||||
->onlyMethods(['get_services'])
|
||||
->setConstructorArgs([$this->storedfile])
|
||||
->getMock();
|
||||
|
||||
$managermock->expects($this->once())
|
||||
->method('get_services')
|
||||
->willReturn(['\\core\fileredact\\services\\dummy_service']);
|
||||
|
||||
/** @var \core\fileredact\manager $managermock */
|
||||
$managermock->execute();
|
||||
$errors = $managermock->get_errors();
|
||||
|
||||
// If execution is OK, then no errors.
|
||||
$this->assertEquals([], $errors);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user