From 166afa05feeee113c87147c965ccabde3b745482 Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Mon, 18 Dec 2023 17:27:59 +0100 Subject: [PATCH] MDL-65978 blog: New WS core_blog_update_entry --- blog/classes/external/update_entry.php | 195 +++++++++++++++++++++ blog/tests/external/external_test.php | 232 +++++++++++++++++++++++++ lib/db/services.php | 6 + 3 files changed, 433 insertions(+) create mode 100644 blog/classes/external/update_entry.php diff --git a/blog/classes/external/update_entry.php b/blog/classes/external/update_entry.php new file mode 100644 index 00000000000..5d7a6eac85b --- /dev/null +++ b/blog/classes/external/update_entry.php @@ -0,0 +1,195 @@ +. + +namespace core_blog\external; + +use core_external\external_api; +use core_external\external_format_value; +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_system; +use context_course; +use context_module; +use moodle_exception; + +/** + * This is the external method for updating a blog post entry. + * + * @package core_blog + * @copyright 2024 Juan Leyva + * @category external + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class update_entry extends external_api { + + /** + * Parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'entryid' => new external_value(PARAM_INT, 'Blog entry id to update'), + 'subject' => new external_value(PARAM_TEXT, 'Blog subject'), + 'summary' => new external_value(PARAM_RAW, 'Blog post content'), + 'summaryformat' => new external_format_value('summary'), + 'options' => new external_multiple_structure ( + new external_single_structure( + [ + 'name' => new external_value(PARAM_ALPHANUM, + 'The allowed keys (value format) are: + inlineattachmentsid (int); the draft file area id for inline attachments. Default to 0. + attachmentsid (int); the draft file area id for attachments. Default to 0. + publishstate (str); the publish state of the entry (draft, site or public). Default to site. + courseassoc (int); the course id to associate the entry with. Default to 0. + modassoc (int); the module id to associate the entry with. Default to 0. + tags (str); the tags to associate the entry with, comma separated. Default to empty.'), + 'value' => new external_value(PARAM_RAW, 'the value of the option (validated inside the function)'), + ] + ), 'Optional settings', VALUE_DEFAULT, [] + ), + ]); + } + + /** + * Update the indicated glossary entry. + * + * @param int $entryid The entry to update + * @param string $subject the glossary subject + * @param string $summary the subject summary + * @param int $summaryformat the subject summary format + * @param array $options additional settings + * @return array with result and warnings + * @throws moodle_exception + */ + public static function execute(int $entryid, string $subject, string $summary, int $summaryformat, + array $options = []): array { + + global $DB, $CFG; + require_once($CFG->dirroot . '/blog/lib.php'); + require_once($CFG->dirroot . '/blog/locallib.php'); + + $params = self::validate_parameters(self::execute_parameters(), compact('entryid', 'subject', 'summary', + 'summaryformat', 'options')); + + if (empty($CFG->enableblogs)) { + throw new moodle_exception('blogdisable', 'blog'); + } + + if (!$entry = new \blog_entry($params['entryid'])) { + throw new moodle_exception('wrongentryid', 'blog'); + } + + if (!blog_user_can_edit_entry($entry)) { + throw new \moodle_exception('cannoteditentryorblog', 'blog'); + } + + // Prepare the entry object. + $entrydata = new \stdClass(); + $entrydata->id = $entry->id; + $entrydata->subject = $params['subject']; + $entrydata->summary_editor = [ + 'text' => $params['summary'], + 'format' => $params['summaryformat'], + ]; + $entrydata->publishstate = $entry->publishstate; + $entrydata->courseassoc = $entry->courseassoc; + $entrydata->modassoc = $entry->modassoc; + $entrydata->tags = \core_tag_tag::get_item_tags_array('core', 'post', $entry->id); + + // Options. + foreach ($params['options'] as $option) { + $name = trim($option['name']); + switch ($name) { + case 'inlineattachmentsid': + $entrydata->summary_editor['itemid'] = clean_param($option['value'], PARAM_INT); + break; + case 'attachmentsid': + $entrydata->attachment_filemanager = clean_param($option['value'], PARAM_INT); + break; + case 'publishstate': + $entrydata->publishstate = clean_param($option['value'], PARAM_ALPHA); + $applicable = \blog_entry::get_applicable_publish_states(); + if (empty($applicable[$entrydata->publishstate])) { + throw new moodle_exception('errorinvalidparam', 'webservice', '', $name); + } + break; + case 'courseassoc': + case 'modassoc': + $entrydata->{$name} = clean_param($option['value'], PARAM_INT); + if (!$CFG->useblogassociations) { + throw new moodle_exception('errorinvalidparam', 'webservice', '', $name); + } + break; + case 'tags': + $entrydata->tags = clean_param($option['value'], PARAM_TAGLIST); + // Convert to the expected format. + $entrydata->tags = explode(',', $entrydata->tags); + break; + default: + throw new moodle_exception('errorinvalidparam', 'webservice', '', $name); + } + } + + $context = context_system::instance(); + + // Validate course association. We need to convert the course id to context. + if (!empty($entrydata->courseassoc)) { + $coursecontext = context_course::instance($entrydata->courseassoc); + $entrydata->courseid = $entrydata->courseassoc; + $entrydata->courseassoc = $coursecontext->id; // Convert to context. + $context = $coursecontext; + } + + // Validate mod association. + if (!empty($entrydata->modassoc)) { + $modcontext = context_module::instance($entrydata->modassoc); + if (!empty($coursecontext) && $coursecontext->id != $modcontext->get_course_context(true)->id) { + throw new moodle_exception('errorinvalidparam', 'webservice', '', 'modassoc'); + } + $entrydata->coursemoduleid = $entrydata->modassoc; + $entrydata->modassoc = $modcontext->id; // Convert to context. + $context = $modcontext; + } + + // Validate context. It might be upated because of the new association. + self::validate_context($context); + + [$summaryoptions, $attachmentoptions] = blog_get_editor_options($entrydata); + + $entry->edit((array) $entrydata, null, $summaryoptions, $attachmentoptions); + + return [ + 'status' => true, + 'warnings' => [], + ]; + } + + /** + * Return. + * + * @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/blog/tests/external/external_test.php b/blog/tests/external/external_test.php index 51d794538dd..bc4ceace2ac 100644 --- a/blog/tests/external/external_test.php +++ b/blog/tests/external/external_test.php @@ -968,4 +968,236 @@ class external_test extends \externallib_advanced_testcase { $this->expectExceptionMessage(get_string('cannoteditentryorblog', 'blog')); prepare_entry_for_edition::execute($this->postid); } + + /** + * Test update_entry + */ + public function test_update_entry() { + global $USER; + + $this->resetAfterTest(true); + + // Add post with attachments. + $this->setAdminUser(); + + // Draft files. + $draftidinlineattach = file_get_unused_draft_itemid(); + $draftidattach = file_get_unused_draft_itemid(); + $usercontext = \context_user::instance($USER->id); + $inlinefilename = 'inlineimage.png'; + $filerecordinline = [ + 'contextid' => $usercontext->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $draftidinlineattach, + 'filepath' => '/', + 'filename' => $inlinefilename, + ]; + $fs = get_file_storage(); + + // Create a file in a draft area for regular attachments. + $filerecordattach = $filerecordinline; + $attachfilename = 'attachment.txt'; + $filerecordattach['filename'] = $attachfilename; + $filerecordattach['itemid'] = $draftidattach; + $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); + $fs->create_file_from_string($filerecordattach, 'simple text attachment'); + + $options = [ + [ + 'name' => 'inlineattachmentsid', + 'value' => $draftidinlineattach, + ], + [ + 'name' => 'attachmentsid', + 'value' => $draftidattach, + ], + [ + 'name' => 'tags', + 'value' => 'tag1, tag2', + ], + [ + 'name' => 'courseassoc', + 'value' => $this->courseid, + ], + ]; + + $subject = 'First post'; + $summary = 'First post summary'; + $result = add_entry::execute($subject, $summary, FORMAT_HTML, $options); + $result = external_api::clean_returnvalue(add_entry::execute_returns(), $result); + $entryid = $result['entryid']; + + // Retrieve file areas. + $result = prepare_entry_for_edition::execute($entryid); + $result = external_api::clean_returnvalue(prepare_entry_for_edition::execute_returns(), $result); + + // Update files. + $inlinefilename = 'inlineimage2.png'; + $filerecordinline = [ + 'contextid' => $usercontext->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $result['inlineattachmentsid'], + 'filepath' => '/', + 'filename' => $inlinefilename, + ]; + $fs = get_file_storage(); + + // Create a file in a draft area for regular attachments. + $filerecordattach = $filerecordinline; + $newattachfilename = 'attachment2.txt'; + $filerecordattach['filename'] = $newattachfilename; + $filerecordattach['itemid'] = $result['attachmentsid']; + $fs->create_file_from_string($filerecordinline, 'image contents (not really)'); + $fs->create_file_from_string($filerecordattach, 'simple text attachment'); + + // Remove one previous attachment file. + $filetoremove = (object) ['filename' => 'attachment.txt', 'filepath' => '/']; + repository_delete_selected_files($usercontext, 'user', 'draft', $result['attachmentsid'], [$filetoremove]); + + // Update. + $options = [ + ['name' => 'inlineattachmentsid', 'value' => $result['inlineattachmentsid']], + ['name' => 'attachmentsid', 'value' => $result['attachmentsid']], + ['name' => 'tags', 'value' => 'tag3'], + ['name' => 'courseassoc', 'value' => $this->courseid], + ['name' => 'modassoc', 'value' => $this->cmid], + ]; + + $subject = 'First post updated'; + $summary = 'First post summary updated'; + $result = update_entry::execute($entryid, $subject, $summary, FORMAT_HTML, $options); + $result = external_api::clean_returnvalue(update_entry::execute_returns(), $result); + + // Retrieve files via WS. + $result = \core_blog\external::get_entries(); + $result = external_api::clean_returnvalue(\core_blog\external::get_entries_returns(), $result); + + foreach ($result['entries'] as $entry) { + if ($entry['id'] == $entryid) { + $this->assertEquals($subject, $entry['subject']); + $this->assertEquals($summary, $entry['summary']); + $this->assertEquals($this->courseid, $entry['courseid']); + $this->assertEquals($this->cmid, $entry['coursemoduleid']); + $this->assertCount(1, $entry['attachmentfiles']); + $this->assertCount(2, $entry['summaryfiles']); + $this->assertCount(1, $entry['tags']); + $this->assertEquals($newattachfilename, $entry['attachmentfiles'][0]['filename']); + } + } + } + + /** + * Test update_entry when blogs not enabled. + */ + public function test_update_entry_blog_not_enabled() { + global $CFG; + + $this->resetAfterTest(true); + $CFG->enableblogs = 0; + $this->setAdminUser(); + + $this->expectException('\moodle_exception'); + $this->expectExceptionMessage(get_string('blogdisable', 'blog')); + update_entry::execute($this->postid, 'Subject', 'Summary', FORMAT_HTML); + } + + /** + * Test update_entry without permissions. + */ + public function test_update_entry_no_permission() { + global $CFG; + + $this->resetAfterTest(true); + + // Remove capability. + $sitecontext = \context_system::instance(); + $this->unassignUserCapability('moodle/blog:create', $sitecontext->id, $CFG->defaultuserroleid); + $user = $this->getDataGenerator()->create_user(); + $this->setuser($user); + + $this->expectException('\moodle_exception'); + $this->expectExceptionMessage(get_string('cannoteditentryorblog', 'blog')); + update_entry::execute($this->postid, 'Subject', 'Summary', FORMAT_HTML); + } + + /** + * Test update_entry invalid parameter. + */ + public function test_update_entry_invalid_parameter() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $this->expectException('\moodle_exception'); + $this->expectExceptionMessage(get_string('errorinvalidparam', 'webservice', 'invalid')); + $options = [['name' => 'invalid', 'value' => 'invalidvalue']]; + update_entry::execute($this->postid, 'Subject', 'Summary', FORMAT_HTML, $options); + } + + /** + * Test update_entry diabled associations. + */ + public function test_update_entry_disabled_assoc() { + global $CFG; + $CFG->useblogassociations = 0; + + $this->resetAfterTest(true); + $this->setAdminUser(); + + $this->expectException('\moodle_exception'); + $this->expectExceptionMessage(get_string('errorinvalidparam', 'webservice', 'modassoc')); + $options = [['name' => 'modassoc', 'value' => 1]]; + update_entry::execute($this->postid, 'Subject', 'Summary', FORMAT_HTML, $options); + } + + /** + * Test update_entry invalid publish state. + */ + public function test_update_entry_invalid_publishstate() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $this->expectException('\moodle_exception'); + $this->expectExceptionMessage(get_string('errorinvalidparam', 'webservice', 'publishstate')); + $options = [['name' => 'publishstate', 'value' => 'something']]; + update_entry::execute($this->postid, 'Subject', 'Summary', FORMAT_HTML, $options); + } + + /** + * Test update_entry invalid association. + */ + public function test_update_entry_invalid_association() { + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $anothercourse = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id]); + + $this->setAdminUser(); + + $this->expectException('\moodle_exception'); + $this->expectExceptionMessage(get_string('errorinvalidparam', 'webservice', 'modassoc')); + $options = [ + ['name' => 'courseassoc', 'value' => $anothercourse->id], + ['name' => 'modassoc', 'value' => $page->cmid], + ]; + update_entry::execute($this->postid, 'Subject', 'Summary', FORMAT_HTML, $options); + } + + /** + * Test update_entry from another user (no permissions) + */ + public function test_update_entry_no_permissions() { + $this->resetAfterTest(true); + + $user = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($user->id, $this->courseid); + + // I can delete my own entry. + $this->setUser($user); + + $this->expectException('\moodle_exception'); + update_entry::execute($this->postid, 'Subject', 'Summary', FORMAT_HTML); + } } diff --git a/lib/db/services.php b/lib/db/services.php index e3bcc83a563..6034597984c 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -175,6 +175,12 @@ $functions = array( 'type' => 'write', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], + 'core_blog_update_entry' => [ + 'classname' => '\core_blog\external\update_entry', + 'description' => 'Updates a blog entry.', + 'type' => 'write', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], 'core_calendar_get_calendar_monthly_view' => array( 'classname' => 'core_calendar_external', 'methodname' => 'get_calendar_monthly_view',