Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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/>.
16
 
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
 */
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/* Include required award criteria library. */
30
require_once($CFG->dirroot . '/badges/criteria/award_criteria.php');
31
 
32
/* Include required user badge exporter */
33
use core_badges\external\user_badge_exporter;
1441 ariadna 34
/* Include required badge class exporter */
35
use core_badges\external\badgeclass_exporter;
1 efrain 36
 
37
/*
38
 * Number of records per page.
39
*/
40
define('BADGE_PERPAGE', 50);
41
 
42
/*
43
 * Badge award criteria aggregation method.
44
 */
45
define('BADGE_CRITERIA_AGGREGATION_ALL', 1);
46
 
47
/*
48
 * Badge award criteria aggregation method.
49
 */
50
define('BADGE_CRITERIA_AGGREGATION_ANY', 2);
51
 
52
/*
53
 * Inactive badge means that this badge cannot be earned and has not been awarded
54
 * yet. Its award criteria can be changed.
55
 */
56
define('BADGE_STATUS_INACTIVE', 0);
57
 
58
/*
59
 * Active badge means that this badge can we earned, but it has not been awarded
60
 * yet. Can be deactivated for the purpose of changing its criteria.
61
 */
62
define('BADGE_STATUS_ACTIVE', 1);
63
 
64
/*
65
 * Inactive badge can no longer be earned, but it has been awarded in the past and
66
 * therefore its criteria cannot be changed.
67
 */
68
define('BADGE_STATUS_INACTIVE_LOCKED', 2);
69
 
70
/*
71
 * Active badge means that it can be earned and has already been awarded to users.
72
 * Its criteria cannot be changed any more.
73
 */
74
define('BADGE_STATUS_ACTIVE_LOCKED', 3);
75
 
76
/*
77
 * Archived badge is considered deleted and can no longer be earned and is not
78
 * displayed in the list of all badges.
79
 */
80
define('BADGE_STATUS_ARCHIVED', 4);
81
 
82
/*
83
 * Badge type for site badges.
84
 */
85
define('BADGE_TYPE_SITE', 1);
86
 
87
/*
88
 * Badge type for course badges.
89
 */
90
define('BADGE_TYPE_COURSE', 2);
91
 
92
/*
93
 * Badge messaging schedule options.
94
 */
95
define('BADGE_MESSAGE_NEVER', 0);
96
define('BADGE_MESSAGE_ALWAYS', 1);
97
define('BADGE_MESSAGE_DAILY', 2);
98
define('BADGE_MESSAGE_WEEKLY', 3);
99
define('BADGE_MESSAGE_MONTHLY', 4);
100
 
101
/*
102
 * URL of backpack. Custom ones can be added.
103
 */
104
define('BADGRIO_BACKPACKAPIURL', 'https://api.badgr.io/v2');
105
define('BADGRIO_BACKPACKWEBURL', 'https://badgr.io');
106
 
1441 ariadna 107
/**
108
 * @deprecated since Moodle 4.5.
109
 * @todo Final deprecation in Moodle 6.0. See MDL-82332.
1 efrain 110
 */
1441 ariadna 111
define('OPEN_BADGES_V1', 1);
1 efrain 112
 
113
/*
114
 * Open Badges specifications.
115
 */
116
define('OPEN_BADGES_V2', 2);
117
define('OPEN_BADGES_V2P1', 2.1);
118
 
119
/*
120
 * Only use for Open Badges 2.0 specification
121
 */
122
define('OPEN_BADGES_V2_CONTEXT', 'https://w3id.org/openbadges/v2');
123
define('OPEN_BADGES_V2_TYPE_ASSERTION', 'Assertion');
124
define('OPEN_BADGES_V2_TYPE_BADGE', 'BadgeClass');
125
define('OPEN_BADGES_V2_TYPE_ISSUER', 'Issuer');
126
define('OPEN_BADGES_V2_TYPE_ENDORSEMENT', 'Endorsement');
127
 
128
define('BACKPACK_MOVE_UP', -1);
129
define('BACKPACK_MOVE_DOWN', 1);
130
 
131
// Global badge class has been moved to the component namespace.
132
class_alias('\core_badges\badge', 'badge');
133
 
1441 ariadna 134
use core_badges\png_metadata_handler;
135
 
1 efrain 136
/**
137
 * Sends notifications to users about awarded badges.
138
 *
139
 * @param \core_badges\badge $badge Badge that was issued
140
 * @param int $userid Recipient ID
141
 * @param string $issued Unique hash of an issued badge
142
 * @param string $filepathhash File path hash of an issued badge for attachments
143
 */
144
function badges_notify_badge_award(badge $badge, $userid, $issued, $filepathhash) {
145
    global $CFG, $DB;
146
 
147
    $admin = get_admin();
148
    $userfrom = new stdClass();
149
    $userfrom->id = $admin->id;
150
    $userfrom->email = !empty($CFG->badges_defaultissuercontact) ? $CFG->badges_defaultissuercontact : $admin->email;
151
    foreach (\core_user\fields::get_name_fields() as $addname) {
152
        $userfrom->$addname = !empty($CFG->badges_defaultissuername) ? '' : $admin->$addname;
153
    }
154
    $userfrom->firstname = !empty($CFG->badges_defaultissuername) ? $CFG->badges_defaultissuername : $admin->firstname;
155
    $userfrom->maildisplay = true;
156
 
157
    $badgeurl = new moodle_url('/badges/badge.php', ['hash' => $issued]);
158
    $issuedlink = html_writer::link($badgeurl, $badge->name);
159
    $userto = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
160
 
161
    $params = new stdClass();
162
    $params->badgename = $badge->name;
163
    $params->username = fullname($userto);
164
    $params->badgelink = $issuedlink;
165
    $message = badge_message_from_template($badge->message, $params);
166
    $plaintext = html_to_text($message);
167
 
168
    // Notify recipient.
169
    $eventdata = new \core\message\message();
170
    $eventdata->courseid          = is_null($badge->courseid) ? SITEID : $badge->courseid; // Profile/site come with no courseid.
171
    $eventdata->component         = 'moodle';
172
    $eventdata->name              = 'badgerecipientnotice';
173
    $eventdata->userfrom          = $userfrom;
174
    $eventdata->userto            = $userto;
175
    $eventdata->notification      = 1;
176
    $eventdata->contexturl        = $badgeurl;
177
    $eventdata->contexturlname    = $badge->name;
178
    $eventdata->subject           = $badge->messagesubject;
179
    $eventdata->fullmessage       = $plaintext;
180
    $eventdata->fullmessageformat = FORMAT_HTML;
181
    $eventdata->fullmessagehtml   = $message;
182
    $eventdata->smallmessage      = '';
183
    $eventdata->customdata        = [
184
        'notificationiconurl' => moodle_url::make_pluginfile_url(
185
            $badge->get_context()->id, 'badges', 'badgeimage', $badge->id, '/', 'f1')->out(),
186
        'hash' => $issued,
187
    ];
188
 
189
    // Attach badge image if possible.
190
    if (!empty($CFG->allowattachments) && $badge->attachment && is_string($filepathhash)) {
191
        $fs = get_file_storage();
192
        $file = $fs->get_file_by_hash($filepathhash);
193
        $eventdata->attachment = $file;
194
        $eventdata->attachname = str_replace(' ', '_', $badge->name) . ".png";
195
 
196
        message_send($eventdata);
197
    } else {
198
        message_send($eventdata);
199
    }
200
 
201
    // Notify badge creator about the award if they receive notifications every time.
202
    if ($badge->notification == 1) {
203
        $userfrom = core_user::get_noreply_user();
204
        $userfrom->maildisplay = true;
205
 
206
        $creator = $DB->get_record('user', array('id' => $badge->usercreated), '*', MUST_EXIST);
207
        $a = new stdClass();
208
        $a->user = fullname($userto);
209
        $a->link = $issuedlink;
210
        $creatormessage = get_string('creatorbody', 'badges', $a);
211
        $creatorsubject = get_string('creatorsubject', 'badges', $badge->name);
212
 
213
        $eventdata = new \core\message\message();
214
        $eventdata->courseid          = $badge->courseid;
215
        $eventdata->component         = 'moodle';
216
        $eventdata->name              = 'badgecreatornotice';
217
        $eventdata->userfrom          = $userfrom;
218
        $eventdata->userto            = $creator;
219
        $eventdata->notification      = 1;
220
        $eventdata->contexturl        = $badgeurl;
221
        $eventdata->contexturlname    = $badge->name;
222
        $eventdata->subject           = $creatorsubject;
223
        $eventdata->fullmessage       = html_to_text($creatormessage);
224
        $eventdata->fullmessageformat = FORMAT_HTML;
225
        $eventdata->fullmessagehtml   = $creatormessage;
226
        $eventdata->smallmessage      = '';
227
        $eventdata->customdata        = [
228
            'notificationiconurl' => moodle_url::make_pluginfile_url(
229
                $badge->get_context()->id, 'badges', 'badgeimage', $badge->id, '/', 'f1')->out(),
230
            'hash' => $issued,
231
        ];
232
 
233
        message_send($eventdata);
234
        $DB->set_field('badge_issued', 'issuernotified', time(), array('badgeid' => $badge->id, 'userid' => $userid));
235
    }
236
}
237
 
238
/**
239
 * Caclulates date for the next message digest to badge creators.
240
 *
241
 * @param int $schedule Type of message schedule BADGE_MESSAGE_DAILY|BADGE_MESSAGE_WEEKLY|BADGE_MESSAGE_MONTHLY.
242
 * @return int Timestamp for next cron
243
 */
244
function badges_calculate_message_schedule($schedule) {
245
    $nextcron = 0;
246
 
247
    switch ($schedule) {
248
        case BADGE_MESSAGE_DAILY:
249
            $tomorrow = new DateTime("1 day", core_date::get_server_timezone_object());
250
            $nextcron = $tomorrow->getTimestamp();
251
            break;
252
        case BADGE_MESSAGE_WEEKLY:
253
            $nextweek = new DateTime("1 week", core_date::get_server_timezone_object());
254
            $nextcron = $nextweek->getTimestamp();
255
            break;
256
        case BADGE_MESSAGE_MONTHLY:
257
            $nextmonth = new DateTime("1 month", core_date::get_server_timezone_object());
258
            $nextcron = $nextmonth->getTimestamp();
259
            break;
260
    }
261
 
262
    return $nextcron;
263
}
264
 
265
/**
266
 * Replaces variables in a message template and returns text ready to be emailed to a user.
267
 *
268
 * @param string $message Message body.
269
 * @return string Message with replaced values
270
 */
271
function badge_message_from_template($message, $params) {
272
    $msg = $message;
273
    foreach ($params as $key => $value) {
274
        $msg = str_replace("%$key%", $value, $msg);
275
    }
276
 
277
    return $msg;
278
}
279
 
280
/**
281
 * Get all badges.
282
 *
283
 * @param int Type of badges to return
284
 * @param int Course ID for course badges
285
 * @param string $sort An SQL field to sort by
286
 * @param string $dir The sort direction ASC|DESC
287
 * @param int $page The page or records to return
288
 * @param int $perpage The number of records to return per page
289
 * @param int $user User specific search
290
 * @return array $badge Array of records matching criteria
291
 */
292
function badges_get_badges($type, $courseid = 0, $sort = '', $dir = '', $page = 0, $perpage = BADGE_PERPAGE, $user = 0) {
293
    global $DB;
294
    $records = array();
295
    $params = array();
296
    $where = "b.status != :deleted AND b.type = :type ";
297
    $params['deleted'] = BADGE_STATUS_ARCHIVED;
298
 
299
    $userfields = array('b.id, b.name, b.status');
300
    $usersql = "";
301
    if ($user != 0) {
302
        $userfields[] = 'bi.dateissued';
303
        $userfields[] = 'bi.uniquehash';
304
        $usersql = " LEFT JOIN {badge_issued} bi ON b.id = bi.badgeid AND bi.userid = :userid ";
305
        $params['userid'] = $user;
306
        $where .= " AND (b.status = 1 OR b.status = 3) ";
307
    }
308
    $fields = implode(', ', $userfields);
309
 
310
    if ($courseid != 0 ) {
311
        $where .= "AND b.courseid = :courseid ";
312
        $params['courseid'] = $courseid;
313
    }
314
 
315
    $sorting = (($sort != '' && $dir != '') ? 'ORDER BY ' . $sort . ' ' . $dir : '');
316
    $params['type'] = $type;
317
 
318
    $sql = "SELECT $fields FROM {badge} b $usersql WHERE $where $sorting";
319
    $records = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
320
 
321
    $badges = array();
322
    foreach ($records as $r) {
323
        $badge = new badge($r->id);
324
        $badges[$r->id] = $badge;
325
        if ($user != 0) {
326
            $badges[$r->id]->dateissued = $r->dateissued;
327
            $badges[$r->id]->uniquehash = $r->uniquehash;
328
        } else {
329
            $badges[$r->id]->awards = $DB->count_records_sql('SELECT COUNT(b.userid)
330
                                        FROM {badge_issued} b INNER JOIN {user} u ON b.userid = u.id
331
                                        WHERE b.badgeid = :badgeid AND u.deleted = 0', array('badgeid' => $badge->id));
332
            $badges[$r->id]->statstring = $badge->get_status_name();
333
        }
334
    }
335
    return $badges;
336
}
337
 
338
/**
339
 * Get badges for a specific user.
340
 *
341
 * @param int $userid User ID
342
 * @param int $courseid Badges earned by a user in a specific course
343
 * @param int $page The page or records to return
344
 * @param int $perpage The number of records to return per page
345
 * @param string $search A simple string to search for
346
 * @param bool $onlypublic Return only public badges
347
 * @return array of badges ordered by decreasing date of issue
348
 */
349
function badges_get_user_badges($userid, $courseid = 0, $page = 0, $perpage = 0, $search = '', $onlypublic = false) {
350
    global $CFG, $DB;
351
 
352
    $params = array(
353
        'userid' => $userid
354
    );
355
    $sql = 'SELECT
356
                bi.uniquehash,
357
                bi.dateissued,
358
                bi.dateexpire,
359
                bi.id as issuedid,
360
                bi.visible,
361
                u.email,
362
                b.*
363
            FROM
364
                {badge} b,
365
                {badge_issued} bi,
366
                {user} u
367
            WHERE b.id = bi.badgeid
368
                AND u.id = bi.userid
369
                AND bi.userid = :userid';
370
 
371
    if (!empty($search)) {
372
        $sql .= ' AND (' . $DB->sql_like('b.name', ':search', false) . ') ';
373
        $params['search'] = '%'.$DB->sql_like_escape($search).'%';
374
    }
375
    if ($onlypublic) {
376
        $sql .= ' AND (bi.visible = 1) ';
377
    }
378
 
379
    if (empty($CFG->badges_allowcoursebadges)) {
380
        $sql .= ' AND b.courseid IS NULL';
381
    } else if ($courseid != 0) {
382
        $sql .= ' AND (b.courseid = :courseid) ';
383
        $params['courseid'] = $courseid;
384
    }
385
    $sql .= ' ORDER BY bi.dateissued DESC';
386
    $badges = $DB->get_records_sql($sql, $params, $page * $perpage, $perpage);
387
 
388
    return $badges;
389
}
390
 
391
/**
392
 * Get badge by hash.
393
 *
394
 * @param string $hash
395
 * @return object|bool
396
 */
397
function badges_get_badge_by_hash(string $hash): object|bool {
398
    global $DB;
399
    $sql = 'SELECT
400
                bi.uniquehash,
401
                bi.dateissued,
402
                bi.userid,
403
                bi.dateexpire,
404
                bi.id as issuedid,
405
                bi.visible,
406
                u.email,
407
                b.*
408
            FROM
409
                {badge} b,
410
                {badge_issued} bi,
411
                {user} u
412
            WHERE b.id = bi.badgeid
413
                AND u.id = bi.userid
414
                AND ' . $DB->sql_compare_text('bi.uniquehash', 40) . ' = ' . $DB->sql_compare_text(':hash', 40);
415
    $badge = $DB->get_record_sql($sql, ['hash' => $hash], IGNORE_MISSING);
416
    return $badge;
417
}
418
 
419
/**
420
 * Update badge instance to external functions.
421
 *
422
 * @param stdClass $badge
423
 * @param stdClass $user
424
 * @return object
425
 */
426
function badges_prepare_badge_for_external(stdClass $badge, stdClass $user): object {
1441 ariadna 427
    global $PAGE, $SITE, $USER;
1 efrain 428
    if ($badge->type == BADGE_TYPE_SITE) {
429
        $context = context_system::instance();
430
    } else {
431
        $context = context_course::instance($badge->courseid);
432
    }
433
    $canconfiguredetails = has_capability('moodle/badges:configuredetails', $context);
434
    // If the user is viewing another user's badge and doesn't have the right capability return only part of the data.
435
    if ($USER->id != $user->id && !$canconfiguredetails) {
436
        $badge = (object) [
437
            'id'            => $badge->id,
438
            'name'          => $badge->name,
439
            'type'          => $badge->type,
440
            'description'   => $badge->description,
441
            'issuername'    => $badge->issuername,
442
            'issuerurl'     => $badge->issuerurl,
443
            'issuercontact' => $badge->issuercontact,
444
            'uniquehash'    => $badge->uniquehash,
445
            'dateissued'    => $badge->dateissued,
446
            'dateexpire'    => $badge->dateexpire,
447
            'version'       => $badge->version,
448
            'language'      => $badge->language,
449
            'imagecaption'     => $badge->imagecaption,
450
        ];
451
    }
452
 
1441 ariadna 453
    // Course.
454
    if ($badge->type == BADGE_TYPE_COURSE) {
455
        $course = get_course($context->instanceid);
456
        $badge->coursefullname = \core_external\util::format_string($course->fullname, $context);
457
    }
458
 
459
    // Recipient (the badge was awarded to this person).
460
    $badge->recipientid = $user->id;
461
    if ($user->deleted) {
462
        $strdata = new stdClass();
463
        $strdata->user = fullname($user);
464
        $strdata->site = format_string($SITE->fullname, true, ['context' => context_system::instance()]);
465
        $badge->recipientfullname = get_string('error:userdeleted', 'badges', $strdata);
466
    } else {
467
        $badge->recipientfullname = fullname($user);
468
    }
469
 
1 efrain 470
    // Create a badge instance to be able to get the endorsement and other info.
471
    $badgeinstance = new badge($badge->id);
472
    $endorsement   = $badgeinstance->get_endorsement();
473
    $relatedbadges = $badgeinstance->get_related_badges();
1441 ariadna 474
    $alignments    = [];
475
    foreach ($badgeinstance->get_alignments() as $alignment) {
476
        $alignmentobj = (object) [
477
            'id' => $alignment->id,
478
            'badgeid' => $alignment->badgeid,
479
            'targetName' => $alignment->targetname,
480
            'targetUrl' => $alignment->targeturl,
481
        ];
482
        // Include only the properties visible by the user.
483
        if ($canconfiguredetails) {
484
            $alignmentobj->targetDescription = $alignment->targetdescription;
485
            $alignmentobj->targetFramework = $alignment->targetframework;
486
            $alignmentobj->targetCode = $alignment->targetcode;
487
        }
488
        $alignments[] = $alignmentobj;
489
    }
1 efrain 490
 
491
    if (!$canconfiguredetails) {
492
        // Return only the properties visible by the user.
493
        if (!empty($relatedbadges)) {
494
            foreach ($relatedbadges as $relatedbadge) {
495
                unset($relatedbadge->version);
496
                unset($relatedbadge->language);
497
                unset($relatedbadge->type);
498
            }
499
        }
500
    }
501
 
502
    $related = [
503
        'context'       => $context,
504
        'endorsement'   => $endorsement ? $endorsement : null,
505
        'alignment'     => $alignments,
506
        'relatedbadges' => $relatedbadges,
507
    ];
508
 
509
    $exporter = new user_badge_exporter($badge, $related);
510
    return $exporter->export($PAGE->get_renderer('core'));
511
}
512
 
513
/**
1441 ariadna 514
 * Prepare badgeclass for external functions.
515
 * @param core_badges\output\badgeclass $badgeclass
516
 * @return stdClass
517
 */
518
function badges_prepare_badgeclass_for_external(core_badges\output\badgeclass $badgeclass): stdClass {
519
    global $PAGE;
520
    $context = $badgeclass->context;
521
    $canconfiguredetails = has_capability('moodle/badges:configuredetails', $context);
522
 
523
    $badgeurl = new \moodle_url('/badges/badgeclass.php', [
524
        'id' => $badgeclass->badge->id,
525
    ]);
526
    $badgeurl = $badgeurl->out(false);
527
    $file = \moodle_url::make_webservice_pluginfile_url(
528
        $badgeclass->context->id,
529
        'badges',
530
        'badgeimage',
531
        $badgeclass->badge->id,
532
        '/',
533
        'f3'
534
    );
535
    $image = $file->out(false);
536
 
537
    $badge = (object) [
538
        'id'            => $badgeurl,
539
        'name'          => $badgeclass->badge->name,
540
        'type'          => OPEN_BADGES_V2_TYPE_BADGE,
541
        'description'   => $badgeclass->badge->description,
542
        'issuer'        => $badgeclass->badge->issuername,
543
        'hostedUrl'     => $badgeclass->badge->issuerurl,
544
        'image'         => $image,
545
    ];
546
 
547
    // Course.
548
    if ($badgeclass->badge->type == BADGE_TYPE_COURSE) {
549
        $course = get_course($badgeclass->badge->courseid);
550
        $badge->coursefullname = \core_external\util::format_string($course->fullname, $context);
551
        if ($canconfiguredetails) {
552
            $badge->courseid = $course->id;
553
        }
554
    }
555
 
556
    // Create a badge instance to be able to get the endorsement and other info.
557
    $badgeinstance = new badge($badgeclass->badge->id);
558
    $endorsement   = $badgeinstance->get_endorsement();
559
    $relatedbadges = $badgeinstance->get_related_badges();
560
    $alignments = [];
561
    foreach ($badgeinstance->get_alignments() as $alignment) {
562
        $alignmentobj = (object) [
563
            'id' => $alignment->id,
564
            'badgeid' => $alignment->badgeid,
565
            'targetName' => $alignment->targetname,
566
            'targetUrl' => $alignment->targeturl,
567
        ];
568
        // Include only the properties visible by the user.
569
        if ($canconfiguredetails) {
570
            $alignmentobj->targetDescription = $alignment->targetdescription;
571
            $alignmentobj->targetFramework = $alignment->targetframework;
572
            $alignmentobj->targetCode = $alignment->targetcode;
573
        }
574
        $alignments[] = $alignmentobj;
575
    }
576
 
577
    if (!$canconfiguredetails) {
578
        // Return only the properties visible by the user.
579
        if (!empty($relatedbadges)) {
580
            foreach ($relatedbadges as $relatedbadge) {
581
                unset($relatedbadge->version);
582
                unset($relatedbadge->language);
583
                unset($relatedbadge->type);
584
            }
585
        }
586
    }
587
 
588
    $related = [
589
        'context'       => $context,
590
        'endorsement'   => $endorsement ? $endorsement : null,
591
        'relatedbadges' => $relatedbadges,
592
    ];
593
 
594
    if (!empty($alignments)) {
595
        $related['alignment'] = $alignments;
596
    }
597
 
598
    $exporter = new badgeclass_exporter($badge, $related);
599
    return $exporter->export($PAGE->get_renderer('core', 'badges'));
600
}
601
 
602
/**
1 efrain 603
 * Extends the course administration navigation with the Badges page
604
 *
605
 * @param navigation_node $coursenode
606
 * @param object $course
607
 */
608
function badges_add_course_navigation(navigation_node $coursenode, stdClass $course) {
609
    global $CFG, $SITE;
610
 
611
    $coursecontext = context_course::instance($course->id);
612
    $isfrontpage = (!$coursecontext || $course->id == $SITE->id);
613
    $canmanage = has_any_capability(array('moodle/badges:viewawarded',
614
                                          'moodle/badges:createbadge',
615
                                          'moodle/badges:awardbadge',
616
                                          'moodle/badges:configurecriteria',
617
                                          'moodle/badges:configuremessages',
618
                                          'moodle/badges:configuredetails',
619
                                          'moodle/badges:deletebadge'), $coursecontext);
620
 
621
    if (!empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges) && !$isfrontpage && $canmanage) {
622
        $coursenode->add(get_string('coursebadges', 'badges'), null,
623
                navigation_node::TYPE_CONTAINER, null, 'coursebadges',
624
                new pix_icon('i/badge', get_string('coursebadges', 'badges')));
625
 
626
        $url = new moodle_url('/badges/index.php', array('type' => BADGE_TYPE_COURSE, 'id' => $course->id));
627
 
628
        $coursenode->get('coursebadges')->add(get_string('managebadges', 'badges'), $url,
629
            navigation_node::TYPE_SETTING, null, 'coursebadges');
630
 
631
        if (has_capability('moodle/badges:createbadge', $coursecontext)) {
1441 ariadna 632
            $url = new moodle_url('/badges/edit.php', ['action' => 'new', 'courseid' => $course->id]);
1 efrain 633
 
634
            $coursenode->get('coursebadges')->add(get_string('newbadge', 'badges'), $url,
635
                    navigation_node::TYPE_SETTING, null, 'newbadge');
636
        }
637
    }
638
}
639
 
640
/**
641
 * Triggered when badge is manually awarded.
642
 *
643
 * @param   object      $data
644
 * @return  boolean
645
 */
646
function badges_award_handle_manual_criteria_review(stdClass $data) {
647
    $criteria = $data->crit;
648
    $userid = $data->userid;
649
    $badge = new badge($criteria->badgeid);
650
 
651
    if (!$badge->is_active() || $badge->is_issued($userid)) {
652
        return true;
653
    }
654
 
655
    if ($criteria->review($userid)) {
656
        $criteria->mark_complete($userid);
657
 
658
        if ($badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($userid)) {
659
            $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($userid);
660
            $badge->issue($userid);
661
        }
662
    }
663
 
664
    return true;
665
}
666
 
667
/**
668
 * Process badge image from form data
669
 *
670
 * @param badge $badge Badge object
671
 * @param string $iconfile Original file
672
 */
673
function badges_process_badge_image(badge $badge, $iconfile) {
674
    global $CFG, $USER;
675
    require_once($CFG->libdir. '/gdlib.php');
676
 
677
    if (!empty($CFG->gdversion)) {
678
        process_new_icon($badge->get_context(), 'badges', 'badgeimage', $badge->id, $iconfile, true);
679
        @unlink($iconfile);
680
    }
681
}
682
 
683
/**
684
 * Print badge image.
685
 *
686
 * @param badge $badge Badge object
687
 * @param stdClass $context
688
 * @param string $size
689
 */
690
function print_badge_image(badge $badge, stdClass $context, $size = 'small') {
691
    $fsize = ($size == 'small') ? 'f2' : 'f1';
692
 
693
    $imageurl = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', $fsize, false);
694
    // Appending a random parameter to image link to forse browser reload the image.
695
    $imageurl->param('refresh', rand(1, 10000));
696
    $attributes = array('src' => $imageurl, 'alt' => s($badge->name), 'class' => 'activatebadge');
697
 
698
    return html_writer::empty_tag('img', $attributes);
699
}
700
 
701
/**
702
 * Bake issued badge.
703
 *
704
 * @param string $hash Unique hash of an issued badge.
705
 * @param int $badgeid ID of the original badge.
706
 * @param int $userid ID of badge recipient (optional).
707
 * @param boolean $pathhash Return file pathhash instead of image url (optional).
708
 * @return string|moodle_url|null Returns either new file path hash or new file URL
709
 */
710
function badges_bake($hash, $badgeid, $userid = 0, $pathhash = false) {
711
    global $CFG, $USER;
712
 
713
    $badge = new badge($badgeid);
714
    $badge_context = $badge->get_context();
715
    $userid = ($userid) ? $userid : $USER->id;
716
    $user_context = context_user::instance($userid);
717
 
718
    $fs = get_file_storage();
719
    if (!$fs->file_exists($user_context->id, 'badges', 'userbadge', $badge->id, '/', $hash . '.png')) {
720
        if ($file = $fs->get_file($badge_context->id, 'badges', 'badgeimage', $badge->id, '/', 'f3.png')) {
721
            $contents = $file->get_content();
722
 
1441 ariadna 723
            $filehandler = new png_metadata_handler($contents);
1 efrain 724
            // For now, the site backpack OB version will be used as default.
725
            $obversion = badges_open_badges_backpack_api();
726
            $assertion = new core_badges_assertion($hash, $obversion);
727
            $assertionjson = json_encode($assertion->get_badge_assertion());
728
            if ($filehandler->check_chunks("iTXt", "openbadges")) {
729
                // Add assertion URL iTXt chunk.
730
                $newcontents = $filehandler->add_chunks("iTXt", "openbadges", $assertionjson);
731
                $fileinfo = array(
732
                        'contextid' => $user_context->id,
733
                        'component' => 'badges',
734
                        'filearea' => 'userbadge',
735
                        'itemid' => $badge->id,
736
                        'filepath' => '/',
737
                        'filename' => $hash . '.png',
738
                );
739
 
740
                // Create a file with added contents.
741
                $newfile = $fs->create_file_from_string($fileinfo, $newcontents);
742
                if ($pathhash) {
743
                    return $newfile->get_pathnamehash();
744
                }
745
            }
746
        } else {
747
            debugging('Error baking badge image!', DEBUG_DEVELOPER);
748
            return;
749
        }
750
    }
751
 
752
    // If file exists and we just need its path hash, return it.
753
    if ($pathhash) {
754
        $file = $fs->get_file($user_context->id, 'badges', 'userbadge', $badge->id, '/', $hash . '.png');
755
        return $file->get_pathnamehash();
756
    }
757
 
758
    $fileurl = moodle_url::make_pluginfile_url($user_context->id, 'badges', 'userbadge', $badge->id, '/', $hash, true);
759
    return $fileurl;
760
}
761
 
762
/**
763
 * Returns external backpack settings and badges from this backpack.
764
 *
765
 * This function first checks if badges for the user are cached and
766
 * tries to retrieve them from the cache. Otherwise, badges are obtained
767
 * through curl request to the backpack.
768
 *
769
 * @param int $userid Backpack user ID.
770
 * @param boolean $refresh Refresh badges collection in cache.
771
 * @return null|object Returns null is there is no backpack or object with backpack settings.
772
 */
773
function get_backpack_settings($userid, $refresh = false) {
774
    global $DB;
775
 
776
    // Try to get badges from cache first.
777
    $badgescache = cache::make('core', 'externalbadges');
778
    $out = $badgescache->get($userid);
779
    if ($out !== false && !$refresh) {
780
        return $out;
781
    }
782
    // Get badges through curl request to the backpack.
783
    $record = $DB->get_record('badge_backpack', array('userid' => $userid));
784
    if ($record) {
785
        $sitebackpack = badges_get_site_backpack($record->externalbackpackid);
786
        $backpack = new \core_badges\backpack_api($sitebackpack, $record);
787
        $out = new stdClass();
788
        $out->backpackid = $sitebackpack->id;
789
 
790
        if ($collections = $DB->get_records('badge_external', array('backpackid' => $record->id))) {
791
            $out->totalcollections = count($collections);
792
            $out->totalbadges = 0;
793
            $out->badges = array();
794
            foreach ($collections as $collection) {
795
                $badges = $backpack->get_badges($collection, true);
796
                if (!empty($badges)) {
797
                    $out->badges = array_merge($out->badges, $badges);
798
                    $out->totalbadges += count($badges);
799
                } else {
800
                    $out->badges = array_merge($out->badges, array());
801
                }
802
            }
803
        } else {
804
            $out->totalbadges = 0;
805
            $out->totalcollections = 0;
806
        }
807
 
808
        $badgescache->set($userid, $out);
809
        return $out;
810
    }
811
 
812
    return null;
813
}
814
 
815
/**
816
 * Download all user badges in zip archive.
817
 *
818
 * @param int $userid ID of badge owner.
819
 */
820
function badges_download($userid) {
821
    global $CFG, $DB;
822
    $context = context_user::instance($userid);
823
    $records = $DB->get_records('badge_issued', array('userid' => $userid));
824
 
825
    // Get list of files to download.
826
    $fs = get_file_storage();
827
    $filelist = array();
828
    foreach ($records as $issued) {
829
        $badge = new badge($issued->badgeid);
830
        // Need to make image name user-readable and unique using filename safe characters.
831
        $name =  $badge->name . ' ' . userdate($issued->dateissued, '%d %b %Y') . ' ' . hash('crc32', $badge->id);
832
        $name = str_replace(' ', '_', $name);
833
        $name = clean_param($name, PARAM_FILE);
834
        if ($file = $fs->get_file($context->id, 'badges', 'userbadge', $issued->badgeid, '/', $issued->uniquehash . '.png')) {
835
            $filelist[$name . '.png'] = $file;
836
        }
837
    }
838
 
839
    // Zip files and sent them to a user.
840
    $tempzip = tempnam($CFG->tempdir.'/', 'mybadges');
841
    $zipper = new zip_packer();
842
    if ($zipper->archive_to_pathname($filelist, $tempzip)) {
843
        send_temp_file($tempzip, 'badges.zip');
844
    } else {
845
        debugging("Problems with archiving the files.", DEBUG_DEVELOPER);
846
        die;
847
    }
848
}
849
 
850
/**
851
 * Checks if user has external backpack connected.
852
 *
853
 * @param int $userid ID of a user.
854
 * @return bool True|False whether backpack connection exists.
855
 */
856
function badges_user_has_backpack($userid) {
857
    global $DB;
858
    return $DB->record_exists('badge_backpack', array('userid' => $userid));
859
}
860
 
861
/**
862
 * Handles what happens to the course badges when a course is deleted.
863
 *
864
 * @param int $courseid course ID.
865
 * @return void.
866
 */
867
function badges_handle_course_deletion($courseid) {
868
    global $CFG, $DB;
869
    include_once $CFG->libdir . '/filelib.php';
870
 
871
    $systemcontext = context_system::instance();
872
    $coursecontext = context_course::instance($courseid);
873
    $fs = get_file_storage();
874
 
875
    // Move badges images to the system context.
876
    $fs->move_area_files_to_new_context($coursecontext->id, $systemcontext->id, 'badges', 'badgeimage');
877
 
878
    // Get all course badges.
879
    $badges = $DB->get_records('badge', array('type' => BADGE_TYPE_COURSE, 'courseid' => $courseid));
880
    foreach ($badges as $badge) {
881
        // Archive badges in this course.
882
        $toupdate = new stdClass();
883
        $toupdate->id = $badge->id;
884
        $toupdate->type = BADGE_TYPE_SITE;
885
        $toupdate->courseid = null;
886
        $toupdate->status = BADGE_STATUS_ARCHIVED;
887
        $DB->update_record('badge', $toupdate);
888
    }
889
}
890
 
891
/**
892
 * Create the site backpack with this data.
893
 *
894
 * @param stdClass $data The new backpack data.
895
 * @return boolean
896
 */
897
function badges_create_site_backpack($data) {
898
    global $DB;
899
    $context = context_system::instance();
900
    require_capability('moodle/badges:manageglobalsettings', $context);
901
 
902
    $max = $DB->get_field_sql('SELECT MAX(sortorder) FROM {badge_external_backpack}');
903
    $data->sortorder = $max + 1;
904
 
905
    return badges_save_external_backpack($data);
906
}
907
 
908
/**
909
 * Update the backpack with this id.
910
 *
911
 * @param integer $id The backpack to edit
912
 * @param stdClass $data The new backpack data.
913
 * @return boolean
914
 */
915
function badges_update_site_backpack($id, $data) {
916
    global $DB;
917
    $context = context_system::instance();
918
    require_capability('moodle/badges:manageglobalsettings', $context);
919
 
920
    if ($backpack = badges_get_site_backpack($id)) {
921
        $data->id = $id;
922
        return badges_save_external_backpack($data);
923
    }
924
    return false;
925
}
926
 
927
 
928
/**
929
 * Delete the backpack with this id.
930
 *
931
 * @param integer $id The backpack to delete.
932
 * @return boolean
933
 */
934
function badges_delete_site_backpack($id) {
935
    global $DB;
936
 
937
    $context = context_system::instance();
938
    require_capability('moodle/badges:manageglobalsettings', $context);
939
 
940
    // Only remove site backpack if it's not the default one.
941
    $defaultbackpack = badges_get_site_primary_backpack();
942
    if ($defaultbackpack->id != $id && $DB->record_exists('badge_external_backpack', ['id' => $id])) {
943
        $transaction = $DB->start_delegated_transaction();
944
 
945
        // Remove connections for users to this backpack.
946
        $sql = "SELECT DISTINCT bb.id
947
                  FROM {badge_backpack} bb
948
                 WHERE bb.externalbackpackid = :backpackid";
949
        $params = ['backpackid' => $id];
950
        $userbackpacks = $DB->get_fieldset_sql($sql, $params);
951
        if ($userbackpacks) {
952
            // Delete user external collections references to this backpack.
953
            list($insql, $params) = $DB->get_in_or_equal($userbackpacks);
954
            $DB->delete_records_select('badge_external', "backpackid $insql", $params);
955
        }
956
        $DB->delete_records('badge_backpack', ['externalbackpackid' => $id]);
957
 
958
        // Delete backpack entry.
959
        $result = $DB->delete_records('badge_external_backpack', ['id' => $id]);
960
 
961
        $transaction->allow_commit();
962
 
963
        return $result;
964
    }
965
 
966
    return false;
967
}
968
 
969
/**
970
 * Perform the actual create/update of external bakpacks. Any checks on the validity of the id will need to be
971
 * performed before it reaches this function.
972
 *
973
 * @param stdClass $data The backpack data we are updating/inserting
974
 * @return int Returns the id of the new/updated record
975
 */
976
function badges_save_external_backpack(stdClass $data) {
977
    global $DB;
978
    if ($data->apiversion == OPEN_BADGES_V2P1) {
979
        // Check if there is an existing issuer for the given backpackapiurl.
980
        foreach (core\oauth2\api::get_all_issuers() as $tmpissuer) {
981
            if ($data->backpackweburl == $tmpissuer->get('baseurl')) {
982
                $issuer = $tmpissuer;
983
                break;
984
            }
985
        }
986
 
987
        // Create the issuer if it doesn't exist yet.
988
        if (empty($issuer)) {
989
            $issuer = new \core\oauth2\issuer(0, (object) [
990
                'name' => $data->backpackweburl,
991
                'baseurl' => $data->backpackweburl,
992
                // Note: This is required because the DB schema is broken and does not accept a null value when it should.
993
                'image' => '',
994
            ]);
995
            $issuer->save();
996
        }
997
 
998
        // This can't be run from PHPUNIT because testing platforms need real URLs.
999
        // In the future, this request can be moved to the moodle-exttests repository.
1000
        if (!PHPUNIT_TEST) {
1001
            // Create/update the endpoints for the issuer.
1002
            \core\oauth2\discovery\imsbadgeconnect::create_endpoints($issuer);
1003
            $data->oauth2_issuerid = $issuer->get('id');
1004
 
1005
            $apibase = \core\oauth2\endpoint::get_record([
1006
                'issuerid' => $data->oauth2_issuerid,
1007
                'name' => 'apiBase',
1008
            ]);
1009
            $data->backpackapiurl = $apibase->get('url');
1010
        }
1011
    }
1012
    $backpack = new stdClass();
1013
 
1014
    $backpack->apiversion = $data->apiversion;
1015
    $backpack->backpackweburl = $data->backpackweburl;
1016
    $backpack->backpackapiurl = $data->backpackapiurl;
1017
    $backpack->oauth2_issuerid = $data->oauth2_issuerid ?? '';
1018
    if (isset($data->sortorder)) {
1019
        $backpack->sortorder = $data->sortorder;
1020
    }
1021
 
1022
    if (empty($data->id)) {
1023
        $backpack->id = $DB->insert_record('badge_external_backpack', $backpack);
1024
    } else {
1025
        $backpack->id = $data->id;
1026
        $DB->update_record('badge_external_backpack', $backpack);
1027
    }
1028
    $data->externalbackpackid = $backpack->id;
1029
 
1030
    unset($data->id);
1031
    badges_save_backpack_credentials($data);
1032
    return $data->externalbackpackid;
1033
}
1034
 
1035
/**
1036
 * Create a backpack with the provided details. Stores the auth details of the backpack
1037
 *
1038
 * @param stdClass $data Backpack specific data.
1039
 * @return int The id of the external backpack that the credentials correspond to
1040
 */
1041
function badges_save_backpack_credentials(stdClass $data) {
1042
    global $DB;
1043
 
1044
    if (isset($data->backpackemail) && isset($data->password)) {
1045
        $backpack = new stdClass();
1046
 
1047
        $backpack->email = $data->backpackemail;
1048
        $backpack->password = !empty($data->password) ? $data->password : '';
1049
        $backpack->externalbackpackid = $data->externalbackpackid;
1050
        $backpack->userid = $data->userid ?? 0;
1051
        $backpack->backpackuid = $data->backpackuid ?? 0;
1052
        $backpack->autosync = $data->autosync ?? 0;
1053
 
1054
        if (!empty($data->badgebackpack)) {
1055
            $backpack->id = $data->badgebackpack;
1056
        } else if (!empty($data->id)) {
1057
            $backpack->id = $data->id;
1058
        }
1059
 
1060
        if (empty($backpack->id)) {
1061
            $backpack->id = $DB->insert_record('badge_backpack', $backpack);
1062
        } else {
1063
            $DB->update_record('badge_backpack', $backpack);
1064
        }
1065
 
1066
        return $backpack->externalbackpackid;
1067
    }
1068
 
1069
    return $data->externalbackpackid ?? 0;
1070
}
1071
 
1072
/**
1073
 * Is any backpack enabled that supports open badges V1?
1074
 * @param int|null $backpackid Check the version of the given id OR if null the sitewide backpack
1075
 * @return boolean
1076
 */
1077
function badges_open_badges_backpack_api(?int $backpackid = null) {
1078
    if (!$backpackid) {
1079
        $backpack = badges_get_site_primary_backpack();
1080
    } else {
1081
        $backpack = badges_get_site_backpack($backpackid);
1082
    }
1083
 
1084
    if (empty($backpack->apiversion)) {
1085
        return OPEN_BADGES_V2;
1086
    }
1087
    return $backpack->apiversion;
1088
}
1089
 
1090
/**
1091
 * Get a site backpacks by id for a particular user or site (if userid is 0)
1092
 *
1093
 * @param int $id The backpack id.
1094
 * @param int $userid The owner of the backpack, 0 if it's a sitewide backpack else a user's site backpack
1095
 * @return stdClass
1096
 */
1097
function badges_get_site_backpack($id, int $userid = 0) {
1098
    global $DB;
1099
 
1100
    $sql = "SELECT beb.*, bb.id AS badgebackpack, bb.password, bb.email AS backpackemail
1101
              FROM {badge_external_backpack} beb
1102
         LEFT JOIN {badge_backpack} bb ON bb.externalbackpackid = beb.id AND bb.userid=:userid
1103
             WHERE beb.id=:id";
1104
 
1105
    return $DB->get_record_sql($sql, ['id' => $id, 'userid' => $userid]);
1106
}
1107
 
1108
/**
1109
 * Get the user backpack for the currently logged in user OR the provided user
1110
 *
1111
 * @param int|null $userid The user whose backpack you're requesting for. If null, get the logged in user's backpack
1112
 * @return mixed The user's backpack or none.
1113
 * @throws dml_exception
1114
 */
1115
function badges_get_user_backpack(?int $userid = 0) {
1116
    global $DB;
1117
 
1118
    if (!$userid) {
1119
        global $USER;
1120
        $userid = $USER->id;
1121
    }
1122
 
1123
    $sql = "SELECT beb.*, bb.id AS badgebackpack, bb.password, bb.email AS backpackemail
1124
              FROM {badge_external_backpack} beb
1125
              JOIN {badge_backpack} bb ON bb.externalbackpackid = beb.id AND bb.userid=:userid";
1126
 
1127
    return $DB->get_record_sql($sql, ['userid' => $userid]);
1128
}
1129
 
1130
/**
1131
 * Get the primary backpack for the site
1132
 *
1133
 * @return stdClass
1134
 */
1135
function badges_get_site_primary_backpack() {
1136
    global $DB;
1137
 
1138
    $sql = 'SELECT *
1139
              FROM {badge_external_backpack}
1140
             WHERE sortorder = (SELECT MIN(sortorder)
1141
                                  FROM {badge_external_backpack} b2)';
1142
    $firstbackpack = $DB->get_record_sql($sql, null, MUST_EXIST);
1143
 
1144
    return badges_get_site_backpack($firstbackpack->id);
1145
}
1146
 
1147
/**
1148
 * List the backpacks at site level.
1149
 *
1150
 * @return array(stdClass)
1151
 */
1152
function badges_get_site_backpacks() {
1153
    global $DB;
1154
 
1155
    $defaultbackpack = badges_get_site_primary_backpack();
1156
    $all = $DB->get_records('badge_external_backpack', null, 'sortorder ASC');
1157
    foreach ($all as $key => $bp) {
1158
        if ($bp->id == $defaultbackpack->id) {
1159
            $all[$key]->sitebackpack = true;
1160
        } else {
1161
            $all[$key]->sitebackpack = false;
1162
        }
1163
    }
1164
 
1165
    return $all;
1166
}
1167
 
1168
/**
1169
 * Moves the backpack in the list one position up or down.
1170
 *
1171
 * @param int $backpackid The backpack identifier to be moved.
1172
 * @param int $direction The direction (BACKPACK_MOVE_UP/BACKPACK_MOVE_DOWN) where to move the backpack.
1173
 *
1174
 * @throws \moodle_exception if attempting to use invalid direction value.
1175
 */
1176
function badges_change_sortorder_backpacks(int $backpackid, int $direction): void {
1177
    global $DB;
1178
 
1179
    if ($direction != BACKPACK_MOVE_UP && $direction != BACKPACK_MOVE_DOWN) {
1180
        throw new \coding_exception(
1181
            'Must use a valid backpack API move direction constant (BACKPACK_MOVE_UP or BACKPACK_MOVE_DOWN)');
1182
    }
1183
 
1184
    $backpacks = badges_get_site_backpacks();
1185
    $backpacktoupdate = $backpacks[$backpackid];
1186
 
1187
    $currentsortorder = $backpacktoupdate->sortorder;
1188
    $targetsortorder = $currentsortorder + $direction;
1189
    if ($targetsortorder > 0 && $targetsortorder <= count($backpacks) ) {
1190
        foreach ($backpacks as $backpack) {
1191
            if ($backpack->sortorder == $targetsortorder) {
1192
                $backpack->sortorder = $backpack->sortorder - $direction;
1193
                $DB->update_record('badge_external_backpack', $backpack);
1194
                break;
1195
            }
1196
        }
1197
        $backpacktoupdate->sortorder = $targetsortorder;
1198
        $DB->update_record('badge_external_backpack', $backpacktoupdate);
1199
    }
1200
}
1201
 
1202
/**
1203
 * List the supported badges api versions.
1204
 *
1205
 * @return array(version)
1206
 */
1207
function badges_get_badge_api_versions() {
1208
    return [
1209
        (string)OPEN_BADGES_V2 => get_string('openbadgesv2', 'badges'),
1210
        (string)OPEN_BADGES_V2P1 => get_string('openbadgesv2p1', 'badges')
1211
    ];
1212
}
1213
 
1214
/**
1215
 * Get the default issuer for a badge from this site.
1216
 *
1217
 * @return array
1218
 */
1219
function badges_get_default_issuer() {
1220
    global $CFG, $SITE;
1221
 
1222
    $sitebackpack = badges_get_site_primary_backpack();
1223
    $issuer = array();
1224
    $issuerurl = new moodle_url('/');
1225
    $issuer['name'] = $CFG->badges_defaultissuername;
1226
    if (empty($issuer['name'])) {
1227
        $issuer['name'] = $SITE->fullname ? $SITE->fullname : $SITE->shortname;
1228
    }
1229
    $issuer['url'] = $issuerurl->out(false);
1230
    $issuer['email'] = $sitebackpack->backpackemail ?: $CFG->badges_defaultissuercontact;
1231
    $issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
1232
    $issuerid = new moodle_url('/badges/issuer_json.php');
1233
    $issuer['id'] = $issuerid->out(false);
1234
    $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
1235
    return $issuer;
1236
}
1237
 
1238
/**
1239
 * Disconnect from the user backpack by deleting the user preferences.
1240
 *
1241
 * @param integer $userid The user to diconnect.
1242
 * @return boolean
1243
 */
1244
function badges_disconnect_user_backpack($userid) {
1245
    global $USER;
1246
 
1247
    // We can only change backpack settings for our own real backpack.
1248
    if ($USER->id != $userid ||
1249
            \core\session\manager::is_loggedinas()) {
1250
 
1251
        return false;
1252
    }
1253
 
1254
    unset_user_preference('badges_email_verify_secret');
1255
    unset_user_preference('badges_email_verify_address');
1256
    unset_user_preference('badges_email_verify_backpackid');
1257
    unset_user_preference('badges_email_verify_password');
1258
 
1259
    return true;
1260
}
1261
 
1262
/**
1263
 * Used to remember which objects we connected with a backpack before.
1264
 *
1265
 * @param integer $sitebackpackid The site backpack to connect to.
1266
 * @param string $type The type of this remote object.
1267
 * @param string $internalid The id for this object on the Moodle site.
1268
 * @param string $param The param we need to return. Defaults to the externalid.
1269
 * @return mixed The id or false if it doesn't exist.
1270
 */
1271
function badges_external_get_mapping($sitebackpackid, $type, $internalid, $param = 'externalid') {
1272
    global $DB;
1273
    // Return externalid if it exists.
1274
    $params = [
1275
        'sitebackpackid' => $sitebackpackid,
1276
        'type' => $type,
1277
        'internalid' => $internalid
1278
    ];
1279
 
1280
    $record = $DB->get_record('badge_external_identifier', $params, $param, IGNORE_MISSING);
1281
    if ($record) {
1282
        return $record->$param;
1283
    }
1284
    return false;
1285
}
1286
 
1287
/**
1288
 * Save the info about which objects we connected with a backpack before.
1289
 *
1290
 * @param integer $sitebackpackid The site backpack to connect to.
1291
 * @param string $type The type of this remote object.
1292
 * @param string $internalid The id for this object on the Moodle site.
1293
 * @param string $externalid The id of this object on the remote site.
1294
 * @return boolean
1295
 */
1296
function badges_external_create_mapping($sitebackpackid, $type, $internalid, $externalid) {
1297
    global $DB;
1298
 
1299
    $params = [
1300
        'sitebackpackid' => $sitebackpackid,
1301
        'type' => $type,
1302
        'internalid' => $internalid,
1303
        'externalid' => $externalid
1304
    ];
1305
 
1306
    return $DB->insert_record('badge_external_identifier', $params);
1307
}
1308
 
1309
/**
1310
 * Delete all external mapping information for a backpack.
1311
 *
1312
 * @param integer $sitebackpackid The site backpack to connect to.
1313
 * @return boolean
1314
 */
1315
function badges_external_delete_mappings($sitebackpackid) {
1316
    global $DB;
1317
 
1318
    $params = ['sitebackpackid' => $sitebackpackid];
1319
 
1320
    return $DB->delete_records('badge_external_identifier', $params);
1321
}
1322
 
1323
/**
1324
 * Delete a specific external mapping information for a backpack.
1325
 *
1326
 * @param integer $sitebackpackid The site backpack to connect to.
1327
 * @param string $type The type of this remote object.
1328
 * @param string $internalid The id for this object on the Moodle site.
1329
 */
1330
function badges_external_delete_mapping($sitebackpackid, $type, $internalid) {
1331
    global $DB;
1332
 
1333
    $params = [
1334
        'sitebackpackid' => $sitebackpackid,
1335
        'type' => $type,
1336
        'internalid' => $internalid
1337
    ];
1338
 
1339
    $DB->delete_record('badge_external_identifier', $params);
1340
}
1341
 
1342
/**
1343
 * Create and send a verification email to the email address supplied.
1344
 *
1345
 * Since we're not sending this email to a user, email_to_user can't be used
1346
 * but this function borrows largely the code from that process.
1347
 *
1348
 * @param string $email the email address to send the verification email to.
1349
 * @param int $backpackid the id of the backpack to connect to
1350
 * @param string $backpackpassword the user entered password to connect to this backpack
1351
 * @return true if the email was sent successfully, false otherwise.
1352
 */
1353
function badges_send_verification_email($email, $backpackid, $backpackpassword) {
1354
    global $DB, $USER;
1355
 
1356
    // Store a user secret (badges_email_verify_secret) and the address (badges_email_verify_address) as users prefs.
1357
    // The address will be used by edit_backpack_form for display during verification and to facilitate the resending
1358
    // of verification emails to said address.
1359
    $secret = random_string(15);
1360
    set_user_preference('badges_email_verify_secret', $secret);
1361
    set_user_preference('badges_email_verify_address', $email);
1362
    set_user_preference('badges_email_verify_backpackid', $backpackid);
1363
    set_user_preference('badges_email_verify_password', $backpackpassword);
1364
 
1365
    // To, from.
1366
    $tempuser = $DB->get_record('user', array('id' => $USER->id), '*', MUST_EXIST);
1367
    $tempuser->email = $email;
1368
    $noreplyuser = core_user::get_noreply_user();
1369
 
1370
    // Generate the verification email body.
1371
    $verificationurl = '/badges/backpackemailverify.php';
1372
    $verificationurl = new moodle_url($verificationurl);
1373
    $verificationpath = $verificationurl->out(false);
1374
 
1375
    $site = get_site();
1376
    $link = $verificationpath . '?data='. $secret;
1377
    // Hard-coded button styles, because CSS can't be used in emails.
1378
    $buttonstyles = [
1379
        'background-color: #0f6cbf',
1380
        'border: none',
1381
        'color: white',
1382
        'padding: 12px',
1383
        'text-align: center',
1384
        'text-decoration: none',
1385
        'display: inline-block',
1386
        'font-size: 20px',
1387
        'font-weight: 800',
1388
        'margin: 4px 2px',
1389
        'cursor: pointer',
1390
        'border-radius: 8px',
1391
    ];
1392
    $button = html_writer::start_tag('center') .
1393
        html_writer::tag(
1394
            'button',
1395
            get_string('verifyemail', 'badges'),
1396
            ['style' => implode(';', $buttonstyles)]) .
1397
        html_writer::end_tag('center');
1398
    $args = [
1399
        'link' => html_writer::link($link, $link),
1400
        'buttonlink' => html_writer::link($link, $button),
1401
        'sitename' => $site->fullname,
1402
        'admin' => generate_email_signoff(),
1403
        'userfirstname' => $USER->firstname,
1404
    ];
1405
 
1406
    $messagesubject = get_string('backpackemailverifyemailsubject', 'badges', $site->fullname);
1407
    $messagetext = get_string('backpackemailverifyemailbody', 'badges', $args);
1408
    $messagehtml = text_to_html($messagetext, false, false, true);
1409
 
1410
    return email_to_user($tempuser, $noreplyuser, $messagesubject, $messagetext, $messagehtml);
1411
}
1412
 
1413
/**
1414
 * Return all the enabled criteria types for this site.
1415
 *
1416
 * @param boolean $enabled
1417
 * @return array
1418
 */
1419
function badges_list_criteria($enabled = true) {
1420
    global $CFG;
1421
 
1422
    $types = array(
1423
        BADGE_CRITERIA_TYPE_OVERALL    => 'overall',
1424
        BADGE_CRITERIA_TYPE_ACTIVITY   => 'activity',
1425
        BADGE_CRITERIA_TYPE_MANUAL     => 'manual',
1426
        BADGE_CRITERIA_TYPE_SOCIAL     => 'social',
1427
        BADGE_CRITERIA_TYPE_COURSE     => 'course',
1428
        BADGE_CRITERIA_TYPE_COURSESET  => 'courseset',
1429
        BADGE_CRITERIA_TYPE_PROFILE    => 'profile',
1430
        BADGE_CRITERIA_TYPE_BADGE      => 'badge',
1431
        BADGE_CRITERIA_TYPE_COHORT     => 'cohort',
1432
        BADGE_CRITERIA_TYPE_COMPETENCY => 'competency',
1433
    );
1434
    if ($enabled) {
1435
        foreach ($types as $key => $type) {
1436
            $class = 'award_criteria_' . $type;
1437
            $file = $CFG->dirroot . '/badges/criteria/' . $class . '.php';
1438
            if (file_exists($file)) {
1439
                require_once($file);
1440
 
1441
                if (!$class::is_enabled()) {
1442
                    unset($types[$key]);
1443
                }
1444
            }
1445
        }
1446
    }
1447
    return $types;
1448
}
1449
 
1450
/**
1451
 * Check if any badge has records for competencies.
1452
 *
1453
 * @param array $competencyids Array of competencies ids.
1454
 * @return boolean Return true if competencies were found in any badge.
1455
 */
1456
function badge_award_criteria_competency_has_records_for_competencies($competencyids) {
1457
    global $DB;
1458
 
1459
    list($insql, $params) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED);
1460
 
1461
    $sql = "SELECT DISTINCT bc.badgeid
1462
                FROM {badge_criteria} bc
1463
                JOIN {badge_criteria_param} bcp ON bc.id = bcp.critid
1464
                WHERE bc.criteriatype = :criteriatype AND bcp.value $insql";
1465
    $params['criteriatype'] = BADGE_CRITERIA_TYPE_COMPETENCY;
1466
 
1467
    return $DB->record_exists_sql($sql, $params);
1468
}
1469
 
1470
/**
1471
 * Creates single message for all notification and sends it out
1472
 *
1473
 * @param object $badge A badge which is notified about.
1474
 */
1475
function badge_assemble_notification(stdClass $badge) {
1476
    global $DB;
1477
 
1478
    $userfrom = core_user::get_noreply_user();
1479
    $userfrom->maildisplay = true;
1480
 
1481
    if ($msgs = $DB->get_records_select('badge_issued', 'issuernotified IS NULL AND badgeid = ?', array($badge->id))) {
1482
        // Get badge creator.
1483
        $creator = $DB->get_record('user', array('id' => $badge->creator), '*', MUST_EXIST);
1484
        $creatorsubject = get_string('creatorsubject', 'badges', $badge->name);
1485
        $creatormessage = '';
1486
 
1487
        // Put all messages in one digest.
1488
        foreach ($msgs as $msg) {
1489
            $issuedlink = html_writer::link(new moodle_url('/badges/badge.php', array('hash' => $msg->uniquehash)), $badge->name);
1490
            $recipient = $DB->get_record('user', array('id' => $msg->userid), '*', MUST_EXIST);
1491
 
1492
            $a = new stdClass();
1493
            $a->user = fullname($recipient);
1494
            $a->link = $issuedlink;
1495
            $creatormessage .= get_string('creatorbody', 'badges', $a);
1496
            $DB->set_field('badge_issued', 'issuernotified', time(), array('badgeid' => $msg->badgeid, 'userid' => $msg->userid));
1497
        }
1498
 
1499
        // Create a message object.
1500
        $eventdata = new \core\message\message();
1501
        $eventdata->courseid          = SITEID;
1502
        $eventdata->component         = 'moodle';
1503
        $eventdata->name              = 'badgecreatornotice';
1504
        $eventdata->userfrom          = $userfrom;
1505
        $eventdata->userto            = $creator;
1506
        $eventdata->notification      = 1;
1507
        $eventdata->subject           = $creatorsubject;
1508
        $eventdata->fullmessage       = format_text_email($creatormessage, FORMAT_HTML);
1509
        $eventdata->fullmessageformat = FORMAT_PLAIN;
1510
        $eventdata->fullmessagehtml   = $creatormessage;
1511
        $eventdata->smallmessage      = $creatorsubject;
1512
 
1513
        message_send($eventdata);
1514
    }
1515
}
1516
 
1517
/**
1518
 * Attempt to authenticate with the site backpack credentials and return an error
1519
 * if the authentication fails. If external backpacks are not enabled, this will
1520
 * not perform any test.
1521
 *
1522
 * @return string
1523
 */
1524
function badges_verify_site_backpack() {
1525
    $defaultbackpack = badges_get_site_primary_backpack();
1526
    return badges_verify_backpack($defaultbackpack->id);
1527
}
1528
 
1529
/**
1530
 * Attempt to authenticate with a backpack credentials and return an error
1531
 * if the authentication fails.
1532
 * If external backpacks are not enabled or the backpack version is different
1533
 * from OBv2, this will not perform any test.
1534
 *
1535
 * @param int $backpackid Backpack identifier to verify.
1536
 * @return string The result of the verification process.
1537
 */
1538
function badges_verify_backpack(int $backpackid) {
1539
    global $OUTPUT, $CFG;
1540
 
1541
    if (empty($CFG->badges_allowexternalbackpack)) {
1542
        return '';
1543
    }
1544
 
1545
    $backpack = badges_get_site_backpack($backpackid);
1546
    if (empty($backpack->apiversion) || ($backpack->apiversion == OPEN_BADGES_V2)) {
1547
        $backpackapi = new \core_badges\backpack_api($backpack);
1548
 
1549
        // Clear any cached access tokens in the session.
1550
        $backpackapi->clear_system_user_session();
1551
 
1552
        // Now attempt a login with these credentials.
1553
        $result = $backpackapi->authenticate();
1554
        if (empty($result) || !empty($result->error)) {
1555
            $warning = $backpackapi->get_authentication_error();
1556
 
1557
            $params = ['id' => $backpack->id, 'action' => 'edit'];
1558
            $backpackurl = (new moodle_url('/badges/backpacks.php', $params))->out(false);
1559
 
1560
            $message = get_string('sitebackpackwarning', 'badges', ['url' => $backpackurl, 'warning' => $warning]);
1561
            $icon = $OUTPUT->pix_icon('i/warning', get_string('warning', 'moodle'));
1562
            return $OUTPUT->container($icon . $message, 'text-danger');
1563
        }
1564
    }
1565
 
1566
    return '';
1567
}
1568
 
1569
/**
1570
 * Generate a public badgr URL that conforms to OBv2. This is done because badgr responses do not currently conform to
1571
 * the spec.
1572
 *
1573
 * WARNING: This is an extremely hacky way of implementing this and should be removed once the standards are conformed to.
1574
 *
1575
 * @param stdClass $backpack The Badgr backpack we are pushing to
1576
 * @param string $type The type of object we are dealing with either Issuer, Assertion OR Badge.
1577
 * @param string $externalid The externalid as provided by the backpack
1578
 * @return ?string The public URL to access Badgr objects
1579
 */
1580
function badges_generate_badgr_open_url($backpack, $type, $externalid) {
1581
    if (badges_open_badges_backpack_api($backpack->id) == OPEN_BADGES_V2) {
1582
        $entity = strtolower($type);
1583
        if ($type == OPEN_BADGES_V2_TYPE_BADGE) {
1584
            $entity = "badge";
1585
        }
1586
        $url = new moodle_url($backpack->backpackapiurl);
1587
        return "{$url->get_scheme()}://{$url->get_host()}/public/{$entity}s/$externalid";
1588
 
1589
    }
1590
}