MDL-46805 SCORM: AICC restricts allowed characters in the username
[moodle.git] / mod / scorm / datamodels / aicclib.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  * functions used by AICC packages.
19  *
20  * @package    mod_scorm
21  * @copyright 1999 onwards Roberto Pinna
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 function scorm_add_time($a, $b) {
26     $aes = explode(':', $a);
27     $bes = explode(':', $b);
28     $aseconds = explode('.', $aes[2]);
29     $bseconds = explode('.', $bes[2]);
30     $change = 0;
32     $acents = 0;  // Cents.
33     if (count($aseconds) > 1) {
34         $acents = $aseconds[1];
35     }
36     $bcents = 0;
37     if (count($bseconds) > 1) {
38         $bcents = $bseconds[1];
39     }
40     $cents = $acents + $bcents;
41     $change = floor($cents / 100);
42     $cents = $cents - ($change * 100);
43     if (floor($cents) < 10) {
44         $cents = '0'. $cents;
45     }
47     $secs = $aseconds[0] + $bseconds[0] + $change;  // Seconds.
48     $change = floor($secs / 60);
49     $secs = $secs - ($change * 60);
50     if (floor($secs) < 10) {
51         $secs = '0'. $secs;
52     }
54     $mins = $aes[1] + $bes[1] + $change;   // Minutes.
55     $change = floor($mins / 60);
56     $mins = $mins - ($change * 60);
57     if ($mins < 10) {
58         $mins = '0' .  $mins;
59     }
61     $hours = $aes[0] + $bes[0] + $change;  // Hours.
62     if ($hours < 10) {
63         $hours = '0' . $hours;
64     }
66     if ($cents != '0') {
67         return $hours . ":" . $mins . ":" . $secs . '.' . $cents;
68     } else {
69         return $hours . ":" . $mins . ":" . $secs;
70     }
71 }
73 /**
74  * Take the header row of an AICC definition file
75  * and returns sequence of columns and a pointer to
76  * the sco identifier column.
77  *
78  * @param string $row AICC header row
79  * @param string $mastername AICC sco identifier column
80  * @return mixed
81  */
82 function scorm_get_aicc_columns($row, $mastername='system_id') {
83     $tok = strtok(strtolower($row), "\",\n\r");
84     $result = new stdClass();
85     $result->columns = array();
86     $i = 0;
87     while ($tok) {
88         if ($tok != '') {
89             $result->columns[] = $tok;
90             if ($tok == $mastername) {
91                 $result->mastercol = $i;
92             }
93             $i++;
94         }
95         $tok = strtok("\",\n\r");
96     }
97     return $result;
98 }
100 /**
101  * Given a colums array return a string containing the regular
102  * expression to match the columns in a text row.
103  *
104  * @param array $column The header columns
105  * @param string $remodule The regular expression module for a single column
106  * @return string
107  */
108 function scorm_forge_cols_regexp($columns, $remodule='(".*")?,') {
109     $regexp = '/^';
110     foreach ($columns as $column) {
111         $regexp .= $remodule;
112     }
113     $regexp = substr($regexp, 0, -1) . '/';
114     return $regexp;
117 /**
118  * Sets up AICC packages
119  * Called whenever package changes
120  * @param object $scorm instance - fields are updated and changes saved into database
121  * @return bool
122  */
123 function scorm_parse_aicc(&$scorm) {
124     global $DB;
126     if ($scorm->scormtype == SCORM_TYPE_AICCURL) {
127         return scorm_aicc_generate_simple_sco($scorm);
128     }
129     if (!isset($scorm->cmid)) {
130         $cm = get_coursemodule_from_instance('scorm', $scorm->id);
131         $scorm->cmid = $cm->id;
132     }
133     $context = context_module::instance($scorm->cmid);
135     $fs = get_file_storage();
137     $files = $fs->get_area_files($context->id, 'mod_scorm', 'content', 0, 'sortorder, itemid, filepath, filename', false);
139     $version = 'AICC';
140     $ids = array();
141     $courses = array();
142     $extaiccfiles = array('crs', 'des', 'au', 'cst', 'ort', 'pre', 'cmp');
144     foreach ($files as $file) {
145         $filename = $file->get_filename();
146         $ext = substr($filename, strrpos($filename, '.'));
147         $extension = strtolower(substr($ext, 1));
148         if (in_array($extension, $extaiccfiles)) {
149             $id = strtolower(basename($filename, $ext));
150             if (!isset($ids[$id])) {
151                 $ids[$id] = new stdClass();
152             }
153             $ids[$id]->$extension = $file;
154         }
155     }
157     foreach ($ids as $courseid => $id) {
158         if (!isset($courses[$courseid])) {
159             $courses[$courseid] = new stdClass();
160         }
161         if (isset($id->crs)) {
162             $contents = $id->crs->get_content();
163             $rows = explode("\r\n", $contents);
164             if (is_array($rows)) {
165                 foreach ($rows as $row) {
166                     if (preg_match("/^(.+)=(.+)$/", $row, $matches)) {
167                         switch (strtolower(trim($matches[1]))) {
168                             case 'course_id':
169                                 $courses[$courseid]->id = trim($matches[2]);
170                             break;
171                             case 'course_title':
172                                 $courses[$courseid]->title = trim($matches[2]);
173                             break;
174                             case 'version':
175                                 $courses[$courseid]->version = 'AICC_'.trim($matches[2]);
176                             break;
177                         }
178                     }
179                 }
180             }
181         }
182         if (isset($id->des)) {
183             $contents = $id->des->get_content();
184             $rows = explode("\r\n", $contents);
185             $columns = scorm_get_aicc_columns($rows[0]);
186             $regexp = scorm_forge_cols_regexp($columns->columns);
187             for ($i = 1; $i < count($rows); $i++) {
188                 if (preg_match($regexp, $rows[$i], $matches)) {
189                     for ($j = 0; $j < count($columns->columns); $j++) {
190                         $column = $columns->columns[$j];
191                         if (!isset($courses[$courseid]->elements[substr(trim($matches[$columns->mastercol + 1]), 1 , -1)])) {
192                             $courses[$courseid]->elements[substr(trim($matches[$columns->mastercol + 1]), 1 , -1)] = new stdClass();
193                         }
194                         $courses[$courseid]->elements[substr(trim($matches[$columns->mastercol + 1]), 1 , -1)]->$column = substr(trim($matches[$j + 1]), 1, -1);
195                     }
196                 }
197             }
198         }
199         if (isset($id->au)) {
200             $contents = $id->au->get_content();
201             $rows = explode("\r\n", $contents);
202             $columns = scorm_get_aicc_columns($rows[0]);
203             $regexp = scorm_forge_cols_regexp($columns->columns);
204             for ($i = 1; $i < count($rows); $i++) {
205                 if (preg_match($regexp, $rows[$i], $matches)) {
206                     for ($j = 0; $j < count($columns->columns); $j++) {
207                         $column = $columns->columns[$j];
208                         $courses[$courseid]->elements[substr(trim($matches[$columns->mastercol + 1]), 1, -1)]->$column = substr(trim($matches[$j + 1]), 1, -1);
209                     }
210                 }
211             }
212         }
213         if (isset($id->cst)) {
214             $contents = $id->cst->get_content();
215             $rows = explode("\r\n", $contents);
216             $columns = scorm_get_aicc_columns($rows[0], 'block');
217             $regexp = scorm_forge_cols_regexp($columns->columns, '(.+)?,');
218             for ($i = 1; $i < count($rows); $i++) {
219                 if (preg_match($regexp, $rows[$i], $matches)) {
220                     for ($j = 0; $j < count($columns->columns); $j++) {
221                         if ($j != $columns->mastercol) {
222                             $element = substr(trim($matches[$j + 1]), 1 , -1);
223                             if (!empty($element)) {
224                                 $courses[$courseid]->elements[$element]->parent = substr(trim($matches[$columns->mastercol + 1]), 1, -1);
225                             }
226                         }
227                     }
228                 }
229             }
230         }
231         if (isset($id->ort)) {
232             $contents = $id->ort->get_content();
233             $rows = explode("\r\n", $contents);
234             $columns = scorm_get_aicc_columns($rows[0], 'course_element');
235             $regexp = scorm_forge_cols_regexp($columns->columns, '(.+)?,');
236             for ($i = 1; $i < count($rows); $i++) {
237                 if (preg_match($regexp, $rows[$i], $matches)) {
238                     for ($j = 0; $j < count($matches) - 1; $j++) {
239                         if ($j != $columns->mastercol) {
240                             $courses[$courseid]->elements[substr(trim($matches[$j + 1]), 1, -1)]->parent = substr(trim($matches[$columns->mastercol + 1]), 1, -1);
241                         }
242                     }
243                 }
244             }
245         }
246         if (isset($id->pre)) {
247             $contents = $id->pre->get_content();
248             $rows = explode("\r\n", $contents);
249             $columns = scorm_get_aicc_columns($rows[0], 'structure_element');
250             $regexp = scorm_forge_cols_regexp($columns->columns, '(.+),');
251             for ($i = 1; $i < count($rows); $i++) {
252                 if (preg_match($regexp, $rows[$i], $matches)) {
253                     $courses[$courseid]->elements[$columns->mastercol + 1]->prerequisites = substr(trim($matches[2 - $columns->mastercol]), 1, -1);
254                 }
255             }
256         }
257         if (isset($id->cmp)) {
258             $contents = $id->cmp->get_content();
259             $rows = explode("\r\n", $contents);
260         }
261     }
263     $oldscoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id));
264     $sortorder = 0;
265     $launch = 0;
266     if (isset($courses)) {
267         foreach ($courses as $course) {
268             $sortorder++;
269             $sco = new stdClass();
270             $sco->identifier = $course->id;
271             $sco->scorm = $scorm->id;
272             $sco->organization = '';
273             $sco->title = $course->title;
274             $sco->parent = '/';
275             $sco->launch = '';
276             $sco->scormtype = '';
277             $sco->sortorder = $sortorder;
279             if ($ss = $DB->get_record('scorm_scoes', array('scorm' => $scorm->id,
280                                                            'identifier' => $sco->identifier))) {
281                 $id = $ss->id;
282                 $sco->id = $id;
283                 $DB->update_record('scorm_scoes', $sco);
284                 unset($oldscoes[$id]);
285             } else {
286                 $id = $DB->insert_record('scorm_scoes', $sco);
287             }
289             if ($launch == 0) {
290                 $launch = $id;
291             }
292             if (isset($course->elements)) {
293                 foreach ($course->elements as $element) {
294                     unset($sco);
295                     $sco = new stdClass();
296                     $sco->identifier = $element->system_id;
297                     $sco->scorm = $scorm->id;
298                     $sco->organization = $course->id;
299                     $sco->title = $element->title;
301                     if (!isset($element->parent)) {
302                         $sco->parent = '/';
303                     } else if (strtolower($element->parent) == 'root') {
304                         $sco->parent = $course->id;
305                     } else {
306                         $sco->parent = $element->parent;
307                     }
308                     $sco->launch = '';
309                     $sco->scormtype = '';
310                     $sco->previous = 0;
311                     $sco->next = 0;
312                     $id = null;
313                     // Is it an Assignable Unit (AU)?
314                     if (isset($element->file_name)) {
315                         $sco->launch = $element->file_name;
316                         $sco->scormtype = 'sco';
317                     }
318                     if ($oldscoid = scorm_array_search('identifier', $sco->identifier, $oldscoes)) {
319                         $sco->id = $oldscoid;
320                         $DB->update_record('scorm_scoes', $sco);
321                         $id = $oldscoid;
322                         $DB->delete_records('scorm_scoes_data', array('scoid' => $oldscoid));
323                         unset($oldscoes[$oldscoid]);
324                     } else {
325                         $id = $DB->insert_record('scorm_scoes', $sco);
326                     }
327                     if (!empty($id)) {
328                         $scodata = new stdClass();
329                         $scodata->scoid = $id;
330                         if (isset($element->web_launch)) {
331                             $scodata->name = 'parameters';
332                             $scodata->value = $element->web_launch;
333                             $dataid = $DB->insert_record('scorm_scoes_data', $scodata);
334                         }
335                         if (isset($element->prerequisites)) {
336                             $scodata->name = 'prerequisites';
337                             $scodata->value = $element->prerequisites;
338                             $dataid = $DB->insert_record('scorm_scoes_data', $scodata);
339                         }
340                         if (isset($element->max_time_allowed)) {
341                             $scodata->name = 'max_time_allowed';
342                             $scodata->value = $element->max_time_allowed;
343                             $dataid = $DB->insert_record('scorm_scoes_data', $scodata);
344                         }
345                         if (isset($element->time_limit_action)) {
346                             $scodata->name = 'time_limit_action';
347                             $scodata->value = $element->time_limit_action;
348                             $dataid = $DB->insert_record('scorm_scoes_data', $scodata);
349                         }
350                         if (isset($element->mastery_score)) {
351                             $scodata->name = 'mastery_score';
352                             $scodata->value = $element->mastery_score;
353                             $dataid = $DB->insert_record('scorm_scoes_data', $scodata);
354                         }
355                         if (isset($element->core_vendor)) {
356                             $scodata->name = 'datafromlms';
357                             $scodata->value = preg_replace('/<cr>/i', "\r\n", $element->core_vendor);
358                             $dataid = $DB->insert_record('scorm_scoes_data', $scodata);
359                         }
360                     }
361                     if ($launch == 0) {
362                         $launch = $id;
363                     }
364                 }
365             }
366         }
367     }
368     if (!empty($oldscoes)) {
369         foreach ($oldscoes as $oldsco) {
370             $DB->delete_records('scorm_scoes', array('id' => $oldsco->id));
371             $DB->delete_records('scorm_scoes_track', array('scoid' => $oldsco->id));
372         }
373     }
375     // Find first launchable object.
376     $sqlselect = 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true);
377     // We use get_records here as we need to pass a limit in the query that works cross db.
378     $scoes = $DB->get_records_select('scorm_scoes', $sqlselect, array($scorm->id), 'sortorder', 'id', 0, 1);
379     if (!empty($scoes)) {
380         $sco = reset($scoes); // We only care about the first record - the above query only returns one.
381         $scorm->launch = $sco->id;
382     } else {
383         $scorm->launch = $launch;
384     }
386     $scorm->version = 'AICC';
388     return true;
391 /**
392  * Given a scormid creates an AICC Session record to allow HACP
393  *
394  * @param int $scormid - id from scorm table
395  * @return string hacpsession
396  */
397 function scorm_aicc_get_hacp_session($scormid) {
398     global $USER, $DB, $SESSION;
399     $cfgscorm = get_config('scorm');
400     if (empty($cfgscorm->allowaicchacp)) {
401         return false;
402     }
403     $now = time();
405     $hacpsession = $SESSION->scorm;
406     $hacpsession->scormid = $scormid;
407     $hacpsession->hacpsession = random_string(20);
408     $hacpsession->userid      = $USER->id;
409     $hacpsession->timecreated = $now;
410     $hacpsession->timemodified = $now;
411     $DB->insert_record('scorm_aicc_session', $hacpsession);
413     return $hacpsession->hacpsession;
416 /**
417  * Check the hacp_session for whether it is valid.
418  *
419  * @param string $hacpsession The hacpsession value to check (optional). Normally leave this blank
420  *      and this function will do required_param('sesskey', ...).
421  * @return mixed - false if invalid, otherwise returns record from scorm_aicc_session table.
422  */
423 function scorm_aicc_confirm_hacp_session($hacpsession) {
424     global $DB;
425     $cfgscorm = get_config('scorm');
426     if (empty($cfgscorm->allowaicchacp)) {
427         return false;
428     }
429     $time = time() - ($cfgscorm->aicchacptimeout * 60);
430     $sql = "hacpsession = ? AND timemodified > ?";
431     $hacpsession = $DB->get_record_select('scorm_aicc_session', $sql, array($hacpsession, $time));
432     if (!empty($hacpsession)) { // Update timemodified as this is still an active session - resets the timeout.
433         $hacpsession->timemodified = time();
434         $DB->update_record('scorm_aicc_session', $hacpsession);
435     }
436     return $hacpsession;
439 /**
440  * generate a simple single activity AICC object
441  * structure to wrap around and externally linked
442  * AICC package URL
443  *
444  * @param object $scorm package record
445  */
446 function scorm_aicc_generate_simple_sco($scorm) {
447     global $DB;
448     // Find the oldest one.
449     $scos = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id), 'id');
450     if (!empty($scos)) {
451         $sco = array_shift($scos);
452     } else {
453         $sco = new stdClass();
454     }
455     // Get rid of old ones.
456     foreach ($scos as $oldsco) {
457         $DB->delete_records('scorm_scoes', array('id' => $oldsco->id));
458         $DB->delete_records('scorm_scoes_track', array('scoid' => $oldsco->id));
459     }
461     $sco->identifier = 'A1';
462     $sco->scorm = $scorm->id;
463     $sco->organization = '';
464     $sco->title = $scorm->name;
465     $sco->parent = '/';
466     // Add the HACP signal to the activity launcher.
467     if (preg_match('/\?/', $scorm->reference)) {
468         $sco->launch = $scorm->reference.'&CMI=HACP';
469     } else {
470         $sco->launch = $scorm->reference.'?CMI=HACP';
471     }
472     $sco->scormtype = 'sco';
473     if (isset($sco->id)) {
474         $DB->update_record('scorm_scoes', $sco);
475         $id = $sco->id;
476     } else {
477         $id = $DB->insert_record('scorm_scoes', $sco);
478     }
479     return $id;
482 /**
483  * Sets up $userdata array and default values for AICC package.
484  *
485  * @param stdClass $userdata an empty stdClass variable that should be set up with user values
486  * @param object $scorm package record
487  * @param string $scoid SCO Id
488  * @param string $attempt attempt number for the user
489  * @param string $mode scorm display mode type
490  * @return array The default values that should be used for AICC package
491  */
492 function get_scorm_default (&$userdata, $scorm, $scoid, $attempt, $mode) {
493     global $USER;
494     $aiccuserid = get_config('scorm', 'aiccuserid');
495     if (!empty($aiccuserid)) {
496         $userdata->student_id = $USER->id;
497     } else {
498         $userdata->student_id = $USER->username;
499     }
500     $userdata->student_name = $USER->lastname .', '. $USER->firstname;
502     if ($usertrack = scorm_get_tracks($scoid, $USER->id, $attempt)) {
503         foreach ($usertrack as $key => $value) {
504             $userdata->$key = $value;
505         }
506     } else {
507         $userdata->status = '';
508         $userdata->score_raw = '';
509     }
511     if ($scodatas = scorm_get_sco($scoid, SCO_DATA)) {
512         foreach ($scodatas as $key => $value) {
513             $userdata->$key = $value;
514         }
515     } else {
516         print_error('cannotfindsco', 'scorm');
517     }
518     if (!$sco = scorm_get_sco($scoid)) {
519         print_error('cannotfindsco', 'scorm');
520     }
522     $userdata->mode = 'normal';
523     if (!empty($mode)) {
524         $userdata->mode = $mode;
525     }
526     if ($userdata->mode == 'normal') {
527         $userdata->credit = 'credit';
528     } else {
529         $userdata->credit = 'no-credit';
530     }
532     if (isset($userdata->status)) {
533         if ($userdata->status == '') {
534             $userdata->entry = 'ab-initio';
535         } else {
536             if (isset($userdata->{'cmi.core.exit'}) && ($userdata->{'cmi.core.exit'} == 'suspend')) {
537                 $userdata->entry = 'resume';
538             } else {
539                 $userdata->entry = '';
540             }
541         }
542     }
544     $def = array();
545     $def['cmi.core.student_id'] = $userdata->student_id;
546     $def['cmi.core.student_name'] = $userdata->student_name;
547     $def['cmi.core.credit'] = $userdata->credit;
548     $def['cmi.core.entry'] = $userdata->entry;
549     $def['cmi.launch_data'] = scorm_isset($userdata, 'datafromlms');
550     $def['cmi.core.lesson_mode'] = $userdata->mode;
551     $def['cmi.student_data.attempt_number'] = scorm_isset($userdata, 'cmi.student_data.attempt_number');
552     $def['cmi.student_data.mastery_score'] = scorm_isset($userdata, 'mastery_score');
553     $def['cmi.student_data.max_time_allowed'] = scorm_isset($userdata, 'max_time_allowed');
554     $def['cmi.student_data.time_limit_action'] = scorm_isset($userdata, 'time_limit_action');
555     $def['cmi.student_data.tries_during_lesson'] = scorm_isset($userdata, 'cmi.student_data.tries_during_lesson');
557     $def['cmi.core.lesson_location'] = scorm_isset($userdata, 'cmi.core.lesson_location');
558     $def['cmi.core.lesson_status'] = scorm_isset($userdata, 'cmi.core.lesson_status');
559     $def['cmi.core.exit'] = scorm_isset($userdata, 'cmi.core.exit');
560     $def['cmi.core.score.raw'] = scorm_isset($userdata, 'cmi.core.score.raw');
561     $def['cmi.core.score.max'] = scorm_isset($userdata, 'cmi.core.score.max');
562     $def['cmi.core.score.min'] = scorm_isset($userdata, 'cmi.core.score.min');
563     $def['cmi.core.total_time'] = scorm_isset($userdata, 'cmi.core.total_time', '00:00:00');
564     $def['cmi.suspend_data'] = scorm_isset($userdata, 'cmi.suspend_data');
565     $def['cmi.comments'] = scorm_isset($userdata, 'cmi.comments');
567     return $def;