diff --git a/badges/classes/external.php b/badges/classes/external.php index b8e856d9be3..f887a938ad4 100644 --- a/badges/classes/external.php +++ b/badges/classes/external.php @@ -128,65 +128,7 @@ class core_badges_external extends external_api { $result['warnings'] = $warnings; foreach ($userbadges as $badge) { - $context = ($badge->type == BADGE_TYPE_SITE) ? context_system::instance() : context_course::instance($badge->courseid); - $canconfiguredetails = has_capability('moodle/badges:configuredetails', $context); - - // If the user is viewing another user's badge and doesn't have the right capability return only part of the data. - if ($USER->id != $user->id and !$canconfiguredetails) { - $badge = (object) array( - 'id' => $badge->id, - 'name' => $badge->name, - 'description' => $badge->description, - 'issuername' => $badge->issuername, - 'issuerurl' => $badge->issuerurl, - 'issuercontact' => $badge->issuercontact, - 'uniquehash' => $badge->uniquehash, - 'dateissued' => $badge->dateissued, - 'dateexpire' => $badge->dateexpire, - 'version' => $badge->version, - 'language' => $badge->language, - 'imageauthorname' => $badge->imageauthorname, - 'imageauthoremail' => $badge->imageauthoremail, - 'imageauthorurl' => $badge->imageauthorurl, - 'imagecaption' => $badge->imagecaption, - ); - } - - // Create a badge instance to be able to get the endorsement and other info. - $badgeinstance = new badge($badge->id); - $endorsement = $badgeinstance->get_endorsement(); - $alignments = $badgeinstance->get_alignments(); - $relatedbadges = $badgeinstance->get_related_badges(); - - if (!$canconfiguredetails) { - // Return only the properties visible by the user. - - if (!empty($alignments)) { - foreach ($alignments as $alignment) { - unset($alignment->targetdescription); - unset($alignment->targetframework); - unset($alignment->targetcode); - } - } - - if (!empty($relatedbadges)) { - foreach ($relatedbadges as $relatedbadge) { - unset($relatedbadge->version); - unset($relatedbadge->language); - unset($relatedbadge->type); - } - } - } - - $related = array( - 'context' => $context, - 'endorsement' => $endorsement ? $endorsement : null, - 'alignment' => $alignments, - 'relatedbadges' => $relatedbadges, - ); - - $exporter = new user_badge_exporter($badge, $related); - $result['badges'][] = $exporter->export($PAGE->get_renderer('core')); + $result['badges'][] = badges_prepare_badge_for_external($badge, $user); } return $result; diff --git a/badges/classes/external/get_user_badge_by_hash.php b/badges/classes/external/get_user_badge_by_hash.php new file mode 100644 index 00000000000..97b94e1d38e --- /dev/null +++ b/badges/classes/external/get_user_badge_by_hash.php @@ -0,0 +1,114 @@ +. + +namespace core_badges\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_multiple_structure; +use core_external\external_value; +use core_external\external_warnings; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/badgeslib.php'); + +/** + * External service to get user badge. + * + * This is mainly used by the mobile application. + * + * @package core_badges + * @category external + * @copyright 2023 Rodrigo Mady + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.3 + */ +class get_user_badge_by_hash extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'hash' => new external_value(PARAM_ALPHANUM, 'Badge issued hash', VALUE_REQUIRED), + ]); + } + + /** + * Execute the get user badge. + * + * @param string $hash + * @return array + * @throws \restricted_context_exception + */ + public static function execute(string $hash): array { + global $CFG; + + // Initialize return variables. + $warnings = []; + $result = []; + + // Validate the hash. + [ + 'hash' => $hash, + ] = self::validate_parameters(self::execute_parameters(), [ + 'hash' => $hash, + ]); + + if (empty($CFG->enablebadges)) { + throw new moodle_exception('badgesdisabled', 'badges'); + } + + // Get the badge by hash. + $badge = badges_get_badge_by_hash($hash); + + if (!empty($badge)) { + // Get the user that issued the badge. + $user = \core_user::get_user($badge->userid, '*', MUST_EXIST); + $result[] = badges_prepare_badge_for_external($badge, $user); + } else { + $warnings[] = [ + 'item' => $hash, + 'warningcode' => 'badgeawardnotfound', + 'message' => get_string('error:badgeawardnotfound', 'badges') + ]; + } + + return [ + 'badge' => $result, + '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([ + 'badge' => new external_multiple_structure( + user_badge_exporter::get_read_structure() + ), + 'warnings' => new external_warnings() + ]); + } +} diff --git a/badges/tests/external/get_user_badge_by_hash_test.php b/badges/tests/external/get_user_badge_by_hash_test.php new file mode 100644 index 00000000000..eca15acf042 --- /dev/null +++ b/badges/tests/external/get_user_badge_by_hash_test.php @@ -0,0 +1,253 @@ +. + +namespace core_badges\external; + +use externallib_advanced_testcase; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); +require_once($CFG->libdir . '/badgeslib.php'); + +/** + * Tests for external function get_user_badge_by_hash. + * + * @package core_badges + * @category external + * @copyright 2023 Rodrigo Mady + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.3 + * @coversDefaultClass \core_badges\external\get_user_badge_by_hash + */ +class get_user_badge_by_hash_test extends externallib_advanced_testcase { + + /** + * Prepare the test. + * + * @return array + */ + private function prepare_test_data(): array { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + + // Setup test data. + $course = $this->getDataGenerator()->create_course(); + + // Create users and enrolments. + $student1 = $this->getDataGenerator()->create_and_enrol($course); + $student2 = $this->getDataGenerator()->create_and_enrol($course); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + + // Mock up a site badge. + $now = time(); + $badge = new \stdClass(); + $badge->id = null; + $badge->name = "Test badge site"; + $badge->description = "Testing badges site"; + $badge->timecreated = $now; + $badge->timemodified = $now; + $badge->usercreated = (int) $teacher->id; + $badge->usermodified = (int) $teacher->id; + $badge->expiredate = null; + $badge->expireperiod = null; + $badge->type = BADGE_TYPE_SITE; + $badge->courseid = null; + $badge->messagesubject = "Test message subject for badge"; + $badge->message = "Test message body for badge"; + $badge->attachment = 1; + $badge->notification = 0; + $badge->status = BADGE_STATUS_ACTIVE; + $badge->version = '1'; + $badge->language = 'en'; + $badge->imageauthorname = 'Image author'; + $badge->imageauthoremail = 'imageauthor@example.com'; + $badge->imageauthorurl = 'http://image-author-url.domain.co.nz'; + $badge->imagecaption = 'Caption'; + + $badgeid = $DB->insert_record('badge', $badge, true); + $badge->id = $badgeid; + $sitebadge = new \badge($badgeid); + $sitebadge->issue($student1->id, true); + $siteissuedbadge = $DB->get_record('badge_issued', [ 'badgeid' => $badge->id ]); + + $badge->issuername = $sitebadge->issuername; + $badge->issuercontact = $sitebadge->issuercontact; + $badge->issuerurl = $sitebadge->issuerurl; + $badge->nextcron = $sitebadge->nextcron; + $badge->issuedid = (int) $siteissuedbadge->id; + $badge->uniquehash = $siteissuedbadge->uniquehash; + $badge->dateissued = (int) $siteissuedbadge->dateissued; + $badge->dateexpire = $siteissuedbadge->dateexpire; + $badge->visible = (int) $siteissuedbadge->visible; + $badge->email = $student1->email; + $context = \context_system::instance(); + $badge->badgeurl = \moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', + 'f3')->out(false); + + // Hack the database to adjust the time each badge was issued. + $DB->set_field('badge_issued', 'dateissued', $now, ['userid' => $student1->id, 'badgeid' => $badgeid]); + $badge->status = BADGE_STATUS_ACTIVE_LOCKED; + + // Add an endorsement for the badge. + $endorsement = new \stdClass(); + $endorsement->badgeid = $badgeid; + $endorsement->issuername = 'Issuer name'; + $endorsement->issuerurl = 'http://endorsement-issuer-url.domain.co.nz'; + $endorsement->issueremail = 'endorsementissuer@example.com'; + $endorsement->claimid = 'http://claim-url.domain.co.nz'; + $endorsement->claimcomment = 'Claim comment'; + $endorsement->dateissued = $now; + $endorsement->id = $sitebadge->save_endorsement($endorsement); + $badge->endorsement = (array) $endorsement; + + // Add 2 alignments. + $alignment = new \stdClass(); + $alignment->badgeid = $badgeid; + $alignment->id = $sitebadge->save_alignment($alignment); + $badge->alignment[] = (array) $alignment; + + $alignment->id = $sitebadge->save_alignment($alignment); + $badge->alignment[] = (array) $alignment; + $badge->relatedbadges = []; + $usersitebadge[] = (array) $badge; + + // Now a course badge. + $badge->id = null; + $badge->name = "Test badge course"; + $badge->description = "Testing badges course"; + $badge->type = BADGE_TYPE_COURSE; + $badge->courseid = (int) $course->id; + + $badge->id = $DB->insert_record('badge', $badge, true); + $coursebadge = new \badge($badge->id ); + $coursebadge->issue($student1->id, true); + $courseissuedbadge = $DB->get_record('badge_issued', [ 'badgeid' => $badge->id ]); + + $badge->issuername = $coursebadge->issuername; + $badge->issuercontact = $coursebadge->issuercontact; + $badge->issuerurl = $coursebadge->issuerurl; + $badge->nextcron = $coursebadge->nextcron; + $badge->issuedid = (int) $courseissuedbadge->id; + $badge->uniquehash = $courseissuedbadge->uniquehash; + $badge->dateissued = (int) $courseissuedbadge->dateissued; + $badge->dateexpire = $courseissuedbadge->dateexpire; + $badge->visible = (int) $courseissuedbadge->visible; + $badge->email = $student1->email; + $context = \context_course::instance($badge->courseid); + $badge->badgeurl = \moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id , '/', + 'f3')->out(false); + + // Hack the database to adjust the time each badge was issued. + $DB->set_field('badge_issued', 'dateissued', $now, ['userid' => $student1->id, 'badgeid' => $badge->id]); + + unset($badge->endorsement); + $badge->alignment = []; + $usercoursebadge[] = (array) $badge; + // Make the site badge a related badge. + $sitebadge->add_related_badges([$badge->id]); + $usersitebadge[0]['relatedbadges'][0] = [ + 'id' => (int) $coursebadge->id, + 'name' => $coursebadge->name + ]; + $usercoursebadge[0]['relatedbadges'][0] = [ + 'id' => (int) $sitebadge->id, + 'name' => $sitebadge->name + ]; + return [ + 'coursebadge' => $usercoursebadge, + 'sitebadge' => $usersitebadge, + 'student1' => $student1, + 'student2' => $student2 + ]; + } + + /** + * Test get user badge by hash. + * These are a basic tests since the badges_get_my_user_badges used by the external function already has unit tests. + * @covers ::execute + */ + public function test_get_user_badge_by_hash() { + $data = $this->prepare_test_data(); + $this->setUser($data['student1']); + + // Site badge. + $result = get_user_badge_by_hash::execute($data['sitebadge'][0]['uniquehash']); + $result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result); + $this->assertEquals($data['sitebadge'], $result['badge']); + $this->assertEmpty($result['warnings']); + + // Course badge. + $result = get_user_badge_by_hash::execute($data['coursebadge'][0]['uniquehash']); + $result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result); + $this->assertEquals($data['coursebadge'], $result['badge']); + $this->assertEmpty($result['warnings']); + + // Wrong hash. + $result = get_user_badge_by_hash::execute('1234'); + $result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result); + $this->assertEmpty($result['badge']); + $this->assertNotEmpty($result['warnings']); + $this->assertEquals('badgeawardnotfound', $result['warnings'][0]['warningcode']); + } + + /** + * Test get user badge by hash with restrictions. + * @covers ::execute + */ + public function test_get_user_badge_by_hash_with_restrictions() { + $data = $this->prepare_test_data(); + $this->setUser($data['student2']); + + // Site badge. + $result = get_user_badge_by_hash::execute($data['sitebadge'][0]['uniquehash']); + $result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result); + $this->assertNotEmpty($result['badge']); + $this->assertEmpty($result['warnings']); + + // Check that we don't have permissions for view the complete information for site badges. + if (isset($result['badge'][0]['type']) && $result['badge'][0]['type'] == BADGE_TYPE_SITE) { + $this->assertFalse(isset($result['badge'][0]['message'])); + + // Check that we have permissions to see all the data in alignments and related badges. + foreach ($result['badge'][0]['alignment'] as $alignment) { + $this->assertTrue(isset($alignment['id'])); + } + + foreach ($result['badge'][0]['relatedbadges'] as $relatedbadge) { + $this->assertTrue(isset($relatedbadge['id'])); + } + } else { + $this->assertTrue(isset($result['badge'][0]['message'])); + } + + // Course badge. + $result = get_user_badge_by_hash::execute($data['coursebadge'][0]['uniquehash']); + $result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result); + $this->assertNotEmpty($result['badge']); + $this->assertEmpty($result['warnings']); + + // Check that we don't have permissions for view the complete information for course badges. + if (isset($result['badge'][0]['type']) && $result['badge'][0]['type'] == BADGE_TYPE_COURSE) { + $this->assertFalse(isset($result['badge'][0]['message'])); + } else { + $this->assertTrue(isset($result['badge'][0]['message'])); + } + } +} diff --git a/lib/badgeslib.php b/lib/badgeslib.php index cd6375bd49a..2c017ed2ef8 100644 --- a/lib/badgeslib.php +++ b/lib/badgeslib.php @@ -29,6 +29,9 @@ defined('MOODLE_INTERNAL') || die(); /* Include required award criteria library. */ require_once($CFG->dirroot . '/badges/criteria/award_criteria.php'); +/* Include required user badge exporter */ +use core_badges\external\user_badge_exporter; + /* * Number of records per page. */ @@ -378,6 +381,105 @@ function badges_get_user_badges($userid, $courseid = 0, $page = 0, $perpage = 0, return $badges; } +/** + * Get badge by hash. + * + * @param string $hash + * @return object|bool + */ +function badges_get_badge_by_hash(string $hash): object|bool { + global $DB; + $sql = 'SELECT + bi.uniquehash, + bi.dateissued, + bi.userid, + bi.dateexpire, + bi.id as issuedid, + bi.visible, + u.email, + b.* + FROM + {badge} b, + {badge_issued} bi, + {user} u + WHERE b.id = bi.badgeid + AND u.id = bi.userid + AND bi.uniquehash = :uniquehash'; + $badge = $DB->get_record_sql($sql, ['uniquehash' => $hash]); + return $badge; +} + +/** + * Update badge instance to external functions. + * + * @param stdClass $badge + * @param stdClass $user + * @return object + */ +function badges_prepare_badge_for_external(stdClass $badge, stdClass $user): object { + global $PAGE, $USER; + $context = ($badge->type == BADGE_TYPE_SITE) ? + context_system::instance() : + context_course::instance($badge->courseid); + $canconfiguredetails = has_capability('moodle/badges:configuredetails', $context); + // If the user is viewing another user's badge and doesn't have the right capability return only part of the data. + if ($USER->id != $user->id && !$canconfiguredetails) { + $badge = (object) [ + 'id' => $badge->id, + 'name' => $badge->name, + 'type' => $badge->type, + 'description' => $badge->description, + 'issuername' => $badge->issuername, + 'issuerurl' => $badge->issuerurl, + 'issuercontact' => $badge->issuercontact, + 'uniquehash' => $badge->uniquehash, + 'dateissued' => $badge->dateissued, + 'dateexpire' => $badge->dateexpire, + 'version' => $badge->version, + 'language' => $badge->language, + 'imageauthorname' => $badge->imageauthorname, + 'imageauthoremail' => $badge->imageauthoremail, + 'imageauthorurl' => $badge->imageauthorurl, + 'imagecaption' => $badge->imagecaption, + ]; + } + + // Create a badge instance to be able to get the endorsement and other info. + $badgeinstance = new badge($badge->id); + $endorsement = $badgeinstance->get_endorsement(); + $alignments = $badgeinstance->get_alignments(); + $relatedbadges = $badgeinstance->get_related_badges(); + + if (!$canconfiguredetails) { + // Return only the properties visible by the user. + if (!empty($alignments)) { + foreach ($alignments as $alignment) { + unset($alignment->targetdescription); + unset($alignment->targetframework); + unset($alignment->targetcode); + } + } + + if (!empty($relatedbadges)) { + foreach ($relatedbadges as $relatedbadge) { + unset($relatedbadge->version); + unset($relatedbadge->language); + unset($relatedbadge->type); + } + } + } + + $related = [ + 'context' => $context, + 'endorsement' => $endorsement ? $endorsement : null, + 'alignment' => $alignments, + 'relatedbadges' => $relatedbadges, + ]; + + $exporter = new user_badge_exporter($badge, $related); + return $exporter->export($PAGE->get_renderer('core')); +} + /** * Extends the course administration navigation with the Badges page * diff --git a/lib/db/services.php b/lib/db/services.php index 9853e36006f..20a7007ed1c 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -127,6 +127,12 @@ $functions = array( 'capabilities' => 'moodle/badges:viewotherbadges', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_badges_get_user_badge_by_hash' => [ + 'classname' => 'core_badges\external\get_user_badge_by_hash', + 'description' => 'Returns the badge awarded to a user by hash.', + 'type' => 'read', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], 'core_blog_get_entries' => array( 'classname' => 'core_blog\external', 'methodname' => 'get_entries', diff --git a/version.php b/version.php index 2d71bd2f467..be0b9322593 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2023062300.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2023062400.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.3dev (Build: 20230623)'; // Human-friendly version name