diff --git a/.upgradenotes/MDL-78293-2024071102223792.yml b/.upgradenotes/MDL-78293-2024071102223792.yml new file mode 100644 index 00000000000..44a5deed164 --- /dev/null +++ b/.upgradenotes/MDL-78293-2024071102223792.yml @@ -0,0 +1,7 @@ +issueNumber: MDL-78293 +notes: + core_user: + - message: > + The visibility of the methods: check_access_for_dynamic_submission() and get_options() + in core_user\form\private_files has been changed from protected to public. + type: changed diff --git a/lib/db/services.php b/lib/db/services.php index feb0ce346c4..2cb1a01c525 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2008,6 +2008,20 @@ $functions = array( 'capabilities' => 'moodle/user:manageownfiles', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_user_prepare_private_files_for_edition' => [ + 'classname' => '\core_user\external\prepare_private_files_for_edition', + 'description' => 'Prepares the draft area for user private files.', + 'type' => 'write', + 'capabilities' => 'moodle/user:manageownfiles', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], + 'core_user_update_private_files' => [ + 'classname' => '\core_user\external\update_private_files', + 'description' => 'Copy files from a draft area to users private files area.', + 'type' => 'write', + 'capabilities' => 'moodle/user:manageownfiles', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], // Competencies functions. 'core_competency_create_competency_framework' => array( diff --git a/user/classes/external/prepare_private_files_for_edition.php b/user/classes/external/prepare_private_files_for_edition.php new file mode 100644 index 00000000000..e7d9b152e3a --- /dev/null +++ b/user/classes/external/prepare_private_files_for_edition.php @@ -0,0 +1,99 @@ +. + +namespace core_user\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_multiple_structure; +use core_external\external_single_structure; +use core_external\external_value; +use core_external\external_warnings; +use context_user; + +/** + * Prepares the draft area for user private files. + * + * @package core_user + * @category external + * @copyright 2024 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class prepare_private_files_for_edition extends external_api { + + /** + * Describes the external function parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([]); + } + + /** + * Prepare a draft area for private files. + * + * @throws \moodle_exception; + * @return array + */ + public static function execute(): array { + global $USER; + + $usercontext = context_user::instance($USER->id); + self::validate_context($usercontext); + + $form = new \core_user\form\private_files(); + // Permission checks. + $form->check_access_for_dynamic_submission(); + + $areaoptions = $form->get_options(); + $draftitemid = 0; + file_prepare_draft_area($draftitemid, $usercontext->id, 'user', 'private', 0, $areaoptions); + + // Just get a structure compatible with external API. + array_walk($areaoptions, function(&$item, $key) { + $item = ['name' => $key, 'value' => $item]; + }); + + return [ + 'draftitemid' => $draftitemid, + 'areaoptions' => $areaoptions, + 'warnings' => [], + ]; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure( + [ + 'draftitemid' => new external_value(PARAM_INT, 'Draft item id for the file area.'), + 'areaoptions' => new external_multiple_structure( + new external_single_structure( + [ + 'name' => new external_value(PARAM_RAW, 'Name of option.'), + 'value' => new external_value(PARAM_RAW, 'Value of option.'), + ] + ), 'Draft file area options.' + ), + 'warnings' => new external_warnings(), + ] + ); + } +} diff --git a/user/classes/external/update_private_files.php b/user/classes/external/update_private_files.php new file mode 100644 index 00000000000..fd5bb746fd0 --- /dev/null +++ b/user/classes/external/update_private_files.php @@ -0,0 +1,112 @@ +. + +namespace core_user\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_value; +use core_external\external_warnings; +use context_user; + +/** + * Updates current user private files. + * + * @package core_user + * @category external + * @copyright 2024 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class update_private_files extends external_api { + + /** + * Describes the external function parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'draftitemid' => new external_value(PARAM_INT, 'The draft item id with the files.'), + ] + ); + } + + /** + * Updates current user private files. + * + * @param int $draftitemid The draft item id with the files. + * @throws \moodle_exception; + * @return array + */ + public static function execute(int $draftitemid): array { + global $USER; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'draftitemid' => $draftitemid, + ]); + $warnings = []; + + $usercontext = context_user::instance($USER->id); + self::validate_context($usercontext); + + $fs = get_file_storage(); + if (empty($fs->get_area_files($usercontext->id, 'user', 'draft', $params['draftitemid']))) { + throw new \moodle_exception('Invalid draft item id.'); + } + + // Data structure for the draft item id. + $data = ['files_filemanager' => $params['draftitemid']]; + // Use existing form for validation. + $form = new \core_user\form\private_files(); + $form->check_access_for_dynamic_submission(); + $errors = $form->validation($data, []); + + if (!empty($errors)) { + $status = false; + foreach ($errors as $itemname => $message) { + $warnings[] = [ + 'item' => $itemname, + 'itemid' => 0, + 'warningcode' => 'fielderror', + 'message' => s($message), + ]; + } + } else { + file_postupdate_standard_filemanager((object) $data, 'files', + $form->get_options(), $usercontext, 'user', 'private', 0); + $status = true; + } + + return [ + 'status' => $status, + 'warnings' => $warnings, + ]; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'status' => new external_value(PARAM_BOOL, 'The update result, true if everything went well.'), + 'warnings' => new external_warnings(), + ]); + } +} diff --git a/user/classes/form/private_files.php b/user/classes/form/private_files.php index 4aabd374e14..e58cc8462f2 100644 --- a/user/classes/form/private_files.php +++ b/user/classes/form/private_files.php @@ -108,7 +108,7 @@ class private_files extends \core_form\dynamic_form { * If necessary, form data is available in $this->_ajaxformdata or * by calling $this->optional_param() */ - protected function check_access_for_dynamic_submission(): void { + public function check_access_for_dynamic_submission(): void { require_capability('moodle/user:manageownfiles', $this->get_context_for_dynamic_submission()); } @@ -131,7 +131,7 @@ class private_files extends \core_form\dynamic_form { * @return array * @throws \coding_exception */ - protected function get_options(): array { + public function get_options(): array { global $CFG; $maxbytes = $CFG->userquota; diff --git a/user/tests/external/prepare_private_files_for_edition_test.php b/user/tests/external/prepare_private_files_for_edition_test.php new file mode 100644 index 00000000000..67c6bb4e53e --- /dev/null +++ b/user/tests/external/prepare_private_files_for_edition_test.php @@ -0,0 +1,81 @@ +. + +namespace core_user\external; + +use core_external\external_api; + +/** + * Tests for the prepare_private_files_for_edition class. + * + * @package core_user + * @category external + * @copyright 2024 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \core_user\external\prepare_private_files_for_edition + */ +final class prepare_private_files_for_edition_test extends \advanced_testcase { + + public function test_execute(): void { + global $USER; + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create some files in the user private file area. + $filename = 'faketxt.txt'; + $filerecordinline = [ + 'contextid' => \context_user::instance($USER->id)->id, + 'component' => 'user', + 'filearea' => 'private', + 'itemid' => 0, + 'filepath' => '/', + 'filename' => $filename, + ]; + $fs = get_file_storage(); + $fs->create_file_from_string($filerecordinline, 'fake txt contents.'); + + $result = prepare_private_files_for_edition::execute(); + + $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); + $draftitemid = $result['draftitemid']; + $this->assertGreaterThan(0, $draftitemid); + $this->assertCount(5, $result['areaoptions']); + $this->assertEmpty($result['warnings']); + + // Check we get the expected user private files in the draft area. + $files = file_get_drafarea_files($draftitemid); + $this->assertCount(1, $files->list); + $this->assertEquals($filename, $files->list[0]->filename); + } + + /** + * Test missing capabilities. + */ + public function test_execute_missing_capabilities(): void { + global $CFG, $DB; + $this->resetAfterTest(); + + $authrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid]); + unassign_capability('moodle/user:manageownfiles', $authrole->id); + accesslib_clear_all_caches_for_unit_testing(); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException('moodle_exception'); + $result = prepare_private_files_for_edition::execute(0); + } +} diff --git a/user/tests/external/update_private_files_test.php b/user/tests/external/update_private_files_test.php new file mode 100644 index 00000000000..5b58afee806 --- /dev/null +++ b/user/tests/external/update_private_files_test.php @@ -0,0 +1,176 @@ +. + +namespace core_user\external; + +use core_external\external_api; + +/** + * Tests for the \core_user\external\update_private_files class. + * + * @package core_user + * @category external + * @copyright 2024 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \core_user\external\update_private_files + */ +final class update_private_files_test extends \advanced_testcase { + + /** + * Test base cases. + */ + public function test_execute(): void { + global $CFG; + require_once($CFG->dirroot . '/files/externallib.php'); + + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $anotheruser = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $context = \context_user::instance($user->id); + + // Create one file in the user private file area. + $filename = 'faketxt.txt'; + $filerecordinline = [ + 'contextid' => $context->id, + 'component' => 'user', + 'filearea' => 'private', + 'itemid' => 0, + 'filepath' => '/', + 'filename' => $filename, + ]; + $fs = get_file_storage(); + $fs->create_file_from_string($filerecordinline, 'fake txt contents.'); + + // Retrieve draft area with existing files. + $result = prepare_private_files_for_edition::execute(); + $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); + $draftitemid = $result['draftitemid']; + + // Add one file to the draft area reusing previous structure. + $newfilename = 'newfaketxt.txt'; + $filerecordinline['itemid'] = $draftitemid; + $filerecordinline['filearea'] = 'draft'; + $filerecordinline['filename'] = $newfilename; + $fs->create_file_from_string($filerecordinline, 'new fake txt contents.'); + $files = file_get_drafarea_files($draftitemid); + $this->assertCount(2, $files->list); // 2 files in the draft area. + + // Update the private files with the new files. + $result = update_private_files::execute($draftitemid); + $result = external_api::clean_returnvalue(update_private_files::execute_returns(), $result); + $this->assertTrue($result['status']); + $this->assertEmpty($result['warnings']); + // Check that the new files are in the private file area. + $files = \core_files_external::get_files($context->id, 'user', 'private', 0, '/', ''); + $this->assertCount(2, $files['files']); // 2 files in the private area. + + // Now, try deleting one. + $result = prepare_private_files_for_edition::execute(); + $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); + $draftitemid = $result['draftitemid']; + + \core_files\external\delete\draft::execute($draftitemid, [ + [ + 'filepath' => '/', + 'filename' => $newfilename, + ], + ]); + // Update to force deletion. + $result = update_private_files::execute($draftitemid); + $result = external_api::clean_returnvalue(update_private_files::execute_returns(), $result); + $this->assertTrue($result['status']); + $this->assertEmpty($result['warnings']); + // Check we only have one now. + $files = \core_files_external::get_files($context->id, 'user', 'private', 0, '/', ''); + $this->assertCount(1, $files['files']); + $this->assertEquals($filename, $files['files'][0]['filename']); + + // Use other's user draft item area. + $result = prepare_private_files_for_edition::execute(); + $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); + $draftitemid = $result['draftitemid']; + + $this->setUser($anotheruser); + $this->expectException('moodle_exception'); + update_private_files::execute($draftitemid); + } + + /** + * Test invalid draftitemid. + */ + public function test_execute_invalid_draftitemid(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->expectException('moodle_exception'); + update_private_files::execute(0); + } + + /** + * Test quota reached. + */ + public function test_execute_quota_reached(): void { + global $CFG; + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $context = \context_user::instance($user->id); + + // Force the quota so we are sure it won't be space to add the new file. + $fileareainfo = file_get_file_area_info($context->id, 'user', 'private'); + $CFG->userquota = 1; + + $result = prepare_private_files_for_edition::execute(); + $result = external_api::clean_returnvalue(prepare_private_files_for_edition::execute_returns(), $result); + $draftitemid = $result['draftitemid']; + $filerecordinline = [ + 'contextid' => $context->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $draftitemid, + 'filepath' => '/', + 'filename' => 'faketxt.txt', + ]; + $fs = get_file_storage(); + $fs->create_file_from_string($filerecordinline, 'new fake txt contents.'); + + $result = update_private_files::execute($draftitemid); + $result = external_api::clean_returnvalue(update_private_files::execute_returns(), $result); + // We should get a warning as because of quota we can add files. + $this->assertFalse($result['status']); + $this->assertNotEmpty($result['warnings']); + } + + /** + * Test missing capabilities. + */ + public function test_execute_missing_capabilities(): void { + global $CFG, $DB; + $this->resetAfterTest(); + + $authrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid]); + unassign_capability('moodle/user:manageownfiles', $authrole->id); + accesslib_clear_all_caches_for_unit_testing(); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException('moodle_exception'); + $result = update_private_files::execute(0); + } +}