Merge branch 'MDL-65243-master' of git://github.com/mihailges/moodle
[moodle.git] / lib / badgeslib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Contains classes, functions and constants used in badges.
19  *
20  * @package    core
21  * @subpackage badges
22  * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
25  */
27 defined('MOODLE_INTERNAL') || die();
29 /* Include required award criteria library. */
30 require_once($CFG->dirroot . '/badges/criteria/award_criteria.php');
32 /*
33  * Number of records per page.
34 */
35 define('BADGE_PERPAGE', 50);
37 /*
38  * Badge award criteria aggregation method.
39  */
40 define('BADGE_CRITERIA_AGGREGATION_ALL', 1);
42 /*
43  * Badge award criteria aggregation method.
44  */
45 define('BADGE_CRITERIA_AGGREGATION_ANY', 2);
47 /*
48  * Inactive badge means that this badge cannot be earned and has not been awarded
49  * yet. Its award criteria can be changed.
50  */
51 define('BADGE_STATUS_INACTIVE', 0);
53 /*
54  * Active badge means that this badge can we earned, but it has not been awarded
55  * yet. Can be deactivated for the purpose of changing its criteria.
56  */
57 define('BADGE_STATUS_ACTIVE', 1);
59 /*
60  * Inactive badge can no longer be earned, but it has been awarded in the past and
61  * therefore its criteria cannot be changed.
62  */
63 define('BADGE_STATUS_INACTIVE_LOCKED', 2);
65 /*
66  * Active badge means that it can be earned and has already been awarded to users.
67  * Its criteria cannot be changed any more.
68  */
69 define('BADGE_STATUS_ACTIVE_LOCKED', 3);
71 /*
72  * Archived badge is considered deleted and can no longer be earned and is not
73  * displayed in the list of all badges.
74  */
75 define('BADGE_STATUS_ARCHIVED', 4);
77 /*
78  * Badge type for site badges.
79  */
80 define('BADGE_TYPE_SITE', 1);
82 /*
83  * Badge type for course badges.
84  */
85 define('BADGE_TYPE_COURSE', 2);
87 /*
88  * Badge messaging schedule options.
89  */
90 define('BADGE_MESSAGE_NEVER', 0);
91 define('BADGE_MESSAGE_ALWAYS', 1);
92 define('BADGE_MESSAGE_DAILY', 2);
93 define('BADGE_MESSAGE_WEEKLY', 3);
94 define('BADGE_MESSAGE_MONTHLY', 4);
96 /*
97  * URL of backpack. Custom ones can be added.
98  */
99 define('BADGE_BACKPACKAPIURL', 'https://backpack.openbadges.org');
100 define('BADGE_BACKPACKWEBURL', 'https://backpack.openbadges.org');
101 define('BADGRIO_BACKPACKAPIURL', 'https://api.badgr.io/v2');
102 define('BADGRIO_BACKPACKWEBURL', 'https://badgr.io');
104 /*
105  * @deprecated since 3.7. Use the urls in the badge_external_backpack table instead.
106  */
107 define('BADGE_BACKPACKURL', 'https://backpack.openbadges.org');
109 /*
110  * Open Badges specifications.
111  */
112 define('OPEN_BADGES_V1', 1);
113 define('OPEN_BADGES_V2', 2);
115 /*
116  * Only use for Open Badges 2.0 specification
117  */
118 define('OPEN_BADGES_V2_CONTEXT', 'https://w3id.org/openbadges/v2');
119 define('OPEN_BADGES_V2_TYPE_ASSERTION', 'Assertion');
120 define('OPEN_BADGES_V2_TYPE_BADGE', 'BadgeClass');
121 define('OPEN_BADGES_V2_TYPE_ISSUER', 'Issuer');
122 define('OPEN_BADGES_V2_TYPE_ENDORSEMENT', 'Endorsement');
123 define('OPEN_BADGES_V2_TYPE_AUTHOR', 'Author');
125 // Global badge class has been moved to the component namespace.
126 class_alias('\core_badges\badge', 'badge');
128 /**
129  * Sends notifications to users about awarded badges.
130  *
131  * @param badge $badge Badge that was issued
132  * @param int $userid Recipient ID
133  * @param string $issued Unique hash of an issued badge
134  * @param string $filepathhash File path hash of an issued badge for attachments
135  */
136 function badges_notify_badge_award(badge $badge, $userid, $issued, $filepathhash) {
137     global $CFG, $DB;
139     $admin = get_admin();
140     $userfrom = new stdClass();
141     $userfrom->id = $admin->id;
142     $userfrom->email = !empty($CFG->badges_defaultissuercontact) ? $CFG->badges_defaultissuercontact : $admin->email;
143     foreach (get_all_user_name_fields() as $addname) {
144         $userfrom->$addname = !empty($CFG->badges_defaultissuername) ? '' : $admin->$addname;
145     }
146     $userfrom->firstname = !empty($CFG->badges_defaultissuername) ? $CFG->badges_defaultissuername : $admin->firstname;
147     $userfrom->maildisplay = true;
149     $issuedlink = html_writer::link(new moodle_url('/badges/badge.php', array('hash' => $issued)), $badge->name);
150     $userto = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
152     $params = new stdClass();
153     $params->badgename = $badge->name;
154     $params->username = fullname($userto);
155     $params->badgelink = $issuedlink;
156     $message = badge_message_from_template($badge->message, $params);
157     $plaintext = html_to_text($message);
159     // Notify recipient.
160     $eventdata = new \core\message\message();
161     $eventdata->courseid          = is_null($badge->courseid) ? SITEID : $badge->courseid; // Profile/site come with no courseid.
162     $eventdata->component         = 'moodle';
163     $eventdata->name              = 'badgerecipientnotice';
164     $eventdata->userfrom          = $userfrom;
165     $eventdata->userto            = $userto;
166     $eventdata->notification      = 1;
167     $eventdata->subject           = $badge->messagesubject;
168     $eventdata->fullmessage       = $plaintext;
169     $eventdata->fullmessageformat = FORMAT_HTML;
170     $eventdata->fullmessagehtml   = $message;
171     $eventdata->smallmessage      = '';
172     $eventdata->customdata        = [
173         'notificationiconurl' => moodle_url::make_pluginfile_url(
174             $badge->get_context()->id, 'badges', 'badgeimage', $badge->id, '/', 'f1')->out(),
175         'hash' => $issued,
176     ];
178     // Attach badge image if possible.
179     if (!empty($CFG->allowattachments) && $badge->attachment && is_string($filepathhash)) {
180         $fs = get_file_storage();
181         $file = $fs->get_file_by_hash($filepathhash);
182         $eventdata->attachment = $file;
183         $eventdata->attachname = str_replace(' ', '_', $badge->name) . ".png";
185         message_send($eventdata);
186     } else {
187         message_send($eventdata);
188     }
190     // Notify badge creator about the award if they receive notifications every time.
191     if ($badge->notification == 1) {
192         $userfrom = core_user::get_noreply_user();
193         $userfrom->maildisplay = true;
195         $creator = $DB->get_record('user', array('id' => $badge->usercreated), '*', MUST_EXIST);
196         $a = new stdClass();
197         $a->user = fullname($userto);
198         $a->link = $issuedlink;
199         $creatormessage = get_string('creatorbody', 'badges', $a);
200         $creatorsubject = get_string('creatorsubject', 'badges', $badge->name);
202         $eventdata = new \core\message\message();
203         $eventdata->courseid          = $badge->courseid;
204         $eventdata->component         = 'moodle';
205         $eventdata->name              = 'badgecreatornotice';
206         $eventdata->userfrom          = $userfrom;
207         $eventdata->userto            = $creator;
208         $eventdata->notification      = 1;
209         $eventdata->subject           = $creatorsubject;
210         $eventdata->fullmessage       = html_to_text($creatormessage);
211         $eventdata->fullmessageformat = FORMAT_HTML;
212         $eventdata->fullmessagehtml   = $creatormessage;
213         $eventdata->smallmessage      = '';
214         $eventdata->customdata        = [
215             'notificationiconurl' => moodle_url::make_pluginfile_url(
216                 $badge->get_context()->id, 'badges', 'badgeimage', $badge->id, '/', 'f1')->out(),
217             'hash' => $issued,
218         ];
220         message_send($eventdata);
221         $DB->set_field('badge_issued', 'issuernotified', time(), array('badgeid' => $badge->id, 'userid' => $userid));
222     }
225 /**
226  * Caclulates date for the next message digest to badge creators.
227  *
228  * @param in $schedule Type of message schedule BADGE_MESSAGE_DAILY|BADGE_MESSAGE_WEEKLY|BADGE_MESSAGE_MONTHLY.
229  * @return int Timestamp for next cron
230  */
231 function badges_calculate_message_schedule($schedule) {
232     $nextcron = 0;
234     switch ($schedule) {
235         case BADGE_MESSAGE_DAILY:
236             $tomorrow = new DateTime("1 day", core_date::get_server_timezone_object());
237             $nextcron = $tomorrow->getTimestamp();
238             break;
239         case BADGE_MESSAGE_WEEKLY:
240             $nextweek = new DateTime("1 week", core_date::get_server_timezone_object());
241             $nextcron = $nextweek->getTimestamp();
242             break;
243         case BADGE_MESSAGE_MONTHLY:
244             $nextmonth = new DateTime("1 month", core_date::get_server_timezone_object());
245             $nextcron = $nextmonth->getTimestamp();
246             break;
247     }
249     return $nextcron;
252 /**
253  * Replaces variables in a message template and returns text ready to be emailed to a user.
254  *
255  * @param string $message Message body.
256  * @return string Message with replaced values
257  */
258 function badge_message_from_template($message, $params) {
259     $msg = $message;
260     foreach ($params as $key => $value) {
261         $msg = str_replace("%$key%", $value, $msg);
262     }
264     return $msg;
267 /**
268  * Get all badges.
269  *
270  * @param int Type of badges to return
271  * @param int Course ID for course badges
272  * @param string $sort An SQL field to sort by
273  * @param string $dir The sort direction ASC|DESC
274  * @param int $page The page or records to return
275  * @param int $perpage The number of records to return per page
276  * @param int $user User specific search
277  * @return array $badge Array of records matching criteria
278  */
279 function badges_get_badges($type, $courseid = 0, $sort = '', $dir = '', $page = 0, $perpage = BADGE_PERPAGE, $user = 0) {
280     global $DB;
281     $records = array();
282     $params = array();
283     $where = "b.status != :deleted AND b.type = :type ";
284     $params['deleted'] = BADGE_STATUS_ARCHIVED;
286     $userfields = array('b.id, b.name, b.status');
287     $usersql = "";
288     if ($user != 0) {
289         $userfields[] = 'bi.dateissued';
290         $userfields[] = 'bi.uniquehash';
291         $usersql = " LEFT JOIN {badge_issued} bi ON b.id = bi.badgeid AND bi.userid = :userid ";
292         $params['userid'] = $user;
293         $where .= " AND (b.status = 1 OR b.status = 3) ";
294     }
295     $fields = implode(', ', $userfields);
297     if ($courseid != 0 ) {
298         $where .= "AND b.courseid = :courseid ";
299         $params['courseid'] = $courseid;
300     }
302     $sorting = (($sort != '' && $dir != '') ? 'ORDER BY ' . $sort . ' ' . $dir : '');
303     $params['type'] = $type;
305     $sql = "SELECT $fields FROM {badge} b $usersql WHERE $where $sorting";
306     $records = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
308     $badges = array();
309     foreach ($records as $r) {
310         $badge = new badge($r->id);
311         $badges[$r->id] = $badge;
312         if ($user != 0) {
313             $badges[$r->id]->dateissued = $r->dateissued;
314             $badges[$r->id]->uniquehash = $r->uniquehash;
315         } else {
316             $badges[$r->id]->awards = $DB->count_records_sql('SELECT COUNT(b.userid)
317                                         FROM {badge_issued} b INNER JOIN {user} u ON b.userid = u.id
318                                         WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $badge->id));
319             $badges[$r->id]->statstring = $badge->get_status_name();
320         }
321     }
322     return $badges;
325 /**
326  * Get badges for a specific user.
327  *
328  * @param int $userid User ID
329  * @param int $courseid Badges earned by a user in a specific course
330  * @param int $page The page or records to return
331  * @param int $perpage The number of records to return per page
332  * @param string $search A simple string to search for
333  * @param bool $onlypublic Return only public badges
334  * @return array of badges ordered by decreasing date of issue
335  */
336 function badges_get_user_badges($userid, $courseid = 0, $page = 0, $perpage = 0, $search = '', $onlypublic = false) {
337     global $CFG, $DB;
339     $params = array(
340         'userid' => $userid
341     );
342     $sql = 'SELECT
343                 bi.uniquehash,
344                 bi.dateissued,
345                 bi.dateexpire,
346                 bi.id as issuedid,
347                 bi.visible,
348                 u.email,
349                 b.*
350             FROM
351                 {badge} b,
352                 {badge_issued} bi,
353                 {user} u
354             WHERE b.id = bi.badgeid
355                 AND u.id = bi.userid
356                 AND bi.userid = :userid';
358     if (!empty($search)) {
359         $sql .= ' AND (' . $DB->sql_like('b.name', ':search', false) . ') ';
360         $params['search'] = '%'.$DB->sql_like_escape($search).'%';
361     }
362     if ($onlypublic) {
363         $sql .= ' AND (bi.visible = 1) ';
364     }
366     if (empty($CFG->badges_allowcoursebadges)) {
367         $sql .= ' AND b.courseid IS NULL';
368     } else if ($courseid != 0) {
369         $sql .= ' AND (b.courseid = :courseid) ';
370         $params['courseid'] = $courseid;
371     }
372     $sql .= ' ORDER BY bi.dateissued DESC';
373     $badges = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
375     return $badges;
378 /**
379  * Extends the course administration navigation with the Badges page
380  *
381  * @param navigation_node $coursenode
382  * @param object $course
383  */
384 function badges_add_course_navigation(navigation_node $coursenode, stdClass $course) {
385     global $CFG, $SITE;
387     $coursecontext = context_course::instance($course->id);
388     $isfrontpage = (!$coursecontext || $course->id == $SITE->id);
389     $canmanage = has_any_capability(array('moodle/badges:viewawarded',
390                                           'moodle/badges:createbadge',
391                                           'moodle/badges:awardbadge',
392                                           'moodle/badges:configurecriteria',
393                                           'moodle/badges:configuremessages',
394                                           'moodle/badges:configuredetails',
395                                           'moodle/badges:deletebadge'), $coursecontext);
397     if (!empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges) && !$isfrontpage && $canmanage) {
398         $coursenode->add(get_string('coursebadges', 'badges'), null,
399                 navigation_node::TYPE_CONTAINER, null, 'coursebadges',
400                 new pix_icon('i/badge', get_string('coursebadges', 'badges')));
402         $url = new moodle_url('/badges/index.php', array('type' => BADGE_TYPE_COURSE, 'id' => $course->id));
404         $coursenode->get('coursebadges')->add(get_string('managebadges', 'badges'), $url,
405             navigation_node::TYPE_SETTING, null, 'coursebadges');
407         if (has_capability('moodle/badges:createbadge', $coursecontext)) {
408             $url = new moodle_url('/badges/newbadge.php', array('type' => BADGE_TYPE_COURSE, 'id' => $course->id));
410             $coursenode->get('coursebadges')->add(get_string('newbadge', 'badges'), $url,
411                     navigation_node::TYPE_SETTING, null, 'newbadge');
412         }
413     }
416 /**
417  * Triggered when badge is manually awarded.
418  *
419  * @param   object      $data
420  * @return  boolean
421  */
422 function badges_award_handle_manual_criteria_review(stdClass $data) {
423     $criteria = $data->crit;
424     $userid = $data->userid;
425     $badge = new badge($criteria->badgeid);
427     if (!$badge->is_active() || $badge->is_issued($userid)) {
428         return true;
429     }
431     if ($criteria->review($userid)) {
432         $criteria->mark_complete($userid);
434         if ($badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($userid)) {
435             $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($userid);
436             $badge->issue($userid);
437         }
438     }
440     return true;
443 /**
444  * Process badge image from form data
445  *
446  * @param badge $badge Badge object
447  * @param string $iconfile Original file
448  */
449 function badges_process_badge_image(badge $badge, $iconfile) {
450     global $CFG, $USER;
451     require_once($CFG->libdir. '/gdlib.php');
453     if (!empty($CFG->gdversion)) {
454         process_new_icon($badge->get_context(), 'badges', 'badgeimage', $badge->id, $iconfile, true);
455         @unlink($iconfile);
457         // Clean up file draft area after badge image has been saved.
458         $context = context_user::instance($USER->id, MUST_EXIST);
459         $fs = get_file_storage();
460         $fs->delete_area_files($context->id, 'user', 'draft');
461     }
464 /**
465  * Print badge image.
466  *
467  * @param badge $badge Badge object
468  * @param stdClass $context
469  * @param string $size
470  */
471 function print_badge_image(badge $badge, stdClass $context, $size = 'small') {
472     $fsize = ($size == 'small') ? 'f2' : 'f1';
474     $imageurl = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', $fsize, false);
475     // Appending a random parameter to image link to forse browser reload the image.
476     $imageurl->param('refresh', rand(1, 10000));
477     $attributes = array('src' => $imageurl, 'alt' => s($badge->name), 'class' => 'activatebadge');
479     return html_writer::empty_tag('img', $attributes);
482 /**
483  * Bake issued badge.
484  *
485  * @param string $hash Unique hash of an issued badge.
486  * @param int $badgeid ID of the original badge.
487  * @param int $userid ID of badge recipient (optional).
488  * @param boolean $pathhash Return file pathhash instead of image url (optional).
489  * @return string|url Returns either new file path hash or new file URL
490  */
491 function badges_bake($hash, $badgeid, $userid = 0, $pathhash = false) {
492     global $CFG, $USER;
493     require_once(__DIR__ . '/../badges/lib/bakerlib.php');
495     $badge = new badge($badgeid);
496     $badge_context = $badge->get_context();
497     $userid = ($userid) ? $userid : $USER->id;
498     $user_context = context_user::instance($userid);
500     $fs = get_file_storage();
501     if (!$fs->file_exists($user_context->id, 'badges', 'userbadge', $badge->id, '/', $hash . '.png')) {
502         if ($file = $fs->get_file($badge_context->id, 'badges', 'badgeimage', $badge->id, '/', 'f3.png')) {
503             $contents = $file->get_content();
505             $filehandler = new PNG_MetaDataHandler($contents);
506             $assertion = new moodle_url('/badges/assertion.php', array('b' => $hash));
507             if ($filehandler->check_chunks("tEXt", "openbadges")) {
508                 // Add assertion URL tExt chunk.
509                 $newcontents = $filehandler->add_chunks("tEXt", "openbadges", $assertion->out(false));
510                 $fileinfo = array(
511                         'contextid' => $user_context->id,
512                         'component' => 'badges',
513                         'filearea' => 'userbadge',
514                         'itemid' => $badge->id,
515                         'filepath' => '/',
516                         'filename' => $hash . '.png',
517                 );
519                 // Create a file with added contents.
520                 $newfile = $fs->create_file_from_string($fileinfo, $newcontents);
521                 if ($pathhash) {
522                     return $newfile->get_pathnamehash();
523                 }
524             }
525         } else {
526             debugging('Error baking badge image!', DEBUG_DEVELOPER);
527             return;
528         }
529     }
531     // If file exists and we just need its path hash, return it.
532     if ($pathhash) {
533         $file = $fs->get_file($user_context->id, 'badges', 'userbadge', $badge->id, '/', $hash . '.png');
534         return $file->get_pathnamehash();
535     }
537     $fileurl = moodle_url::make_pluginfile_url($user_context->id, 'badges', 'userbadge', $badge->id, '/', $hash, true);
538     return $fileurl;
541 /**
542  * Returns external backpack settings and badges from this backpack.
543  *
544  * This function first checks if badges for the user are cached and
545  * tries to retrieve them from the cache. Otherwise, badges are obtained
546  * through curl request to the backpack.
547  *
548  * @param int $userid Backpack user ID.
549  * @param boolean $refresh Refresh badges collection in cache.
550  * @return null|object Returns null is there is no backpack or object with backpack settings.
551  */
552 function get_backpack_settings($userid, $refresh = false) {
553     global $DB;
555     // Try to get badges from cache first.
556     $badgescache = cache::make('core', 'externalbadges');
557     $out = $badgescache->get($userid);
558     if ($out !== false && !$refresh) {
559         return $out;
560     }
561     // Get badges through curl request to the backpack.
562     $record = $DB->get_record('badge_backpack', array('userid' => $userid));
563     if ($record) {
564         $sitebackpack = badges_get_site_backpack($record->externalbackpackid);
565         $backpack = new \core_badges\backpack_api($sitebackpack, $record);
566         $out = new stdClass();
567         $out->backpackid = $sitebackpack->id;
569         if ($collections = $DB->get_records('badge_external', array('backpackid' => $record->id))) {
570             $out->totalcollections = count($collections);
571             $out->totalbadges = 0;
572             $out->badges = array();
573             foreach ($collections as $collection) {
574                 $badges = $backpack->get_badges($collection, true);
575                 if (!empty($badges)) {
576                     $out->badges = array_merge($out->badges, $badges);
577                     $out->totalbadges += count($badges);
578                 } else {
579                     $out->badges = array_merge($out->badges, array());
580                 }
581             }
582         } else {
583             $out->totalbadges = 0;
584             $out->totalcollections = 0;
585         }
587         $badgescache->set($userid, $out);
588         return $out;
589     }
591     return null;
594 /**
595  * Download all user badges in zip archive.
596  *
597  * @param int $userid ID of badge owner.
598  */
599 function badges_download($userid) {
600     global $CFG, $DB;
601     $context = context_user::instance($userid);
602     $records = $DB->get_records('badge_issued', array('userid' => $userid));
604     // Get list of files to download.
605     $fs = get_file_storage();
606     $filelist = array();
607     foreach ($records as $issued) {
608         $badge = new badge($issued->badgeid);
609         // Need to make image name user-readable and unique using filename safe characters.
610         $name =  $badge->name . ' ' . userdate($issued->dateissued, '%d %b %Y') . ' ' . hash('crc32', $badge->id);
611         $name = str_replace(' ', '_', $name);
612         $name = clean_param($name, PARAM_FILE);
613         if ($file = $fs->get_file($context->id, 'badges', 'userbadge', $issued->badgeid, '/', $issued->uniquehash . '.png')) {
614             $filelist[$name . '.png'] = $file;
615         }
616     }
618     // Zip files and sent them to a user.
619     $tempzip = tempnam($CFG->tempdir.'/', 'mybadges');
620     $zipper = new zip_packer();
621     if ($zipper->archive_to_pathname($filelist, $tempzip)) {
622         send_temp_file($tempzip, 'badges.zip');
623     } else {
624         debugging("Problems with archiving the files.", DEBUG_DEVELOPER);
625         die;
626     }
629 /**
630  * Checks if badges can be pushed to external backpack.
631  *
632  * @return string Code of backpack accessibility status.
633  */
634 function badges_check_backpack_accessibility() {
635     if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) {
636         // For behat sites, do not poll the remote badge site.
637         // Behat sites should not be available, but we should pretend as though they are.
638         return 'available';
639     }
641     if (badges_open_badges_backpack_api() == OPEN_BADGES_V2) {
642         return 'available';
643     }
645     global $CFG;
646     include_once $CFG->libdir . '/filelib.php';
648     // Using fake assertion url to check whether backpack can access the web site.
649     $fakeassertion = new moodle_url('/badges/assertion.php', array('b' => 'abcd1234567890'));
651     // Curl request to backpack baker.
652     $curl = new curl();
653     $options = array(
654         'FRESH_CONNECT' => true,
655         'RETURNTRANSFER' => true,
656         'HEADER' => 0,
657         'CONNECTTIMEOUT' => 2,
658     );
659     // BADGE_BACKPACKURL and the "baker" API is deprecated and should never be used in future.
660     $location = BADGE_BACKPACKURL . '/baker';
661     $out = $curl->get($location, array('assertion' => $fakeassertion->out(false)), $options);
663     $data = json_decode($out);
664     if (!empty($curl->error)) {
665         return 'curl-request-timeout';
666     } else {
667         if (isset($data->code) && $data->code == 'http-unreachable') {
668             return 'http-unreachable';
669         } else {
670             return 'available';
671         }
672     }
674     return false;
677 /**
678  * Checks if user has external backpack connected.
679  *
680  * @param int $userid ID of a user.
681  * @return bool True|False whether backpack connection exists.
682  */
683 function badges_user_has_backpack($userid) {
684     global $DB;
685     return $DB->record_exists('badge_backpack', array('userid' => $userid));
688 /**
689  * Handles what happens to the course badges when a course is deleted.
690  *
691  * @param int $courseid course ID.
692  * @return void.
693  */
694 function badges_handle_course_deletion($courseid) {
695     global $CFG, $DB;
696     include_once $CFG->libdir . '/filelib.php';
698     $systemcontext = context_system::instance();
699     $coursecontext = context_course::instance($courseid);
700     $fs = get_file_storage();
702     // Move badges images to the system context.
703     $fs->move_area_files_to_new_context($coursecontext->id, $systemcontext->id, 'badges', 'badgeimage');
705     // Get all course badges.
706     $badges = $DB->get_records('badge', array('type' => BADGE_TYPE_COURSE, 'courseid' => $courseid));
707     foreach ($badges as $badge) {
708         // Archive badges in this course.
709         $toupdate = new stdClass();
710         $toupdate->id = $badge->id;
711         $toupdate->type = BADGE_TYPE_SITE;
712         $toupdate->courseid = null;
713         $toupdate->status = BADGE_STATUS_ARCHIVED;
714         $DB->update_record('badge', $toupdate);
715     }
718 /**
719  * Loads JS files required for backpack support.
720  *
721  * @uses   $CFG, $PAGE
722  * @return void
723  */
724 function badges_setup_backpack_js() {
725     global $CFG, $PAGE;
726     if (!empty($CFG->badges_allowexternalbackpack)) {
727         if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
728             $PAGE->requires->string_for_js('error:backpackproblem', 'badges');
729             // The issuer.js API is deprecated and should not be used in future.
730             $PAGE->requires->js(new moodle_url(BADGE_BACKPACKURL . '/issuer.js'), true);
731             // The backpack.js file is deprecated and should not be used in future.
732             $PAGE->requires->js('/badges/backpack.js', true);
733         }
734     }
737 /**
738  * No js files are required for backpack support.
739  * This only exists to directly support the custom V1 backpack api.
740  *
741  * @param boolean $checksite Call check site function.
742  * @return void
743  */
744 function badges_local_backpack_js($checksite = false) {
745     global $CFG, $PAGE;
746     if (!empty($CFG->badges_allowexternalbackpack)) {
747         if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
748             $PAGE->requires->js('/badges/backpack.js', true);
749             if ($checksite) {
750                 $PAGE->requires->js_init_call('check_site_access', null, false);
751             }
752         }
753     }
756 /**
757  * Create the backpack with this data.
758  *
759  * @param stdClass $data The new backpack data.
760  * @return boolean
761  */
762 function badges_create_site_backpack($data) {
763     global $DB;
764     $context = context_system::instance();
765     require_capability('moodle/badges:manageglobalsettings', $context);
767     $count = $DB->count_records('badge_external_backpack');
769     $backpack = new stdClass();
770     $backpack->apiversion = $data->apiversion;
771     $backpack->backpackapiurl = $data->backpackapiurl;
772     $backpack->backpackweburl = $data->backpackweburl;
773     $backpack->sortorder = $count;
774     $DB->insert_record('badge_external_backpack', $backpack);
775     return true;
778 /**
779  * Update the backpack with this id.
780  *
781  * @param integer $id The backpack to edit
782  * @param stdClass $data The new backpack data.
783  * @return boolean
784  */
785 function badges_update_site_backpack($id, $data) {
786     global $DB;
787     $context = context_system::instance();
788     require_capability('moodle/badges:manageglobalsettings', $context);
790     if ($backpack = badges_get_site_backpack($id)) {
791         $backpack = new stdClass();
792         $backpack->id = $id;
793         $backpack->apiversion = $data->apiversion;
794         $backpack->backpackweburl = $data->backpackweburl;
795         $backpack->backpackapiurl = $data->backpackapiurl;
796         $backpack->password = $data->password;
797         $DB->update_record('badge_external_backpack', $backpack);
798         return true;
799     }
800     return false;
803 /**
804  * Is any backpack enabled that supports open badges V1?
805  * @return boolean
806  */
807 function badges_open_badges_backpack_api() {
808     global $CFG;
810     $backpack = badges_get_site_backpack($CFG->badges_site_backpack);
812     if (empty($backpack->apiversion)) {
813         return OPEN_BADGES_V2;
814     }
815     return $backpack->apiversion;
818 /**
819  * Get a site backpacks by id or url.
820  *
821  * @param int $id The backpack id.
822  * @return array(stdClass)
823  */
824 function badges_get_site_backpack($id) {
825     global $DB;
827     return $DB->get_record('badge_external_backpack', ['id' => $id]);
830 /**
831  * List the backpacks at site level.
832  *
833  * @return array(stdClass)
834  */
835 function badges_get_site_backpacks() {
836     global $DB, $CFG;
838     $all = $DB->get_records('badge_external_backpack');
840     foreach ($all as $key => $bp) {
841         if ($bp->id == $CFG->badges_site_backpack) {
842             $all[$key]->sitebackpack = true;
843         } else {
844             $all[$key]->sitebackpack = false;
845         }
846     }
847     return $all;
850 /**
851  * List the supported badges api versions.
852  *
853  * @return array(version)
854  */
855 function badges_get_badge_api_versions() {
856     return [
857         OPEN_BADGES_V1 => get_string('openbadgesv1', 'badges'),
858         OPEN_BADGES_V2 => get_string('openbadgesv2', 'badges')
859     ];
862 /**
863  * Get the default issuer for a badge from this site.
864  *
865  * @return array
866  */
867 function badges_get_default_issuer() {
868     global $CFG, $SITE;
870     $issuer = array();
871     $issuerurl = new moodle_url('/badges/issuer.php');
872     $issuer['name'] = $CFG->badges_defaultissuername;
873     if (empty($issuer['name'])) {
874         $issuer['name'] = $SITE->fullname ? $SITE->fullname : $SITE->shortname;
875     }
876     $issuer['url'] = $issuerurl->out(false);
877     $issuer['email'] = $CFG->badges_defaultissuercontact;
878     $issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
879     $issuer['id'] = $issuerurl->out(false);
880     $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
881     return $issuer;
884 /**
885  * Disconnect from the user backpack by deleting the user preferences.
886  *
887  * @param integer $userid The user to diconnect.
888  * @return boolean
889  */
890 function badges_disconnect_user_backpack($userid) {
891     global $USER;
893     // We can only change backpack settings for our own real backpack.
894     if ($USER->id != $userid ||
895             \core\session\manager::is_loggedinas()) {
897         return false;
898     }
900     unset_user_preference('badges_email_verify_secret');
901     unset_user_preference('badges_email_verify_address');
902     unset_user_preference('badges_email_verify_backpackid');
903     unset_user_preference('badges_email_verify_password');
905     return true;
908 /**
909  * Used to remember which objects we connected with a backpack before.
910  *
911  * @param integer $sitebackpackid The site backpack to connect to.
912  * @param string $type The type of this remote object.
913  * @param string $internalid The id for this object on the Moodle site.
914  * @return mixed The id or false if it doesn't exist.
915  */
916 function badges_external_get_mapping($sitebackpackid, $type, $internalid) {
917     global $DB;
918     // Return externalid if it exists.
919     $params = [
920         'sitebackpackid' => $sitebackpackid,
921         'type' => $type,
922         'internalid' => $internalid
923     ];
925     $record = $DB->get_record('badge_external_identifier', $params, 'externalid', IGNORE_MISSING);
926     if ($record) {
927         return $record->externalid;
928     }
929     return false;
932 /**
933  * Save the info about which objects we connected with a backpack before.
934  *
935  * @param integer $sitebackpackid The site backpack to connect to.
936  * @param string $type The type of this remote object.
937  * @param string $internalid The id for this object on the Moodle site.
938  * @param string $externalid The id of this object on the remote site.
939  * @return boolean
940  */
941 function badges_external_create_mapping($sitebackpackid, $type, $internalid, $externalid) {
942     global $DB;
944     $params = [
945         'sitebackpackid' => $sitebackpackid,
946         'type' => $type,
947         'internalid' => $internalid,
948         'externalid' => $externalid
949     ];
951     return $DB->insert_record('badge_external_identifier', $params);
954 /**
955  * Delete all external mapping information for a backpack.
956  *
957  * @param integer $sitebackpackid The site backpack to connect to.
958  * @return boolean
959  */
960 function badges_external_delete_mappings($sitebackpackid) {
961     global $DB;
963     $params = ['sitebackpackid' => $sitebackpackid];
965     return $DB->delete_records('badge_external_identifier', $params);
968 /**
969  * Delete a specific external mapping information for a backpack.
970  *
971  * @param integer $sitebackpackid The site backpack to connect to.
972  * @param string $type The type of this remote object.
973  * @param string $internalid The id for this object on the Moodle site.
974  * @return boolean
975  */
976 function badges_external_delete_mapping($sitebackpackid, $type, $internalid) {
977     global $DB;
979     $params = [
980         'sitebackpackid' => $sitebackpackid,
981         'type' => $type,
982         'internalid' => $internalid
983     ];
985     $DB->delete_record('badge_external_identifier', $params);
988 /**
989  * Create and send a verification email to the email address supplied.
990  *
991  * Since we're not sending this email to a user, email_to_user can't be used
992  * but this function borrows largely the code from that process.
993  *
994  * @param string $email the email address to send the verification email to.
995  * @param int $backpackid the id of the backpack to connect to
996  * @param string $backpackpassword the user entered password to connect to this backpack
997  * @return true if the email was sent successfully, false otherwise.
998  */
999 function badges_send_verification_email($email, $backpackid, $backpackpassword) {
1000     global $DB, $USER;
1002     // Store a user secret (badges_email_verify_secret) and the address (badges_email_verify_address) as users prefs.
1003     // The address will be used by edit_backpack_form for display during verification and to facilitate the resending
1004     // of verification emails to said address.
1005     $secret = random_string(15);
1006     set_user_preference('badges_email_verify_secret', $secret);
1007     set_user_preference('badges_email_verify_address', $email);
1008     set_user_preference('badges_email_verify_backpackid', $backpackid);
1009     set_user_preference('badges_email_verify_password', $backpackpassword);
1011     // To, from.
1012     $tempuser = $DB->get_record('user', array('id' => $USER->id), '*', MUST_EXIST);
1013     $tempuser->email = $email;
1014     $noreplyuser = core_user::get_noreply_user();
1016     // Generate the verification email body.
1017     $verificationurl = '/badges/backpackemailverify.php';
1018     $verificationurl = new moodle_url($verificationurl);
1019     $verificationpath = $verificationurl->out(false);
1021     $site = get_site();
1022     $args = new stdClass();
1023     $args->link = $verificationpath . '?data='. $secret;
1024     $args->sitename = $site->fullname;
1025     $args->admin = generate_email_signoff();
1027     $messagesubject = get_string('backpackemailverifyemailsubject', 'badges', $site->fullname);
1028     $messagetext = get_string('backpackemailverifyemailbody', 'badges', $args);
1029     $messagehtml = text_to_html($messagetext, false, false, true);
1031     return email_to_user($tempuser, $noreplyuser, $messagesubject, $messagetext, $messagehtml);
1034 /**
1035  * Return all the enabled criteria types for this site.
1036  *
1037  * @param boolean $enabled
1038  * @return array
1039  */
1040 function badges_list_criteria($enabled = true) {
1041     global $CFG;
1043     $types = array(
1044         BADGE_CRITERIA_TYPE_OVERALL    => 'overall',
1045         BADGE_CRITERIA_TYPE_ACTIVITY   => 'activity',
1046         BADGE_CRITERIA_TYPE_MANUAL     => 'manual',
1047         BADGE_CRITERIA_TYPE_SOCIAL     => 'social',
1048         BADGE_CRITERIA_TYPE_COURSE     => 'course',
1049         BADGE_CRITERIA_TYPE_COURSESET  => 'courseset',
1050         BADGE_CRITERIA_TYPE_PROFILE    => 'profile',
1051         BADGE_CRITERIA_TYPE_BADGE      => 'badge',
1052         BADGE_CRITERIA_TYPE_COHORT     => 'cohort',
1053         BADGE_CRITERIA_TYPE_COMPETENCY => 'competency',
1054     );
1055     if ($enabled) {
1056         foreach ($types as $key => $type) {
1057             $class = 'award_criteria_' . $type;
1058             $file = $CFG->dirroot . '/badges/criteria/' . $class . '.php';
1059             if (file_exists($file)) {
1060                 require_once($file);
1062                 if (!$class::is_enabled()) {
1063                     unset($types[$key]);
1064                 }
1065             }
1066         }
1067     }
1068     return $types;
1071 /**
1072  * Check if any badge has records for competencies.
1073  *
1074  * @param array $competencyids Array of competencies ids.
1075  * @return boolean Return true if competencies were found in any badge.
1076  */
1077 function badge_award_criteria_competency_has_records_for_competencies($competencyids) {
1078     global $DB;
1080     list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED);
1082     $sql = "SELECT DISTINCT bc.badgeid
1083                 FROM {badge_criteria} bc
1084                 JOIN {badge_criteria_param} bcp ON bc.id = bcp.critid
1085                 WHERE bc.criteriatype = :criteriatype AND bcp.value $insql";
1086     $params['criteriatype'] = BADGE_CRITERIA_TYPE_COMPETENCY;
1088     return $DB->record_exists_sql($sql, $params);
1091 /**
1092  * Creates single message for all notification and sends it out
1093  *
1094  * @param object $badge A badge which is notified about.
1095  */
1096 function badge_assemble_notification(stdClass $badge) {
1097     global $DB;
1099     $userfrom = core_user::get_noreply_user();
1100     $userfrom->maildisplay = true;
1102     if ($msgs = $DB->get_records_select('badge_issued', 'issuernotified IS NULL AND badgeid = ?', array($badge->id))) {
1103         // Get badge creator.
1104         $creator = $DB->get_record('user', array('id' => $badge->creator), '*', MUST_EXIST);
1105         $creatorsubject = get_string('creatorsubject', 'badges', $badge->name);
1106         $creatormessage = '';
1108         // Put all messages in one digest.
1109         foreach ($msgs as $msg) {
1110             $issuedlink = html_writer::link(new moodle_url('/badges/badge.php', array('hash' => $msg->uniquehash)), $badge->name);
1111             $recipient = $DB->get_record('user', array('id' => $msg->userid), '*', MUST_EXIST);
1113             $a = new stdClass();
1114             $a->user = fullname($recipient);
1115             $a->link = $issuedlink;
1116             $creatormessage .= get_string('creatorbody', 'badges', $a);
1117             $DB->set_field('badge_issued', 'issuernotified', time(), array('badgeid' => $msg->badgeid, 'userid' => $msg->userid));
1118         }
1120         // Create a message object.
1121         $eventdata = new \core\message\message();
1122         $eventdata->courseid          = SITEID;
1123         $eventdata->component         = 'moodle';
1124         $eventdata->name              = 'badgecreatornotice';
1125         $eventdata->userfrom          = $userfrom;
1126         $eventdata->userto            = $creator;
1127         $eventdata->notification      = 1;
1128         $eventdata->subject           = $creatorsubject;
1129         $eventdata->fullmessage       = format_text_email($creatormessage, FORMAT_HTML);
1130         $eventdata->fullmessageformat = FORMAT_PLAIN;
1131         $eventdata->fullmessagehtml   = $creatormessage;
1132         $eventdata->smallmessage      = $creatorsubject;
1134         message_send($eventdata);
1135     }
1138 /**
1139  * Attempt to authenticate with the site backpack credentials and return an error
1140  * if the authentication fails. If external backpacks are not enabled, this will
1141  * not perform any test.
1142  *
1143  * @return string
1144  */
1145 function badges_verify_site_backpack() {
1146     global $OUTPUT, $CFG;
1148     if (empty($CFG->badges_allowexternalbackpack)) {
1149         return '';
1150     }
1152     $backpack = badges_get_site_backpack($CFG->badges_site_backpack);
1154     if (empty($backpack->apiversion) || ($backpack->apiversion == OPEN_BADGES_V2)) {
1155         $backpackapi = new \core_badges\backpack_api($backpack);
1157         // Clear any cached access tokens in the session.
1158         $backpackapi->clear_system_user_session();
1160         // Now attempt a login with these credentials.
1161         $result = $backpackapi->authenticate();
1162         if (empty($result) || !empty($result->error)) {
1163             $warning = $backpackapi->get_authentication_error();
1165             $params = ['id' => $backpack->id, 'action' => 'edit'];
1166             $backpackurl = (new moodle_url('/badges/backpacks.php', $params))->out(false);
1168             $message = get_string('sitebackpackwarning', 'badges', ['url' => $backpackurl, 'warning' => $warning]);
1169             $icon = $OUTPUT->pix_icon('i/warning', get_string('warning', 'moodle'));
1170             return $OUTPUT->container($icon . $message, 'text-error');
1171         }
1172     }
1173     return '';