diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..e69de29
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dbe9c82
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.vscode/
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index aef8c13..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "github.copilot.enable": {
-
- "*": true,
- "plaintext": false,
- "markdown": false,
- "scminput": false
- },
- "github.copilot.advanced": {}
-}
\ No newline at end of file
diff --git a/AUview.php b/AUview.php
index c6da1a8..13318ea 100755
--- a/AUview.php
+++ b/AUview.php
@@ -1,259 +1,211 @@
-.
-
-/**
- * Prints an AUs session information annd allows retreival of session or start of new one.
- *
- * @copyright 2023 Megan Bohland
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-//namespace cmi5;
-//For some reason using the namespace cmi5; here breaks the code.
-//It cannot find html_table class.
-
-
-require_once(dirname(dirname(dirname(__FILE__))) . '/config.php');
-require('header.php');
-
-//For connecting to Progress class - MB
-require_once("$CFG->dirroot/mod/cmi5launch/cmi5PHP/src/Progress.php");
-require_once("$CFG->dirroot/mod/cmi5launch/cmi5PHP/src/sessionHelpers.php");
-require_once("$CFG->dirroot/lib/outputcomponents.php");
-
-global $cmi5launch, $USER;
-
-//Classes and functions
-$aus_helpers = new Au_Helpers;
-$ses_helpers = new Session_Helpers;
-$progress = new progress;
-$getProgress = $progress->getRetrieveStatement();
-$updateSession = $ses_helpers->getUpdateSession();
-$getAUs = $aus_helpers->getAUsFromDB();
-
-// Trigger module viewed event.
-$event = \mod_cmi5launch\event\course_module_viewed::create(array(
- 'objectid' => $cmi5launch->id,
- 'context' => $context,
-));
-$event->add_record_snapshot('course', $course);
-$event->add_record_snapshot('cmi5launch', $cmi5launch);
-$event->add_record_snapshot('course_modules', $cm);
-$event->trigger();
-
-// Print the page header.
-$PAGE->set_url('/mod/cmi5launch/view.php', array('id' => $cm->id));
-$PAGE->set_title(format_string($cmi5launch->name));
-$PAGE->set_heading(format_string($course->fullname));
-$PAGE->set_context($context);
-$PAGE->requires->jquery();
-
-// Output starts here.
-echo $OUTPUT->header();
-
-if ($cmi5launch->intro) { // Conditions to show the intro can change to look for own settings or whatever.
- echo $OUTPUT->box(
- format_module_intro('cmi5launch', $cmi5launch, $cm->id),
- 'generalbox mod_introbox',
- 'cmi5launchintro'
- );
-}
-
-// TODO: Put all the php inserted data as parameters on the functions and put the functions in a separate JS file.
-?>
-
-
-get_record('cmi5launch', array('id' => $cmi5launch->id));
-
-//Reload user course instance
-$usersCourse = $DB->get_record('cmi5launch_course', ['courseid' => $record->courseid, 'userid' => $USER->id]);
-
-//Retrieve the registration id
-$regid = $usersCourse->registrationid;
-
-//TODO
-//For later to change student view vs teacher
-//Lets check for the certain capability and display a message if it is found/not found
-//Excellent! The test works, now lets introduce like a flag, and use that to display progress or not
-//Well call it canSee for now
-/*
-$canSee = true | false;
-//now change it based on capability
-$context = context_module::instance($cm->id);
-if (has_capability('mod/cmi5launch:addinstance', $context)) {
- //This is someone we want to let see grades/progress!!!";
- $canSee = true;
-}else{
- //This is not someone to see grades!";
- echo "
";
- $canSee = false;
-}
-*/
-
-//If it is null there have been no previous sessions
-if (!$au->sessions == NULL) {
-
- //Array to hold info for table population
- $tableData = array();
-
- //Build table
- $table = new html_table();
- $table->id = 'cmi5launch_auSessionTable';
- $table->caption = get_string('modulenameplural', 'cmi5launch');
- $table->head = array(
- get_string('cmi5launchviewfirstlaunched', 'cmi5launch'),
- get_string('cmi5launchviewlastlaunched', 'cmi5launch'),
- get_string('cmi5launchviewprogress', 'cmi5launch'),
- get_string('cmi5launchviewgradeheader', 'cmi5launch'),
- get_string('cmi5launchviewlaunchlinkheader', 'cmi5launch'),
- );
-
- //Retrieve session ids
- $sessionIDs = json_decode($au->sessions);
-
- //Iterate through each session by id
- foreach($sessionIDs as $key => $sessionID){
-
- //Retrieve new info (if any) from CMI5 player on session
- $session = $updateSession($sessionID, $cmi5launch->id);
-
- //array to hold data for table
- $sessionInfo = array();
-
- //Retrieve createdAt and format
- $date = new DateTime($session->createdAt, new DateTimeZone('US/Eastern'));
- $date->setTimezone(new DateTimeZone('America/New_York'));
- $sessionInfo[] = $date->format('D d M Y H:i:s');
-
- ///Retrieve lastRequestTime and format
- $date = new DateTime($session->lastRequestTime, new DateTimeZone('US/Eastern'));
- $date->setTimezone(new DateTimeZone('America/New_York'));
- $sessionInfo[] = $date->format('D d M Y H:i:s');
-
- //Get progress from LRS
- $session = $getProgress($regid, $cmi5launch->id, $session);
- $sessionInfo[] = ("
" . implode("\n ", json_decode($session->progress) ) . ""); - - //add score to table - $sessionInfo[] = $session->score; - //Add score to array for AU - $sessionScores[] = $session->score; - - //Update session in DB - $DB->update_record('cmi5launch_sessions', $session); - - //Build launch link to continue session - $newSession = "false"; - $infoForNextPage = $sessionID . "," . $newSession; - - $sessionInfo[] = "" - . get_string('cmi5launchviewlaunchlink', 'cmi5launch') . ""; - - //add to be fed to table - $tableData[] = $sessionInfo; - } - - //Write table - $table->data = $tableData; - echo html_writer::table($table); - - //Save the session scores to AU, it is ok to overwrite - $au->scores = json_encode($sessionScores); - - //Update AU in table with new info - $DB->update_record('cmi5launch_aus', $au); -} - -//Build the new session link -$newSession = "true"; -//Create a string to pass the auid and new session info to next page (launch.php) -$infoForNextPage = $auID . "," . $newSession; -//New attempt -echo "
" - . get_string('cmi5launch_attempt', 'cmi5launch') - . "
"; - -// Add a form to be posted based on the attempt selected. -?> - -footer(); \ No newline at end of file +. + +/** + * Prints an AUs session information annd allows start of new one. + * @copyright 2023 Megan Bohland + * @copyright Based on work by 2013 Andrew Downes + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_cmi5launch\local\session_helpers; +use mod_cmi5launch\local\au_helpers; + +require_once(dirname(dirname(dirname(__FILE__))) . '/config.php'); +require('header.php'); +require_once("$CFG->dirroot/lib/outputcomponents.php"); + +require_login($course, false, $cm); + +global $cmi5launch, $USER; + +// Classes and functions. +$auhelper = new au_helpers; +$sessionhelper = new session_helpers; +$retrievesession = $sessionhelper->cmi5launch_get_retrieve_sessions_from_db(); +$updatesession = $sessionhelper->cmi5launch_get_update_session(); +$retrieveaus = $auhelper->get_cmi5launch_retrieve_aus_from_db(); + +// MB - Not currently using events, but may in future. +/* +// Trigger module viewed event. +$event = \mod_cmi5launch\event\course_module_viewed::create(array( + 'objectid' => $cmi5launch->id, + 'context' => $context, +)); +$event->add_record_snapshot('course', $course); +$event->add_record_snapshot('cmi5launch', $cmi5launch); +$event->add_record_snapshot('course_modules', $cm); +$event->trigger(); +*/ + +// Print the page header. +$PAGE->set_url('/mod/cmi5launch/view.php', array('id' => $cm->id)); +$PAGE->set_title(format_string($cmi5launch->name)); +$PAGE->set_heading(format_string($course->fullname)); +$PAGE->set_context($context); +$PAGE->requires->jquery(); + +// Output starts here. +echo $OUTPUT->header(); + +// Create the back button. +?> + + + + +id); + +// Retrieve appropriate AU from DB. +$au = $retrieveaus($auid); + +// Array to hold session scores for the AU. +$sessionscores = array(); + +// Reload cmi5 instance. +$record = $DB->get_record('cmi5launch', array('id' => $cmi5launch->id)); + +// Reload user course instance. +$userscourse = $DB->get_record('cmi5launch_usercourse', ['courseid' => $record->courseid, 'userid' => $USER->id]); + +// Retrieve the registration id. +$regid = $userscourse->registrationid; + +// If it is null there have been no previous sessions. +if (!$au->sessions == null) { + + // Array to hold info for table population. + $tabledata = array(); + + // Build table. + $table = new html_table(); + $table->id = 'cmi5launch_auSessionTable'; + $table->caption = get_string('modulenameplural', 'cmi5launch'); + $table->head = array( + get_string('cmi5launchviewfirstlaunched', 'cmi5launch'), + get_string('cmi5launchviewlastlaunched', 'cmi5launch'), + get_string('cmi5launchviewprogress', 'cmi5launch'), + get_string('cmi5launchviewgradeheader', 'cmi5launch'), + ); + + // Retrieve session ids. + $sessionids = json_decode($au->sessions); + + // Iterate through each session by id. + foreach ($sessionids as $key => $sessionid) { + + // Get the session from DB with session id. + $session = $retrievesession($sessionid); + + // Array to hold data for table. + $sessioninfo = array(); + + + // Retrieve createdAt and format. + $date = new DateTime($session->createdat, new DateTimeZone('US/Eastern')); + $date->setTimezone(new DateTimeZone('America/New_York')); + $sessioninfo[] = $date->format('D d M Y H:i:s'); + + // Retrieve lastRequestTime and format. + $date = new DateTime($session->updatedat, new DateTimeZone('US/Eastern')); + $date->setTimezone(new DateTimeZone('America/New_York')); + $sessioninfo[] = $date->format('D d M Y H:i:s'); + + // Add progress to table. + $sessioninfo[] = ("" . implode("\n ", json_decode($session->progress) ) . ""); + + // Add score to table. + $sessioninfo[] = $session->score; + // Add score to array for AU. + $sessionscores[] = $session->score; + + // Add to be fed to table. + $tabledata[] = $sessioninfo; + } + + // Write table. + $table->data = $tabledata; + echo html_writer::table($table); + + // Update AU in table with new info. + $DB->update_record('cmi5launch_aus', $au); +} + +// Build the new session link. +$newsession = "true"; +// Create a string to pass the auid and new session info to next page (launch.php). +$infofornextpage = $auid . "," . $newsession; + +// New attempt button. +echo ""; + +// Add a form to be posted based on the attempt selected. +?> + + +footer(); diff --git a/README.md b/README.md index 5122a7c..2e2184c 100755 --- a/README.md +++ b/README.md @@ -1,59 +1,161 @@ cmi5launch + ============ A plug in for Moodle that allows the launch of cmi5 content which is then played in a cmi5 player and tracked to a separate LRS. +### Thanks + +This plugin is based on the xAPI Launch Link (Tin can launch) plugin developed by David Pesce and Andrew Downes. That plugin is located at https://github.com/davidpesce/moodle-mod_tincanlaunch. + +We also based many functions off examples from SCORM plugin that is packaged with Moodle. + ## What you will need To use this plugin you will need the following: + + * Moodle 4.0+ + + * Login details for the admin account + + * A Moodle course setup where you would like to add the activity + + * A cmi5 package in compliance with the cmi5 Specification (https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md) + + * A Learning Record Store (LRS) as defined in the xAPI specification (https://opensource.ieee.org/xapi/xapi-base-standard-documentation) + + * A running instance of the cmi5 prototype player (https://github.com/adlnet/CATAPULT/tree/main/player) + + * A copy of this plugin + ## Installation Instructions + Once the plugin is downloaded, be sure it is entirely in a file named 'cmi5launch.' Then compress that file into zip. The zip file _must_ be named 'cmi5_moodle.zip' to be uploaded to Moodle. + ### Install plugin in Moodle + To install the CMI5 plugin into Moodle - + + - Go to _Site Administration_ + + - Go to _Plugins_ + + - Click on _Install Plugins_ + + - Currently the only way to install plugin is via zip file + + - Under _Install plugin from ZIP file_ upload the plugin. It must be in a zip file named 'cmi5_moodle.zip'. + + - Follow the Moodle instructions to upload plugin to the site -Once Moodle has finished uploading the plugin it will brin up the _cmi5 Launch Link_ settings page. This will enable you to set the information to connect to your LRS and instance of your running CMI5 player. These are the fields: + + +Once Moodle has finished uploading the plugin it will bring up the _cmi5 Launch Link_ settings page. This will enable you to set the information to connect to your LRS and instance of your running CMI5 player. These are the fields: + + + - Endpoint + + - The LRS endpoint, to enable the plugin to communicate with your LRS. Ex: https://lrsendpoint.com/data/xAPI/. Must include trailing forward slash. + + - LRS integration + + - This is an optional dropdown for addtional LRS's. Defaults to 'None.' + + - LRS: Basic Username + + - The basic username for connecting to your LRS. + + - LRS: Basic Password + + - Your password for connecting to your LRS. + + - Duration + + - Used with 'LRS integrated basic authentication'. Requests the LRS to keep credentials valid for this number of minutes. Default is set to 9000. + + - Custom account homepage + + - If entered, Moodle will use this homePage in conjunction with the ID number user profile field to identify the learner. Defaults to https://moodle.com. + + - Identify by email + + - If selected, learners will be identified by their email address if they have one recorded in Moodle. + + - cmi5 Player URL - -The URL to communicate with the CMI5 player, can include port number(e.g. http://player.example.com or http://localhost:63398). Must NOT include a trailing forward slash. + : The URL to communicate with the CMI5 player, can include port number(e.g. http://player.example.com or http://localhost:63398). Must NOT include a trailing forward slash. + + + - cmi5 Player: Basic Username + + - This is the tenant username for the CMI5 player + + - cmi5 Player: Basic Password + + - This is the basic password to login to player. (IS this needed? check code - MB) + + - cmi5 Player: Bearer Token + + - The cmi5 tenant bearer token (should be a long string). -There are two sections "Default ccalues foor CMI5 LAunch Link activity grades" and "Default values for CMI5 Launch Link activity attempts and completion." These can be ignored for now as they are works in progress for future features. + + + +The next section "Default values for CMI5 Launch Link activity grades" deals with the grade setting. + + +- Grading method + + + - How the overall grade for CMI5 activities is calculated. If "Highest" the highest grade will be used, if "Average" the scores will be averaged. + + +- Maximum grade + + + - The Maxium the grade can be (default 100). + + +The section "Default values for CMI5 Launch Link activity attempts and completion" can be ignored for now as it is a work in progress for future features. + The plugin is now accessible as a resource to be added to a Moodle course! Under your Moodle courses in edit mode select _Add activity or resource_ and you will see _cmi5 Launch Link_ as an available option. Currently the Launch link can take a folder or XML compliant with cmi5 specification. @@ -62,78 +164,278 @@ The plugin is now accessible as a resource to be added to a Moodle course! Under This flowchart shows the path a user takes to get to a cmi5 Lesson Link. Once the link is clicked, the cmi5 Player opens in a new tab or window. The Moodle Application negotiates the connection by supplying credentials, tenant, and the current user session information. The Lesson Link contains a token in which both sides can track the user. + ```mermaid + flowchart TB + subgraph MOODLE[Moodle Application] + direction TB + Course(Course) --> Assignment(Assignment) --> Activity --> Link + Webhook[Progress\nEndpoint] + subgraph Activity + Link(Lesson Link) + end + end + MOODLE -.-> MoodleDB[(Moodle DB)] subgraph cmi5[cmi5 Player] + direction TB + Lesson --> Lesson + end cmi5 -.-> Webhook + Link <---> cmi5 + Link -.-> LRS[(LRS)] + cmi5 -.-> cmi5DB[(cmi5 DB)] + cmi5 -.-> LRS + ``` + + ## Sequence diagrams for connecting to CMI5 player + Following are the two functions Moodle uses to create a course and retrieve a course URL from the CMI5 player. + ### Create course + ```mermaid + sequenceDiagram + title: Create Course - + participant Moodle + participant CMI5 + participant C-DB as CMI5's MySQL Database + participant M-DB as Moodle's MySQL Database - - + Moodle->>+CMI5: Send POST to /api/v1/course + break error + CMI5-->>+Moodle: response other than 200 + Note over Moodle, CMI5: Check content-type, package, or token + end + CMI5->>+C-DB: Create course in DB + CMI5->>+Moodle: 200, returns JSON body + Moodle->>+M-DB: Save lmsId, Id, course metadata ``` + + ### Retrieve launch URL + + ```mermaid + sequenceDiagram + title: Retrieve URL - + participant Moodle + participant CMI5 + participant M-DB as Moodle's MySQL Database - - + Moodle->>+CMI5: Send POST to /api/v1/course/{courseId}/launchurl/{AU index} + break error + CMI5-->>+Moodle: response other than 200 + Note over Moodle, CMI5: Check actor account info, returnurl, token + end - + + CMI5->>+Moodle: 200, returns JSON body with session id, launch method, and launch url + + Moodle->>+M-DB: Save returned course info to DB ``` ## User progress -The cmi5 player tracks user progress by tracking whether an AU is satisfied or not. The LRS is a better way to track a users progress in any detail. Currently the Moodle plugin queries the LRS for progress. This in shows AUs as complete, in-progress, or not attempted. It also breaks down sessions into detail, ex; " The actor watched video at certain time". +The CMI5 player tracks user progress by tracking whether an AU is satisfied or not. The LRS is a better way to track a users progress in any detail. Currently the Moodle plugin queries the LRS for progress. This in shows AUs as complete, in-progress, or not attempted. It also breaks down sessions into detail, ex; " The actor watched video at certain time". + + +### Notes + + +#### Note about large packages. + + +You can control the size of file uploads to Moodle in Site Administration/General/Security/Site Security/Maximum uploaded file size. This setting is capped by the PHP settings post_max_size and upload_max_filesize, as well as the Apache setting LimitRequestBody. Be sure to increase these values, as well as php_max_execution or php_memory_limit if files are hanging. These can be adjusted in php.ini. If using Apache or similar their config files will need to be adjusted instead. + + +#### Notes about capabilities. + +This plugin has a built in capability that allows users to view all the grades for a course, showing all enrolled user's grades. This is 'mod/cmi5launch:viewgrades'. It is automatically assigned to the roles of 'teacher' and 'course creator'. It can be added to other roles as required. + + +## Database tables + + +This plugin use several of it's own tables. Their full descriptions can be seen in _db->install.xml_. This plugin tries to use the correct size and datatypes to account for different database languages Moodle supports. If there is a field error, database fields can be adjust in _Site Administration -> Development -> XMLDB editor_. + + +### Table - cmi5launch + +This table is the main activity table. It holds the basics of a CMI5 activity. + +| Field name | Field Type | Description | +| :---------- | :------------ |:------------ | +| id | int | Main ID assigned by Moodle| +| course | int | Course (id) cmi5launch activity belongs to | +| name | char | Name of course | +| intro | text | General introduction of the cmi5launch activity | +| introformat | int | Format of the intro field (MOODLE, HTML, MARKDOWN...) | +| cmi5activityid | char | The LMS id | +| registrationid | char | Course registration ID returned by CMI5 player. We want to use this to connect to LRS | +| returnurl | char | Tenants return URL, where it will go after closing course from launch URL | +| courseid | int | This is the course ID returned from the CMI5 player | +| cmi5verbid | char | Unique verb ID | +| cmi5expiry | int | Number of days to expire completion after | +| overridedefaults | int | Determines if the activity is inheriting the global defaults or has overridden them at the activity level | +| cmi5multipleregs | int | Allow multiple registrations (not recommended) | +| timecreated | int | The time the course was created | +| timemodified | int | Last time the course was modified | +| courseinfo | text | The full string of course info | +| aus | text | The AUs of the course, saved as array | + +### Table - cmi5launch_usercourse + +This table holds particular instances of a CMI5 activity (unique to user). + +| Field name | Field Type | Description | +| :---------- | :------------ |:------------ | +| id | int | Main ID for tracking | +| courseid | int | Course ID, assigned by CMI5 player | +| userid | int | User ID, combined with courseid can be used to retrieve unique records | +| cmi5launchurl | char | Launch URL | +| introformat | int | Format of the intro field (MOODLE, HTML, MARKDOWN...) | +| cmi5activityid | char | The LMS id | +| registrationid | char | Course registration ID returned by CMI5 player. We want to use this to connect to LRS | +| returnurl | char | Tenants return URL, where it will go after closing course from launch URL | +| aus | text | The AUs of the course, their DB IDs, saved as array | +| ausgrades | text | All the AUs and their grades (overall session grades) saved in this format: AU lmsid => [AU Title => [Scores from that title's sessions] ] | +| grade | int | The current overall grade for the course | + + +### Table cmi5launch_player + +This table stores the variables for API communication with the CMI5 player. + +| Field name | Field Type | Description | +| :---------- | :------------ |:------------ | +| id | int | The id of returned course IN MOODLE (This ID matches with the field ID in the cmi5launch table.) Moodle assigns ids sequentially. CMI5 also will assign course ids. These are separate. | +| name | char | Name field to hold the course name in Moodle. This may be different than the course name in it's own xml file. | +| tenantid | int | The Tenant ID. | +| tenantname | char | The name of the user for communication with the CMI5 player. This is set in plugin settings | +| courseid | int | The id of the returned course set by CMI5 player | +| Launchmethod | char | Whether the course's retrieved URL opens in it's own window or a new one | +| returnurl | char | The tenants return URL, where browser will redirect to after closing the course from launch URL | +| homepage | char | The tenant's homepage | +| registrationid | char | Course registration id returned by CMI5 player | +| sessionid | int | ID created for each session, when the AU launch url is requested | +| launchurl | char | The AU launchurl generated by and returned from the CMI5 player | +| cmi5playerurl | char | The URL the CMI5 player is located at | +| cmi5playerport | int | The port the CMI5 player can be communicated with (if applicable) | + + +### Table cmi5launch_sessions + +This table stores information pertaining to each users individual session. Just as one course can have multiple AUs, one AU can have multiple sessions. Every time the user requests a launch link from the player a new session is started. + +| Field name | Field Type | Description | +| :---------- | :------------ |:------------ | +| id | int | Main ID for tracking, assigned by Moodle | +| sessionid | int | The session ID. Created by the CMI5 player and returned after launch URL request | +| userid | int | The user ID, which when combined with a course id can be used to retrieve unique records | +| registrationscoursesausid | int | ID assigned by player to AUs | +| tenantname | char | The tenant name | +| lmsid | int | The lmsid of the AU to match with object id from the LRS for pulling progress info | +| createdat | char | The time a session was started | +| updatedat | char | The time the session was last updated | +| code | char | Unique code for each session assigned by CMI5 player | +| launchtokenid | char | Launchtoken assigned by CMI5 player | +| lastrequesttime | char | Time a session was last requested | +| launchmode | char | Launch mode, which is separate then launchmethod. It can be 'Normal', 'Browse', or 'Review' | +| masteryscore | int | The amount the score goes toward a masteryscore (may not be applicable) | +| score | int | The score of the session (returned from the 'result' parameter in statements from the LRS) | +| response | text | The response returned from 'result' parameter (in the statements from the LRS) | +| islaunched | int | Whether the session has been launched | +| isinitialized | int | Whether the session has been initialized | +| initializedat | char | Time a session was initialized | +| duration | char | Time a session lasted ( retrieved from 'result' parameter in statements from LRS) | +| iscompleted | int | Whether the session has been completed | +| ispassed | int | Whether the session has been passed | +| isfailed | int | Whether the session has been failed | +| isterminated | int | Whether the session has been terminated | +| isabandoned | int | Whether the session has been abandoned | +| progress | text | The full string of the sessions progress reported in a statement from the LRS | +| courseid | int | The id of the course generated by the CMI5 player | +| launchmethod | char | Whether the course's retrieved URL opens in it own window or not | +| launchurl | char | The launch URL returned from the CMI5 player | + + + +### Table cmi5launch_aus + +This table stores information pertaining to each users individual AU. One course can have multiple AUs. + +| Field name | Field Type | Description | +| :---------- | :------------ |:------------ | +| id | int | Main ID for tracking, assigned by Moodle | +| attempt | int | The attempt of the AU, ie, first, second, third, etc. | +| launchmethod | char | Whether the course's retrieved URL opens in it own window or no | +| lmsid | char | The AU ID from the course packet. The singular CMI5 compliant ID | +| url | char | The ending to be added to an URL to direct to a specific AU | +| type | char | The AU activity type | +| title | char | The AU title from the course package | +| moveon | char | The requirements for the AU to be marked as done | +| auindex | int | The AU's index (used to separate the AUs in a CMI5 activity | +| parents | char | The AU's parent activities | +| objectives | text | The AU's objectives | +| description | text | The AU's description | +| activitytype | char | Activity type of AU | +| masteryscore | int | The amount the score goes toward a masteryscore (may not be applicable) | +| completed | int | Whether an AU has met completed criteria (0 if false, 1 if true) | +| passed | int | Whether an AU has met passed criteria (0 if false, 1 if true) | +| inprogress | int | Whether an AU is in progress or not (has been started) (0 if false, 1 if true) | +| noattempt | int | Whether an AU has been attempted (0 if false, 1 if true) +| satisfied | char | Whether an AU has been satisfied (true, false, or null ) | +| sessions | text | The session IDs of the AU, saved as array | +| scores | text | The session's scores, saved as array | +| grade | int | The overall grade of the AU (based on what grade type setting is diff --git a/backup/moodle2/backup_cmi5launch_activity_task.class.php b/backup/moodle2/backup_cmi5launch_activity_task.class.php new file mode 100755 index 0000000..cee9d0b --- /dev/null +++ b/backup/moodle2/backup_cmi5launch_activity_task.class.php @@ -0,0 +1,73 @@ +. + +/** + * Description of cmi5launch backup task + * + * @package mod_cmi5launch + * @copyright 2023 Megan Bohland + * @copyright Based on work by 2016 onward Remote-Learner.net Inc + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot.'/mod/cmi5launch/backup/moodle2/backup_cmi5launch_stepslib.php'); // Because it exists (must). +require_once($CFG->dirroot.'/mod/cmi5launch/backup/moodle2/backup_cmi5launch_settingslib.php'); // Because it exists (optional). + +class backup_cmi5launch_activity_task extends backup_activity_task { + + /** + * Define (add) particular settings this activity can have. + * + * @return void + */ + protected function define_my_settings() { + // No particular settings for this activity. + } + + /** + * Define (add) particular steps this activity can have. + * + * @return void + */ + protected function define_my_steps() { + // Module cmi5launch only has one structure step. + $this->add_step(new backup_cmi5launch_activity_structure_step('cmi5launch_structure', 'cmi5launch.xml')); + } + + /** + * Code the transformations to perform in the activity in + * order to get transportable (encoded) links. + * + * @param string $content + * @return string encoded content + */ + public static function encode_content_links($content) { + global $CFG; + + $base = preg_quote($CFG->wwwroot, "/"); + + // Link to the list of cmi5launchs. + $search = "/($base\/mod\/cmi5launch\/index.php\?id=)([0-9]+)/"; + $content = preg_replace($search, '$@cmi5launchINDEX*$2@$', $content); + + $search = "/($base\/mod\/cmi5launch\/view.php\?id=)([0-9]+)/"; + $content = preg_replace($search, '$@cmi5launchVIEWBYID*$2@$', $content); + + return $content; + } +} diff --git a/cmi5PHP/src/aus.php b/backup/moodle2/backup_cmi5launch_settingslib.php old mode 100644 new mode 100755 similarity index 51% rename from cmi5PHP/src/aus.php rename to backup/moodle2/backup_cmi5launch_settingslib.php index c2689df..7c90bb5 --- a/cmi5PHP/src/aus.php +++ b/backup/moodle2/backup_cmi5launch_settingslib.php @@ -13,26 +13,18 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see
Error attempting to get AU data from DB. Check AU id. AU id is: " . $auid ."
"; + + return false; + } else { + + $auitem = $DB->get_record('cmi5launch_aus', array('id' => $auid)); + + $au = new au($auitem); + } + + // Return our new list of AU. + return $au; + } + +} diff --git a/classes/local/cmi5_connectors.php b/classes/local/cmi5_connectors.php new file mode 100755 index 0000000..82ab582 --- /dev/null +++ b/classes/local/cmi5_connectors.php @@ -0,0 +1,482 @@ +. + +/** + * Class to hold ways to communicate with CMI5 player through its API's. + * + * @copyright 2023 Megan Bohland + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + namespace mod_cmi5launch\local; + defined('MOODLE_INTERNAL') || die(); + +class cmi5_connectors { + + public function cmi5launch_get_create_tenant() { + return [$this, 'cmi5launch_create_tenant']; + } + public function cmi5launch_get_retrieve_token() { + return [$this, 'cmi5launch_retrieve_token']; + } + public function cmi5launch_get_retrieve_url() { + return [$this, 'cmi5launch_retrieve_url']; + } + public function cmi5launch_get_create_course() { + return [$this, 'cmi5launch_create_course']; + } + public function cmi5launch_get_session_info() { + return [$this, 'cmi5launch_retrieve_session_info_from_player']; + + } + public function cmi5launch_get_registration_with_post() { + return [$this, 'cmi5launch_retrieve_registration_with_post']; + } + public function cmi5launch_get_registration_with_get() { + return [$this, 'cmi5launch_retrieve_registration_with_get']; + } + public function cmi5launch_get_send_request_to_cmi5_player_post() { + return [$this, 'cmi5launch_send_request_to_cmi5_player_post']; + + } + + /** + * Function to create a course. + * @param mixed $id - tenant id in Moodle. + * @param mixed $tenanttoken - tenant bearer token. + * @param mixed $filename -- The filename of the course to be imported, to be added to url POST request. + * @return bool|string - Response from cmi5 player. + */ + public function cmi5launch_create_course($id, $tenanttoken, $filename) { + + global $DB, $CFG; + $settings = cmi5launch_settings($id); + + // Build URL to import course to. + $url = $settings['cmi5launchplayerurl'] . "/api/v1/course"; + + // To determine the headers. + $filetype = "zip"; + + $databody = $filename->get_content(); + // Sends the stream to the specified URL. + $result = $this->cmi5launch_send_request_to_cmi5_player_post($databody, $url, $filetype, $tenanttoken); + + // Check result and display message if not 200. + $resulttest = $this->cmi5launch_connectors_error_message($result, "creating the course"); + + if ($resulttest == true) { + // Return an array with course info. + return $result; + } + } + + /** + * Function to create a tenant. + * @param $urltosend - URL retrieved from user in URL textbox. + * @param $username - username. + * @param $password - password. + * @param $newtenantname - the name the new tenant will be, retreived from Tenant Name textbox. + */ + public function cmi5launch_create_tenant($urltosend, $username, $password, $newtenantname) { + + global $CFG; + + // The body of the request must be made as array first. + $data = array( + 'code' => $newtenantname); + + // To determine the headers. + $filetype = "json"; + + // Data needs to be JSON encoded. + $data = json_encode($data); + + // Sends the stream to the specified URL. + $result = $this->cmi5launch_send_request_to_cmi5_player_post($data, $urltosend, $filetype, $username, $password); + + // Check result and display message if not 200. + $resulttest = $this->cmi5launch_connectors_error_message($result, "creating the tenant"); + + if ($resulttest == true) { + + // Decode returned response into array. + $returnedinfo = json_decode($result, true); + + // Return an array with tenant name and info. + return $returnedinfo; + }; + } + + /** + * Function to retrieve registration from cmi5 player. + * This way uses the registration ID and GET request. + * Registration is "code" in returned json body. + * @param $registration - registration UUID + * @param $id - cmi5 launch id + */ + public function cmi5launch_retrieve_registration_with_get($registration, $id) { + + $settings = cmi5launch_settings($id); + + $token = $settings['cmi5launchtenanttoken']; + $playerurl = $settings['cmi5launchplayerurl']; + + global $CFG; + + // Build URL for launch URL request. + $url = $playerurl . "/api/v1/registration/" . $registration; + + // Sends the stream to the specified URL. + $result = $this->cmi5launch_send_request_to_cmi5_player_get($token, $url); + + // Check result and display message if not 200. + $resulttest = $this->cmi5launch_connectors_error_message($result, "retrieving the registration"); + + if ($resulttest == true) { + + return $result; + } + } + + /** + * Function to retrieve registration from cmi5 player. + * This way uses the course id and actor name. + * As this is a POST request it returns a new code everytime it is called. + * Registration is "code" in returned json body. + * @param $courseid - course id - The course ID in the CMI5 player. + * @param $id - the course id in MOODLE. + */ + public function cmi5launch_retrieve_registration_with_post($courseid, $id) { + + global $USER; + + $settings = cmi5launch_settings($id); + + $actor = $USER->username; + $token = $settings['cmi5launchtenanttoken']; + $playerurl = $settings['cmi5launchplayerurl']; + $homepage = $settings['cmi5launchcustomacchp']; + global $CFG; + + // Build URL for launch URL request. + $url = $playerurl . "/api/v1/registration"; + + // The body of the request must be made as array first. + $data = array( + 'courseId' => $courseid, + 'actor' => array( + 'account' => array( + "homePage" => $homepage, + "name" => $actor, + ), + ), + ); + + // Data needs to be JSON encoded. + $data = json_encode($data); + // To determine the headers. + $filetype = "json"; + + // Sends the stream to the specified URL. + $result = $this->cmi5launch_send_request_to_cmi5_player_post($data, $url, $filetype, $token); + + // Check result and display message if not 200. + $resulttest = $this->cmi5launch_connectors_error_message($result, "retrieving the registration"); + + // Catch errors. + if ($resulttest == true) { + + $registrationinfo = json_decode($result, true); + + // The returned 'registration info' is a large json object. + // Code is the registration id we want. + $registration = $registrationinfo["code"]; + + return $registration; + } + } + + /** + * Function to retrieve a token from cmi5 player. + * @param $url - URL to send request to + * @param $username - username + * @param $password - password + * @param $audience - the name the of the audience using the token, + * @param #tenantid - the id of the tenant + */ + public function cmi5launch_retrieve_token($url, $username, $password, $audience, $tenantid) { + + global $CFG; + + // The body of the request must be made as array first. + $data = array( + 'tenantId' => $tenantid, + 'audience' => $audience, + ); + $filetype = "json"; + + // Data needs to be JSON encoded. + $data = json_encode($data); + + // Sends the stream to the specified URL. + $result = $this->cmi5launch_send_request_to_cmi5_player_post($data, $url, $filetype, $username, $password); + + // Check result and display message if not 200. + $resulttest = $this->cmi5launch_connectors_error_message($result, "retrieving the registration"); + + if ($resulttest == true) { + + return $result; + } + } + + /** + * Function to retrieve a launch URL for an AU. + * @param $id - courses's ID in MOODLE to retrieve corect record. + * @param $auindex -AU's index to send to request for launch url. + */ + public function cmi5launch_retrieve_url($id, $auindex) { + + global $DB, $USER; + + // Retrieve actor record, this enables correct actor info for URL storage. + $record = $DB->get_record("cmi5launch", array('id' => $id)); + + $settings = cmi5launch_settings($id); + + $userscourse = $DB->get_record('cmi5launch_usercourse', ['courseid' => $record->courseid, 'userid' => $USER->id]); + + $registrationid = $userscourse->registrationid; + + $homepage = $settings['cmi5launchcustomacchp']; + $returnurl = $userscourse->returnurl; + $actor = $USER->username; + $token = $settings['cmi5launchtenanttoken']; + $playerurl = $settings['cmi5launchplayerurl']; + $courseid = $userscourse->courseid; + + // Build URL for launch URL request. + $url = $playerurl . "/api/v1/course/" . $courseid ."/launch-url/" . $auindex; + + $data = array( + 'actor' => array( + 'account' => array( + "homePage" => $homepage, + "name" => $actor, + ), + ), + 'returnUrl' => $returnurl, + 'reg' => $registrationid, + ); + + // To determine the headers. + $filetype = "json"; + + // Data needs to be JSON encoded. + $data = json_encode($data); + // Sends the stream to the specified URL. + $result = $this->cmi5launch_send_request_to_cmi5_player_post($data, $url, $filetype, $token); + + // Check result and display message if not 200. + $resulttest = $this->cmi5launch_connectors_error_message($result, "retrieving launch url"); + + // Catch errors. + if ($resulttest == true) { + + // Only return the URL. + $urldecoded = json_decode($result, true); + + return $urldecoded; + } + } + + /** + * Function to construct, send an URL, and save result as POST message to player. + * @param $databody - the data that will be used to construct the body of request as JSON. + * @param $url - The URL the request will be sent to. + * @param $filetype - The type of file being sent, either zip or json. + * @param ...$tokenorpassword is a variable length param. If one is passed, it is $token, if two it is $username and $password. + * @return - $result is the response from cmi5 player. + */ + public function cmi5launch_send_request_to_cmi5_player_post($databody, $url, $filetype, ...$tokenorpassword) { + + // Determine content type to be used in header. + // It is also the same as accepted type. + $contenttype = $filetype; + if ($contenttype == "zip") { + $contenttype = "application/zip\r\n"; + } else if ("json") { + $contenttype = "application/json\r\n"; + } + + // If number of args is greater than one it is for retrieving tenant info and args are username and password. + if (count($tokenorpassword) == 2 ) { + + $username = $tokenorpassword[0]; + $password = $tokenorpassword[1]; + + // Use key 'http' even if you send the request to https://... + // There can be multiple headers but as an array under the ONE header. + // Content(body) must be JSON encoded here, as that is what CMI5 player accepts. + $options = array( + 'http' => array( + 'method' => 'POST', + 'header' => array('Authorization: Basic '. base64_encode("$username:$password"), + "Content-Type: " .$contenttype . + "Accept: " . $contenttype), + 'content' => ($databody), + ), + ); + + // The options are here placed into a stream to be sent. + $context = stream_context_create($options); + + // Sends the stream to the specified URL and stores results. + // The false is use_include_path, which we dont want in this case, we want to go to the url. + $result = file_get_contents( $url, false, $context ); + + // Else the args are what we need for posting a course. + } else { + + // First arg will be token. + $token = $tokenorpassword[0]; + + // Use key 'http' even if you send the request to https://... + // There can be multiple headers but as an array under the ONE header + // content(body) must be JSON encoded here, as that is what CMI5 player accepts + // JSON_UNESCAPED_SLASHES used so http addresses are displayed correctly. + $options = array( + 'http' => array( + 'method' => 'POST', + 'ignore_errors' => true, + 'header' => array("Authorization: Bearer ". $token, + "Content-Type: " .$contenttype . + "Accept: " . $contenttype), + 'content' => ($databody), + ), + ); + + // The options are placed into a stream to be sent. + $context = stream_context_create(($options)); + + // Sends the stream to the specified URL and stores results. + // The false is use_include_path, which we dont want in this case, we want to go to the url. + $result = file_get_contents( $url, false, $context ); + + } + + // Return response. + return $result; + } + + /** + * Function to construct and send GET request to CMI5 player. + * @param $token - the token that will be used to authenticate the request. + * @param $url - The URL the request will be sent to. + * @return - $sessionDecoded is the response from cmi5 player. + */ + public function cmi5launch_send_request_to_cmi5_player_get($token, $url) { + // Use key 'http' even if you send the request to https://... + // There can be multiple headers but as an array under the ONE header + // content(body) must be JSON encoded here, as that is what CMI5 player accepts + // JSON_UNESCAPED_SLASHES used so http addresses are displayed correctly. + $options = array ( + 'http' => array ( + 'method' => 'GET', + 'ignore_errors' => true, + 'header' => array ("Authorization: Bearer ". $token, + "Content-Type: application/json\r\n" . + "Accept: application/json\r\n"), + ), + ); + + // The options are here placed into a stream to be sent. + $context = stream_context_create(($options)); + + // Sends the stream to the specified URL and stores results. False is to not use_include_path, we want to go to the url. + $launchresponse = file_get_contents( $url, false, $context ); + + $sessiondecoded = json_decode($launchresponse, true); + + return $sessiondecoded; + } + + /** + * Retrieve session info from cmi5player + * @param mixed $sessionid - the session id to retrieve + * @param mixed $id - cmi5 id + * @return mixed $sessionDecoded - the session info from cmi5 player. + */ + public function cmi5launch_retrieve_session_info_from_player($sessionid, $id) { + + global $DB; + + $settings = cmi5launch_settings($id); + + $token = $settings['cmi5launchtenanttoken']; + $playerurl = $settings['cmi5launchplayerurl']; + + // Build URL for launch URL request. + $url = $playerurl . "/api/v1/session/" . $sessionid; + + // Sends the stream to the specified URL. + $result = $this->cmi5launch_send_request_to_cmi5_player_get($token, $url); + + // Check result and display message if not 200. + $resulttest = $this->cmi5launch_connectors_error_message($result, "retrieving session info"); + + if ($resulttest == true) { + + return $result; + } + } + + + // An error message catcher. + /** + * Function to test returns from cmi5 player and display error message if found to be false + * // or not 200. + * @param mixed $resulttotest - The result to test. + * @param string $type - The type missing to be added to the error message. + * @return bool + */ + public function cmi5launch_connectors_error_message($resulttotest, $type) { + + // Decode result because if it is not 200 then something went wrong + // If it's a string, decode it. + if (is_string($resulttotest)) { + $resulttest = json_decode($resulttotest, true); + } else { + $resulttest = $resulttotest; + } + + if ($resulttest === false || array_key_exists("statusCode", $resulttest) && $resulttest["statusCode"] != 200) { + + echo "Error attempting to get session data from DB. Check session id.
"; + echo ""; + var_dump($sessionid); + echo ""; + + } else { + + $sessionitem = $DB->get_record('cmi5launch_sessions', array('sessionid' => $sessionid)); + + $session = new session($sessionitem); + + } + + // Return new session object. + return $session; + } + +} diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php new file mode 100644 index 0000000..5dc4905 --- /dev/null +++ b/classes/privacy/provider.php @@ -0,0 +1,440 @@ +. + +/** + * Class to implement Moodle's privacy APIs. + * + * @copyright 2024 Megan Bohland + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + namespace mod_cmi5launch\privacy; + + use core_privacy\local\metadata\collection; + use core_privacy\local\request\approved_contextlist; + use core_privacy\local\request\approved_userlist; + use core_privacy\local\request\contextlist; + use core_privacy\local\request\helper; + use core_privacy\local\request\userlist; + use core_privacy\local\request\writer; +class provider implements + // This plugin does store personal user data. + \core_privacy\local\metadata\provider, + \core_privacy\local\request\core_userlist_provider, + \core_privacy\local\request\plugin\provider + { + + public static function get_metadata(collection $collection): collection { + + // Database tables. + $collection->add_database_table( + 'cmi5launch_usercourse', + [ + 'id' => 'privacy:metadata:cmi5launch_usercourse:id', + 'userid' => 'privacy:metadata:cmi5launch_usercourse:userid', + 'registrationid' => 'privacy:metadata:cmi5launch_usercourse:registrationid', + 'ausgrades' => 'privacy:metadata:cmi5launch_usercourse:ausgrades', + 'grade' => 'privacy:metadata:cmi5launch_usercourse:grade', + ], + + 'privacy:metadata:cmi5launch_usercourse', + ); + $collection->add_database_table( + 'cmi5launch_sessions', + [ + 'id' => 'privacy:metadata:cmi5launch_sessions:id', + 'sessionid' => 'privacy:metadata:cmi5launch_sessions:sessionid', + 'userid' => 'privacy:metadata:cmi5launch_sessions:userid', + 'registrationscoursesausid' => 'privacy:metadata:cmi5launch_sessions:registrationscoursesausid', + 'createdat' => 'privacy:metadata:cmi5launch_sessions:createdat', + 'updatedat' => 'privacy:metadata:cmi5launch_sessions:updatedat', + 'code' => 'privacy:metadata:cmi5launch_sessions:code', + 'launchtokenid' => 'privacy:metadata:cmi5launch_sessions:launchtokenid', + 'lastrequesttime' => 'privacy:metadata:cmi5launch_sessions:lastrequesttime', + 'score' => 'privacy:metadata:cmi5launch_sessions:score', + 'islaunched' => 'privacy:metadata:cmi5launch_sessions:islaunched', + 'isinitialized' => 'privacy:metadata:cmi5launch_sessions:isinitialized', + 'initializedat' => 'privacy:metadata:cmi5launch_sessions:initializedat', + 'duration' => 'privacy:metadata:cmi5launch_sessions:duration', + 'iscompleted' => 'privacy:metadata:cmi5launch_sessions:iscompleted', + 'ispassed' => 'privacy:metadata:cmi5launch_sessions:ispassed', + 'isfailed' => 'privacy:metadata:cmi5launch_sessions:isfailed', + 'isterminated' => 'privacy:metadata:cmi5launch_sessions:isterminated', + 'isabandoned' => 'privacy:metadata:cmi5launch_sessions:isabandoned', + 'progress' => 'privacy:metadata:cmi5launch_sessions:progress', + 'launchurl' => 'privacy:metadata:cmi5launch_sessions:launchurl', + + ], + + 'privacy:metadata:cmi5launch_sessions', + ); + + $collection->add_database_table( + 'cmi5launch_aus', + [ + 'id' => 'privacy:metadata:cmi5launch_aus:id', + 'userid' => 'privacy:metadata:cmi5launch_aus:userid', + 'attempt' => 'privacy:metadata:cmi5launch_aus:attempt', + 'lmsid' => 'privacy:metadata:cmi5launch_aus:lmsid', + 'completed' => 'privacy:metadata:cmi5launch_aus:completed', + 'passed' => 'privacy:metadata:cmi5launch_aus:passed', + 'inprogress' => 'privacy:metadata:cmi5launch_aus:inprogress', + 'noattempt' => 'privacy:metadata:cmi5launch_aus:noattempt', + 'satisfied' => 'privacy:metadata:cmi5launch_aus:satisfied', + 'sessions' => 'privacy:metadata:cmi5launch_aus:sessions', + 'scores' => 'privacy:metadata:cmi5launch_aus:scores', + 'grade' => 'privacy:metadata:cmi5launch_aus:grade', + ], + + 'privacy:metadata:cmi5launch_aus', + ); + + // External systems. + $collection->add_external_location_link('lrs', [ + 'registrationid' => 'privacy:metadata:lrs:registrationid', + 'createdat' => 'privacy:metadata:lrs:createdat', + ], 'privacy:metadata:lrs'); + + $collection->add_external_location_link('cmi5_player', [ + 'registrationid' => 'privacy:metadata:cmi5_player:registrationid', + 'actor' => 'privacy:metadata:cmi5_player:actor', + 'courseid' => 'privacy:metadata:cmi5_player:courseid', + 'returnurl' => 'privacy:metadata:cmi5_player:returnurl', + 'sessionid' => 'privacy:metadata:cmi5_player:sessionid', + ], 'privacy:metadata:cmi5_player'); + + + return $collection; + } + + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + + global $DB; + + if (empty($contextlist)) { + echo"empty contexts"; + return; + } + + // Get the user. + $user = $contextlist->get_user(); + $userid = $user->id; + + // Get the list of contexts that contain user information for the specified user. + // (The context->instanceid = cm->id and the cm.instance equals moodlecourseid). + foreach ($contextlist as $context) { + + $data = helper::get_context_data($context, $user); + + // Retrieve the coursemodule + $cm = get_coursemodule_from_id('cmi5launch', $context->instanceid); + + // The course modules instance correlates to the moodle course id in our tables. + // Combined with the user id, we can get the specific records we need. + $mid = $cm->instance; + $params = array ('userid' => $userid, 'moodlecourseid'=> $mid); + + // Start getting data on usercourse table + $recordset = $DB->get_recordset('cmi5launch_usercourse', $params); + + // To hold the course data. + $coursedata = []; + + // Cycle through recordset in case there are multiple. + foreach ($recordset as $record) { + + // Make user friendly names for data. + $userfriendly = array( 'ID of instance' => $record->id, + 'User ID' => $record->userid, + 'Course ID' => $record->courseid, + 'Moodle Course ID' => $record->moodlecourseid, + 'Registration ID' => $record->registrationid, + 'Return URL' => $record->returnurl, + 'AUs of instance' => $record->aus, + 'Grades of AUs' => $record->ausgrades, + 'Overall grade of instance' => $record->grade); + + // Then add as ONE item in array, that way if there is more than one it unpacks nicely. + $coursedata = ['User information' => $userfriendly]; + } + + // Combine the course data with the usercourse data. + $contextdata = (object)array_merge((array)$data, $coursedata); + + // Write data out. + writer::with_context($context)->export_data( + ['Course info pertaining to user'], + (object) $contextdata + ); + + // Now get the AU data. + $recordset = $DB->get_recordset('cmi5launch_aus', $params); + + // To hold the AU data. + $ausdata = []; + + // Cycle through recordset in case there are multiple. + foreach ($recordset as $record) { + + // Make user friendly names for data display. + $userfriendly = array( 'ID of AU instance' => $record->id, + 'User ID' => $record->userid, + 'The attempt number of the AU' => $record->attempt, + 'LMS ID of the AU' => $record->lmsid, + 'Moodle Course ID' => $record->moodlecourseid, + 'URL of the AU' => $record->url, + 'The type of the AU' => $record->type, + 'The title of the AU' => $record->title, + 'The description of the AU' => $record->description, + 'The objectives of the AU' => $record->objectives, + 'The move on value of the AU' => $record->moveon, + 'The AU index of the AU' => $record->auindex, + 'The cmi5 activity type of the AU' => $record->activitytype, + 'The amount it goes to mastery score' => $record->masteryscore, + 'Whether it has been completed' => $record->completed, + 'Whether it has been passed' => $record->passed, + 'Whether it is in progress' => $record->inprogress, + 'Whether it has been satisfied' => $record->satisfied, + 'Whether it has not been attempted' => $record->noattempt, + 'This AUs individual session\'s IDs' => $record->sessions, + 'The scores of the AU, as array' => $record->scores, + 'The overall grade of the AU' => $record->grade, + ); + + // Then add as ONE item in array, that way if there is more than one it unpacks nicely. + $ausdata = ['AU info' => $userfriendly]; + } + + // Combine the course data with the au data. + $contextdata = (object)array_merge((array)$data, $ausdata); + + // Write data out. + writer::with_context($context)->export_data( + ['AU info pertaining to user'], + (object) $contextdata + ); + + // Now get the sessions data. + $recordset = $DB->get_recordset('cmi5launch_sessions', $params); + + // To hold session information. + $sessiondata = []; + + // Cycle through recordset in case there are multiple. + foreach ($recordset as $record) { + + // Make user friendly names for display. + $userfriendly = array( 'ID of session instance' => $record->id, + 'Session ID' => $record->sessionid, + 'User ID' => $record->userid, + 'Moodle Course ID' => $record->moodlecourseid, + 'Registration Courses AUs ID' => $record->registrationscoursesausid, + 'Time a session was started' => $record->creeatedat, + 'Time a session was updated' => $record->updatedat, + 'Code' => $record->code, + 'Launch token ID' => $record->launchtokenid, + 'Last request time' => $record->lastrequesttime, + 'Amount it goes to Mastery Score' => $record->masteryscore, + 'The score of the session' => $record->score, + 'If it was launched' => $record->islaunched, + 'If it was initialized' => $record->isinitialized, + 'Time it was initialized' => $record->initializedat, + 'Duration of session' => $record->duration, + 'If it was completed' => $record->iscompleted, + 'If it was passed' => $record->ispassed, + 'If it was failed' => $record->isfailed, + 'If it was terminated' => $record->isterminated, + 'If it was abandoned' => $record->isabandoned, + 'Progress of session in recordments from LRS' => ("
" . implode("\n ", json_decode($record->progress) ) . ""), + 'Launch URL' => $record->launchurl); + + // Then add as ONE item in array, that way if there is more than one it unpacks nicely. + $sessiondata = ['Session info' => $userfriendly]; + } + + // Combine the course data with the session data. + $contextdata = (object)array_merge((array)$data, $sessiondata); + + // Write data out. + writer::with_context($context)->export_data( + ['Session info pertaining to user'], + (object) $contextdata + ); + + } + + } + + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + + + global $DB; + + $sql = "SELECT ctx.id + FROM {context} ctx + JOIN {course_modules} cm + ON cm.id = ctx.instanceid + AND ctx.contextlevel = :context + JOIN {modules} m + ON m.id = cm.module + AND m.name = 'cmi5launch' + JOIN {%s} c5l + ON c5l.moodlecourseid = cm.instance + WHERE c5l.userid = :userid"; + + $params = ['context' => CONTEXT_MODULE, 'userid' => $userid]; + + $contextlist = new contextlist(); + $contextlist->add_from_sql(sprintf($sql, 'cmi5launch_usercourse'), $params); + $contextlist->add_from_sql(sprintf($sql, 'cmi5launch_sessions'), $params); + $contextlist->add_from_sql(sprintf($sql, 'cmi5launch_aus'), $params); + + return $contextlist; + } + + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + $context = $userlist->get_context(); + + if (!is_a($context, \context_module::class)) { + return; + } + + $sql = "SELECT c5l.userid + FROM {%s} c5l + JOIN {modules} m + ON m.name = 'cmi5launch' + JOIN {course_modules} cm + ON cm.module = m.id + JOIN {context} ctx + ON ctx.instanceid = cm.id + AND ctx.contextlevel = :modlevel + WHERE ctx.id = :contextid"; + + $params = ['modlevel' => CONTEXT_MODULE, 'contextid' => $context->id]; + + $userlist->add_from_sql('userid', sprintf($sql, 'cmi5launch_usercourse'), $params); + $userlist->add_from_sql('userid', sprintf($sql, 'cmi5launch_sessions'), $params); + $userlist->add_from_sql('userid', sprintf($sql, 'cmi5launch_aus'), $params); + + } + + /** + * Delete all user data which matches the specified context. + * + * @param context $context A user context. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + + global $DB; + + // This should not happen, but just in case. + if ($context->contextlevel != CONTEXT_MODULE) { + return; + } + + $cm = get_coursemodule_from_id('cmi5launch', $context->instanceid); + if (!$cm) { + return; + } + + // This table needs a diferent key, but to be deleted still + $DB->delete_records('cmi5launch', ['id' => $cm->instance]); + + // Tables to delete from with same key. + $tables = ['cmi5launch_usercourse', 'cmi5launch_sessions', 'cmi5launch_aus']; + + foreach ($tables as $table) { + + $DB->delete_records($table, ['moodlecourseid' => $cm->instance]); + + } + } + + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + $userid = $contextlist->get_user()->id; + + + foreach ($contextlist->get_contexts() as $context) { + + //Retrieve the instance id from the context. + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + + // Tables to delete from with same key if context matches. + $tables = ['cmi5launch_usercourse', 'cmi5launch_sessions', 'cmi5launch_aus']; + + foreach ($tables as $table) { + + $sql = array("moodlecourseid" => $instanceid, "userid" => $userid); + + $deleted = $DB->delete_records($table, $sql); + + } + } + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + + global $DB; + + $context = $userlist->get_context(); + + $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); + + list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); + $params = array_merge(['moodlecourseid' => $cm->instance], $userinparams); + $sql = "moodlecourseid = :moodlecourseid AND userid {$userinsql}"; + + $DB->delete_records_select('cmi5lauch_usercourse', $sql, $params); + $DB->delete_records_select('cmi5lauch_sessions', $sql, $params); + $DB->delete_records_select('cmi5lauch_aus', $sql, $params); + } + + + } diff --git a/classes/task/check_completion.php b/classes/task/check_completion.php index 6b68950..4b07d6c 100755 --- a/classes/task/check_completion.php +++ b/classes/task/check_completion.php @@ -1,85 +1,85 @@ -. - -/** - * @package mod_cmi5launch - * @copyright 2013 Andrew Downes - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mod_cmi5launch\task; -defined('MOODLE_INTERNAL') || die(); -require_once(dirname(dirname(dirname(__FILE__))).'/lib.php'); -require_once($CFG->dirroot.'/lib/completionlib.php'); - -class check_completion extends \core\task\scheduled_task { - public function get_name() { - return get_string('checkcompletion', 'mod_cmi5launch'); - } - - public function execute() { - global $DB; - - $module = $DB->get_record('modules', array('name' => 'cmi5launch'), '*', MUST_EXIST); - $modules = $DB->get_records('cmi5launch'); - $courses = array(); // Cache course data incase the multiple modules exist in a course. - - foreach ($modules as $cmi5launch) { - echo ('Checking module id '.$cmi5launch->id.'. '.PHP_EOL); - $cm = $DB->get_record( - 'course_modules', - array('module' => $module->id, - 'instance' => $cmi5launch->id), - '*', - MUST_EXIST - ); - if (!isset($courses[$cm->course])) { - $courses[$cm->course] = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); - $courses[$cm->course]->enrolments = $DB->get_records('user_enrolments', array('status' => 0)); - } - $course = $courses[$cm->course]; - $completion = new \completion_info($course); - - $possibleresult = COMPLETION_COMPLETE; - - if ($cmi5launch->cmi5expiry > 0) { - $possibleresult = COMPLETION_UNKNOWN; - } - - if ($completion->is_enabled($cm) && $cmi5launch->cmi5verbid) { - foreach ($course->enrolments as $enrolment) { - echo ('Checking user id '.$enrolment->userid.'. '); - $oldstate = $completion->get_data($cm, false, $enrolment->userid)->completionstate; - echo ('Old completion state was '.$oldstate.'. '); - $completion->update_state($cm, $possibleresult, $enrolment->userid); - $newstate = $completion->get_data($cm, false, $enrolment->userid)->completionstate; - echo ('New completion state is '.$newstate.'. '.PHP_EOL); - if ($oldstate !== $newstate) { - // Trigger Activity completed event. - $event = \mod_cmi5launch\event\activity_completed::create(array( - 'objectid' => $cmi5launch->id, - 'context' => \context_module::instance($cm->id), - 'userid' => $enrolment->userid - )); - $event->add_record_snapshot('course_modules', $cm); - $event->add_record_snapshot('cmi5launch', $cmi5launch); - $event->trigger(); - } - } - } - } - } -} \ No newline at end of file +. + +/** + * @package mod_cmi5launch + * @copyright 2013 Andrew Downes + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_cmi5launch\task; +defined('MOODLE_INTERNAL') || die(); +require_once(dirname(dirname(dirname(__FILE__))).'/lib.php'); +require_once($CFG->dirroot.'/lib/completionlib.php'); + +class check_completion extends \core\task\scheduled_task { + public function get_name() { + return get_string('checkcompletion', 'mod_cmi5launch'); + } + + public function execute() { + global $DB; + + $module = $DB->get_record('modules', array('name' => 'cmi5launch'), '*', MUST_EXIST); + $modules = $DB->get_records('cmi5launch'); + $courses = array(); // Cache course data incase the multiple modules exist in a course. + + foreach ($modules as $cmi5launch) { + echo ('Checking module id '.$cmi5launch->id.'. '.PHP_EOL); + $cm = $DB->get_record( + 'course_modules', + array('module' => $module->id, + 'instance' => $cmi5launch->id), + '*', + MUST_EXIST + ); + if (!isset($courses[$cm->course])) { + $courses[$cm->course] = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + $courses[$cm->course]->enrolments = $DB->get_records('user_enrolments', array('status' => 0)); + } + $course = $courses[$cm->course]; + $completion = new \completion_info($course); + + $possibleresult = COMPLETION_COMPLETE; + + if ($cmi5launch->cmi5expiry > 0) { + $possibleresult = COMPLETION_UNKNOWN; + } + + if ($completion->is_enabled($cm) && $cmi5launch->cmi5verbid) { + foreach ($course->enrolments as $enrolment) { + echo ('Checking user id '.$enrolment->userid.'. '); + $oldstate = $completion->get_data($cm, false, $enrolment->userid)->completionstate; + echo ('Old completion state was '.$oldstate.'. '); + $completion->update_state($cm, $possibleresult, $enrolment->userid); + $newstate = $completion->get_data($cm, false, $enrolment->userid)->completionstate; + echo ('New completion state is '.$newstate.'. '.PHP_EOL); + if ($oldstate !== $newstate) { + // Trigger Activity completed event. + $event = \mod_cmi5launch\event\activity_completed::create(array( + 'objectid' => $cmi5launch->id, + 'context' => \context_module::instance($cm->id), + 'userid' => $enrolment->userid, + )); + $event->add_record_snapshot('course_modules', $cm); + $event->add_record_snapshot('cmi5launch', $cmi5launch); + $event->trigger(); + } + } + } + } + } +} diff --git a/cmi5PHP/src/About.php b/cmi5PHP/src/About.php deleted file mode 100755 index a3d5421..0000000 --- a/cmi5PHP/src/About.php +++ /dev/null @@ -1,55 +0,0 @@ -_fromArray($arg); - } - - if (! isset($this->version)) { - $this->setVersion(array()); - } - if (! isset($this->extensions)) { - $this->setExtensions(array()); - } - } - - public function setVersion($value) { $this->version = $value; return $this; } - public function getVersion() { return $this->version; } - - public function setExtensions($value) { - if (! $value instanceof Extensions) { - $value = new Extensions($value); - } - - $this->extensions = $value; - - return $this; - } - public function getExtensions() { return $this->extensions; } -} diff --git a/cmi5PHP/src/Activity.php b/cmi5PHP/src/Activity.php deleted file mode 100755 index 8110336..0000000 --- a/cmi5PHP/src/Activity.php +++ /dev/null @@ -1,55 +0,0 @@ -_fromArray($arg); - } - } - - public function getObjectType() { return $this->objectType; } - - // FEATURE: check IRI? - public function setId($value) { $this->id = $value; return $this; } - public function getId() { return $this->id; } - - public function setDefinition($value) { - if (! $value instanceof ActivityDefinition && is_array($value)) { - $value = new ActivityDefinition($value); - } - - $this->definition = $value; - - return $this; - } - public function getDefinition() { return $this->definition; } -} diff --git a/cmi5PHP/src/ActivityDefinition.php b/cmi5PHP/src/ActivityDefinition.php deleted file mode 100755 index 291008d..0000000 --- a/cmi5PHP/src/ActivityDefinition.php +++ /dev/null @@ -1,115 +0,0 @@ -_fromArray($arg); - } - - foreach ( - [ - 'name', - 'description', - 'extensions', - ] as $k - ) { - $method = 'set' . ucfirst($k); - - if (! isset($this->$k)) { - $this->$method(array()); - } - } - } - - // FEATURE: check URI? - public function setType($value) { $this->type = $value; return $this; } - public function getType() { return $this->type; } - - public function setName($value) { - if (! $value instanceof LanguageMap) { - $value = new LanguageMap($value); - } - - $this->name = $value; - - return $this; - } - public function getName() { return $this->name; } - - public function setDescription($value) { - if (! $value instanceof LanguageMap) { - $value = new LanguageMap($value); - } - - $this->description = $value; - - return $this; - } - public function getDescription() { return $this->description; } - - public function setMoreInfo($value) { $this->moreInfo = $value; return $this; } - public function getMoreInfo() { return $this->moreInfo; } - - public function setExtensions($value) { - if (! $value instanceof Extensions) { - $value = new Extensions($value); - } - - $this->extensions = $value; - - return $this; - } - public function getExtensions() { return $this->extensions; } - - public function setInteractionType($value) { $this->interactionType = $value; return $this; } - public function getInteractionType() { return $this->interactionType; } - public function setCorrectResponsesPattern($value) { $this->correctResponsesPattern = $value; return $this; } - public function getCorrectResponsesPattern() { return $this->correctResponsesPattern; } - - // TODO: make these arrays of InteractionComponents - public function setChoices($value) { $this->choices = $value; return $this; } - public function getChoices() { return $this->choices; } - public function setScale($value) { $this->scale = $value; return $this; } - public function getScale() { return $this->scale; } - public function setSource($value) { $this->source = $value; return $this; } - public function getSource() { return $this->source; } - public function setTarget($value) { $this->target = $value; return $this; } - public function getTarget() { return $this->target; } - public function setSteps($value) { $this->steps = $value; return $this; } - public function getSteps() { return $this->steps; } -} diff --git a/cmi5PHP/src/ActivityProfile.php b/cmi5PHP/src/ActivityProfile.php deleted file mode 100755 index d2f25ff..0000000 --- a/cmi5PHP/src/ActivityProfile.php +++ /dev/null @@ -1,34 +0,0 @@ -activity = $value; - - return $this; - } - public function getActivity() { return $this->activity; } -} diff --git a/cmi5PHP/src/Agent.php b/cmi5PHP/src/Agent.php deleted file mode 100755 index ce50a51..0000000 --- a/cmi5PHP/src/Agent.php +++ /dev/null @@ -1,166 +0,0 @@ -_fromArray($arg); - } - } - - public function asVersion($version) { - $result = array( - 'objectType' => $this->objectType - ); - - if (isset($this->name)) { - $result['name'] = $this->name; - } - - // - // only one of these, note that if 'account' has been set - // but is returned empty then no IFI will be included - // - if (isset($this->account)) { - $versioned_acct = $this->account->asVersion($version); - if (! empty($versioned_acct)) { - $result['account'] = $versioned_acct; - } - } - elseif (isset($this->mbox_sha1sum)) { - $result['mbox_sha1sum'] = $this->mbox_sha1sum; - } - elseif (isset($this->mbox)) { - $result['mbox'] = $this->mbox; - } - elseif (isset($this->openid)) { - $result['openid'] = $this->openid; - } - - return $result; - } - - public function isIdentified() { - return (isset($this->mbox) || isset($this->mbox_sha1sum) || isset($this->openid) || isset($this->account)); - } - - // - // having multiple IFIs set shouldn't cause a problem here - // so long as all of their values match their counterparts - // - // we could allow for multiple IFIs in the `this` object and - // ignore them missing in the signature but discussion ruled - // against that need since the serialization via asVersion - // shouldn't result in that ever for a valid statement - // - public function compareWithSignature($fromSig) { - // - // mbox and mbox_sha1sum are a special case where they can be - // equal but have to be transformed for comparison, check them - // first and short circuit - // - if (isset($this->mbox) && isset($fromSig->mbox_sha1sum)) { - if ($this->getMbox_sha1sum() === $fromSig->mbox_sha1sum) { - return array('success' => true, 'reason' => null); - } - - return array('success' => false, 'reason' => 'Comparison of this.mbox to signature.mbox_sha1sum failed: no match'); - } - elseif (isset($fromSig->mbox) && isset($this->mbox_sha1sum)) { - if ($fromSig->getMbox_sha1sum() === $this->mbox_sha1sum) { - return array('success' => true, 'reason' => null); - } - - return array('success' => false, 'reason' => 'Comparison of this.mbox_sha1sum to signature.mbox failed: no match'); - } - - foreach (array('mbox', 'mbox_sha1sum', 'openid') as $property) { - if (isset($this->$property) || isset($fromSig->$property)) { - if ($this->$property !== $fromSig->$property) { - return array('success' => false, 'reason' => "Comparison of $property failed: value is not the same"); - } - } - } - if (isset($this->account) || isset($fromSig->account)) { - if (! isset($fromSig->account)) { - return array('success' => false, 'reason' => "Comparison of account failed: value not in signature"); - } - if (! isset($this->account)) { - return array('success' => false, 'reason' => "Comparison of account failed: value not in this"); - } - - $acctResult = $this->account->compareWithSignature($fromSig->account); - if (! $acctResult['success']) { - return array('success' => false, 'reason' => "Comparison of account failed: " . $acctResult['reason']); - } - } - - return array('success' => true, 'reason' => null); - } - - public function getObjectType() { return $this->objectType; } - - public function setName($value) { $this->name = $value; return $this; } - public function getName() { return $this->name; } - - public function setMbox($value) { - if (isset($value) && (! (stripos($value, 'mailto:') === 0))) { - $value = 'mailto:' . $value; - } - $this->mbox = $value; - return $this; - } - public function getMbox() { return $this->mbox; } - - public function setMbox_sha1sum($value) { $this->mbox_sha1sum = $value; return $this; } - public function getMbox_sha1sum() { - if (isset($this->mbox_sha1sum)) { - return $this->mbox_sha1sum; - } - - if (isset($this->mbox)) { - return sha1($this->mbox); - } - } - public function setOpenid($value) { $this->openid = $value; return $this; } - public function getOpenid() { return $this->openid; } - - public function setAccount($value) { - if (! $value instanceof AgentAccount && is_array($value)) { - $value = new AgentAccount($value); - } - - $this->account = $value; - - return $this; - } - public function getAccount() { return $this->account; } -} diff --git a/cmi5PHP/src/AgentAccount.php b/cmi5PHP/src/AgentAccount.php deleted file mode 100755 index b1350d9..0000000 --- a/cmi5PHP/src/AgentAccount.php +++ /dev/null @@ -1,39 +0,0 @@ -_fromArray($arg); - } - } - - public function setName($value) { $this->name = $value; return $this; } - public function getName() { return $this->name; } - public function setHomePage($value) { $this->homePage = $value; return $this; } - public function getHomePage() { return $this->homePage; } -} diff --git a/cmi5PHP/src/AgentProfile.php b/cmi5PHP/src/AgentProfile.php deleted file mode 100755 index 05879a9..0000000 --- a/cmi5PHP/src/AgentProfile.php +++ /dev/null @@ -1,39 +0,0 @@ -agent = $value; - - return $this; - } - public function getAgent() { return $this->agent; } -} diff --git a/cmi5PHP/src/ArraySetterTrait.php b/cmi5PHP/src/ArraySetterTrait.php deleted file mode 100755 index 46195e6..0000000 --- a/cmi5PHP/src/ArraySetterTrait.php +++ /dev/null @@ -1,30 +0,0 @@ - $v) { - $method = 'set' . ucfirst($k); - if (isset($options[$k]) && method_exists($this, $method)) { - $this->$method($options[$k]); - } - } - } -} diff --git a/cmi5PHP/src/AsVersionTrait.php b/cmi5PHP/src/AsVersionTrait.php deleted file mode 100755 index 87f7eb0..0000000 --- a/cmi5PHP/src/AsVersionTrait.php +++ /dev/null @@ -1,84 +0,0 @@ - $value) { - // - // skip properties that start with an underscore to allow - // storing information that isn't included in statement - // structure etc. (see Attachment.content for example) - // - if (strpos($property, '_') === 0) { - continue; - } - - if ($value instanceof VersionableInterface) { - $value = $value->asVersion($version); - } - elseif (is_array($value) && !empty($value)) { - $tmp_value = array(); - foreach ($value as $element) { - if ($element instanceof VersionableInterface) { - array_push($tmp_value, $element->asVersion($version)); - } - else { - array_push($tmp_value, $element); - } - } - $value = $tmp_value; - } - - if (isset($value) && (!is_array($value) || !empty($value))) { - $result[$property] = $value; - } - } - - if (method_exists($this, '_asVersion')) { - $this->_asVersion($result, $version); - } - - return $result; - } - - /** - * Prevent external mutation - * - * @param string $property - * @param mixed $value - * @throws DomainException - */ - final public function __set($property, $value) { - throw new DomainException(__CLASS__ . ' is immutable'); - } -} diff --git a/cmi5PHP/src/Attachment.php b/cmi5PHP/src/Attachment.php deleted file mode 100755 index 5c0388e..0000000 --- a/cmi5PHP/src/Attachment.php +++ /dev/null @@ -1,102 +0,0 @@ -_fromArray($arg); - - if (isset($arg['content'])) { - $this->setContent($arg['content']); - } - } - - foreach ( - [ - 'display', - 'description', - ] as $k - ) { - $method = 'set' . ucfirst($k); - - if (! isset($this->$k)) { - $this->$method(array()); - } - } - } - - public function setUsageType($value) { $this->usageType = $value; return $this; } - public function getUsageType() { return $this->usageType; } - - public function setDisplay($value) { - if (! $value instanceof LanguageMap) { - $value = new LanguageMap($value); - } - - $this->display = $value; - - return $this; - } - public function getDisplay() { return $this->display; } - - public function setDescription($value) { - if (! $value instanceof LanguageMap) { - $value = new LanguageMap($value); - } - - $this->description = $value; - - return $this; - } - public function getDescription() { return $this->description; } - - public function setContentType($value) { $this->contentType = $value; return $this; } - public function getContentType() { return $this->contentType; } - public function setLength($value) { $this->length = $value; return $this; } - public function getLength() { return $this->length; } - public function setSha2($value) { $this->sha2 = $value; return $this; } - public function getSha2() { return $this->sha2; } - public function setFileUrl($value) { $this->fileUrl = $value; return $this; } - public function getFileUrl() { return $this->fileUrl; } - - public function setContent($value) { - $this->_content = $value; - $this->setLength(strlen($value)); - $this->setSha2(hash("sha256", $value)); - return $this; - } - public function getContent() { return $this->_content; } - public function hasContent() { return isset($this->_content); } -} diff --git a/cmi5PHP/src/ComparableInterface.php b/cmi5PHP/src/ComparableInterface.php deleted file mode 100755 index b8deb8c..0000000 --- a/cmi5PHP/src/ComparableInterface.php +++ /dev/null @@ -1,23 +0,0 @@ -_fromArray($arg); - } - - foreach ( - [ - 'contextActivities', - 'extensions', - ] as $k - ) { - $method = 'set' . ucfirst($k); - - if (! isset($this->$k)) { - $this->$method(array()); - } - } - } - - public function setRegistration($value) { - if (isset($value) && ! preg_match(Util::UUID_REGEX, $value)) { - throw new InvalidArgumentException('arg1 must be a UUID'); - } - $this->registration = $value; - return $this; - } - public function getRegistration() { return $this->registration; } - - public function setInstructor($value) { - if (! ($value instanceof Agent || $value instanceof Group) && is_array($value)) { - if (isset($value['objectType']) && $value['objectType'] === "Group") { - $value = new Group($value); - } - else { - $value = new Agent($value); - } - } - - $this->instructor = $value; - - return $this; - } - public function getInstructor() { return $this->instructor; } - - public function setTeam($value) { - if (! $value instanceof Group && is_array($value)) { - $value = new Group($value); - } - - $this->team = $value; - - return $this; - } - public function getTeam() { return $this->team; } - - public function setContextActivities($value) { - if (! $value instanceof ContextActivities) { - $value = new ContextActivities($value); - } - - $this->contextActivities = $value; - - return $this; - } - public function getContextActivities() { return $this->contextActivities; } - - public function setRevision($value) { $this->revision = $value; return $this; } - public function getRevision() { return $this->revision; } - public function setPlatform($value) { $this->platform = $value; return $this; } - public function getPlatform() { return $this->platform; } - public function setLanguage($value) { $this->language = $value; return $this; } - public function getLanguage() { return $this->language; } - - public function setStatement($value) { - if (! $value instanceof StatementRef && is_array($value)) { - $value = new StatementRef($value); - } - - $this->statement = $value; - - return $this; - } - public function getStatement() { return $this->statement; } - - public function setExtensions($value) { - if (! $value instanceof Extensions) { - $value = new Extensions($value); - } - - $this->extensions = $value; - - return $this; - } - public function getExtensions() { return $this->extensions; } -} diff --git a/cmi5PHP/src/ContextActivities.php b/cmi5PHP/src/ContextActivities.php deleted file mode 100755 index 634d2e5..0000000 --- a/cmi5PHP/src/ContextActivities.php +++ /dev/null @@ -1,68 +0,0 @@ -_fromArray($arg); - } - } - - private function _listSetter($prop, $value) { - if (is_array($value)) { - if (isset($value['id'])) { - array_push($this->$prop, new Activity($value)); - } - else { - foreach ($value as $k => $v) { - if (! $value[$k] instanceof Activity) { - $value[$k] = new Activity($value[$k]); - } - } - $this->$prop = $value; - } - } - elseif ($value instanceof Activity) { - array_push($this->$prop, $value); - } - else { - throw new \InvalidArgumentException('type of arg1 must be Activity, array of Activity properties, or array of Activity/array of Activity properties'); - } - return $this; - } - - public function setCategory($value) { return $this->_listSetter('category', $value); } - public function getCategory() { return $this->category; } - public function setParent($value) { return $this->_listSetter('parent', $value); } - public function getParent() { return $this->parent; } - public function setGrouping($value) { return $this->_listSetter('grouping', $value); } - public function getGrouping() { return $this->grouping; } - public function setOther($value) { return $this->_listSetter('other', $value); } - public function getOther() { return $this->other; } -} diff --git a/cmi5PHP/src/Document.php b/cmi5PHP/src/Document.php deleted file mode 100755 index ff862ef..0000000 --- a/cmi5PHP/src/Document.php +++ /dev/null @@ -1,67 +0,0 @@ -_fromArray($arg); - } - } - - public function setId($value) { $this->id = $value; return $this; } - public function getId() { return $this->id; } - public function setContentType($value) { $this->contentType = $value; return $this; } - public function getContentType() { return $this->contentType; } - public function setContent($value) { $this->content = $value; return $this; } - public function getContent() { return $this->content; } - public function setEtag($value) { $this->etag = $value; return $this; } - public function getEtag() { return $this->etag; } - - public function setTimestamp($value) { - if (isset($value)) { - if ($value instanceof \DateTime) { - // Use format('c') instead of format(\DateTime::ISO8601) due to bug in format(\DateTime::ISO8601) that generates an invalid timestamp. - $value = $value->format('c'); - } - elseif (is_string($value)) { - $value = $value; - } - else { - throw new \InvalidArgumentException('type of arg1 must be string or DateTime'); - } - } - - $this->timestamp = $value; - - return $this; - } - public function getTimestamp() { return $this->timestamp; } -} diff --git a/cmi5PHP/src/Extensions.php b/cmi5PHP/src/Extensions.php deleted file mode 100755 index 43b4f10..0000000 --- a/cmi5PHP/src/Extensions.php +++ /dev/null @@ -1,46 +0,0 @@ -_map; - - $keys = array_unique( - array_merge( - isset($this->_map) ? array_keys($this->_map) : array(), - isset($sigMap) ? array_keys($sigMap) : array() - ) - ); - - foreach ($keys as $key) { - if (! isset($sigMap[$key])) { - return array('success' => false, 'reason' => "$key not in signature"); - } - if (! isset($this->_map[$key])) { - return array('success' => false, 'reason' => "$key not in this"); - } - if ($this->_map[$key] != $sigMap[$key]) { - return array('success' => false, 'reason' => "$key does not match"); - } - } - - return array('success' => true, 'reason' => null); - } -} diff --git a/cmi5PHP/src/FromJSONTrait.php b/cmi5PHP/src/FromJSONTrait.php deleted file mode 100755 index b16a30d..0000000 --- a/cmi5PHP/src/FromJSONTrait.php +++ /dev/null @@ -1,35 +0,0 @@ -member)) { - $this->setMember(array()); - } - } - - public function asVersion($version) { - $result = parent::asVersion($version); - - if (count($this->member) > 0) { - $result['member'] = array(); - - foreach ($this->member as $v) { - array_push($result['member'], $v->asVersion($version)); - } - } - - return $result; - } - - public function compareWithSignature($fromSig) { - // - // if this group is identified then it is the comparison - // of the identifier that matters - // - if ($this->isIdentified() || $fromSig->isIdentified()) { - return parent::compareWithSignature($fromSig); - } - - // - // anonymous groups get their member list compared, - // short circuit when they don't have the same length - // - if (count($this->member) !== count($fromSig->member)) { - return array('success' => false, 'reason' => 'Comparison of member list failed: array lengths differ'); - } - - for ($i = 0; $i < count($this->member); $i++) { - $comparison = $this->member[$i]->compareWithSignature($fromSig->member[$i]); - if (! $comparison['success']) { - return array('success' => false, 'reason' => "Comparison of member $i failed: " . $comparison['reason']); - } - } - - return array('success' => true, 'reason' => null); - } - - public function setMember($value) { - foreach ($value as $k => $v) { - if (! $v instanceof Agent) { - $value[$k] = new Agent($v); - } - } - - $this->member = $value; - - return $this; - } - public function getMember() { return $this->member; } - public function addMember($value) { - if (! $value instanceof Agent) { - $value = new Agent($value); - } - - array_push($this->member, $value); - - return $this; - } -} diff --git a/cmi5PHP/src/JSONParseErrorException.php b/cmi5PHP/src/JSONParseErrorException.php deleted file mode 100755 index d3addbc..0000000 --- a/cmi5PHP/src/JSONParseErrorException.php +++ /dev/null @@ -1,49 +0,0 @@ -malformedValue = $malformedValue; - $this->jsonErrorNumber = (int) $jsonErrorNumber; - $this->jsonErrorMessage = $jsonErrorMessage; - - $message = sprintf(self::$format, $malformedValue, $jsonErrorMessage, $jsonErrorNumber); - - parent::__construct($message, $jsonErrorNumber, $previous); - } - - public function malformedValue() { - return $this->malformedValue; - } - - public function jsonErrorNumber() { - return $this->jsonErrorNumber; - } - - public function jsonErrorMessage() { - return $this->jsonErrorMessage; - } -} diff --git a/cmi5PHP/src/LRSInterface.php b/cmi5PHP/src/LRSInterface.php deleted file mode 100755 index 7eeb8f3..0000000 --- a/cmi5PHP/src/LRSInterface.php +++ /dev/null @@ -1,49 +0,0 @@ -success = (bool) $success; - $this->content = $content; - $this->httpResponse = $httpResponse; - } -} diff --git a/cmi5PHP/src/LanguageMap.php b/cmi5PHP/src/LanguageMap.php deleted file mode 100755 index f841fa6..0000000 --- a/cmi5PHP/src/LanguageMap.php +++ /dev/null @@ -1,45 +0,0 @@ -_map); - $preferredLanguage = $negotiator->getBest($acceptLanguage, $availableLanguages); - - $key = $availableLanguages[0]; - if (isset($preferredLanguage)) { - $key = $preferredLanguage->getValue(); - } - elseif (isset($this->_map['und'])) { - $key = 'und'; - } - - return $this->_map[$key]; - } -} diff --git a/cmi5PHP/src/Map.php b/cmi5PHP/src/Map.php deleted file mode 100755 index 685c5d6..0000000 --- a/cmi5PHP/src/Map.php +++ /dev/null @@ -1,61 +0,0 @@ -_map = func_get_arg(0); - } - else { - $this->_map = array(); - } - } - - public function asVersion($version = null) { - if (! $this->isEmpty()) { - return $this->_map; - } - } - - public function set($code, $value) { - $this->_map[$code] = $value; - } - private function _unset($code) { - unset($this->_map[$code]); - } - - public function isEmpty() { - return count($this->_map) === 0; - } - - public function __call($func, $args) { - switch ($func) { - case 'unset': - return $this->_unset($args[0]); - break; - default: - throw new \BadMethodCallException(get_class($this) . "::$func() does not exist"); - } - } -} diff --git a/cmi5PHP/src/Person.php b/cmi5PHP/src/Person.php deleted file mode 100755 index 1cd572b..0000000 --- a/cmi5PHP/src/Person.php +++ /dev/null @@ -1,84 +0,0 @@ -_fromArray($arg); - } - } - - public function asVersion($version) { - $result = array( - 'objectType' => $this->objectType - ); - if (isset($this->name)) { - $result['name'] = $this->name; - } - if (isset($this->account)) { - $result['account'] = array(); - foreach ($this->account as $account) { - if (! $account instanceof AgentAccount && is_array($account)) { - $account = new AgentAccount($account); - } - array_push($result['account'], $account->asVersion($version)); - } - } - if (isset($this->mbox_sha1sum)) { - $result['mbox_sha1sum'] = $this->mbox_sha1sum; - } - if (isset($this->mbox)) { - $result['mbox'] = $this->mbox; - } - if (isset($this->openid)) { - $result['openid'] = $this->openid; - } - - return $result; - } - - public function getObjectType() { return $this->objectType; } - - public function setName($value) { $this->name = $value; return $this; } - public function getName() { return $this->name; } - - public function setMbox($value) { $this->mbox = $value; return $this; } - public function getMbox() { return $this->mbox; } - - public function setMbox_sha1sum($value) { $this->mbox_sha1sum = $value; return $this; } - public function getMbox_sha1sum() {return $this->mbox_sha1sum;} - - public function setOpenid($value) { $this->openid = $value; return $this; } - public function getOpenid() { return $this->openid; } - - public function setAccount($value) { $this->account = $value; return $this; } - public function getAccount() { return $this->account; } -} diff --git a/cmi5PHP/src/Progress.php b/cmi5PHP/src/Progress.php deleted file mode 100644 index 72fbcf3..0000000 --- a/cmi5PHP/src/Progress.php +++ /dev/null @@ -1,401 +0,0 @@ - $regId, - 'since' => $session->createdAt - ); - - $statements = $this->sendRequestToLRS($data, $regId); - //The results come back as nested array under more then statements. We only want statements, and we want them unique - $statement = array_chunk($statements["statements"], 1); - - $length = count($statement); - - for ($i = 0; $i < $length; $i++){ - - //This separates the larger statement into the separate sessions and verbs - $current = ($statement[$i]); - array_push($result, array ($regId => $current) ); - } - - return $result; - } - - - /** - * Builds and sends requests to LRS - * @param mixed $data - * @param mixed $id - * @return mixed - */ - public function sendRequestToLRS($data, $id) - { - $settings = cmi5launch_settings($id); - - //Url to request statements from - $url = $settings['cmi5launchlrsendpoint'] . "statements"; - //Build query with data above - $url = $url . '?' . http_build_query($data,"", '&', PHP_QUERY_RFC1738); - - //LRS username and password - $user = $settings['cmi5launchlrslogin']; - $pass = $settings['cmi5launchlrspass']; - - // use key 'http' even if you send the request to https://... - //There can be multiple headers but as an array under the ONE header - //content(body) must be JSON encoded here, as that is what CMI5 player accepts - $options = array( - 'http' => array( - 'method' => 'GET', - 'header' => array('Authorization: Basic '. base64_encode("$user:$pass"), - "Content-Type: application/json\r\n" . - "X-Experience-API-Version:1.0.3", - ) - ) - ); - //the options are here placed into a stream to be sent - $context = stream_context_create($options); - - //sends the stream to the specified URL and stores results (the false is use_include_path, which we dont want in this case, we want to go to the url) - $result = file_get_contents( $url, false, $context ); - - $resultDecoded = json_decode($result, true); - - return $resultDecoded; - } - - /** - * Returns an actor (name) retrieved from collected LRS data based on registration id - * @param mixed $resultChunked - data retrieved from LRS, usually an array - * @param mixed $i - the registration id - * @return mixed - actor - */ - public function retrieveActor($info, $regid){ - - $actor = $info[$regid][0]["actor"]["account"]["name"]; - return $actor; - } - - /** - * Returns a verb retrieved from collected LRS data based on registration id - * @param mixed $resultChunked - data retrieved from LRS, usually an array - * @param mixed $i - the registration id - * @return mixed - verb - */ - public function retrieveVerbsOrig($resultChunked, $i){ - - //Some verbs do not have an easy to display 'language' option, we need to check if 'display' is present - $verbInfo = $resultChunked[0][0][$i]["statements"][0]["verb"]; - $display = array_key_exists("display", $verbInfo); - - //If it is null then there is no display, so go by verb id - if(!$display ){ - //retrieve id - $verbId = $resultChunked[$i][0]["verb"]["id"]; - - //SPLITS id in two on 'verbs/', we want the end which is the actual verb - $split = explode('verbs/', $verbId); - $verb = $split[1]; - - }else{ - //IF it is not null then there is a language easy to read version of verb display, such as 'en' or 'en-us' - $verbLang = $resultChunked[$i][0]["verb"]["display"]; - //Retreive the language - $lang = array_key_first($verbLang); - //use it to retreive verb - $verb = [$verbLang][0][$lang]; - } - return $verb; - } - - public function retrieveVerbs($resultChunked, $i){ - - //Some verbs do not have an easy to display 'language' option, we need to check if 'display' is present - $verbInfo = $resultChunked[$i][0]["verb"]; - $display = array_key_exists("display", $verbInfo); - - //If it is null then there is no display, so go by verb id - if(!$display ){ - //retrieve id - $verbId = $resultChunked[$i][0]["verb"]["id"]; - - //SPLITS id in two on 'verbs/', we want the end which is the actual verb - $split = explode('verbs/', $verbId); - $verb = $split[1]; - - }else{ - //IF it is not null then there is a language easy to read version of verb display, such as 'en' or 'en-us' - $verbLang = $resultChunked[$i][0]["verb"]["display"]; - //Retreive the language - $lang = array_key_first($verbLang); - //use it to retreive verb - $verb = [$verbLang][0][$lang]; - } - return $verb; - } - - /** - * Returns an object (au title?) retrieved from collected LRS data based on registration id - * @param mixed $resultChunked - data retrieved from LRS, usually an array - * @param mixed $i - the registration id - * @return mixed - object name - */ - public function retrieveObject($resultChunked, $i){ - //THIS is the SECOND chunk, this is the problem - $objectInfo = $resultChunked[$i][0]["object"]; - $definition = array_key_exists("definition", $objectInfo); - //If it is null then there is no "definition", so go by object id - if(!$definition ){ - //retrieve id - $object = $resultChunked[$i][0]["object"]["id"]; - //I have noticed that in the LRS when it can't find a name it references the WHOLE id as in "actor did WHOLEID", so I will do the same here - }else{ - //IF it is not null then there is a language easy to read version of object definition, such as 'en' or 'en-us' - $objectLang = $resultChunked[$i][0]["object"]["definition"]["name"]; - //Retreive the language - $lang = array_key_first($objectLang); - //use it to retreive verb - $object = [$objectLang][0][$lang]; - } - return $object; - } - /** - * TODO MB - This is able to get all results for later grading - * Result params when returned with statements can have 5 fields (not including extensions) - * Success - a true/false to provide for a pass/fail of Activity - * Completion - a true/false to provide for completion of Activity - * Score - takes a Score object - * Response - a string value that can contain anything, such as an answer to a question - * Duration - length of time taken for experience - * - * We are concerned with the top three for Moodle reporting purposes - * - * Summary of retrieveResult - * @param mixed $resultChunked - data retrieved from LRS, usually an array - * @param mixed $i - the registration id - * @return mixed - */ - public function retrieveResult($resultChunked, $i){ - - //Verify this statement has a 'result' param - if (array_key_exists("result", $resultChunked ) ) - { - //If it exists, grab it - $resultInfo = $resultChunked[$i][0]["result"]; - - //Check which keys exist in 'result' - $success = array_key_exists("success", $resultInfo); - $completion = array_key_exists("completion", $resultInfo); - $score = array_key_exists("score", $resultInfo); - //Andy seeemed interested in durations? - $duration = array_key_exists("score", $resultInfo); - $response = array_key_exists("response", $resultInfo); - - } - - //How should we save and return these infos? A key value array maybe? - //If it is null then the item in question doesn't exist in this statement - if($success){ - //no need to make new variable, save over - $success = $resultChunked[$i][0]["result"]["success"]; - - //now that we have success, save to db. This means we need an object right? Can we update afield? - //even if we could we need id to find it... - }else{ - } - - //Maybe it would be better to just have a 'retrieveScore' for now - } - - /** - * Returns a timestamp retrieved from collected LRS data based on registration id - * @param mixed $resultChunked - data retrieved from LRS, usually an array - * @param mixed $i - the registration id - * @return string - date/time - */ - public function retrieveTimestamp($resultChunked, $i){ - - $date = new DateTime($resultChunked[$i][0]["timestamp"], new DateTimeZone('US/Eastern')); - - $date->setTimezone(new DateTimeZone('America/New_York')); - - $date = $date->format('d-m-Y' . " ". 'h:i a'); - - return $date; - } - - /** - * - * Summary of retrieveScore - * @param mixed $resultChunked - data retrieved from LRS, usually an array - * @param mixed $registrationid - the registration id - * @return mixed - */ - //Ok, if we change so session id goes through, can we update DB in this func - public function retrieveScore($resultChunked, $registrationid){ - - //variable to hold score - $score = null; - - //Verify this statement has a 'result' param - if (array_key_exists("result", $resultChunked[$registrationid][0] ) ) - { - //If it exists, grab it - $resultInfo = $resultChunked[$registrationid][0]["result"]; - - $score = array_key_exists("score", $resultInfo); - - } - - //If it is null then the item in question doesn't exist in this statement - if ($score) { - - $score = $resultChunked[$registrationid][0]["result"]["score"]; - - //Raw score preferred to scaled - if($score["raw"]){ - - $returnScore = $score["raw"]; - return $returnScore; - } - elseif($score["scaled"]){ - - $returnScore = round($score["scaled"], 2) ; - return $returnScore; - } - - } - - } - - /** - * Summary of retrieveStatement - * //Retrieves statements from LRS - * @param mixed $regId - * @param mixed $id - * @param mixed $lmsId - * @return array
Error attempting to get AU data from DB. Check AU id.
"; - echo ""; - var_dump($auID); - echo ""; - } - else{ - $auItem = $DB->get_record('cmi5launch_aus', array('id' => $auID)); - - $au = new au($auItem); - } - - //Return our new list of AU! - return $au; - } - -} -?> \ No newline at end of file diff --git a/cmi5PHP/src/cmi5Connector.php b/cmi5PHP/src/cmi5Connector.php deleted file mode 100755 index 7b6e58d..0000000 --- a/cmi5PHP/src/cmi5Connector.php +++ /dev/null @@ -1,471 +0,0 @@ -"; - var_dump($url); - echo "
Error attempting to get session data from DB. Check session id.
"; - echo ""; - var_dump($sessionID); - echo ""; - - } else { - - $sessionItem = $DB->get_record('cmi5launch_sessions', array('sessionid' => $sessionID)); - - $session = new session($sessionItem); - - } - - //Return new session object! - return $session; - } -} - -?> \ No newline at end of file diff --git a/cmi5PHP/src/sessions.php b/cmi5PHP/src/sessions.php deleted file mode 100644 index b2b261b..0000000 --- a/cmi5PHP/src/sessions.php +++ /dev/null @@ -1,61 +0,0 @@ -. - -/** - *Class to handle Sessions. - * TODO The question is, to we want to build this exactly like the session table in cmi5??? - * - * @copyright 2023 Megan Bohland - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -class Session { - // Properties - //$id is session id - public $id, $tenantname, $tenantId, $registrationsCoursesAusId, $lmsid, $firstlaunch, $lastlaunch, - $progress = [], $auid, $aulaunchurl, $launchurl, - $completed_passed, $grade, $registrationid, $lrscode, - $createdAt, $updatedAt, $registrationCourseAusId, - $code, $lastRequestTime, $launchTokenId, $launchMode, $masteryScore, - $contextTemplate, $isLaunched, $isInitialized, $initializedAt, $isCompleted, - $isPassed, $isFailed, $isTerminated, $isAbandoned, $courseid; - - - //Why did I ake these arrays? they should just be bools - public $completed, $passed, $inprogress = true | false; - //public $passed = array('finished'=> true|false, 'info' =>""); - //public $inProgress = array('finished'=> true|false, 'info' =>""); - - // Methods - function set_name($name) { - $this->name = $name; - } - function get_name() { - return $this->name; - } - -//Constructs sessionss. Is fed array and where array key matches property, sets the property. -function __construct($statement){ - - foreach($statement as $key => $value){ - - $this->$key = ($value); - } - -} - -} -?> \ No newline at end of file diff --git a/cmi5PHP/tests/AboutTest.php b/cmi5PHP/tests/AboutTest.php deleted file mode 100755 index 1b29dbe..0000000 --- a/cmi5PHP/tests/AboutTest.php +++ /dev/null @@ -1,66 +0,0 @@ -assertInstanceOf('cmi5\About', $obj); - $this->assertAttributeEmpty('version', $obj, 'version empty'); - $this->assertAttributeNotEmpty('extensions', $obj, 'extenstions not empty'); - } - - public function testUsesArraySetterTrait() { - $this->assertContains('cmi5\ArraySetterTrait', class_uses('cmi5\About')); - } - - public function testUsesFromJSONTrait() { - $this->assertContains('cmi5\FromJSONTrait', class_uses('cmi5\About')); - } - - public function testUsesAsVersionTrait() { - $this->assertContains('cmi5\AsVersionTrait', class_uses('cmi5\About')); - } - - public function testVersion() { - $obj = new About(); - $version = [self::VERSION_1]; - $this->assertSame($obj, $obj->setVersion($version)); - $this->assertSame($version, $obj->getVersion()); - } - - public function testExtensionsWithArray() { - $obj = new About(); - $this->assertSame($obj, $obj->setExtensions(['foo' => 'bar'])); - $this->assertInstanceOf('cmi5\Extensions', $obj->getExtensions()); - $this->assertFalse($obj->getExtensions()->isEmpty()); - } - - // TODO: need to loop versions - public function testAsVersion() { - $args = ['version' => [self::VERSION_1]]; - $obj = new About($args); - $versioned = $obj->asVersion(self::VERSION_1); - - $this->assertEquals($versioned, $args, "version only: 1.0.0"); - } -} diff --git a/cmi5PHP/tests/ActivityDefinitionTest.php b/cmi5PHP/tests/ActivityDefinitionTest.php deleted file mode 100755 index 3b493b1..0000000 --- a/cmi5PHP/tests/ActivityDefinitionTest.php +++ /dev/null @@ -1,119 +0,0 @@ -assertInstanceOf('cmi5\ActivityDefinition', $obj); - foreach ($this->emptyProperties as $property) { - $this->assertAttributeEmpty($property, $obj, "$property empty"); - } - foreach ($this->nonEmptyProperties as $property) { - $this->assertAttributeNotEmpty($property, $obj, "$property not empty"); - } - } - - public function testUsesArraySetterTrait() { - $this->assertContains('cmi5\ArraySetterTrait', class_uses('cmi5\ActivityDefinition')); - } - - public function testUsesFromJSONTrait() { - $this->assertContains('cmi5\FromJSONTrait', class_uses('cmi5\ActivityDefinition')); - } - - public function testUsesAsVersionTrait() { - $this->assertContains('cmi5\AsVersionTrait', class_uses('cmi5\ActivityDefinition')); - } - - // TODO: need more robust test (happy-path) - public function testAsVersion() { - $args = [ - 'name' => ['en' => self::NAME], - 'extensions' => [] - ]; - $args['extensions'][COMMON_EXTENSION_ID_1] = 'test'; - - $obj = ActivityDefinition::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = ActivityDefinition::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionEmptyLanguageMap() { - $args = ['name' => []]; - - $obj = ActivityDefinition::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - unset($args['name']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmptyExtensions() { - $args = ['extensions' => []]; - - $obj = ActivityDefinition::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - unset($args['extensions']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmptyStringInLanguageMap() { - $args = ['name' => ['en' => '']]; - - $obj = ActivityDefinition::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } -} diff --git a/cmi5PHP/tests/ActivityProfileTest.php b/cmi5PHP/tests/ActivityProfileTest.php deleted file mode 100755 index 2c973e7..0000000 --- a/cmi5PHP/tests/ActivityProfileTest.php +++ /dev/null @@ -1,30 +0,0 @@ -setActivity(['id' => COMMON_ACTIVITY_ID]); - - $this->assertInstanceOf('cmi5\Activity', $profile->getActivity()); - } -} diff --git a/cmi5PHP/tests/ActivityTest.php b/cmi5PHP/tests/ActivityTest.php deleted file mode 100755 index f9fd4d9..0000000 --- a/cmi5PHP/tests/ActivityTest.php +++ /dev/null @@ -1,178 +0,0 @@ - 'http://id.cmi5api.com/activitytype/unit-test', - 'name' => [ - 'en-US' => 'test', - 'en-GB' => 'test', - 'es' => 'prueba' - ] - ]; - } - - public function testInstantiation() { - $obj = new Activity(); - $this->assertInstanceOf('cmi5\Activity', $obj); - $this->assertAttributeEmpty('id', $obj, 'id empty'); - $this->assertAttributeEmpty('definition', $obj, 'definition empty'); - } - - public function testFromJSONInvalidNull() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Activity::fromJSON(null); - } - - public function testFromJSONInvalidEmptyString() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Activity::fromJSON(''); - } - - public function testFromJSONInvalidMalformed() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Activity::fromJSON('{id:"some value"}'); - } - - public function testFromJSONIDOnly() { - $obj = Activity::fromJSON('{"id":"' . COMMON_ACTIVITY_ID . '"}'); - $this->assertInstanceOf('cmi5\Activity', $obj); - $this->assertAttributeEquals(COMMON_ACTIVITY_ID, 'id', $obj, 'id matches'); - $this->assertAttributeEmpty('definition', $obj, 'definition empty'); - } - - // TODO: need to loop versions - public function testAsVersion() { - $args = [ - 'id' => COMMON_ACTIVITY_ID, - 'definition' => [ - 'name' => [ - 'en' => 'test' - ] - ] - ]; - - $obj = Activity::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Activity'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = Activity::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Activity'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionIdOnly() { - $args = [ 'id' => COMMON_ACTIVITY_ID ]; - - $obj = Activity::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Activity'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmptyDefinition() { - $args = [ - 'id' => COMMON_ACTIVITY_ID, - 'definition' => [] - ]; - - $obj = Activity::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Activity'; - unset($args['definition']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testCompareWithSignature() { - $full = [ - 'id' => COMMON_ACTIVITY_ID, - 'definition' => self::$DEFINITION - ]; - $definition2 = array_replace(self::$DEFINITION, ['type' => 'http://id.cmi5api.com/activitytype/unit-test-suite']); - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ], - [ - 'description' => 'id', - 'objArgs' => ['id' => COMMON_ACTIVITY_ID] - ], - [ - 'description' => 'definition', - 'objArgs' => ['definition' => self::$DEFINITION] - ], - [ - 'description' => 'all', - 'objArgs' => $full - ], - - // - // definitions are not matched for signature purposes because they - // are not supposed to affect the meaning of the statement and may - // be supplied in canonical format, etc. - // - [ - 'description' => 'definition only: mismatch (allowed)', - 'objArgs' => ['definition' => self::$DEFINITION ], - 'sigArgs' => ['definition' => $definition2 ] - ], - [ - 'description' => 'full: definition mismatch (allowed)', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['definition' => $definition2 ]) - ], - - [ - 'description' => 'id only: mismatch', - 'objArgs' => ['id' => COMMON_ACTIVITY_ID ], - 'sigArgs' => ['id' => COMMON_ACTIVITY_ID . '/invalid' ], - 'reason' => 'Comparison of id failed: value is not the same' - ], - [ - 'description' => 'full: id mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['id' => COMMON_ACTIVITY_ID . '/invalid']), - 'reason' => 'Comparison of id failed: value is not the same' - ] - ]; - $this->runSignatureCases("cmi5\Activity", $cases); - } -} diff --git a/cmi5PHP/tests/AgentAccountTest.php b/cmi5PHP/tests/AgentAccountTest.php deleted file mode 100755 index fb8ea47..0000000 --- a/cmi5PHP/tests/AgentAccountTest.php +++ /dev/null @@ -1,149 +0,0 @@ -assertInstanceOf('cmi5\AgentAccount', $obj); - $this->assertAttributeEmpty('homePage', $obj, 'homePage empty'); - $this->assertAttributeEmpty('name', $obj, 'name empty'); - } - - public function testUsesArraySetterTrait() { - $this->assertContains('cmi5\ArraySetterTrait', class_uses('cmi5\AgentAccount')); - } - - public function testUsesFromJSONTrait() { - $this->assertContains('cmi5\FromJSONTrait', class_uses('cmi5\AgentAccount')); - } - - public function testUsesAsVersionTrait() { - $this->assertContains('cmi5\AsVersionTrait', class_uses('cmi5\AgentAccount')); - } - - public function testName() { - $obj = new AgentAccount(); - $name = COMMON_ACCT_NAME; - $this->assertSame($obj, $obj->setName($name)); - $this->assertSame($name, $obj->getName()); - } - - public function testHomePage() { - $obj = new AgentAccount(); - $homePage = COMMON_ACCT_HOMEPAGE; - $this->assertSame($obj, $obj->setHomePage($homePage)); - $this->assertSame($homePage, $obj->getHomePage()); - } - - public function testAsVersion() { - $args = [ - 'name' => COMMON_ACCT_NAME, - 'homePage' => COMMON_ACCT_HOMEPAGE - ]; - - $obj = AgentAccount::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = AgentAccount::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionEmptyStrings() { - $args = [ - 'name' => '', - 'homePage' => '' - ]; - - $obj = AgentAccount::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testCompareWithSignature() { - $full = [ - 'homePage' => COMMON_ACCT_HOMEPAGE, - 'name' => COMMON_ACCT_NAME - ]; - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ], - [ - 'description' => 'homePage', - 'objArgs' => ['homePage' => COMMON_ACCT_HOMEPAGE] - ], - [ - 'description' => 'name', - 'objArgs' => ['name' => COMMON_ACCT_NAME] - ], - [ - 'description' => 'all', - 'objArgs' => $full - ], - [ - 'description' => 'homepage only: mismatch', - 'objArgs' => ['homePage' => COMMON_ACCT_HOMEPAGE], - 'sigArgs' => ['homePage' => COMMON_ACCT_HOMEPAGE . '/invalid'], - 'reason' => 'Comparison of homePage failed: value is not the same' - ], - [ - 'description' => 'name only: mismatch', - 'objArgs' => ['name' => COMMON_ACCT_NAME], - 'sigArgs' => ['name' => 'diff'], - 'reason' => 'Comparison of name failed: value is not the same' - ], - [ - 'description' => 'full: homePage mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['homePage' => COMMON_ACCT_HOMEPAGE . '/invalid']), - 'reason' => 'Comparison of homePage failed: value is not the same' - ], - [ - 'description' => 'full: name mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['name' => 'diff']), - 'reason' => 'Comparison of name failed: value is not the same' - ], - [ - 'description' => 'full: both mismatch', - 'objArgs' => $full, - 'sigArgs' => ['homePage' => COMMON_ACCT_HOMEPAGE . '/invalid', 'name' => 'diff'], - 'reason' => 'Comparison of name failed: value is not the same' - ] - ]; - $this->runSignatureCases("cmi5\AgentAccount", $cases); - } -} diff --git a/cmi5PHP/tests/AgentProfileTest.php b/cmi5PHP/tests/AgentProfileTest.php deleted file mode 100755 index 0c4bf65..0000000 --- a/cmi5PHP/tests/AgentProfileTest.php +++ /dev/null @@ -1,35 +0,0 @@ -setAgent(['mbox' => COMMON_MBOX]); - - $this->assertInstanceOf('cmi5\Agent', $profile->getAgent()); - - $profile->setAgent(['objectType' => 'Group']); - - $this->assertInstanceOf('cmi5\Group', $profile->getAgent()); - } -} diff --git a/cmi5PHP/tests/AgentTest.php b/cmi5PHP/tests/AgentTest.php deleted file mode 100755 index d34d23d..0000000 --- a/cmi5PHP/tests/AgentTest.php +++ /dev/null @@ -1,331 +0,0 @@ -assertInstanceOf('cmi5\Agent', $obj); - $this->assertAttributeEmpty('name', $obj, 'name empty'); - $this->assertAttributeEmpty('mbox', $obj, 'mbox empty'); - $this->assertAttributeEmpty('mbox_sha1sum', $obj, 'mbox_sha1sum empty'); - $this->assertAttributeEmpty('openid', $obj, 'openid empty'); - $this->assertAttributeEmpty('account', $obj, 'account empty'); - } - - public function testFromJSONInvalidNull() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Agent::fromJSON(null); - } - - public function testFromJSONInvalidEmptyString() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Agent::fromJSON(''); - } - - public function testFromJSONInvalidMalformed() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Agent::fromJSON('{name:"some value"}'); - } - - // TODO: need to loop possible configs - public function testFromJSONInstantiations() { - $obj = Agent::fromJSON('{"mbox":"' . COMMON_MBOX . '"}'); - $this->assertInstanceOf('cmi5\Agent', $obj); - $this->assertSame(COMMON_MBOX, $obj->getMbox(), 'mbox value'); - } - - // TODO: need to loop versions - public function testAsVersionMbox() { - $args = [ - 'mbox' => COMMON_MBOX - ]; - - $obj = Agent::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Agent'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionMboxSha1() { - $args = [ - 'mbox_sha1sum' => COMMON_MBOX_SHA1 - ]; - - $obj = Agent::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Agent'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionAccount() { - $args = [ - 'account' => [ - 'name' => COMMON_ACCT_NAME, - 'homePage' => COMMON_ACCT_HOMEPAGE - ] - ]; - - $obj = Agent::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Agent'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionAccountEmptyStrings() { - $args = [ - 'account' => [ - 'name' => '', - 'homePage' => '' - ] - ]; - - $obj = Agent::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Agent'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmptyAccount() { - $args = [ - 'account' => [] - ]; - - $obj = Agent::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Agent'; - unset($args['account']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = Agent::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Agent'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testIsIdentified() { - $identified = [ - [ - 'description' => 'mbox', - 'args' => ['mbox' => COMMON_MBOX] - ], - [ - 'description' => 'mbox_sha1sum', - 'args' => ['mbox_sha1sum' => COMMON_MBOX_SHA1] - ], - [ - 'description' => 'openid', - 'args' => ['openid' => COMMON_OPENID] - ], - [ - 'description' => 'account', - 'args' => ['account' => ['homePage' => COMMON_ACCT_HOMEPAGE, 'name' => COMMON_ACCT_NAME]] - ] - ]; - foreach ($identified as $case) { - $obj = new Agent ($case['args']); - $this->assertTrue($obj->isIdentified(), 'identified ' . $case['description']); - } - - $notIdentified = [ - [ - 'description' => 'empty', - 'args' => [] - ], - [ - 'description' => 'name only', - 'args' => ['name' => 'Test'] - ] - ]; - foreach ($notIdentified as $case) { - $obj = new Agent ($case['args']); - $this->assertFalse($obj->isIdentified(), 'not identified ' . $case['description']); - } - } - - public function testSetMbox() { - $obj = new Agent(); - - $obj->setMbox(COMMON_MBOX); - $this->assertSame(COMMON_MBOX, $obj->getMbox()); - - $obj->setMbox(COMMON_EMAIL); - $this->assertSame(COMMON_MBOX, $obj->getMbox()); - - // - // make sure it doesn't add mailto when null - // - $obj->setMbox(null); - $this->assertAttributeEmpty('mbox', $obj); - } - - public function testGetMbox_sha1sum() { - $obj = new Agent(['mbox_sha1sum' => COMMON_MBOX_SHA1]); - $this->assertSame($obj->getMbox_sha1sum(), COMMON_MBOX_SHA1, 'original sha1'); - - $obj = new Agent(['mbox' => COMMON_MBOX]); - $this->assertSame($obj->getMbox_sha1sum(), COMMON_MBOX_SHA1, 'sha1 from mbox'); - } - - public function testCompareWithSignature() { - $name = 'Test Name'; - $acct1 = new AgentAccount( - [ - 'homePage' => COMMON_ACCT_HOMEPAGE, - 'name' => COMMON_ACCT_NAME - ] - ); - $acct2 = new AgentAccount( - [ - 'homePage' => COMMON_ACCT_HOMEPAGE, - 'name' => COMMON_ACCT_NAME . '-diff' - ] - ); - - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ], - [ - 'description' => 'mbox', - 'objArgs' => ['mbox' => COMMON_MBOX] - ], - [ - 'description' => 'mbox_sha1sum', - 'objArgs' => ['mbox_sha1sum' => COMMON_MBOX_SHA1] - ], - [ - 'description' => 'openid', - 'objArgs' => ['openid' => COMMON_OPENID] - ], - [ - 'description' => 'account', - 'objArgs' => ['account' => $acct1] - ], - [ - 'description' => 'mbox with name', - 'objArgs' => ['mbox' => COMMON_MBOX, 'name' => $name] - ], - [ - 'description' => 'mbox_sha1sum with name', - 'objArgs' => ['mbox_sha1sum' => COMMON_MBOX_SHA1, 'name' => $name] - ], - [ - 'description' => 'openid with name', - 'objArgs' => ['openid' => COMMON_OPENID, 'name' => $name] - ], - [ - 'description' => 'account with name', - 'objArgs' => ['account' => $acct1, 'name' => $name] - ], - [ - 'description' => 'mbox with mismatch name', - 'objArgs' => ['mbox' => COMMON_MBOX, 'name' => $name], - 'sigArgs' => ['mbox' => COMMON_MBOX, 'name' => $name . ' diff'] - ], - [ - 'description' => 'mbox_sha1sum with mismatch name', - 'objArgs' => ['mbox_sha1sum' => COMMON_MBOX_SHA1, 'name' => $name], - 'sigArgs' => ['mbox_sha1sum' => COMMON_MBOX_SHA1, 'name' => $name . ' diff'] - ], - [ - 'description' => 'openid with mismatch name', - 'objArgs' => ['openid' => COMMON_OPENID, 'name' => $name], - 'sigArgs' => ['openid' => COMMON_OPENID, 'name' => $name . ' diff'] - ], - [ - 'description' => 'account with mismatch name', - 'objArgs' => ['account' => $acct1, 'name' => $name], - 'sigArgs' => ['account' => $acct1, 'name' => $name . ' diff'] - ], - [ - 'description' => 'mbox only: mismatch', - 'objArgs' => ['mbox' => COMMON_MBOX ], - 'sigArgs' => ['mbox' => 'diff-' . COMMON_MBOX ], - 'reason' => 'Comparison of mbox failed: value is not the same' - ], - [ - 'description' => 'mbox_sha1sum only: mismatch', - 'objArgs' => ['mbox_sha1sum' => COMMON_MBOX_SHA1 ], - 'sigArgs' => ['mbox_sha1sum' => 'diff-' . COMMON_MBOX_SHA1 ], - 'reason' => 'Comparison of mbox_sha1sum failed: value is not the same' - ], - [ - 'description' => 'openid only: mismatch', - 'objArgs' => ['openid' => COMMON_OPENID ], - 'sigArgs' => ['openid' => COMMON_OPENID . 'diff/' ], - 'reason' => 'Comparison of openid failed: value is not the same' - ], - [ - 'description' => 'account only: mismatch', - 'objArgs' => ['account' => $acct1 ], - 'sigArgs' => ['account' => $acct2 ], - 'reason' => 'Comparison of account failed: Comparison of name failed: value is not the same' - ], - - // - // special cases where we can try to equate an mbox and an mbox SHA1 sum - // - [ - 'description' => 'this.mbox to signature.mbox_sha1sum', - 'objArgs' => ['mbox' => COMMON_MBOX], - 'sigArgs' => ['mbox_sha1sum' => COMMON_MBOX_SHA1] - ], - [ - 'description' => 'this.mbox_sha1sum to signature.mbox', - 'objArgs' => ['mbox_sha1sum' => COMMON_MBOX_SHA1], - 'sigArgs' => ['mbox' => COMMON_MBOX] - ], - [ - 'description' => 'this.mbox to signature.mbox_sha1sum non-matching', - 'objArgs' => ['mbox' => COMMON_MBOX], - 'sigArgs' => ['mbox_sha1sum' => COMMON_MBOX_SHA1 . '-diff'], - 'reason' => 'Comparison of this.mbox to signature.mbox_sha1sum failed: no match' - ], - [ - 'description' => 'this.mbox_sha1sum to signature.mbox non-matching', - 'objArgs' => ['mbox_sha1sum' => COMMON_MBOX_SHA1 . '-diff'], - 'sigArgs' => ['mbox' => COMMON_MBOX], - 'reason' => 'Comparison of this.mbox_sha1sum to signature.mbox failed: no match' - ], - ]; - $this->runSignatureCases("cmi5\Agent", $cases); - } -} diff --git a/cmi5PHP/tests/AsVersionTraitTest.php b/cmi5PHP/tests/AsVersionTraitTest.php deleted file mode 100755 index 7b1f909..0000000 --- a/cmi5PHP/tests/AsVersionTraitTest.php +++ /dev/null @@ -1,36 +0,0 @@ -assertTrue(trait_exists('cmi5\AsVersionTrait')); - } - - public function testAsVersionReturnsArray() { - $trait = $this->getMockForTrait('cmi5\AsVersionTrait'); - $this->assertInternalType('array', $trait->asVersion('test')); - } - - public function testMagicSetThrowsException() { - $this->setExpectedException('DomainException'); - $trait = $this->getMockForTrait('cmi5\AsVersionTrait'); - $trait->foo = 'bar'; - } -} diff --git a/cmi5PHP/tests/AttachmentTest.php b/cmi5PHP/tests/AttachmentTest.php deleted file mode 100755 index a55bd2c..0000000 --- a/cmi5PHP/tests/AttachmentTest.php +++ /dev/null @@ -1,271 +0,0 @@ -assertInstanceOf('cmi5\Attachment', $obj); - foreach ($this->emptyProperties as $property) { - $this->assertAttributeEmpty($property, $obj, "$property empty"); - } - foreach ($this->nonEmptyProperties as $property) { - $this->assertAttributeNotEmpty($property, $obj, "$property not empty"); - } - } - - public function testUsesArraySetterTrait() { - $this->assertContains('cmi5\ArraySetterTrait', class_uses('cmi5\Attachment')); - } - - public function testUsesFromJSONTrait() { - $this->assertContains('cmi5\FromJSONTrait', class_uses('cmi5\Attachment')); - } - - public function testUsesAsVersionTrait() { - $this->assertContains('cmi5\AsVersionTrait', class_uses('cmi5\Attachment')); - } - - public function testContent() { - $obj = new Attachment(); - $obj->setContent(self::CONTENT_STR); - - $this->assertSame($obj->getContent(), self::CONTENT_STR, 'content body'); - $this->assertSame($obj->getLength(), self::CONTENT_LENGTH, 'length'); - $this->assertSame($obj->getSha2(), self::CONTENT_SHA2, 'sha2'); - } - - public function testHasContent() { - $no_content = new Attachment(); - $this->assertFalse($no_content->hasContent()); - - $has_content = new Attachment( - [ - 'content' => self::CONTENT_STR - ] - ); - $this->assertTrue($has_content->hasContent()); - - $set_content = new Attachment(); - $set_content->setContent(self::CONTENT_STR); - $this->assertTrue($set_content->hasContent()); - } - - // TODO: need more robust test (happy-path) - public function testAsVersion() { - $args = [ - 'usageType' => self::USAGE_TYPE, - 'display' => ['en-US' => self::DISPLAY], - 'description' => ['en-US' => self::DESCRIPTION], - 'contentType' => self::CONTENT_TYPE, - 'length' => self::CONTENT_LENGTH, - 'sha2' => self::CONTENT_SHA2, - 'fileUrl' => self::FILE_URL - ]; - $obj = new Attachment($args); - $versioned = $obj->asVersion(Version::latest()); - $this->assertEquals($versioned, $args, '1.0.0'); - - $obj = new Attachment( - [ - 'content' => self::CONTENT_STR - ] - ); - $this->assertEquals( - $obj->asVersion(Version::latest()), - ['length' => self::CONTENT_LENGTH, 'sha2' => self::CONTENT_SHA2], - 'auto populated properties but content not returned' - ); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = Attachment::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionEmptyLanguageMap() { - $args = ['display' => []]; - - $obj = Attachment::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - unset($args['display']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testCompareWithSignature() { - $full = [ - 'usageType' => self::USAGE_TYPE, - 'display' => ['en-US' => self::DISPLAY], - 'description' => ['en-US' => self::DESCRIPTION], - 'contentType' => self::CONTENT_TYPE, - 'length' => self::CONTENT_LENGTH, - 'sha2' => self::CONTENT_SHA2, - 'fileUrl' => self::FILE_URL - ]; - - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ], - [ - 'description' => 'usageType', - 'objArgs' => ['usageType' => self::USAGE_TYPE] - ], - [ - 'description' => 'display', - 'objArgs' => ['display' => self::DISPLAY] - ], - [ - 'description' => 'description', - 'objArgs' => ['description' => self::DESCRIPTION] - ], - [ - 'description' => 'contentType', - 'objArgs' => ['contentType' => self::CONTENT_TYPE] - ], - [ - 'description' => 'length', - 'objArgs' => ['length' => self::CONTENT_LENGTH] - ], - [ - 'description' => 'sha2', - 'objArgs' => ['sha2' => self::CONTENT_SHA2] - ], - [ - 'description' => 'fileUrl', - 'objArgs' => ['fileUrl' => self::FILE_URL] - ], - [ - 'description' => 'all', - 'objArgs' => $full - ], - - // - // display and description are language maps which we aren't - // checking for meaningful differences, so even though they - // differ they should not cause signature comparison to fail - // - [ - 'description' => 'display only with difference', - 'objArgs' => ['display' => [ 'en-US' => self::DISPLAY ]], - 'sigArgs' => ['display' => [ 'en-US' => self::DISPLAY . ' invalid' ]] - ], - [ - 'description' => 'description only with difference', - 'objArgs' => ['description' => [ 'en-US' => self::DESCRIPTION ]], - 'sigArgs' => ['description' => [ 'en-US' => self::DESCRIPTION . ' invalid' ]] - ], - - [ - 'description' => 'usageType only: mismatch', - 'objArgs' => ['usageType' => self::USAGE_TYPE ], - 'sigArgs' => ['usageType' => self::USAGE_TYPE . '/invalid' ], - 'reason' => 'Comparison of usageType failed: value is not the same' - ], - [ - 'description' => 'contentType only: mismatch', - 'objArgs' => ['contentType' => self::CONTENT_TYPE ], - 'sigArgs' => ['contentType' => 'application/octet-stream' ], - 'reason' => 'Comparison of contentType failed: value is not the same' - ], - [ - 'description' => 'length only: mismatch', - 'objArgs' => ['length' => self::CONTENT_LENGTH ], - 'sigArgs' => ['length' => self::CONTENT_LENGTH + 2 ], - 'reason' => 'Comparison of length failed: value is not the same' - ], - [ - 'description' => 'sha2 only: mismatch', - 'objArgs' => ['sha2' => self::CONTENT_SHA2], - 'sigArgs' => ['sha2' => self::CONTENT_SHA2 . self::CONTENT_SHA2], - 'reason' => 'Comparison of sha2 failed: value is not the same' - ], - [ - 'description' => 'fileUrl only: mismatch', - 'objArgs' => ['fileUrl' => self::FILE_URL], - 'sigArgs' => ['fileUrl' => self::FILE_URL . '/invalid'], - 'reason' => 'Comparison of fileUrl failed: value is not the same' - ], - [ - 'description' => 'full: usageType mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['usageType' => self::USAGE_TYPE . '/invalid']), - 'reason' => 'Comparison of usageType failed: value is not the same' - ], - [ - 'description' => 'full: contentType mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['contentType' => 'application/octet-stream']), - 'reason' => 'Comparison of contentType failed: value is not the same' - ], - [ - 'description' => 'full: length mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['length' => self::CONTENT_LENGTH + 2]), - 'reason' => 'Comparison of length failed: value is not the same' - ], - [ - 'description' => 'full: sha2 mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['sha2' => self::CONTENT_SHA2 . self::CONTENT_SHA2]), - 'reason' => 'Comparison of sha2 failed: value is not the same' - ], - [ - 'description' => 'full: fileUrl mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['fileUrl' => self::FILE_URL . '/invalid']), - 'reason' => 'Comparison of fileUrl failed: value is not the same' - ] - ]; - $this->runSignatureCases("cmi5\Attachment", $cases); - } -} diff --git a/cmi5PHP/tests/ContextActivitiesTest.php b/cmi5PHP/tests/ContextActivitiesTest.php deleted file mode 100755 index 82d4630..0000000 --- a/cmi5PHP/tests/ContextActivitiesTest.php +++ /dev/null @@ -1,301 +0,0 @@ - COMMON_ACTIVITY_ID - ]; - - public function testInstantiation() { - $obj = new ContextActivities(); - $this->assertInstanceOf('cmi5\ContextActivities', $obj); - foreach (self::$listProps as $k) { - $this->assertAttributeEquals([], $k, $obj, "$k empty array"); - } - } - - public function testUsesArraySetterTrait() { - $this->assertContains('cmi5\ArraySetterTrait', class_uses('cmi5\ContextActivities')); - } - - public function testUsesFromJSONTrait() { - $this->assertContains('cmi5\FromJSONTrait', class_uses('cmi5\ContextActivities')); - } - - public function testUsesAsVersionTrait() { - $this->assertContains('cmi5\AsVersionTrait', class_uses('cmi5\ContextActivities')); - } - - public function testFromJSONInstantiations() { - $common_activity = new Activity(self::$common_activity_cfg); - - $all_json = array(); - foreach (self::$listProps as $k) { - $getMethod = 'get' . ucfirst($k); - - $prop_json = '"' . $k . '":[' . json_encode($common_activity->asVersion('1.0.0')) . ']'; - - array_push($all_json, $prop_json); - - $obj = ContextActivities::fromJSON('{' . $prop_json . '}'); - - $this->assertInstanceOf('cmi5\ContextActivities', $obj); - $this->assertEquals([$common_activity], $obj->$getMethod(), "$k list"); - } - - $obj = ContextActivities::fromJSON('{' . join(",", $all_json) . "}"); - - $this->assertInstanceOf('cmi5\ContextActivities', $obj); - $this->assertEquals([$common_activity], $obj->getCategory(), "all props: category list"); - $this->assertEquals([$common_activity], $obj->getParent(), "all props: parent list"); - $this->assertEquals([$common_activity], $obj->getGrouping(), "all props: grouping list"); - $this->assertEquals([$common_activity], $obj->getOther(), "all props: other list"); - } - - // TODO: need to loop versions - public function testAsVersionWithSingleList() { - $keys = ['category', 'parent', 'grouping', 'other']; - foreach ($keys as $k) { - $args = []; - $args[$k] = [ self::$common_activity_cfg ]; - - $obj = ContextActivities::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args[$k][0]['objectType'] = 'Activity'; - - $this->assertEquals($versioned, $args, "serialized version matches original"); - - unset($args[$k][0]['objectType']); - } - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = ContextActivities::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionWithEmptyList() { - $keys = ['category', 'parent', 'grouping', 'other']; - foreach ($keys as $k) { - $args = []; - $args[$k] = []; - - $obj = ContextActivities::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - unset($args[$k]); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - } - - public function testListSetters() { - $common_activity = new Activity(self::$common_activity_cfg); - - foreach (self::$listProps as $k) { - $setMethod = 'set' . ucfirst($k); - $getMethod = 'get' . ucfirst($k); - - $obj = new ContextActivities(); - - $obj->$setMethod($common_activity); - $this->assertEquals([$common_activity], $obj->$getMethod(), "$k: single Activity"); - - $obj->$setMethod([]); - $this->assertEquals([], $obj->$getMethod(), "$k: empty array"); - - $obj->$setMethod([$common_activity]); - $this->assertEquals([$common_activity], $obj->$getMethod(), "$k: array of single Activity"); - - $obj->$setMethod([]); - - $obj->$setMethod(self::$common_activity_cfg); - $this->assertEquals([$common_activity], $obj->$getMethod(), "$k: single Activity configuration"); - - $obj->$setMethod([]); - - $obj->$setMethod([self::$common_activity_cfg]); - $this->assertEquals([$common_activity], $obj->$getMethod(), "$k: array of single Activity configuration"); - } - } - - public function testCompareWithSignature() { - $acts = [ - new Activity( - ['id' => COMMON_ACTIVITY_ID . '/0'] - ), - new Activity( - ['id' => COMMON_ACTIVITY_ID . '/1'] - ), - new Activity( - ['id' => COMMON_ACTIVITY_ID . '/2'] - ), - new Activity( - ['id' => COMMON_ACTIVITY_ID . '/3'] - ) - ]; - - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ] - ]; - foreach (self::$listProps as $k) { - array_push( - $cases, - [ - 'description' => "$k single", - 'objArgs' => [$k => [ $acts[0] ]], - ], - [ - 'description' => "$k multiple", - 'objArgs' => [$k => [ $acts[0], $acts[1] ]], - ], - [ - 'description' => "$k single empty sig (no $k set)", - 'objArgs' => [$k => [ $acts[0] ]], - 'sigArgs' => [], - 'reason' => "Comparison of $k failed: array lengths differ" - ], - [ - 'description' => "$k single empty sig", - 'objArgs' => [$k => [ $acts[0] ]], - 'sigArgs' => [$k => []], - 'reason' => "Comparison of $k failed: array lengths differ" - ], - [ - 'description' => "$k multiple single sig", - 'objArgs' => [$k => [ $acts[0], $acts[1] ]], - 'sigArgs' => [$k => [ $acts[1] ]], - 'reason' => "Comparison of $k failed: array lengths differ" - ], - [ - 'description' => "$k single multiple sig", - 'objArgs' => [$k => [ $acts[0] ]], - 'sigArgs' => [$k => [ $acts[0], $acts[1] ]], - 'reason' => "Comparison of $k failed: array lengths differ" - ], - [ - 'description' => "$k single diff sig", - 'objArgs' => [$k => [ $acts[0] ]], - 'sigArgs' => [$k => [ $acts[1] ]], - 'reason' => 'Comparison of ' . $k . '[0] failed: Comparison of id failed: value is not the same' - ], - [ - 'description' => "$k multiple diff order", - 'objArgs' => [$k => [ $acts[0], $acts[1] ]], - 'sigArgs' => [$k => [ $acts[1], $acts[0] ]], - 'reason' => 'Comparison of ' . $k . '[0] failed: Comparison of id failed: value is not the same' - ] - ); - } - - foreach ([ - ['category', 'parent'], - ['category', 'other'], - ['category', 'grouping'], - ['parent', 'other'], - ['parent', 'grouping'], - ['grouping', 'other'], - ['category', 'parent', 'other'], - ['category', 'parent', 'grouping'], - ['category', 'other', 'grouping'], - ['parent', 'other', 'grouping'], - self::$listProps - ] as $set) { - $prefix = implode(', ', $set); - $new_cases = [ - [ - 'description' => $prefix, - 'objArgs' => [], - 'sigArgs' => [], - ], - [ - 'description' => "$prefix: empty sig", - 'objArgs' => [], - 'sigArgs' => [], - 'reason' => 'Comparison of ' . $set[0] . ' failed: array lengths differ' - ], - [ - 'description' => "$prefix: one missing this", - 'objArgs' => [], - 'sigArgs' => [], - 'reason' => 'Comparison of ' . $set[0] . ' failed: array lengths differ' - ], - [ - 'description' => "$prefix: one missing signature", - 'objArgs' => [], - 'sigArgs' => [], - 'reason' => 'Comparison of ' . $set[0] . ' failed: array lengths differ' - ] - ]; - - for ($i = 0; $i < count($set); $i++) { - $new_cases[0]['objArgs'][ $set[$i] ] = $acts[$i]; - $new_cases[0]['sigArgs'][ $set[$i] ] = $acts[$i]; - - $new_cases[1]['objArgs'][ $set[$i] ] = $acts[$i]; - - $new_cases[2]['objArgs'][ $set[$i] ] = $acts[$i]; - $new_cases[3]['objArgs'][ $set[$i] ] = $acts[$i]; - if ($i !== 0) { - $new_cases[2]['sigArgs'][ $set[$i] ] = $acts[$i]; - $new_cases[3]['sigArgs'][ $set[$i] ] = $acts[$i]; - } - } - - $cases = array_merge($cases, $new_cases); - } - $this->runSignatureCases("cmi5\ContextActivities", $cases); - } - - /** - * @dataProvider invalidListSetterDataProvider - */ - public function testListSetterThrowsInvalidArgumentException($publicMethodName, $invalidValue) { - $this->setExpectedException( - 'InvalidArgumentException', - 'type of arg1 must be Activity, array of Activity properties, or array of Activity/array of Activity properties' - ); - $obj = new ContextActivities(); - $obj->$publicMethodName($invalidValue); - } - - public function invalidListSetterDataProvider() { - $invalidValue = 1; - return [ - ["setCategory", $invalidValue], - ["setParent", $invalidValue], - ["setGrouping", $invalidValue], - ["setOther", $invalidValue] - ]; - } -} diff --git a/cmi5PHP/tests/ContextTest.php b/cmi5PHP/tests/ContextTest.php deleted file mode 100755 index ffabfaa..0000000 --- a/cmi5PHP/tests/ContextTest.php +++ /dev/null @@ -1,381 +0,0 @@ -assertInstanceOf('cmi5\Context', $obj); - foreach ($this->emptyProperties as $property) { - $this->assertAttributeEmpty($property, $obj, "$property empty"); - } - foreach ($this->nonEmptyProperties as $property) { - $this->assertAttributeNotEmpty($property, $obj, "$property not empty"); - } - } - - public function testUsesArraySetterTrait() { - $this->assertContains('cmi5\ArraySetterTrait', class_uses('cmi5\Context')); - } - - public function testUsesFromJSONTrait() { - $this->assertContains('cmi5\FromJSONTrait', class_uses('cmi5\Context')); - } - - public function testUsesAsVersionTrait() { - $this->assertContains('cmi5\AsVersionTrait', class_uses('cmi5\Context')); - } - - /* - // TODO: need to loop possible configs - public function testFromJSONInstantiations() { - $obj = Context::fromJSON('{"mbox":"' . COMMON_GROUP_MBOX . '", "member":[{"mbox":"' . COMMON_MBOX . '"}]}'); - $this->assertInstanceOf('cmi5\Context', $obj); - $this->assertSame(COMMON_GROUP_MBOX, $obj->getMbox(), 'mbox value'); - $this->assertEquals([['mbox' => COMMON_MBOX]], $obj->getMember(), 'member list'); - } - */ - - public function testAsVersion() { - $args = [ - 'registration' => Util::getUUID(), - 'instructor' => [ - 'name' => 'test agent' - ], - 'team' => [ - 'name' => 'test group' - ], - 'contextActivities' => [ - 'category' => [ - [ - 'id' => 'test category' - ] - ] - ], - 'revision' => 'test revision', - 'platform' => 'test platform', - 'language' => 'test language', - 'statement' => [ - 'id' => Util::getUUID() - ], - 'extensions' => [], - ]; - $args['extensions'][COMMON_EXTENSION_ID_1] = "test"; - - $obj = Context::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['instructor']['objectType'] = 'Agent'; - $args['team']['objectType'] = 'Group'; - $args['contextActivities']['category'][0]['objectType'] = 'Activity'; - $args['statement']['objectType'] = 'StatementRef'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = Context::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionEmptyLists() { - $args = [ - 'contextActivities' => [ - 'category' => [] - ], - 'extensions' => [], - ]; - - $obj = Context::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - unset($args['contextActivities']); - unset($args['extensions']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testSetInstructor() { - $common_agent_cfg = [ 'mbox' => COMMON_MBOX ]; - $common_agent = new Agent($common_agent_cfg); - $common_group_cfg = [ 'mbox' => COMMON_MBOX, 'objectType' => 'Group' ]; - $common_group = new Group($common_agent_cfg); - - $obj = new Context(); - - $obj->setInstructor($common_agent_cfg); - $this->assertEquals($common_agent, $obj->getInstructor(), "agent config"); - - $obj->setInstructor(null); - $this->assertEmpty($obj->getInstructor(), "empty"); - } - - public function testCompareWithSignature() { - $registration1 = Util::getUUID(); - $registration2 = Util::getUUID(); - $instructor1 = new Agent( - [ 'mbox' => COMMON_MBOX ] - ); - $instructor2 = new Agent( - [ 'account' => [ 'homePage' => COMMON_ACCT_HOMEPAGE, 'name' => COMMON_ACCT_NAME ]] - ); - $team1 = new Agent( - [ 'mbox' => COMMON_MBOX ] - ); - $team2 = new Agent( - [ 'account' => [ 'homePage' => COMMON_ACCT_HOMEPAGE, 'name' => COMMON_ACCT_NAME ]] - ); - $contextActivities1 = new ContextActivities( - [ 'parent' => [ COMMON_ACTIVITY_ID ]] - ); - $contextActivities2 = new ContextActivities( - [ 'parent' => [ COMMON_ACTIVITY_ID . '/parent' ]], - [ 'grouping' => [ COMMON_ACTIVITY_ID ]] - ); - $ref1 = new StatementRef( - [ 'id' => Util::getUUID() ] - ); - $ref2 = new StatementRef( - [ 'id' => Util::getUUID() ] - ); - $extensions1 = new Extensions( - [ - COMMON_EXTENSION_ID_1 => 'test1', - COMMON_EXTENSION_ID_2 => 'test2' - ] - ); - $extensions2 = new Extensions( - [ - COMMON_EXTENSION_ID_1 => 'test1' - ] - ); - - $full = [ - 'registration' => $registration1, - 'instructor' => $instructor1, - 'team' => $team1, - 'contextActivities' => $contextActivities1, - 'revision' => '1', - 'platform' => 'mobile', - 'language' => 'en-US', - 'statement' => $ref1, - 'extensions' => $extensions1 - ]; - - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ], - [ - 'description' => 'registration', - 'objArgs' => ['registration' => $registration1] - ], - [ - 'description' => 'instructor', - 'objArgs' => ['instructor' => $instructor1] - ], - [ - 'description' => 'team', - 'objArgs' => ['team' => $team1] - ], - [ - 'description' => 'contextActivities', - 'objArgs' => ['contextActivities' => $contextActivities1] - ], - [ - 'description' => 'revision', - 'objArgs' => ['revision' => '1'] - ], - [ - 'description' => 'platform', - 'objArgs' => ['platform' => 'mobile'] - ], - [ - 'description' => 'language', - 'objArgs' => ['language' => 'en-US'] - ], - [ - 'description' => 'statement', - 'objArgs' => ['statement' => $ref1] - ], - [ - 'description' => 'extensions', - 'objArgs' => ['extensions' => $extensions1] - ], - [ - 'description' => 'all', - 'objArgs' => $full - ], - [ - 'description' => 'registration only: mismatch', - 'objArgs' => ['registration' => $registration1 ], - 'sigArgs' => ['registration' => $registration2 ], - 'reason' => 'Comparison of registration failed: value is not the same' - ], - [ - 'description' => 'instructor only: mismatch', - 'objArgs' => ['instructor' => $instructor1 ], - 'sigArgs' => ['instructor' => $instructor2 ], - 'reason' => 'Comparison of instructor failed: Comparison of mbox failed: value is not the same' - ], - [ - 'description' => 'team only: mismatch', - 'objArgs' => ['team' => $team1 ], - 'sigArgs' => ['team' => $team2 ], - 'reason' => 'Comparison of team failed: Comparison of mbox failed: value is not the same' - ], - [ - 'description' => 'contextActivities only: mismatch', - 'objArgs' => ['contextActivities' => $contextActivities1 ], - 'sigArgs' => ['contextActivities' => $contextActivities2 ], - 'reason' => 'Comparison of contextActivities failed: Comparison of parent failed: array lengths differ' - ], - [ - 'description' => 'revision only: mismatch', - 'objArgs' => ['revision' => '1' ], - 'sigArgs' => ['revision' => '2' ], - 'reason' => 'Comparison of revision failed: value is not the same' - ], - [ - 'description' => 'platform only: mismatch', - 'objArgs' => ['platform' => 'mobile' ], - 'sigArgs' => ['platform' => 'desktop' ], - 'reason' => 'Comparison of platform failed: value is not the same' - ], - [ - 'description' => 'language only: mismatch', - 'objArgs' => ['language' => 'en-US' ], - 'sigArgs' => ['language' => 'en-GB' ], - 'reason' => 'Comparison of language failed: value is not the same' - ], - [ - 'description' => 'statement only: mismatch', - 'objArgs' => ['statement' => $ref1 ], - 'sigArgs' => ['statement' => $ref2 ], - 'reason' => 'Comparison of statement failed: Comparison of id failed: value is not the same' - ], - [ - 'description' => 'extensions only: mismatch', - 'objArgs' => ['extensions' => $extensions1 ], - 'sigArgs' => ['extensions' => $extensions2 ], - 'reason' => 'Comparison of extensions failed: http://id.cmi5api.com/extension/location not in signature' - ], - [ - 'description' => 'full: registration mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['registration' => $registration2]), - 'reason' => 'Comparison of registration failed: value is not the same' - ], - [ - 'description' => 'full: instructor mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['instructor' => $instructor2]), - 'reason' => 'Comparison of instructor failed: Comparison of mbox failed: value is not the same' - ], - [ - 'description' => 'full: team mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['team' => $team2]), - 'reason' => 'Comparison of team failed: Comparison of mbox failed: value is not the same' - ], - [ - 'description' => 'full: contextActivities mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['contextActivities' => $contextActivities2]), - 'reason' => 'Comparison of contextActivities failed: Comparison of parent failed: array lengths differ' - ], - [ - 'description' => 'full: revision mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['revision' => '2']), - 'reason' => 'Comparison of revision failed: value is not the same' - ], - [ - 'description' => 'full: platform mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['platform' => 'desktop']), - 'reason' => 'Comparison of platform failed: value is not the same' - ], - [ - 'description' => 'full: language mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['language' => 'en-GB']), - 'reason' => 'Comparison of language failed: value is not the same' - ], - [ - 'description' => 'full: statement mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['statement' => $ref2]), - 'reason' => 'Comparison of statement failed: Comparison of id failed: value is not the same' - ], - [ - 'description' => 'full: extensions mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['extensions' => $extensions2]), - 'reason' => 'Comparison of extensions failed: http://id.cmi5api.com/extension/location not in signature' - ] - ]; - $this->runSignatureCases("cmi5\Context", $cases); - } - - public function testSetInstructorConvertToGroup() { - $obj = new Context(); - $obj->setInstructor( - [ - 'objectType' => 'Group' - ] - ); - $this->assertInstanceOf('cmi5\Group', $obj->getInstructor()); - } - - public function testSetRegistrationInvalidArgumentException() { - $this->setExpectedException( - 'InvalidArgumentException', - 'arg1 must be a UUID' - ); - $obj = new Context(); - $obj->setRegistration('232....3.3..3./2/2/1m3m3m3'); - } -} diff --git a/cmi5PHP/tests/DocumentTest.php b/cmi5PHP/tests/DocumentTest.php deleted file mode 100755 index fcbbe03..0000000 --- a/cmi5PHP/tests/DocumentTest.php +++ /dev/null @@ -1,34 +0,0 @@ -setExpectedException( - "InvalidArgumentException", - 'type of arg1 must be string or DateTime' - ); - - $obj = new StubDocument(); - $obj->setTimestamp(1); - } -} diff --git a/cmi5PHP/tests/ExtensionsTest.php b/cmi5PHP/tests/ExtensionsTest.php deleted file mode 100755 index a015952..0000000 --- a/cmi5PHP/tests/ExtensionsTest.php +++ /dev/null @@ -1,112 +0,0 @@ -assertInstanceOf('cmi5\Extensions', $obj); - } - - public function testAsVersion() { - $args = []; - $args[COMMON_EXTENSION_ID_1] = 'test'; - $args[COMMON_EXTENSION_ID_2] = 'test2'; - - $obj = new Extensions($args); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "version: 1.0.0"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = Extensions::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, null, "serialized version matches original"); - } - - public function testCompareWithSignature() { - $success = ['success' => true, 'reason' => null]; - - $content_1 = 'some value'; - $content_2 = 'some other value'; - - $extensions1 = [ COMMON_EXTENSION_ID_1 => 'some value' ]; - $extensions2 = [ COMMON_EXTENSION_ID_1 => 'some value', COMMON_EXTENSION_ID_2 => 'some other value' ]; - $extensions3 = [ COMMON_EXTENSION_ID_2 => 'some other value' ]; - - $cases = [ - [ - 'description' => 'empty', - 'objArgs' => [] - ], - [ - 'description' => 'single', - 'objArgs' => $extensions1 - ], - [ - 'description' => 'multiple', - 'objArgs' => $extensions2 - ], - [ - 'description' => 'empty sig: mismatch', - 'objArgs' => $extensions1, - 'sigArgs' => [], - 'reason' => 'http://id.cmi5api.com/extension/topic not in signature' - ], - [ - 'description' => 'empty this: mismatch', - 'objArgs' => [], - 'sigArgs' => $extensions1, - 'reason' => 'http://id.cmi5api.com/extension/topic not in this' - ], - [ - 'description' => 'missing in sig: mismatch', - 'objArgs' => $extensions2, - 'sigArgs' => $extensions3, - 'reason' => 'http://id.cmi5api.com/extension/topic not in signature' - ], - [ - 'description' => 'missing in this: mismatch', - 'objArgs' => $extensions3, - 'sigArgs' => $extensions2, - 'reason' => 'http://id.cmi5api.com/extension/topic not in this' - ], - [ - 'description' => 'single diff value in sig: mismatch', - 'objArgs' => $extensions2, - 'sigArgs' => array_replace($extensions2, [COMMON_EXTENSION_ID_1 => 'diff']), - 'reason' => 'http://id.cmi5api.com/extension/topic does not match' - ], - [ - 'description' => 'single diff value in this: mismatch', - 'objArgs' => array_replace($extensions2, [COMMON_EXTENSION_ID_1 => 'diff']), - 'sigArgs' => $extensions2, - 'reason' => 'http://id.cmi5api.com/extension/topic does not match' - ] - ]; - $this->runSignatureCases("cmi5\Extensions", $cases); - } -} diff --git a/cmi5PHP/tests/GroupTest.php b/cmi5PHP/tests/GroupTest.php deleted file mode 100755 index c10e3c4..0000000 --- a/cmi5PHP/tests/GroupTest.php +++ /dev/null @@ -1,333 +0,0 @@ -assertInstanceOf('cmi5\Agent', $obj); - $this->assertInstanceOf('cmi5\Group', $obj); - $this->assertAttributeEquals([], 'member', $obj, 'member empty array'); - } - - // TODO: need to loop possible configs - public function testFromJSONInstantiations() { - $obj = Group::fromJSON('{"mbox":"' . COMMON_GROUP_MBOX . '", "member":[{"mbox":"' . COMMON_MBOX . '"}]}'); - $this->assertInstanceOf('cmi5\Group', $obj); - $this->assertSame(COMMON_GROUP_MBOX, $obj->getMbox(), 'mbox value'); - $this->assertEquals([new Agent(['mbox' => COMMON_MBOX])], $obj->getMember(), 'member list'); - } - - // TODO: need to loop versions - public function testAsVersionMbox() { - $args = [ - 'mbox' => COMMON_GROUP_MBOX - ]; - - $obj = Group::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Group'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionMboxSha1() { - $args = [ - 'mbox_sha1sum' => COMMON_GROUP_MBOX_SHA1 - ]; - - $obj = Group::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Group'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionAccount() { - $args = [ - 'account' => [ - 'name' => COMMON_ACCT_NAME, - 'homePage' => COMMON_ACCT_HOMEPAGE - ] - ]; - - $obj = Group::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Group'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionAccountEmptyStrings() { - $args = [ - 'account' => [ - 'name' => '', - 'homePage' => '' - ] - ]; - - $obj = Group::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Group'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmptyAccount() { - $args = [ - 'account' => [] - ]; - - $obj = Group::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Group'; - unset($args['account']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmptyMember() { - $args = [ - 'member' => [] - ]; - - $obj = Group::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Group'; - unset($args['member']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = Group::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'Group'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAddMember() { - $common_agent = new Agent(['mbox' => COMMON_MBOX]); - - $obj = new Group(); - - $obj->addMember([ 'mbox' => COMMON_MBOX ]); - $this->assertEquals([$common_agent], $obj->getMember(), 'member list create Agent'); - - $obj->setMember([]); - - $obj->addMember($common_agent); - $this->assertEquals([$common_agent], $obj->getMember(), 'member list existing Agent'); - - $versioned = $obj->asVersion('1.0.0'); - $this->assertSame($versioned['member'][0], $common_agent->asVersion('1.0.0')); - } - - public function testCompareWithSignature() { - $name = 'Test Group Name'; - $acct1 = new AgentAccount( - [ - 'homePage' => COMMON_ACCT_HOMEPAGE, - 'name' => COMMON_ACCT_NAME - ] - ); - $acct2 = new AgentAccount( - [ - 'homePage' => COMMON_ACCT_HOMEPAGE, - 'name' => COMMON_ACCT_NAME . '-diff' - ] - ); - - $member1 = new Agent( - [ 'mbox' => COMMON_MBOX ] - ); - $member2 = new Agent( - [ 'account' => $acct1 ] - ); - - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ], - [ - 'description' => 'mbox', - 'objArgs' => ['mbox' => COMMON_GROUP_MBOX] - ], - [ - 'description' => 'mbox_sha1sum', - 'objArgs' => ['mbox_sha1sum' => COMMON_GROUP_MBOX_SHA1] - ], - [ - 'description' => 'openid', - 'objArgs' => ['openid' => COMMON_OPENID] - ], - [ - 'description' => 'account', - 'objArgs' => ['account' => $acct1] - ], - [ - 'description' => 'mbox with name', - 'objArgs' => ['mbox' => COMMON_GROUP_MBOX, 'name' => $name] - ], - [ - 'description' => 'mbox_sha1sum with name', - 'objArgs' => ['mbox_sha1sum' => COMMON_GROUP_MBOX_SHA1, 'name' => $name] - ], - [ - 'description' => 'openid with name', - 'objArgs' => ['openid' => COMMON_OPENID, 'name' => $name] - ], - [ - 'description' => 'account with name', - 'objArgs' => ['account' => $acct1, 'name' => $name] - ], - [ - 'description' => 'mbox with mismatch name', - 'objArgs' => ['mbox' => COMMON_GROUP_MBOX, 'name' => $name], - 'sigArgs' => ['mbox' => COMMON_GROUP_MBOX, 'name' => $name . ' diff'] - ], - [ - 'description' => 'mbox_sha1sum with mismatch name', - 'objArgs' => ['mbox_sha1sum' => COMMON_GROUP_MBOX_SHA1, 'name' => $name], - 'sigArgs' => ['mbox_sha1sum' => COMMON_GROUP_MBOX_SHA1, 'name' => $name . ' diff'] - ], - [ - 'description' => 'openid with mismatch name', - 'objArgs' => ['openid' => COMMON_OPENID, 'name' => $name], - 'sigArgs' => ['openid' => COMMON_OPENID, 'name' => $name . ' diff'] - ], - [ - 'description' => 'account with mismatch name', - 'objArgs' => ['account' => $acct1, 'name' => $name], - 'sigArgs' => ['account' => $acct1, 'name' => $name . ' diff'] - ], - [ - 'description' => 'mbox only: mismatch', - 'objArgs' => ['mbox' => COMMON_GROUP_MBOX ], - 'sigArgs' => ['mbox' => 'diff-' . COMMON_GROUP_MBOX ], - 'reason' => 'Comparison of mbox failed: value is not the same' - ], - [ - 'description' => 'mbox_sha1sum only: mismatch', - 'objArgs' => ['mbox_sha1sum' => COMMON_GROUP_MBOX_SHA1 ], - 'sigArgs' => ['mbox_sha1sum' => 'diff-' . COMMON_GROUP_MBOX_SHA1 ], - 'reason' => 'Comparison of mbox_sha1sum failed: value is not the same' - ], - [ - 'description' => 'openid only: mismatch', - 'objArgs' => ['openid' => COMMON_OPENID ], - 'sigArgs' => ['openid' => COMMON_OPENID . 'diff/' ], - 'reason' => 'Comparison of openid failed: value is not the same' - ], - [ - 'description' => 'account only: mismatch', - 'objArgs' => ['account' => $acct1 ], - 'sigArgs' => ['account' => $acct2 ], - 'reason' => 'Comparison of account failed: Comparison of name failed: value is not the same' - ], - - // - // special cases where we can try to equate an mbox and an mbox SHA1 sum - // - [ - 'description' => 'this.mbox to signature.mbox_sha1sum', - 'objArgs' => ['mbox' => COMMON_GROUP_MBOX], - 'sigArgs' => ['mbox_sha1sum' => COMMON_GROUP_MBOX_SHA1] - ], - [ - 'description' => 'this.mbox_sha1sum to signature.mbox', - 'objArgs' => ['mbox_sha1sum' => COMMON_GROUP_MBOX_SHA1], - 'sigArgs' => ['mbox' => COMMON_GROUP_MBOX] - ], - [ - 'description' => 'this.mbox to signature.mbox_sha1sum non-matching', - 'objArgs' => ['mbox' => COMMON_GROUP_MBOX], - 'sigArgs' => ['mbox_sha1sum' => COMMON_GROUP_MBOX_SHA1 . '-diff'], - 'reason' => 'Comparison of this.mbox to signature.mbox_sha1sum failed: no match' - ], - [ - 'description' => 'this.mbox_sha1sum to signature.mbox non-matching', - 'objArgs' => ['mbox_sha1sum' => COMMON_GROUP_MBOX_SHA1 . '-diff'], - 'sigArgs' => ['mbox' => COMMON_GROUP_MBOX], - 'reason' => 'Comparison of this.mbox_sha1sum to signature.mbox failed: no match' - ], - - // special cases for unidentified groups, member list needs to match - [ - 'description' => 'anonymous match: empty member list', - 'objArgs' => ['member' => []], - ], - [ - 'description' => 'anonymous match: single member', - 'objArgs' => ['member' => [$member1]], - ], - [ - 'description' => 'anonymous match: multiple members', - 'objArgs' => ['member' => [$member1, $member2]], - ], - [ - 'description' => 'anonymous non-match: sig member missing (empty)', - 'objArgs' => ['member' => [$member1]], - 'sigArgs' => ['member' => []], - 'reason' => 'Comparison of member list failed: array lengths differ' - ], - [ - 'description' => 'anonymous non-match: this member missing (empty)', - 'objArgs' => ['member' => []], - 'sigArgs' => ['member' => [$member1]], - 'reason' => 'Comparison of member list failed: array lengths differ' - ], - [ - 'description' => 'anonymous non-match: sig member missing', - 'objArgs' => ['member' => [$member1, $member2]], - 'sigArgs' => ['member' => [$member1]], - 'reason' => 'Comparison of member list failed: array lengths differ' - ], - [ - 'description' => 'anonymous non-match: this member missing', - 'objArgs' => ['member' => [$member1]], - 'sigArgs' => ['member' => [$member1, $member2]], - 'reason' => 'Comparison of member list failed: array lengths differ' - ], - [ - 'description' => 'anonymous non-match: different order', - 'objArgs' => ['member' => [$member2, $member1]], - 'sigArgs' => ['member' => [$member1, $member2]], - 'reason' => 'Comparison of member 0 failed: Comparison of mbox failed: value is not the same' - ] - ]; - $this->runSignatureCases("cmi5\Group", $cases); - } -} diff --git a/cmi5PHP/tests/ISO8601Test.php b/cmi5PHP/tests/ISO8601Test.php deleted file mode 100755 index be29f81..0000000 --- a/cmi5PHP/tests/ISO8601Test.php +++ /dev/null @@ -1,54 +0,0 @@ -setDate(2014, 12, 15); - $datetime->setTime(19, 16, 05); - - $statement = new Statement(); - $statement->setStored($datetime); - $statement->setTimestamp($datetime); - - $document = new State(); - $document->setTimestamp($datetime); - - $this->assertEquals($statement->getStored(), $str_datetime, 'stored matches'); - $this->assertEquals($statement->getTimestamp(), $str_datetime, 'timestamp matches'); - $this->assertEquals($document->getTimestamp(), $str_datetime, 'document timestamp matches'); - - $datetime->setTimezone(new DateTimeZone('America/Chicago')); - $statement->setStored($datetime); - $statement->setTimestamp($datetime); - $document->setTimestamp($datetime); - - $this->assertEquals($statement->getStored(), $str_datetime_tz, 'stored matches'); - $this->assertEquals($statement->getTimestamp(), $str_datetime_tz, 'timestamp matches'); - $this->assertEquals($document->getTimestamp(), $str_datetime_tz, 'document timestamp matches'); - } -} diff --git a/cmi5PHP/tests/JSONParseErrorExceptionTest.php b/cmi5PHP/tests/JSONParseErrorExceptionTest.php deleted file mode 100755 index 5f7f4bc..0000000 --- a/cmi5PHP/tests/JSONParseErrorExceptionTest.php +++ /dev/null @@ -1,48 +0,0 @@ -jsonErrorNumber = JSON_ERROR_SYNTAX; - $this->jsonErrorMessage = 'Syntax error, malformed JSON'; - $this->exception = new JSONParseErrorException( - $this->malformedValue, - $this->jsonErrorNumber, - $this->jsonErrorMessage - ); - } - - public function testFetchErrorInformation() { - $this->assertEquals($this->malformedValue, $this->exception->malformedValue()); - $this->assertEquals($this->jsonErrorNumber, $this->exception->jsonErrorNumber()); - $this->assertEquals($this->jsonErrorMessage, $this->exception->jsonErrorMessage()); - } - - public function testGetMessage() { - $format = 'Invalid JSON "%s": %s (%d)'; - $expected = sprintf($format, $this->malformedValue, $this->jsonErrorMessage, $this->jsonErrorNumber); - $this->assertEquals($expected, $this->exception->getMessage()); - } -} diff --git a/cmi5PHP/tests/LRSResponseTest.php b/cmi5PHP/tests/LRSResponseTest.php deleted file mode 100755 index 0940d30..0000000 --- a/cmi5PHP/tests/LRSResponseTest.php +++ /dev/null @@ -1,29 +0,0 @@ -assertTrue($obj->success); - $this->assertEquals('', $obj->content); - $this->assertFalse($obj->httpResponse); - } -} diff --git a/cmi5PHP/tests/LanguageMapTest.php b/cmi5PHP/tests/LanguageMapTest.php deleted file mode 100755 index 499f385..0000000 --- a/cmi5PHP/tests/LanguageMapTest.php +++ /dev/null @@ -1,110 +0,0 @@ -assertInstanceOf('cmi5\LanguageMap', $obj); - } - - public function testAsVersion() { - $args = ['en' => [self::NAME]]; - - $obj = LanguageMap::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion(); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = LanguageMap::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, null, "serialization returns null"); - } - - public function testAsVersionValueEmptyString() { - $args = ['en' => ['']]; - - $obj = LanguageMap::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion(); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testGetNegotiatedLanguageString() { - $langs = [ - 'en-GB' => 'petrol', - 'en-US' => 'gasoline' - ]; - $obj = new LanguageMap($langs); - - $usValue = $obj->getNegotiatedLanguageString('en-US;q=0.8, en-GB;q=0.6'); - $ukValue = $obj->getNegotiatedLanguageString('en-GB;q=0.8, en-US;q=0.6'); - - $this->assertEquals($usValue, $langs['en-US'], 'US name equal'); - $this->assertEquals($ukValue, $langs['en-GB'], 'UK name equal'); - - $nullValue = $obj->getNegotiatedLanguageString(); - $this->assertEquals($nullValue, $langs['en-GB'], 'from null: UK name equal'); - - if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { - $restore = $_SERVER['HTTP_ACCEPT_LANGUAGE']; - } - $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en-US'; - - $nullAcceptValue = $obj->getNegotiatedLanguageString(); - $this->assertEquals($nullAcceptValue, $langs['en-US'], 'from server: US name equal'); - - if (isset($restore)) { - $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $restore; - } - else { - unset($_SERVER['HTTP_ACCEPT_LANGUAGE']); - } - - $langs = [ - 'en-US' => 'gasoline' - ]; - $obj = new LanguageMap($langs); - - $this->assertEquals($obj->getNegotiatedLanguageString('en'), $langs['en-US'], 'from prefix'); - - $langs = [ - 'fr-FR' => 'essence' - ]; - $obj = new LanguageMap($langs); - - $this->assertEquals($obj->getNegotiatedLanguageString('en, *'), $langs['fr-FR'], 'no matched'); - - $langs = [ - 'fr-FR' => 'essence', - 'und' => 'fuel', - ]; - $obj = new LanguageMap($langs); - - $this->assertEquals($obj->getNegotiatedLanguageString('en'), $langs['und'], 'no matched with und'); - } -} diff --git a/cmi5PHP/tests/MapTest.php b/cmi5PHP/tests/MapTest.php deleted file mode 100755 index 666bfbf..0000000 --- a/cmi5PHP/tests/MapTest.php +++ /dev/null @@ -1,60 +0,0 @@ -assertTrue($obj->isEmpty()); - } - - public function testSetUnset() { - $obj = new StubMap(); - - $code = 'code'; - $value = 'value'; - - $obj->set($code, $value); - - $this->assertEquals($value, $obj->asVersion()[$code]); - - $obj->unset($code); - - $this->assertFalse(isset($obj->asVersion()[$code])); - } - - public function testExceptionOnBadMethodCall() { - $badName ="dsadasdasdasdasdasdas"; - - $this->setExpectedException( - '\BadMethodCallException', - get_class(new StubMap) . "::$badName() does not exist" - ); - - $obj = new StubMap(); - $obj->$badName(); - } -} diff --git a/cmi5PHP/tests/PersonTest.php b/cmi5PHP/tests/PersonTest.php deleted file mode 100755 index 2d27de4..0000000 --- a/cmi5PHP/tests/PersonTest.php +++ /dev/null @@ -1,105 +0,0 @@ -assertInstanceOf('cmi5\Person', $obj); - $this->assertAttributeEmpty('name', $obj, 'name empty'); - $this->assertAttributeEmpty('mbox', $obj, 'mbox empty'); - $this->assertAttributeEmpty('mbox_sha1sum', $obj, 'mbox_sha1sum empty'); - $this->assertAttributeEmpty('openid', $obj, 'openid empty'); - $this->assertAttributeEmpty('account', $obj, 'account empty'); - } - - public function testFromJSONInvalidNull() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Person::fromJSON(null); - } - - public function testFromJSONInvalidEmptyString() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Person::fromJSON(''); - } - - public function testFromJSONInvalidMalformed() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Person::fromJSON('{name:"some value"}'); - } - - // TODO: need to loop possible configs - public function testFromJSONInstantiations() { - $obj = Person::fromJSON('{"mbox":["' . COMMON_MBOX . '","'.COMMON_MBOX.'"]}'); - $this->assertInstanceOf('cmi5\Person', $obj); - $this->assertSame(array(COMMON_MBOX, COMMON_MBOX), $obj->getMbox(), 'mbox value'); - } - - // TODO: need to loop versions - public function testAsVersion() { - $obj = new Person( - [ - 'mbox' => array(COMMON_MBOX), - 'account' => array( - array( - 'name' => COMMON_ACCT_NAME, - 'homePage' => COMMON_ACCT_HOMEPAGE - ) - ) - ] - ); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals( - [ - 'objectType' => 'Person', - 'mbox' => array(COMMON_MBOX), - 'account' => array( - array( - 'name' => COMMON_ACCT_NAME, - 'homePage' => COMMON_ACCT_HOMEPAGE - ) - ) - ], - $versioned, - "mbox only: 1.0.0" - ); - } - - public function testSetMbox() { - $obj = new Person(); - - $obj->setMbox(array(COMMON_MBOX)); - $this->assertSame(array(COMMON_MBOX), $obj->getMbox()); - - // - // make sure it doesn't add mailto when null - // - $obj->setMbox(null); - $this->assertAttributeEmpty('mbox', $obj); - } - - public function testGetMbox_sha1sum() { - $obj = new Person(['mbox_sha1sum' => array(COMMON_MBOX_SHA1)]); - $this->assertSame($obj->getMbox_sha1sum(), array(COMMON_MBOX_SHA1), 'original sha1'); - } -} diff --git a/cmi5PHP/tests/RemoteLRSTest.php b/cmi5PHP/tests/RemoteLRSTest.php deleted file mode 100755 index 7f2ac94..0000000 --- a/cmi5PHP/tests/RemoteLRSTest.php +++ /dev/null @@ -1,594 +0,0 @@ -assertInstanceOf('cmi5\RemoteLRS', $lrs); - $this->assertAttributeEmpty('endpoint', $lrs, 'endpoint empty'); - $this->assertAttributeEmpty('auth', $lrs, 'auth empty'); - $this->assertAttributeEmpty('extended', $lrs, 'extended empty'); - $this->assertSame(Version::latest(), $lrs->getVersion(), 'version set to latest'); - } - - public function testAbout() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->about(); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success); - } - - public function testSaveStatement() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $statement = new Statement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID - ]) - ] - ); - - $response = $lrs->saveStatement($statement); - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success, 'success'); - $this->assertSame($response->content, $statement, 'content'); - } - - public function testSaveStatementStamped() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $statement = new Statement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID - ]) - ] - ); - $statement->stamp(); - - $response = $lrs->saveStatement($statement); - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success, 'success'); - $this->assertSame($response->content, $statement, 'content'); - } - - public function testSaveStatements() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $statements = [ - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID - ]) - ], - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/2' - ]) - ], - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/3' - ]) - ] - ]; - - $response = $lrs->saveStatements($statements); - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success, 'success'); - $this->assertTrue(is_array($response->content), 'content is array'); - $this->assertSame(count($response->content), 3, 'content has 3 values'); - foreach ($response->content as $i => $st) { - $this->assertInstanceof('cmi5\Statement', $st, "$i: is statement"); - $id = $st->getId(); - $this->assertTrue(isset($id), "$i: id set"); - } - } - - public function testSaveStatementsWithAttachments() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $attachment1 = [ - 'usageType' => 'http://id.cmi5api.com/attachment/supporting_media', - 'display' => ['en-US' => 'RemoteLRSTest::testSaveStatements'], - 'contentType' => 'text/plain; charset=ascii', - 'content' => 'Attachment 1 content created at: ' . Util::getTimestamp() - ]; - $attachment2 = [ - 'usageType' => 'http://id.cmi5api.com/attachment/supporting_media', - 'display' => ['en-US' => 'RemoteLRSTest::testSaveStatements'], - 'contentType' => 'text/plain; charset=ascii', - 'content' => 'Attachment 2 content created at: ' . Util::getTimestamp() - ]; - $statements = [ - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID - ]), - 'attachments' => [ - $attachment1 - ] - ], - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/2' - ]), - 'attachments' => [ - // provide a matching attachment to make sure only 2 in request - $attachment1, - $attachment2 - ] - ] - ]; - - $response = $lrs->saveStatements($statements); - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success, 'success'); - $this->assertTrue(is_array($response->content), 'content is array'); - $this->assertSame(count($response->content), 2, 'content has 2 values'); - foreach ($response->content as $i => $st) { - $this->assertInstanceof('cmi5\Statement', $st, "$i: is statement"); - $id = $st->getId(); - $this->assertTrue(isset($id), "$i: id set"); - } - } - - public function testRetrieveStatement() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - - $saveResponse = $lrs->saveStatement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID - ]) - ] - ); - if (! $saveResponse->success) { - $this->fail("save statement setup failed: " . $saveResponse->content); - } - - $response = $lrs->retrieveStatement($saveResponse->content->getId()); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success); - $this->assertInstanceOf('cmi5\Statement', $response->content); - } - - public function testRetrieveStatementWithAttachments() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $content = json_encode(['foo' => 'bar']); - - $saveResponse = $lrs->saveStatement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID - ]), - 'attachments' => [ - new Attachment([ - 'usageType' => 'http://id.cmi5api.com/attachment/supporting_media', - 'display' => ['en-US' => 'Test Display'], - 'contentType' => 'application/json', - 'content' => $content, - ]) - ] - ] - ); - $this->assertTrue($saveResponse->success, 'save succeeded'); - if (! $saveResponse->success) { - $this->fail("save statement setup failed: " . $saveResponse->content); - } - - $response = $lrs->retrieveStatement($saveResponse->content->getId(), [ 'attachments' => true ]); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success); - $this->assertInstanceOf('cmi5\Statement', $response->content); - $this->assertTrue(count($response->content->getAttachments()) === 1, 'attachment count'); - $this->assertSame($content, $response->content->getAttachments()[0]->getContent(), 'attachment content'); - } - - public function testRetrieveVoidedStatement() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - - $saveResponse = $lrs->saveStatement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID - ]) - ] - ); - if (! $saveResponse->success) { - $this->fail("save statement setup failed: " . $saveResponse->content); - } - - $voidResponse = $lrs->saveStatement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => Verb::Voided(), - 'object' => new StatementRef([ - 'id' => $saveResponse->content->getId() - ]) - ] - ); - if (! $voidResponse->success) { - $this->fail("void statement setup failed: " . $voidResponse->content); - } - - $retrieveResponse = $lrs->retrieveVoidedStatement($saveResponse->content->getId()); - - $this->assertInstanceOf('cmi5\LRSResponse', $retrieveResponse); - $this->assertTrue($retrieveResponse->success); - $this->assertInstanceOf('cmi5\Statement', $retrieveResponse->content); - } - - public function testQueryStatements() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->queryStatements(['limit' => 4]); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success, 'success'); - $this->assertInstanceOf('cmi5\StatementsResult', $response->content); - } - - public function testQueryStatementsWithAttachments() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->queryStatements(['limit' => 4, 'attachments' => true]); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success, 'success'); - $this->assertInstanceOf('cmi5\StatementsResult', $response->content); - } - - public function testMoreStatements() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $queryResponse = $lrs->queryStatements(['limit' => 1]); - if (! $queryResponse->success) { - $this->fail("query statements setup failed: " . $queryResponse->content); - } - - if (! $queryResponse->content->getMore()) { - $this->markTestSkipped('No more property in StatementsResult (not enough statements in endpoint?)'); - } - - $response = $lrs->moreStatements($queryResponse->content); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success, 'success'); - $this->assertInstanceOf('cmi5\StatementsResult', $response->content, 'content'); - } - - public function testMoreStatementsWithAttachments() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $queryResponse = $lrs->queryStatements(['limit' => 1, 'attachments' => true]); - if (! $queryResponse->success) { - $this->fail("query statements setup failed: " . $queryResponse->content); - } - - if (! $queryResponse->content->getMore()) { - $this->markTestSkipped('No more property in StatementsResult (not enough statements in endpoint?)'); - } - - $response = $lrs->moreStatements($queryResponse->content); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success, 'success'); - $this->assertInstanceOf('cmi5\StatementsResult', $response->content, 'content'); - } - - public function testRetrieveStateIds() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->retrieveStateIds( - new Activity( - [ 'id' => COMMON_ACTIVITY_ID ] - ), - new Agent( - [ 'mbox' => COMMON_MBOX ] - ), - array( - 'registration' => Util::getUUID(), - 'since' => '2014-01-07T08:24:30Z' - ) - ); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - } - - public function testDeleteState() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->deleteState( - new Activity( - [ 'id' => COMMON_ACTIVITY_ID ] - ), - new Agent( - [ 'mbox' => COMMON_MBOX ] - ), - 'testKey', - [ - 'registration' => Util::getUUID() - ] - ); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - } - - public function testClearState() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->clearState( - new Activity( - [ 'id' => COMMON_ACTIVITY_ID ] - ), - new Agent( - [ 'mbox' => COMMON_MBOX ] - ), - [ - 'registration' => Util::getUUID() - ] - ); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - } - - public function testRetrieveActivityProfileIds() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->retrieveActivityProfileIds( - new Activity( - [ 'id' => COMMON_ACTIVITY_ID ] - ), - array( - 'since' => '2014-01-07T08:24:30Z' - ) - ); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - } - - public function testRetrieveActivityProfile() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->saveActivityProfile( - new Activity( - [ 'id' => COMMON_ACTIVITY_ID ] - ), - 'testKey', - json_encode(['testProperty' => 'testValue']), - [ - 'contentType' => 'application/json', - ] - ); - if (! $response->success) { - $this->fail("save activity profile setup failed: " . $response->content); - } - - $response = $lrs->retrieveActivityProfile( - new Activity( - [ 'id' => COMMON_ACTIVITY_ID ] - ), - 'testKey' - ); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - } - - public function testSaveActivityProfile() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->saveActivityProfile( - new Activity( - [ 'id' => COMMON_ACTIVITY_ID ] - ), - 'testKey', - json_encode(['testProperty' => 'testValue']), - [ - 'contentType' => 'application/json', - ] - ); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - } - - public function testDeleteActivityProfile() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->deleteActivityProfile( - new Activity( - [ 'id' => COMMON_ACTIVITY_ID ] - ), - 'testKey' - ); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - } - - public function testRetrieveActivity() { - $testActivity = new Activity([ - 'id' => COMMON_ACTIVITY_ID. '/testRetrieveActivity', - 'definition' => [ - 'name' => [ - 'en' => 'This is a test activity.' - ] - ] - ]); - - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $statement = new Statement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => $testActivity - ] - ); - $response = $lrs->saveStatement($statement); - if (! $response->success) { - $this->fail("save statement setup failed: " . $response->content); - } - - $response = $lrs->retrieveActivity($testActivity->getId()); - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertEquals($testActivity, $response->content, 'retrieved activity'); - } - - public function testRetrieveActivityWithHttpAcceptLanguageHeader() { - $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en-US'; - $this->testRetrieveActivity(); - } - - public function testRetrieveAgentProfileIds() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->retrieveAgentProfileIds( - new Agent( - [ 'mbox' => COMMON_MBOX ] - ), - array( - 'since' => '2014-01-07T08:24:30Z' - ) - ); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - } - - public function testRetrieveAgentProfile() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->retrieveAgentProfile( - new Agent( - [ 'mbox' => COMMON_MBOX ] - ), - 'testKey' - ); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - } - - public function testDeleteAgentProfile() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - $response = $lrs->deleteAgentProfile( - new Agent( - [ 'mbox' => COMMON_MBOX ] - ), - 'testKey' - ); - - $this->assertInstanceOf('cmi5\LRSResponse', $response); - } - - public function testRetrievePerson() { - $lrs = new RemoteLRS(self::$endpoint, self::$version, self::$username, self::$password); - - $testAgent = new Agent( - [ - 'mbox' => COMMON_MBOX. '.testretrieveperson', - 'name' => COMMON_NAME - ] - ); - - $testPerson = new Person( - [ - 'mbox' => [ COMMON_MBOX. '.testretrieveperson' ], - 'name' => [ COMMON_NAME ] - ] - ); - - $response = $lrs->retrievePerson($testAgent); - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertEquals($testPerson, $response->content, 'retrieved person'); - } -} diff --git a/cmi5PHP/tests/ResultTest.php b/cmi5PHP/tests/ResultTest.php deleted file mode 100755 index b4936f1..0000000 --- a/cmi5PHP/tests/ResultTest.php +++ /dev/null @@ -1,292 +0,0 @@ -assertInstanceOf('cmi5\Result', $obj); - foreach ($this->emptyProperties as $property) { - $this->assertAttributeEmpty($property, $obj, "$property empty"); - } - foreach ($this->nonEmptyProperties as $property) { - $this->assertAttributeNotEmpty($property, $obj, "$property not empty"); - } - } - - public function testUsesArraySetterTrait() { - $this->assertContains('cmi5\ArraySetterTrait', class_uses('cmi5\Result')); - } - - public function testUsesFromJSONTrait() { - $this->assertContains('cmi5\FromJSONTrait', class_uses('cmi5\Result')); - } - - public function testUsesAsVersionTrait() { - $this->assertContains('cmi5\AsVersionTrait', class_uses('cmi5\Result')); - } - - // TODO: need more robust test (happy-path) - public function testAsVersion() { - $args = [ - 'success' => true, - 'completion' => true, - 'duration' => 'PT15S', - 'response' => 'a', - 'score' => [ - 'raw' => 100, - 'scaled' => 1, - 'min' => 0, - 'max' => 100 - ] - ]; - $obj = new Result($args); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = Result::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionScoreEmpty() { - $args = [ - 'score' => [] - ]; - - $obj = Result::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - unset($args['score']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionScoreZeroRaw() { - $args = [ - 'score' => [ - 'raw' => 0, - ] - ]; - - $obj = Result::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionResponseEmptyString() { - $args = [ - 'response' => '' - ]; - - $obj = Result::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionDurationEmptyString() { - $args = [ - 'duration' => '' - ]; - - $obj = Result::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - unset($args['duration']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testCompareWithSignature() { - $score1 = new Score( - [ - 'raw' => 97, - 'scaled' => 0.97, - 'min' => 0, - 'max' => 100 - ] - ); - $score2 = new Score( - [ - 'raw' => 15, - 'scaled' => 0.50 - ] - ); - $extensions1 = new Extensions( - [ - COMMON_EXTENSION_ID_1 => 'test1', - COMMON_EXTENSION_ID_2 => 'test2' - ] - ); - $extensions2 = new Extensions( - [ - COMMON_EXTENSION_ID_1 => 'test1' - ] - ); - - $full = [ - 'success' => true, - 'completion' => true, - 'duration' => 'PT15S', - 'response' => 'a', - 'score' => $score1, - 'extensions' => $extensions1 - ]; - - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ], - [ - 'description' => 'success', - 'objArgs' => ['success' => true] - ], - [ - 'description' => 'completion', - 'objArgs' => ['completion' => true] - ], - [ - 'description' => 'duration', - 'objArgs' => ['duration' => 'PT15S'] - ], - [ - 'description' => 'response', - 'objArgs' => ['response' => 'a'] - ], - [ - 'description' => 'score', - 'objArgs' => ['score' => $score1] - ], - [ - 'description' => 'extensions', - 'objArgs' => ['extensions' => $extensions1] - ], - [ - 'description' => 'all', - 'objArgs' => [ - 'success' => false, - 'completion' => false, - 'duration' => 'PT15S', - 'response' => 'b', - 'score' => $score2, - 'extensions' => $extensions2 - ] - ], - [ - 'description' => 'success only: mismatch', - 'objArgs' => ['success' => true ], - 'sigArgs' => ['success' => false ], - 'reason' => 'Comparison of success failed: value is not the same' - ], - [ - 'description' => 'completion only: mismatch', - 'objArgs' => ['completion' => true ], - 'sigArgs' => ['completion' => false ], - 'reason' => 'Comparison of completion failed: value is not the same' - ], - [ - 'description' => 'duration only: mismatch', - 'objArgs' => ['duration' => 'PT15S' ], - 'sigArgs' => ['duration' => 'PT180S' ], - 'reason' => 'Comparison of duration failed: value is not the same' - ], - [ - 'description' => 'response only: mismatch', - 'objArgs' => ['response' => 'a' ], - 'sigArgs' => ['response' => 'b' ], - 'reason' => 'Comparison of response failed: value is not the same' - ], - [ - 'description' => 'score only: mismatch', - 'objArgs' => ['score' => $score1 ], - 'sigArgs' => ['score' => $score2 ], - 'reason' => 'Comparison of score failed: Comparison of scaled failed: value is not the same' - ], - [ - 'description' => 'extensions only: mismatch', - 'objArgs' => ['extensions' => $extensions1 ], - 'sigArgs' => ['extensions' => $extensions2 ], - 'reason' => 'Comparison of extensions failed: http://id.cmi5api.com/extension/location not in signature' - ], - [ - 'description' => 'full: success mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['success' => false]), - 'reason' => 'Comparison of success failed: value is not the same' - ], - [ - 'description' => 'full: completion mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['completion' => false]), - 'reason' => 'Comparison of completion failed: value is not the same' - ], - [ - 'description' => 'full: duration mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['duration' => 'PT150S']), - 'reason' => 'Comparison of duration failed: value is not the same' - ], - [ - 'description' => 'full: response mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['response' => 'b']), - 'reason' => 'Comparison of response failed: value is not the same' - ], - [ - 'description' => 'full: score mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['score' => $score2]), - 'reason' => 'Comparison of score failed: Comparison of scaled failed: value is not the same' - ], - [ - 'description' => 'full: extensions mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['extensions' => $extensions2]), - 'reason' => 'Comparison of extensions failed: http://id.cmi5api.com/extension/location not in signature' - ] - ]; - $this->runSignatureCases("cmi5\Result", $cases); - } -} diff --git a/cmi5PHP/tests/ScoreTest.php b/cmi5PHP/tests/ScoreTest.php deleted file mode 100755 index 5af145b..0000000 --- a/cmi5PHP/tests/ScoreTest.php +++ /dev/null @@ -1,305 +0,0 @@ -assertInstanceOf('cmi5\Score', $obj); - foreach ($this->emptyProperties as $property) { - $this->assertAttributeEmpty($property, $obj, "$property empty"); - } - } - - public function testUsesArraySetterTrait() { - $this->assertContains('cmi5\ArraySetterTrait', class_uses('cmi5\Score')); - } - - public function testUsesFromJSONTrait() { - $this->assertContains('cmi5\FromJSONTrait', class_uses('cmi5\Score')); - } - - public function testUsesAsVersionTrait() { - $this->assertContains('cmi5\AsVersionTrait', class_uses('cmi5\Score')); - } - - public function testScaled() { - $score = new Score; - $score->setScaled(0.9); - - $this->assertEquals($score->getScaled(), 0.9); - $this->assertInternalType('float', $score->getScaled()); - } - - public function testSetScaledBelowMin() { - $this->setExpectedException( - 'InvalidArgumentException', - sprintf('Value must be greater than or equal to %s [-5]', Score::SCALE_MIN) - ); - $score = new Score; - $score->setScaled(-5); - } - - public function testSetScaledAboveMax() { - $this->setExpectedException( - 'InvalidArgumentException', - sprintf('Value must be less than or equal to %s [5]', Score::SCALE_MAX) - ); - $score = new Score; - $score->setScaled(5); - } - - public function testRaw() { - $score = new Score; - $score->setRaw(90); - - $this->assertEquals($score->getRaw(), 90); - $this->assertInternalType('float', $score->getRaw()); - - $score = new Score(['min' => 65, 'max' => 85]); - $score->setRaw(75); - $this->assertEquals($score->getRaw(), 75, 'between min and max'); - - $score = new Score(['min' => 65]); - $score->setRaw(65); - $this->assertEquals($score->getRaw(), 65, 'same as min'); - - $score = new Score(['max' => 65]); - $score->setRaw(65); - $this->assertEquals($score->getRaw(), 65, 'same as max'); - } - - public function testSetRawBelowMin() { - $this->setExpectedException( - 'InvalidArgumentException', - 'Value must be greater than or equal to \'min\' (60) [50]' - ); - $score = new Score(['min' => 60]); - $score->setRaw(50); - } - - public function testSetRawAboveMax() { - $this->setExpectedException( - 'InvalidArgumentException', - 'Value must be less than or equal to \'max\' (90) [95]' - ); - $score = new Score(['max' => 90]); - $score->setRaw(95); - } - - public function testMin() { - $score = new Score; - $score->setMin(9); - - $this->assertEquals($score->getMin(), 9); - $this->assertInternalType('float', $score->getMin()); - - $score = new Score(['raw' => 65, 'max' => 85]); - $score->setMin(35); - $this->assertEquals($score->getMin(), 35, 'below raw'); - - $score = new Score(['raw' => 35, 'max' => 85]); - $score->setMin(35); - $this->assertEquals($score->getMin(), 35, 'equal to raw'); - } - - public function testSetMinAboveRaw() { - $this->setExpectedException( - 'InvalidArgumentException', - 'Value must be less than or equal to \'raw\' (50) [60]' - ); - $score = new Score(['raw' => 50]); - $score->setMin(60); - } - - public function testSetMinAboveMax() { - $this->setExpectedException( - 'InvalidArgumentException', - 'Value must be less than \'max\' (90) [95]' - ); - $score = new Score(['max' => 90]); - $score->setMin(95); - } - - public function testMax() { - $score = new Score; - $score->setMax(96.3); - - $this->assertEquals($score->getMax(), 96.3); - $this->assertInternalType('float', $score->getMax()); - - $score = new Score(['raw' => 65, 'min' => 35]); - $score->setMax(85.4); - $this->assertEquals($score->getMax(), 85.4, 'above raw'); - - $score = new Score(['raw' => 35, 'min' => 15]); - $score->setMax(35); - $this->assertEquals($score->getMax(), 35, 'equal to raw'); - } - - public function testSetMaxBelowRaw() { - $this->setExpectedException( - 'InvalidArgumentException', - 'Value must be greater than or equal to \'raw\' (60) [50]' - ); - $score = new Score(['raw' => 60]); - $score->setMax(50); - } - - public function testSetMaxBelowMin() { - $this->setExpectedException( - 'InvalidArgumentException', - 'Value must be greater than \'min\' (10) [5]' - ); - $score = new Score(['min' => 10]); - $score->setMax(5); - } - - /** - * @dataProvider asVersionDataProvider - */ - public function testAsVersion($args) { - $obj = new Score($args); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "version: 1.0.0"); - } - - public function asVersionDataProvider() { - return [ - 'basic' => [ - [ - 'raw' => '1.5', - 'min' => '1.0', - 'max' => '2.0', - 'scaled' => '.95' - ] - ], - 'empty' => [[]], - 'zero raw' => [ - [ 'raw' => 0 ] - ], - 'zero scaled' => [ - [ 'scaled' => 0 ] - ], - 'multiple zeros' => [ - [ - 'raw' => '0', - 'min' => '-1.0', - 'max' => 2.0, - 'scaled' => 0 - ] - ] - ]; - } - - public function testCompareWithSignature() { - $full = [ - 'raw' => 97, - 'scaled' => 0.97, - 'min' => 0, - 'max' => 100 - ]; - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ], - [ - 'description' => 'raw', - 'objArgs' => ['raw' => 97] - ], - [ - 'description' => 'scaled', - 'objArgs' => ['scaled' => 0.97] - ], - [ - 'description' => 'min', - 'objArgs' => ['min' => 60] - ], - [ - 'description' => 'max', - 'objArgs' => ['max' => 99] - ], - [ - 'description' => 'all', - 'objArgs' => $full - ], - [ - 'description' => 'raw only: mismatch', - 'objArgs' => ['raw' => 97 ], - 'sigArgs' => ['raw' => 87 ], - 'reason' => 'Comparison of raw failed: value is not the same' - ], - [ - 'description' => 'scaled only: mismatch', - 'objArgs' => ['scaled' => 0.97 ], - 'sigArgs' => ['scaled' => 0.87 ], - 'reason' => 'Comparison of scaled failed: value is not the same' - ], - [ - 'description' => 'min only: mismatch', - 'objArgs' => ['min' => 0 ], - 'sigArgs' => ['min' => 1 ], - 'reason' => 'Comparison of min failed: value is not the same' - ], - [ - 'description' => 'max only: mismatch', - 'objArgs' => ['max' => 97 ], - 'sigArgs' => ['max' => 100 ], - 'reason' => 'Comparison of max failed: value is not the same' - ], - [ - 'description' => 'full: raw mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['raw' => 79]), - 'reason' => 'Comparison of raw failed: value is not the same' - ], - [ - 'description' => 'full: scaled mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['scaled' => 0.96]), - 'reason' => 'Comparison of scaled failed: value is not the same' - ], - [ - 'description' => 'full: min mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['min' => 1]), - 'reason' => 'Comparison of min failed: value is not the same' - ], - [ - 'description' => 'full: max mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['max' => 98]), - 'reason' => 'Comparison of max failed: value is not the same' - ] - ]; - $this->runSignatureCases("cmi5\Score", $cases); - } -} diff --git a/cmi5PHP/tests/SignatureComparisonTraitTest.php b/cmi5PHP/tests/SignatureComparisonTraitTest.php deleted file mode 100755 index 1b1864b..0000000 --- a/cmi5PHP/tests/SignatureComparisonTraitTest.php +++ /dev/null @@ -1,43 +0,0 @@ -assertFalse($result['success']); - $this->assertEquals("Comparison of $description failed: not a " . get_class($a) . " value", $result['reason']); - - $result = SignatureComparisonStub::runDoMatch([], false, $description); - - $this->assertFalse($result['success']); - $this->assertEquals("Comparison of $description failed: not an array in signature", $result['reason']); - } -} diff --git a/cmi5PHP/tests/StateTest.php b/cmi5PHP/tests/StateTest.php deleted file mode 100755 index 2c40877..0000000 --- a/cmi5PHP/tests/StateTest.php +++ /dev/null @@ -1,55 +0,0 @@ - COMMON_ACTIVITY_ID, - 'definition' => [] - ]; - - $state = new State(); - $state->setActivity($args); - } - - public function testSetAgent() { - $obj = new State(); - $obj->setAgent(['mbox' => COMMON_MBOX]); - - $this->assertInstanceOf('cmi5\Agent', $obj->getAgent()); - - $group = new Group(); - $obj->setAgent(['objectType' => 'Group']); - - $this->assertInstanceOf('cmi5\Group', $obj->getAgent()); - } - - public function testExceptionOnInvalidRegistrationUUID() { - $this->setExpectedException( - "InvalidArgumentException", - 'arg1 must be a UUID' - ); - - $obj = new State(); - $obj->setRegistration('232....3.3..3./2/2/1m3m3m3'); - } -} diff --git a/cmi5PHP/tests/StatementBaseTest.php b/cmi5PHP/tests/StatementBaseTest.php deleted file mode 100755 index 1385ddf..0000000 --- a/cmi5PHP/tests/StatementBaseTest.php +++ /dev/null @@ -1,173 +0,0 @@ - 'Agent' - ]; - $obj->setTarget($ss); - $this->assertInstanceOf('cmi5\Agent', $obj->getTarget()); - } - - public function testSetTargetAsGroup() { - $obj = new StubStatementBase(); - $ss = [ - 'objectType' => 'Group' - ]; - $obj->setTarget($ss); - $this->assertInstanceOf('cmi5\Group', $obj->getTarget()); - } - - public function testSetTargetAsSubStatement() { - $obj = new StubStatementBase(); - $ss = [ - 'objectType' => 'SubStatement' - ]; - $obj->setTarget($ss); - $this->assertInstanceOf('cmi5\SubStatement', $obj->getTarget()); - } - - public function testSetTargetInvalidArgumentException() { - $badObjectType = 'imABadObjectType'; - $this->setExpectedException( - "InvalidArgumentException", - "arg1 must implement the StatementTargetInterface objectType not recognized:$badObjectType" - ); - $obj = new StubStatementBase(); - $ss = [ - 'objectType' => $badObjectType - ]; - $obj->setTarget($ss); - } - - public function testSetActorAsGroup() { - $obj = new StubStatementBase(); - $ss = [ - 'objectType' => 'Group' - ]; - $obj->setActor($ss); - $this->assertInstanceOf('cmi5\Group', $obj->getActor()); - } - - public function testSetTimestampInvalidArgumentException() { - $this->setExpectedException( - "InvalidArgumentException", - 'type of arg1 must be string or DateTime' - ); - - $obj = new StubStatementBase(); - $obj->setTimestamp(1); - } - - /** - * @dataProvider statementPropertyValueProvider - */ - public function testCompareWithSignaturePropertyMissing($property, $value) { - $signature = new \stdClass; - $setMethodName = 'set' . ucfirst($property); - - $obj = new StubStatementBase(); - $obj->$setMethodName($value); - - $result = $obj->compareWithSignature($signature); - $this->assertFalse($result['success']); - $this->assertEquals("Comparison of $property failed: value not in signature", $result['reason']); - - $obj = new StubStatementBase(); - $signature->$property = $value; - - $result = $obj->compareWithSignature($signature); - $this->assertFalse($result['success']); - $this->assertEquals("Comparison of $property failed: value not in this", $result['reason']); - } - - public function statementPropertyValueProvider() { - return [ - ['actor', new Agent()], - ['verb', new Verb()], - ['target', new Agent()], - ['context', new Context()], - ['result', new Result()], - ]; - } - - public function testCompareWithSignatureTimestampMissing() { - $timestamp = "2004-02-12T15:19:21+00:00"; - $signature = new \stdClass; - - $obj = new StubStatementBase(); - $obj->setTimestamp($timestamp); - - $result = $obj->compareWithSignature($signature); - - $this->assertFalse($result['success']); - $this->assertEquals("Comparison of timestamp failed: value not in signature", $result['reason']); - - $obj = new StubStatementBase(); - $signature->timestamp = $timestamp; - - $result = $obj->compareWithSignature($signature); - - $this->assertFalse($result['success']); - $this->assertEquals("Comparison of timestamp failed: value not in this", $result['reason']); - } - - public function testCompareWithSignatureTimestampNotEqual() { - $timestamp = "2004-02-12T15:19:21+00:00"; - $signature = new \stdClass; - - $obj = new StubStatementBase(); - $obj->setTimestamp($timestamp); - $signature->timestamp = "2005-02-12T15:19:21+00:00"; - - $result = $obj->compareWithSignature($signature); - - $this->assertFalse($result['success']); - $this->assertEquals("Comparison of timestamp failed: value is not the same", $result['reason']); - - //Now check with microseconds. - $timestamp = "2012-07-08 11:14:15.638276"; - $obj = new StubStatementBase(); - $obj->setTimestamp($timestamp); - $signature->timestamp = "2012-07-08 11:14:15.638286"; - - $dt = new \DateTime($timestamp); - $dt2 = new \DateTime($signature->timestamp); - - $result = $obj->compareWithSignature($signature); - - $this->assertFalse($result['success']); - $this->assertEquals("Comparison of timestamp failed: value is not the same", $result['reason']); - } -} diff --git a/cmi5PHP/tests/StatementRefTest.php b/cmi5PHP/tests/StatementRefTest.php deleted file mode 100755 index b65822d..0000000 --- a/cmi5PHP/tests/StatementRefTest.php +++ /dev/null @@ -1,91 +0,0 @@ -assertInstanceOf('cmi5\StatementRef', $obj); - $this->assertAttributeEmpty('id', $obj, 'id empty'); - $this->assertAttributeNotEmpty('objectType', $obj, 'objectType not empty'); - } - - public function testGetObjectType() { - $obj = new StatementRef(); - $this->assertSame('StatementRef', $obj->getObjectType()); - } - - public function testId() { - $obj = new StatementRef(); - $id = Util::getUUID(); - $this->assertSame($obj, $obj->setId($id)); - $this->assertSame($id, $obj->getId()); - } - - public function testSetIdThrowsException() { - $this->setExpectedException( - 'InvalidArgumentException', - 'arg1 must be a UUID' - ); - $obj = new StatementRef(['id' => 'foo']); - } - - // TODO: need to loop versions - public function testAsVersion() { - $args = [ - 'id' => Util::getUUID(), - ]; - - $obj = StatementRef::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'StatementRef'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = StatementRef::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'StatementRef'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testCompareWithSignature() { - $success = ['success' => true, 'reason' => null]; - $failure = ['success' => false, 'reason' => null]; - - $id = Util::getUUID(); - $obj = new StatementRef(['id' => $id]); - $sig = new StatementRef(['id' => $id]); - - $this->assertSame($success, $obj->compareWithSignature($sig), 'id only: match'); - - $sig->setId(Util::getUUID()); - $failure['reason'] = 'Comparison of id failed: value is not the same'; - - $this->assertSame($failure, $obj->compareWithSignature($sig), 'id only: mismatch'); - } -} diff --git a/cmi5PHP/tests/StatementTest.php b/cmi5PHP/tests/StatementTest.php deleted file mode 100755 index e39b376..0000000 --- a/cmi5PHP/tests/StatementTest.php +++ /dev/null @@ -1,1028 +0,0 @@ -assertInstanceOf('cmi5\Statement', $obj); - $this->assertAttributeEmpty('id', $obj, 'id empty'); - $this->assertAttributeEmpty('actor', $obj, 'actor empty'); - $this->assertAttributeEmpty('verb', $obj, 'verb empty'); - $this->assertAttributeEmpty('target', $obj, 'target empty'); - $this->assertAttributeEmpty('context', $obj, 'context empty'); - $this->assertAttributeEmpty('result', $obj, 'result empty'); - $this->assertAttributeEmpty('timestamp', $obj, 'timestamp empty'); - $this->assertAttributeEmpty('stored', $obj, 'stored empty'); - $this->assertAttributeEmpty('authority', $obj, 'authority empty'); - $this->assertAttributeEmpty('version', $obj, 'version empty'); - } - - public function testFromJSONInvalidNull() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Statement::fromJSON(null); - } - - public function testFromJSONInvalidEmptyString() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Statement::fromJSON(''); - } - - public function testFromJSONInvalidMalformed() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Statement::fromJSON('{id:"some value"}'); - } - - public function testConstructionFromArrayWithId() { - $id = Util::getUUID(); - $cfg = [ - 'id' => $id, - 'actor' => [ - 'mbox' => COMMON_MBOX, - ], - 'verb' => [ - 'id' => COMMON_VERB_ID, - ], - 'object' => [ - 'id' => COMMON_ACTIVITY_ID, - ], - ]; - $obj = new Statement($cfg); - - $this->assertSame($obj->getId(), $id, 'id'); - } - - public function testStamp() { - $obj = new Statement(); - $obj->stamp(); - - $this->assertAttributeInternalType('string', 'timestamp', $obj, 'timestamp is string'); - $this->assertRegExp(Util::UUID_REGEX, $obj->getId(), 'id is UUId'); - } - - public function testSetId() { - $this->setExpectedException( - 'InvalidArgumentException', - 'arg1 must be a UUID "some invalid id"' - ); - - $obj = new Statement(); - $obj->setId('some invalid id'); - } - - public function testSetStoredInvalidArgumentException() { - $this->setExpectedException( - 'InvalidArgumentException', - 'type of arg1 must be string or DateTime' - ); - - $obj = new Statement(); - $obj->setStored(1); - } - - // TODO: need to loop versions - public function testAsVersion() { - $args = [ - 'actor' => [ - 'mbox' => COMMON_MBOX, - ], - 'verb' => [ - 'id' => COMMON_VERB_ID, - 'display' => [ - 'en-US' => 'experienced' - ] - ], - 'object' => [ - 'id' => COMMON_ACTIVITY_ID, - 'definition' => [ - 'type' => 'Invalid type', - 'name' => [ - 'en-US' => 'Test', - ], - 'description' => [ - 'en-US' => 'Test description', - ], - 'extensions' => [ - 'http://someuri' => 'some value' - ], - ] - ], - 'context' => [ - 'contextActivities' => [ - 'parent' => [ - [ - 'id' => COMMON_ACTIVITY_ID . '/1', - 'definition' => [ - 'name' => [ - 'en-US' => 'Test: 1', - ], - ], - ] - ], - ], - 'registration' => Util::getUUID(), - ], - 'result' => [ - 'completion' => true, - 'success' => false, - 'score' => [ - 'raw' => '97', - 'min' => '65', - 'max' => '100', - 'scaled' => '.97' - ] - ], - 'version' => '1.0.0', - 'attachments' => [ - [ - 'usageType' => 'http://test', - 'display' => ['en-US' => 'test display'], - 'contentType' => 'text/plain; charset=ascii', - 'length' => 0, - 'sha2' => hash('sha256', json_encode(['foo', 'bar'])) - ] - ] - ]; - - $obj = Statement::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $obj->stamp(); - - $versioned = $obj->asVersion('1.0.0'); - - $args['id'] = $obj->getId(); - $args['timestamp'] = $obj->getTimestamp(); - $args['actor']['objectType'] = 'Agent'; - $args['object']['objectType'] = 'Activity'; - $args['context']['contextActivities']['parent'][0]['objectType'] = 'Activity'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = Statement::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionEmptySubObjects() { - $args = [ - 'actor' => [ - 'mbox' => COMMON_MBOX, - ], - 'verb' => [ - 'id' => COMMON_VERB_ID, - 'display' => [] - ], - 'object' => [ - 'id' => COMMON_ACTIVITY_ID, - 'definition' => [ - 'type' => 'Invalid type', - 'name' => [], - 'description' => [], - 'extensions' => [], - ] - ], - 'context' => [ - 'contextActivities' => [ - 'parent' => [], - ], - 'registration' => Util::getUUID(), - ], - 'result' => [ - 'completion' => true, - 'success' => false, - 'score' => [] - ], - 'version' => '1.0.0', - 'attachments' => [] - ]; - - $obj = Statement::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['actor']['objectType'] = 'Agent'; - $args['object']['objectType'] = 'Activity'; - unset($args['verb']['display']); - unset($args['object']['definition']['name']); - unset($args['object']['definition']['description']); - unset($args['object']['definition']['extensions']); - unset($args['context']['contextActivities']); - unset($args['result']['score']); - unset($args['attachments']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionSubObjectWithEmptyValue() { - $args = [ - 'actor' => [ - 'mbox' => COMMON_MBOX, - ], - 'verb' => [ - 'id' => COMMON_VERB_ID, - ], - 'object' => [ - 'id' => COMMON_ACTIVITY_ID, - 'definition' => [ - 'type' => 'Invalid type', - 'name' => [ - 'en-US' => '' - ], - ] - ], - 'context' => [ - 'contextActivities' => [], - ], - 'result' => [ - 'completion' => true, - 'success' => false, - 'score' => [ - 'raw' => 0 - ] - ] - ]; - - $obj = Statement::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['actor']['objectType'] = 'Agent'; - $args['object']['objectType'] = 'Activity'; - unset($args['context']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testCompareWithSignature() { - $id1 = Util::getUUID(); - $id2 = Util::getUUID(); - $actor1 = new Agent( - [ 'mbox' => COMMON_MBOX ] - ); - $actor2 = new Agent( - [ 'account' => [ 'homePage' => COMMON_ACCT_HOMEPAGE, 'name' => COMMON_ACCT_NAME ]] - ); - $verb1 = new Verb( - [ 'id' => COMMON_VERB_ID ] - ); - $verb2 = new Verb( - [ 'id' => COMMON_VERB_ID . '/2' ] - ); - $activity1 = new Activity( - [ 'id' => COMMON_ACTIVITY_ID ] - ); - $activity2 = new Activity( - [ 'id' => COMMON_ACTIVITY_ID . '/2' ] - ); - $context1 = new Context( - [ 'registration' => Util::getUUID() ] - ); - $context2 = new Context( - [ - 'contextActivities' => [ - [ 'parent' => [ COMMON_ACTIVITY_ID . '/parent' ]], - [ 'grouping' => [ COMMON_ACTIVITY_ID ]] - ] - ] - ); - $result1 = new Result( - [ 'raw' => 87 ] - ); - $result2 = new Result( - [ 'response' => 'a' ] - ); - $timestamp1 = '2015-01-28T14:23:37.159Z'; - $timestamp1_tz = '2015-01-28T08:23:37.159-06:00'; - $timestamp1_subsecond = '2015-01-28T14:23:37.348Z'; - $timestamp2 = '2015-01-28T15:49:11.089Z'; - - $attachments1 = new Attachment( - [ - 'usageType' => 'http://id.cmi5api.com/attachment/supporting_media', - 'display' => ['en-US' => 'Test Display'], - 'contentType' => 'application/json', - 'content' => json_encode(['foo', 'bar']), - ] - ); - $attachments2 = new Attachment( - [ - 'usageType' => 'http://id.cmi5api.com/attachment/supporting_media', - 'display' => ['en-US' => 'Test Display'], - 'contentType' => 'application/json', - 'content' => json_encode(['bar', 'foo']), - ] - ); - - $full = [ - 'id' => $id1, - 'actor' => $actor1, - 'verb' => $verb1, - 'target' => $activity1, - 'context' => $context1, - 'result' => $result1, - 'timestamp' => $timestamp1, - 'attachments' => [ $attachments1 ] - ]; - - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ], - [ - 'description' => 'id', - 'objArgs' => ['id' => $id1] - ], - [ - 'description' => 'actor', - 'objArgs' => ['actor' => $actor1] - ], - [ - 'description' => 'verb', - 'objArgs' => ['verb' => $verb1] - ], - [ - 'description' => 'object', - 'objArgs' => ['target' => $activity1] - ], - [ - 'description' => 'result', - 'objArgs' => ['result' => $result1] - ], - [ - 'description' => 'context', - 'objArgs' => ['context' => $context1] - ], - [ - 'description' => 'timestamp', - 'objArgs' => ['timestamp' => $timestamp1] - ], - [ - 'description' => 'attachments', - 'objArgs' => ['attachments' => [ $attachments1 ]] - ], - [ - 'description' => 'all', - 'objArgs' => $full - ], - - // - // special case where timestamp marks the same point in time but - // is provided in a different timezone - // - [ - 'description' => 'timestamp timezone difference', - 'objArgs' => ['timestamp' => $timestamp1], - 'sigArgs' => ['timestamp' => $timestamp1_tz] - ], - - // - // special case where we make sure sub-second precision is handled - // - [ - 'description' => 'timestamp subsecond difference', - 'objArgs' => ['timestamp' => $timestamp1], - 'sigArgs' => ['timestamp' => $timestamp1_subsecond], - 'reason' => 'Comparison of timestamp failed: value is not the same' - ], - - [ - 'description' => 'id this only: mismatch', - 'objArgs' => ['id' => $id1], - 'sigArgs' => [], - 'reason' => 'Comparison of id failed: value not in signature' - ], - [ - 'description' => 'id sig only: mismatch', - 'objArgs' => [], - 'sigArgs' => ['id' => $id1], - 'reason' => 'Comparison of id failed: value not in this' - ], - [ - 'description' => 'id only: mismatch', - 'objArgs' => ['id' => $id1], - 'sigArgs' => ['id' => $id2], - 'reason' => 'Comparison of id failed: value is not the same' - ], - [ - 'description' => 'actor only: mismatch', - 'objArgs' => ['actor' => $actor1], - 'sigArgs' => ['actor' => $actor2], - 'reason' => 'Comparison of actor failed: Comparison of mbox failed: value is not the same' - ], - [ - 'description' => 'verb only: mismatch', - 'objArgs' => ['verb' => $verb1], - 'sigArgs' => ['verb' => $verb2], - 'reason' => 'Comparison of verb failed: Comparison of id failed: value is not the same' - ], - [ - 'description' => 'object only: mismatch', - 'objArgs' => ['target' => $activity1], - 'sigArgs' => ['target' => $activity2], - 'reason' => 'Comparison of target failed: Comparison of id failed: value is not the same' - ], - [ - 'description' => 'result only: mismatch', - 'objArgs' => ['result' => $result1], - 'sigArgs' => ['result' => $result2], - 'reason' => 'Comparison of result failed: Comparison of response failed: value not present in this or signature' - ], - [ - 'description' => 'context only: mismatch', - 'objArgs' => ['context' => $context1], - 'sigArgs' => ['context' => $context2], - 'reason' => 'Comparison of context failed: Comparison of registration failed: value not present in this or signature' - ], - [ - 'description' => 'timestamp only: mismatch', - 'objArgs' => ['timestamp' => $timestamp1], - 'sigArgs' => ['timestamp' => $timestamp2], - 'reason' => 'Comparison of timestamp failed: value is not the same' - ], - [ - 'description' => 'attachments this only: mismatch', - 'objArgs' => ['attachments' => [$attachments1]], - 'sigArgs' => ['attachments' => []], - 'reason' => 'Comparison of attachments list failed: array lengths differ' - ], - [ - 'description' => 'attachments sig only: mismatch', - 'objArgs' => ['attachments' => []], - 'sigArgs' => ['attachments' => [$attachments2]], - 'reason' => 'Comparison of attachments list failed: array lengths differ' - ], - [ - 'description' => 'attachments only: mismatch', - 'objArgs' => ['attachments' => [$attachments1]], - 'sigArgs' => ['attachments' => [$attachments2]], - 'reason' => 'Comparison of attachment 0 failed: Comparison of sha2 failed: value is not the same' - ], - [ - 'description' => 'full: id mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['id' => $id2]), - 'reason' => 'Comparison of id failed: value is not the same' - ], - [ - 'description' => 'full: actor mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['actor' => $actor2]), - 'reason' => 'Comparison of actor failed: Comparison of mbox failed: value is not the same' - ], - [ - 'description' => 'full: verb mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['verb' => $verb2]), - 'reason' => 'Comparison of verb failed: Comparison of id failed: value is not the same' - ], - [ - 'description' => 'full: target mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['target' => $activity2]), - 'reason' => 'Comparison of target failed: Comparison of id failed: value is not the same' - ], - [ - 'description' => 'full: result mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['result' => $result2]), - 'reason' => 'Comparison of result failed: Comparison of response failed: value not present in this or signature' - ], - [ - 'description' => 'full: context mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['context' => $context2]), - 'reason' => 'Comparison of context failed: Comparison of registration failed: value not present in this or signature' - ], - [ - 'description' => 'full: timestamp mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['timestamp' => $timestamp2]), - 'reason' => 'Comparison of timestamp failed: value is not the same' - ], - [ - 'description' => 'full: attachments mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['attachments' => [$attachments2]]), - 'reason' => 'Comparison of attachment 0 failed: Comparison of sha2 failed: value is not the same' - ], - ]; - $this->runSignatureCases("cmi5\Statement", $cases); - } - - public function testHasAttachments() { - $stNoAttachments = new Statement(); - $this->assertFalse($stNoAttachments->hasAttachments()); - - $stWithAttachments = new Statement( - [ - 'attachments' => [ - [ - 'usageType' => 'http://test', - 'display' => ['en-US' => 'test display'], - 'contentType' => 'text/plain; charset=ascii', - 'length' => 0, - 'sha2' => hash('sha256', json_encode(['foo', 'bar'])) - ] - ] - ] - ); - $this->assertTrue($stWithAttachments->hasAttachments()); - } - - public function testHasAttachmentWithContent() { - $content = 'Just some test content'; - - $stNoAttachments = new Statement(); - $this->assertFalse($stNoAttachments->hasAttachmentsWithContent()); - - $stWithAttachmentNoContent = new Statement( - [ - 'attachments' => [ - [ - 'usageType' => 'http://test', - 'display' => ['en-US' => 'test display'], - 'contentType' => 'text/plain; charset=ascii', - 'length' => strlen($content), - 'sha2' => hash('sha256', $content) - ] - ] - ] - ); - $this->assertFalse($stWithAttachmentNoContent->hasAttachmentsWithContent()); - - $stWithAttachmentWithContent = new Statement( - [ - 'attachments' => [ - [ - 'usageType' => 'http://test', - 'display' => ['en-US' => 'test display'], - 'contentType' => 'text/plain; charset=ascii', - 'length' => strlen($content), - 'sha2' => hash('sha256', $content), - 'content' => $content - ] - ] - ] - ); - $this->assertTrue($stWithAttachmentWithContent->hasAttachmentsWithContent()); - } - - public function testSignNoArgs() { - $obj = new Statement(); - - $this->setExpectedException( - 'PHPUnit_Framework_Error_Warning', - (getenv('TRAVIS_PHP_VERSION') == "hhvm" ? 'sign() expects at least 2 parameters, 0 given' : 'Missing argument 1') - ); - $obj->sign(); - } - - public function testSignOneArg() { - $obj = new Statement(); - - $this->setExpectedException( - 'PHPUnit_Framework_Error_Warning', - (getenv('TRAVIS_PHP_VERSION') == "hhvm" ? 'sign() expects at least 2 parameters, 1 given' : 'Missing argument 2') - ); - $obj->sign('test'); - } - - public function testSignNoActor() { - $this->setExpectedException( - 'InvalidArgumentException', - 'actor must be present in signed statement' - ); - - $obj = new Statement(); - $obj->sign('test', 'test'); - } - - public function testSignNoVerb() { - $this->setExpectedException( - 'InvalidArgumentException', - 'verb must be present in signed statement' - ); - - $obj = new Statement( - [ - 'actor' => ['mbox' => COMMON_MBOX] - ] - ); - $obj->sign('test', 'test'); - } - - public function testSignNoObject() { - $this->setExpectedException( - 'InvalidArgumentException', - 'object must be present in signed statement' - ); - - $obj = new Statement( - [ - 'actor' => ['mbox' => COMMON_MBOX], - 'verb' => [ 'id' => COMMON_VERB_ID ] - ] - ); - $obj->sign('test', 'test'); - } - - public function testSignInvalidAlgorithm() { - $this->setExpectedException( - 'InvalidArgumentException', - "Invalid signing algorithm: 'not right'" - ); - - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignNoPassword' - ]) - ] - ); - $obj->sign('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password'], ['algorithm' => 'not right']); - } - - public function testSignEmptyPassword() { - $this->setExpectedException( - 'Exception', - 'Unable to get private key: error:0906A068:PEM routines:PEM_do_header:bad password read' - ); - - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignNoPassword' - ]) - ] - ); - $obj->sign('file://' . $GLOBALS['KEYs']['private'], ''); - } - - public function testSignInvalidPassword() { - $this->setExpectedExceptionRegExp( - 'Exception', - '/Unable to get private key: error:.*:bad decrypt/' - ); - - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignNoPassword' - ]) - ] - ); - $obj->sign('file://' . $GLOBALS['KEYs']['private'], 'notthecorrectpasswordhopefully'); - } - - public function testSignInvalidX5cErrorToException() { - $this->setExpectedExceptionRegExp( - 'PHPUnit_Framework_Error', - '/supplied parameter cannot be coerced into an X509 certificate!/' - ); - - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignNoPassword' - ]) - ] - ); - $obj->sign('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password'], [ 'x5c' => 'invalid' ]); - } - - public function testSignInvalidX5cNoError() { - $this->setExpectedExceptionRegExp( - 'Exception', - '/Unable to read certificate for x5c inclusion: .*/' - ); - - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignNoPassword' - ]) - ] - ); - @$obj->sign('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password'], [ 'x5c' => 'invalid' ]); - } - - public function testVerifyNoSignature() { - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignAndVerify' - ]) - ] - ); - $result = $obj->verify(); - - $this->assertFalse($result['success'], 'success'); - $this->assertSame($result['reason'], 'Unable to locate signature attachment (usage type)', 'reason'); - } - - public function testVerifyInvalidJWS() { - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignAndVerify' - ]), - 'attachments' => [ - [ - 'usageType' => 'http://adlnet.gov/expapi/attachments/signature', - 'display' => ['en-US' => 'test display'], - 'contentType' => 'application/octet-stream', - 'content' => 'not a signature' - ] - ] - ] - ); - $result = $obj->verify(); - - $this->assertFalse($result['success'], 'success'); - $this->assertStringStartsWith( - 'Failed to load JWS', - $result['reason'], - 'reason' - ); - } - - public function testVerifyInvalidX5cErrorToException() { - $this->setExpectedExceptionRegExp( - 'PHPUnit_Framework_Error', - '/supplied parameter cannot be coerced into an X509 certificate!/' - ); - - $content = new JWS( - [ - 'alg' => 'RS256', - 'x5c' => ['notAValidCertificate'] - ] - ); - $content->setPayload(['prop' => 'val'], false); - $content->sign(openssl_pkey_get_private('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password'])); - - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignNoPassword' - ]), - 'attachments' => [ - [ - 'usageType' => 'http://adlnet.gov/expapi/attachments/signature', - 'display' => ['en-US' => 'test display'], - 'contentType' => 'application/octet-stream', - 'content' => $content->getTokenString() - ] - ] - ] - ); - $result = $obj->verify(); - } - - public function testVerifyInvalidX5cNoError() { - $content = new JWS( - [ - 'alg' => 'RS256', - 'x5c' => ['notAValidCertificate'] - ] - ); - $content->setPayload(['prop' => 'val'], false); - $content->sign(openssl_pkey_get_private('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password'])); - - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignNoPassword' - ]), - 'attachments' => [ - [ - 'usageType' => 'http://adlnet.gov/expapi/attachments/signature', - 'display' => ['en-US' => 'test display'], - 'contentType' => 'application/octet-stream', - 'content' => $content->getTokenString() - ] - ] - ] - ); - @$result = $obj->verify(); - - $this->assertFalse($result['success'], 'success'); - $this->assertSame('failed to read cert in x5c: error:0906D06C:PEM routines:PEM_read_bio:no start line', $result['reason'], 'reason'); - } - - public function testVerifyNoPubKey() { - $content = new JWS( - [ - 'alg' => 'RS256' - ] - ); - $content->setPayload(['prop' => 'val'], false); - $content->sign(openssl_pkey_get_private('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password'])); - - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignNoPassword' - ]), - 'attachments' => [ - [ - 'usageType' => 'http://adlnet.gov/expapi/attachments/signature', - 'display' => ['en-US' => 'test display'], - 'contentType' => 'application/octet-stream', - 'content' => $content->getTokenString() - ] - ] - ] - ); - $result = $obj->verify(); - - $this->assertFalse($result['success'], 'success'); - $this->assertSame($result['reason'], 'No public key found or provided for verification', 'reason'); - } - - public function testVerifyIncorrectPubKey() { - $content = new JWS( - [ - 'alg' => 'RS256' - ] - ); - $content->setPayload(['prop' => 'val'], false); - $content->sign(openssl_pkey_get_private('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password'])); - - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignNoPassword' - ]), - 'attachments' => [ - [ - 'usageType' => 'http://adlnet.gov/expapi/attachments/signature', - 'display' => ['en-US' => 'test display'], - 'contentType' => 'application/octet-stream', - 'content' => $content->getTokenString() - ] - ] - ] - ); - - $newKey = openssl_pkey_new( - [ - 'private_key_bits' => 2048, - 'private_key_type' => OPENSSL_KEYTYPE_RSA - ] - ); - $pubKey = openssl_pkey_get_details($newKey); - $pubKey = $pubKey["key"]; - - $result = $obj->verify(['publicKey' => $pubKey]); - - $this->assertFalse($result['success'], 'success'); - $this->assertSame($result['reason'], 'Failed to verify signature', 'reason'); - } - - public function testVerifyDiffStatement() { - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testVerifyDiffStatement' - ]) - ] - ); - $obj->sign('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password'], ['x5c' => 'file://' . $GLOBALS['KEYs']['public']]); - - $diff = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testVerifyDiffStatement-diff' - ]), - 'attachments' => $obj->getAttachments() - ] - ); - $result = $diff->verify(); - - $this->assertFalse($result['success'], 'success'); - $this->assertSame($result['reason'], 'Statement to signature comparison failed: Comparison of target failed: Comparison of id failed: value is not the same', 'reason'); - } - - public function testSignAndVerify() { - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignAndVerify' - ]) - ] - ); - $obj->sign('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password']); - - $attachment = $obj->getAttachments()[0]; - - $this->assertSame($attachment->getUsageType(), 'http://adlnet.gov/expapi/attachments/signature', 'usage type value'); - $this->assertSame($attachment->getContentType(), 'application/octet-stream', 'content type value'); - - $result = $obj->verify(['publicKey' => 'file://' . $GLOBALS['KEYs']['public']]); - if (! $result['success']) { - print $result['reason']; - } - $this->assertTrue($result['success'], 'success return value'); - } - - public function testSignAndVerifyFromEmbedded() { - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignAndVerifyFromEmbedded' - ]) - ] - ); - $obj->sign('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password'], [ 'x5c' => 'file://' . $GLOBALS['KEYs']['public'] ]); - - $attachment = $obj->getAttachments()[0]; - - $this->assertSame($attachment->getUsageType(), 'http://adlnet.gov/expapi/attachments/signature', 'usage type value'); - $this->assertSame($attachment->getContentType(), 'application/octet-stream', 'content type value'); - - $result = $obj->verify(); - if (! $result['success']) { - print $result['reason']; - } - $this->assertTrue($result['success'], 'success return value'); - } - - public function testSignAndVerifyNoPubKey() { - $obj = new Statement( - [ - 'actor' => [ 'mbox' => COMMON_MBOX ], - 'verb' => [ 'id' => COMMON_VERB_ID ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementTest/testSignAndVerify' - ]) - ] - ); - $obj->sign('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password']); - - $result = $obj->verify(); - $this->assertFalse($result['success'], 'success return value'); - $this->assertSame($result['reason'], 'No public key found or provided for verification', 'reason'); - } -} diff --git a/cmi5PHP/tests/StatementVariationsTest.php b/cmi5PHP/tests/StatementVariationsTest.php deleted file mode 100755 index c3ccb66..0000000 --- a/cmi5PHP/tests/StatementVariationsTest.php +++ /dev/null @@ -1,227 +0,0 @@ - [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementVariationsTest/Basic' - ]) - ] - ); - - foreach (self::$lrss as $lrs) { - $response = $lrs->saveStatement($statement); - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success, "successful request"); - } - } - - public function testAttachmentsMetaOnly() { - $text_content = "Content created at: " . Util::getTimestamp(); - - $statement = new Statement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementVariationsTest/Basic' - ]), - 'attachments' => [ - [ - 'usageType' => 'http://id.cmi5api.com/attachment/supporting_media', - 'display' => ['en-US' => 'StatementVariantsTest::testAttachmentsMetaOnly'], - 'contentType' => 'text/plain; charset=ascii', - 'length' => 25, - 'sha2' => hash('sha256', $text_content), - 'fileUrl' => 'http://cmi5api.com/cmi5PHP/Test/AttachmentFileUrl' - ] - ] - ] - ); - - foreach (self::$lrss as $lrs) { - $response = $lrs->saveStatement($statement); - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success, "successful request"); - } - } - - public function testAttachmentsString() { - $text_content = "Content created at: " . Util::getTimestamp(); - - $statement = new Statement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementVariationsTest/AttachmentsString' - ]), - 'attachments' => [ - [ - 'usageType' => 'http://id.cmi5api.com/attachment/supporting_media', - 'display' => ['en-US' => 'StatementVariantsTest::testAttachmentsString'], - 'contentType' => 'text/plain; charset=ascii', - 'content' => $text_content - ] - ] - ] - ); - - foreach (self::$lrss as $lrs) { - $response = $lrs->saveStatement($statement); - $this->assertInstanceOf('cmi5\LRSResponse', $response); - $this->assertTrue($response->success, "successful request"); - } - } - - public function testAttachmentsBinaryFile() { - $file = 'tests/files/image.jpg'; - - $statement = new Statement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementVariationsTest/AttachmentsBinary' - ]), - 'attachments' => [ - [ - 'usageType' => 'http://id.cmi5api.com/attachment/supporting_media', - 'display' => ['en-US' => 'StatementVariantsTest::testAttachmentsString'], - 'contentType' => 'text/plain; charset=ascii', - 'content' => file_get_contents($file) - ] - ] - ] - ); - - foreach (self::$lrss as $lrs) { - $saveResponse = $lrs->saveStatement($statement); - $this->assertInstanceOf('cmi5\LRSResponse', $saveResponse); - $this->assertTrue($saveResponse->success, "successful request"); - - $retrieveResponse = $lrs->retrieveStatement($saveResponse->content->getId(), ['attachments' => true]); - $this->assertInstanceOf('cmi5\LRSResponse', $retrieveResponse); - $this->assertTrue($retrieveResponse->success); - $this->assertInstanceOf('cmi5\Statement', $retrieveResponse->content); - - $this->assertSame($retrieveResponse->content->getAttachments()[0]->getSha2(), hash('sha256', file_get_contents($file)), 'verify content'); - } - } - - public function testSignedAndVerified() { - $statement = new Statement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementVariationsTest/Signed' - ]) - ] - ); - $statement->sign('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password']); - - foreach (self::$lrss as $lrs) { - $saveResponse = $lrs->saveStatement($statement); - $this->assertInstanceOf('cmi5\LRSResponse', $saveResponse); - $this->assertTrue($saveResponse->success, "successful request"); - - $retrieveResponse = $lrs->retrieveStatement($saveResponse->content->getId(), ['attachments' => true]); - $this->assertInstanceOf('cmi5\LRSResponse', $retrieveResponse); - $this->assertTrue($retrieveResponse->success); - $this->assertInstanceOf('cmi5\Statement', $retrieveResponse->content); - - $verificationResult = $retrieveResponse->content->verify(['publicKey' => 'file://' . $GLOBALS['KEYs']['public']]); - $this->assertTrue($verificationResult['success'], 'verify signature'); - } - } - - public function testSignedAndVerifiedX5c() { - $statement = new Statement( - [ - 'actor' => [ - 'mbox' => COMMON_MBOX - ], - 'verb' => [ - 'id' => COMMON_VERB_ID - ], - 'object' => new Activity([ - 'id' => COMMON_ACTIVITY_ID . '/StatementVariationsTest/Signed' - ]) - ] - ); - $statement->sign('file://' . $GLOBALS['KEYs']['private'], $GLOBALS['KEYs']['password'], ['x5c' => 'file://' . $GLOBALS['KEYs']['public']]); - - foreach (self::$lrss as $lrs) { - $saveResponse = $lrs->saveStatement($statement); - $this->assertInstanceOf('cmi5\LRSResponse', $saveResponse); - if (! $saveResponse->success) { - print_r($saveResponse); - } - $this->assertTrue($saveResponse->success, "successful request"); - - $retrieveResponse = $lrs->retrieveStatement($saveResponse->content->getId(), ['attachments' => true]); - $this->assertInstanceOf('cmi5\LRSResponse', $retrieveResponse); - $this->assertTrue($retrieveResponse->success); - $this->assertInstanceOf('cmi5\Statement', $retrieveResponse->content); - - $verificationResult = $retrieveResponse->content->verify(); - $this->assertTrue($verificationResult['success'], 'verify signature'); - } - } -} diff --git a/cmi5PHP/tests/SubStatementTest.php b/cmi5PHP/tests/SubStatementTest.php deleted file mode 100755 index 5614b43..0000000 --- a/cmi5PHP/tests/SubStatementTest.php +++ /dev/null @@ -1,387 +0,0 @@ -assertInstanceOf('cmi5\StatementBase', $obj); - } - - public function testGetObjectType() { - $obj = new SubStatement(); - $this->assertSame('SubStatement', $obj->getObjectType()); - } - - // TODO: need to loop versions - public function testAsVersion() { - $args = [ - 'timestamp' => '2015-01-28T14:23:37.159Z', - 'actor' => [ - 'mbox' => COMMON_MBOX, - ], - 'verb' => [ - 'id' => COMMON_VERB_ID, - 'display' => [ - 'en-US' => 'experienced' - ] - ], - 'object' => [ - 'id' => COMMON_ACTIVITY_ID, - 'definition' => [ - 'type' => 'Invalid type', - 'name' => [ - 'en-US' => 'Test', - ], - 'description' => [ - 'en-US' => 'Test description', - ], - 'extensions' => [ - 'http://someuri' => 'some value' - ], - ] - ], - 'context' => [ - 'contextActivities' => [ - 'parent' => [ - [ - 'id' => COMMON_ACTIVITY_ID . '/1', - 'definition' => [ - 'name' => [ - 'en-US' => 'Test: 1', - ], - ], - ] - ], - ], - 'registration' => Util::getUUID(), - ], - 'result' => [ - 'completion' => true, - 'success' => false, - 'score' => [ - 'raw' => '97', - 'min' => '65', - 'max' => '100', - 'scaled' => '.97' - ] - ], - ]; - - $obj = SubStatement::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'SubStatement'; - $args['timestamp'] = $obj->getTimestamp(); - $args['actor']['objectType'] = 'Agent'; - $args['object']['objectType'] = 'Activity'; - $args['context']['contextActivities']['parent'][0]['objectType'] = 'Activity'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = SubStatement::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'SubStatement'; - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmptySubObjects() { - $args = [ - 'actor' => [ - 'mbox' => COMMON_MBOX, - ], - 'verb' => [ - 'id' => COMMON_VERB_ID, - 'display' => [] - ], - 'object' => [ - 'id' => COMMON_ACTIVITY_ID, - 'definition' => [ - 'type' => 'Invalid type', - 'name' => [], - 'description' => [], - 'extensions' => [], - ] - ], - 'context' => [ - 'contextActivities' => [ - 'parent' => [], - ], - 'registration' => Util::getUUID(), - ], - 'result' => [ - 'completion' => true, - 'success' => false, - 'score' => [] - ], - ]; - - $obj = SubStatement::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'SubStatement'; - $args['actor']['objectType'] = 'Agent'; - $args['object']['objectType'] = 'Activity'; - unset($args['verb']['display']); - unset($args['object']['definition']['name']); - unset($args['object']['definition']['description']); - unset($args['object']['definition']['extensions']); - unset($args['context']['contextActivities']); - unset($args['result']['score']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionSubObjectWithEmptyValue() { - $args = [ - 'actor' => [ - 'mbox' => COMMON_MBOX, - ], - 'verb' => [ - 'id' => COMMON_VERB_ID, - ], - 'object' => [ - 'id' => COMMON_ACTIVITY_ID, - 'definition' => [ - 'type' => 'Invalid type', - 'name' => [ - 'en-US' => '' - ], - ] - ], - 'context' => [ - 'contextActivities' => [], - ], - 'result' => [ - 'completion' => true, - 'success' => false, - 'score' => [ - 'raw' => 0 - ] - ] - ]; - - $obj = SubStatement::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $args['objectType'] = 'SubStatement'; - $args['actor']['objectType'] = 'Agent'; - $args['object']['objectType'] = 'Activity'; - unset($args['context']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - - public function testCompareWithSignature() { - $actor1 = new Agent( - [ 'mbox' => COMMON_MBOX ] - ); - $actor2 = new Agent( - [ 'account' => [ 'homePage' => COMMON_ACCT_HOMEPAGE, 'name' => COMMON_ACCT_NAME ]] - ); - $verb1 = new Verb( - [ 'id' => COMMON_VERB_ID ] - ); - $verb2 = new Verb( - [ 'id' => COMMON_VERB_ID . '/2' ] - ); - $activity1 = new Activity( - [ 'id' => COMMON_ACTIVITY_ID ] - ); - $activity2 = new Activity( - [ 'id' => COMMON_ACTIVITY_ID . '/2' ] - ); - $context1 = new Context( - [ 'registration' => Util::getUUID() ] - ); - $context2 = new Context( - [ - 'contextActivities' => [ - [ 'parent' => [ COMMON_ACTIVITY_ID . '/parent' ]], - [ 'grouping' => [ COMMON_ACTIVITY_ID ]] - ] - ] - ); - $result1 = new Result( - [ 'raw' => 87 ] - ); - $result2 = new Result( - [ 'response' => 'a' ] - ); - $timestamp1 = '2015-01-28T14:23:37.159Z'; - $timestamp1_tz = '2015-01-28T08:23:37.159-06:00'; - $timestamp1_subsecond = '2015-01-28T14:23:37.348Z'; - $timestamp2 = '2015-01-28T15:49:11.089Z'; - - $full = [ - 'actor' => $actor1, - 'verb' => $verb1, - 'target' => $activity1, - 'context' => $context1, - 'result' => $result1, - 'timestamp' => $timestamp1 - ]; - - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ], - [ - 'description' => 'actor', - 'objArgs' => ['actor' => $actor1] - ], - [ - 'description' => 'verb', - 'objArgs' => ['verb' => $verb1] - ], - [ - 'description' => 'object', - 'objArgs' => ['target' => $activity1] - ], - [ - 'description' => 'result', - 'objArgs' => ['result' => $result1] - ], - [ - 'description' => 'context', - 'objArgs' => ['context' => $context1] - ], - [ - 'description' => 'timestamp', - 'objArgs' => ['timestamp' => $timestamp1] - ], - [ - 'description' => 'all', - 'objArgs' => $full - ], - - // - // special case where timestamp marks the same point in time but - // is provided in a different timezone - // - [ - 'description' => 'timestamp timezone difference', - 'objArgs' => ['timestamp' => $timestamp1], - 'sigArgs' => ['timestamp' => $timestamp1_tz] - ], - - // - // special case where we make sure sub-second precision is handled - // - [ - 'description' => 'timestamp subsecond difference', - 'objArgs' => ['timestamp' => $timestamp1], - 'sigArgs' => ['timestamp' => $timestamp1_subsecond], - 'reason' => 'Comparison of timestamp failed: value is not the same' - ], - - [ - 'description' => 'actor only: mismatch', - 'objArgs' => ['actor' => $actor1], - 'sigArgs' => ['actor' => $actor2], - 'reason' => 'Comparison of actor failed: Comparison of mbox failed: value is not the same' - ], - [ - 'description' => 'verb only: mismatch', - 'objArgs' => ['verb' => $verb1], - 'sigArgs' => ['verb' => $verb2], - 'reason' => 'Comparison of verb failed: Comparison of id failed: value is not the same' - ], - [ - 'description' => 'object only: mismatch', - 'objArgs' => ['target' => $activity1], - 'sigArgs' => ['target' => $activity2], - 'reason' => 'Comparison of target failed: Comparison of id failed: value is not the same' - ], - [ - 'description' => 'result only: mismatch', - 'objArgs' => ['result' => $result1], - 'sigArgs' => ['result' => $result2], - 'reason' => 'Comparison of result failed: Comparison of response failed: value not present in this or signature' - ], - [ - 'description' => 'context only: mismatch', - 'objArgs' => ['context' => $context1], - 'sigArgs' => ['context' => $context2], - 'reason' => 'Comparison of context failed: Comparison of registration failed: value not present in this or signature' - ], - [ - 'description' => 'timestamp only: mismatch', - 'objArgs' => ['timestamp' => $timestamp1], - 'sigArgs' => ['timestamp' => $timestamp2], - 'reason' => 'Comparison of timestamp failed: value is not the same' - ], - [ - 'description' => 'full: actor mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['actor' => $actor2]), - 'reason' => 'Comparison of actor failed: Comparison of mbox failed: value is not the same' - ], - [ - 'description' => 'full: verb mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['verb' => $verb2]), - 'reason' => 'Comparison of verb failed: Comparison of id failed: value is not the same' - ], - [ - 'description' => 'full: target mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['target' => $activity2]), - 'reason' => 'Comparison of target failed: Comparison of id failed: value is not the same' - ], - [ - 'description' => 'full: result mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['result' => $result2]), - 'reason' => 'Comparison of result failed: Comparison of response failed: value not present in this or signature' - ], - [ - 'description' => 'full: context mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['context' => $context2]), - 'reason' => 'Comparison of context failed: Comparison of registration failed: value not present in this or signature' - ], - [ - 'description' => 'full: timestamp mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['timestamp' => $timestamp2]), - 'reason' => 'Comparison of timestamp failed: value is not the same' - ], - ]; - $this->runSignatureCases("cmi5\SubStatement", $cases); - } -} diff --git a/cmi5PHP/tests/TestCompareWithSignatureTrait.php b/cmi5PHP/tests/TestCompareWithSignatureTrait.php deleted file mode 100755 index bf7b19e..0000000 --- a/cmi5PHP/tests/TestCompareWithSignatureTrait.php +++ /dev/null @@ -1,35 +0,0 @@ - true, 'reason' => null]; - - for ($i = 0; $i < count($cases); $i++) { - $obj = new $class($cases[$i]['objArgs']); - $sig = new $class(isset($cases[$i]['sigArgs']) ? $cases[$i]['sigArgs'] : $cases[$i]['objArgs']); - if (isset($cases[$i]['reason'])) { - $failure = ['success' => false, 'reason' => $cases[$i]['reason']]; - } - - $this->assertSame(isset($cases[$i]['reason']) ? $failure : $success, $obj->compareWithSignature($sig), $cases[$i]['description']); - } - } -} diff --git a/cmi5PHP/tests/UtilTest.php b/cmi5PHP/tests/UtilTest.php deleted file mode 100755 index c08164c..0000000 --- a/cmi5PHP/tests/UtilTest.php +++ /dev/null @@ -1,39 +0,0 @@ -assertRegExp(Util::UUID_REGEX, $result); - } - - public function testGetTimestamp() { - $result = Util::getTimestamp(); - - // - // this isn't intended to match all ISO8601 just *our* format of it, so it should - // catch regressions, at least more than will be accepted by an LRS which is really - // ultimately what we want in our tests - // - $this->assertRegExp('/\d\d\d\d-[01]\d-[0123]\dT[012]\d:[012345]\d:[012345]\d\.\d\d\d\+00:00/', $result); - } -} diff --git a/cmi5PHP/tests/VerbTest.php b/cmi5PHP/tests/VerbTest.php deleted file mode 100755 index 5141e71..0000000 --- a/cmi5PHP/tests/VerbTest.php +++ /dev/null @@ -1,162 +0,0 @@ - 'experienced', - 'en-GB' => 'experienced', - 'es' => 'experimentado', - 'fr' => 'expérimenté', - 'it' => 'esperto' - ]; - } - - public function testInstantiation() { - $obj = new Verb(); - $this->assertInstanceOf('cmi5\Verb', $obj); - $this->assertAttributeEmpty('id', $obj, 'id empty'); - $this->assertAttributeInstanceOf('cmi5\LanguageMap', 'display', $obj, 'display is LanguageMap'); - } - - public function testFromJSONInvalidNull() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Verb::fromJSON(null); - } - - public function testFromJSONInvalidEmptyString() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Verb::fromJSON(''); - } - - public function testFromJSONInvalidMalformed() { - $this->setExpectedException('cmi5\JSONParseErrorException'); - $obj = Verb::fromJSON('{id:"some value"}'); - } - - public function testFromJSONIDOnly() { - $obj = Verb::fromJSON('{"id":"' . COMMON_VERB_ID . '"}'); - $this->assertInstanceOf('cmi5\Verb', $obj); - $this->assertAttributeEquals(COMMON_VERB_ID, 'id', $obj, 'id matches'); - $this->assertTrue($obj->getDisplay()->isEmpty(), 'display empty'); - } - - // TODO: need to loop versions - public function testAsVersion() { - $args = [ - 'id' => COMMON_VERB_ID, - 'display' => self::$DISPLAY - ]; - - $obj = Verb::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionEmpty() { - $args = []; - - $obj = Verb::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testAsVersionEmptyLanguageMap() { - $args = ['display' => []]; - - $obj = Verb::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - unset($args['display']); - - $this->assertEquals($versioned, $args, "serialized version matches corrected"); - } - - public function testAsVersionEmptyStringInLanguageMap() { - $args = ['display' => ['en' => '']]; - - $obj = Verb::fromJSON(json_encode($args, JSON_UNESCAPED_SLASHES)); - $versioned = $obj->asVersion('1.0.0'); - - $this->assertEquals($versioned, $args, "serialized version matches original"); - } - - public function testCompareWithSignature() { - $full = [ - 'id' => COMMON_VERB_ID, - 'display' => self::$DISPLAY - ]; - $display2 = array_replace(self::$DISPLAY, ['en-US' => 'not experienced']); - $cases = [ - [ - 'description' => 'all null', - 'objArgs' => [] - ], - [ - 'description' => 'id', - 'objArgs' => ['id' => COMMON_VERB_ID] - ], - [ - 'description' => 'display', - 'objArgs' => ['display' => self::$DISPLAY] - ], - [ - 'description' => 'all', - 'objArgs' => $full - ], - - // - // display is not matched for signature purposes because it - // is not supposed to affect the meaning of the statement - // - [ - 'description' => 'display only: mismatch (allowed)', - 'objArgs' => ['display' => self::$DISPLAY ], - 'sigArgs' => ['display' => $display2 ] - ], - [ - 'description' => 'full: display mismatch (allowed)', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['display' => $display2 ]) - ], - - [ - 'description' => 'id only: mismatch', - 'objArgs' => ['id' => COMMON_VERB_ID ], - 'sigArgs' => ['id' => COMMON_VERB_ID . '/invalid' ], - 'reason' => 'Comparison of id failed: value is not the same' - ], - [ - 'description' => 'full: id mismatch', - 'objArgs' => $full, - 'sigArgs' => array_replace($full, ['id' => COMMON_VERB_ID . '/invalid']), - 'reason' => 'Comparison of id failed: value is not the same' - ] - ]; - $this->runSignatureCases("cmi5\Verb", $cases); - } -} diff --git a/cmi5PHP/tests/VersionTest.php b/cmi5PHP/tests/VersionTest.php deleted file mode 100755 index c59c3ab..0000000 --- a/cmi5PHP/tests/VersionTest.php +++ /dev/null @@ -1,73 +0,0 @@ -assertInstanceOf("cmi5\Version", Version::v101(), "factory returns instance"); - } - - public function testToString() { - $this->assertInternalType("string", (string) Version::v101(), "object converts to string"); - } - - public function testHasValueReturnsBool() { - $this->assertTrue(Version::v101()->hasValue(Version::V101), "object has correct value"); - } - - public function testHasAnyValueReturnsBool() { - $this->assertFalse(Version::v101()->hasAnyValue([Version::V100, Version::V095]), "object does not have values"); - } - - public function testIsSupportedReturnsBool() { - $this->assertTrue(Version::v100()->isSupported(), "1.0.0 should be supported"); - $this->assertFalse(Version::v095()->isSupported(), "0.95 should not be supported"); - } - - public function testIsLatestReturnsBool() { - $this->assertTrue(Version::v101()->isLatest(), "1.0.1 should be the latest version"); - $this->assertFalse(Version::v095()->isLatest(), "0.95 should not be the latest version"); - } - - public function testSupported() { - $result = Version::supported(); - $this->assertNotContains(Version::V095, $result, "0.95 not included"); - } - - public function testLatest() { - $this->assertSame(Version::V101, Version::latest(), "match latest"); - } - - public function testVersionFromString() { - $number = '1.0.1'; - $version = Version::fromString($number); - - $this->assertTrue($version->hasValue($number)); - } - - public function testInvalidArgumentExceptionIsThrown() { - $number = '1.8.01'; - $this->setExpectedException( - 'InvalidArgumentException', - "Invalid version [$number]" - ); - $version = Version::fromString($number); - } -} diff --git a/cmi5PHP/tests/ausTest.php b/cmi5PHP/tests/auTest.php old mode 100644 new mode 100755 similarity index 80% rename from cmi5PHP/tests/ausTest.php rename to cmi5PHP/tests/auTest.php index 3f161f0..ea85501 --- a/cmi5PHP/tests/ausTest.php +++ b/cmi5PHP/tests/auTest.php @@ -2,7 +2,7 @@ namespace cmi5Test; use PHPUnit\Framework\TestCase; -use Au; +use mod_cmi5launch\local\au; /** * Class AuTest. @@ -10,16 +10,18 @@ * @copyright 2023 Megan Bohland * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * - * @covers \Au + * @covers \au */ -class AusTest extends TestCase +class auTest extends TestCase { private $auProperties, $emptyStatement, $mockStatementValues; protected function setUp(): void { + // All the properties in an AU object. $this->auProperties = array( 'id', + 'attempt', 'url', 'type', 'lmsid', @@ -42,14 +44,15 @@ protected function setUp(): void 'noattempt', 'completed', 'passed', - 'inprogress', + 'masteryScore', 'inprogress', 'launchMethod', 'lmsId', 'moveOn', 'auIndex', 'activityType' ); $this->emptyStatement = array(); - //Perhaps a good test would be to test the constructor with a statement that has all the properties set. + // Perhaps a good test would be to test the constructor with a statement that has all the properties set. $this->mockStatementValues = array( 'id' => 'id', + 'attempt' => 'attempt', 'url' => 'url', 'type' => 'type', 'lmsid' => 'lmsid', @@ -73,6 +76,7 @@ protected function setUp(): void 'completed' => 'completed', 'passed' => 'passed', 'inprogress' => 'inprogress', + 'masteryScore' => 'masteryScore', 'launchMethod' => 'launchMethod', 'lmsId' => 'lmsId', 'moveOn' => 'moveOn', 'auIndex' => 'auIndex', 'activityType' => 'activityType' ); } @@ -84,10 +88,11 @@ protected function tearDown(): void public function testInstantiationWithEmpty() { - $obj = new Au($this->emptyStatement); + $obj = new au($this->emptyStatement); + + // Is an AU object? + $this->assertInstanceOf(au::class, $obj); - //Is an AU object - $this->assertInstanceOf('Au', $obj); //It is saying AU is not transversable //Implementing traversable in AU is breaking the code, //Make sure the AU object does not have any 'extra' properties, only the amount passed in @@ -107,10 +112,11 @@ public function testInstantiationWithEmpty() public function testInstantiationWithValues() { - $obj = new Au($this->mockStatementValues); + $obj = new au($this->mockStatementValues); - //Is an AU object - $this->assertInstanceOf('Au', $obj); + // Is an AU object? + $this->assertInstanceOf(au::class, $obj); + //It is saying AU is not transversable //Implementing traversable in AU is breaking the code, //Make sure the AU object does not have any 'extra' properties, only the amount passed in diff --git a/cmi5PHP/tests/ausHelpersTest.php b/cmi5PHP/tests/ausHelpersTest.php old mode 100644 new mode 100755 index eda94ac..7d9ec90 --- a/cmi5PHP/tests/ausHelpersTest.php +++ b/cmi5PHP/tests/ausHelpersTest.php @@ -2,9 +2,8 @@ namespace cmi5Test; use PHPUnit\Framework\TestCase; -use Au; -use Au_Helpers; - +use mod_cmi5launch\local\au; +use mod_cmi5launch\local\au_helpers; /** * Tests for AuHelpers class. * @@ -14,14 +13,18 @@ * @covers \auHelpers * @covers \auHelpers::getAuProperties */ -class AusHelpersTest extends TestCase +class ausHelpersTest extends TestCase { - // private $auProperties, $emptyStatement, $mockStatementValues; + private $auProperties, $emptyStatement, $mockStatementValues, $mockStatement2, $returnedAUids; + + public $auidForTest; protected function setUp(): void { + // All the properties in an AU object. $this->auProperties = array( 'id', + 'attempt', 'url', 'type', 'lmsid', @@ -38,7 +41,6 @@ protected function setUp(): void 'masteryscore', 'satisfied', 'launchurl', - 'sessionid', 'sessions', 'progress', 'noattempt', @@ -49,9 +51,49 @@ protected function setUp(): void $this->emptyStatement = array(); - //Perhaps a good test would be to test the constructor with a statement that has all the properties set. + // Based on created AU in program, but with some values removed. + $this->mockStatement2 = array( + "id" => "https://exampleau", + "attempt" => NULL, + "url" => "example.html?pages=1&complete=launch", + "type" => "au example", + "lmsid" => NULL, + "grade" => NULL, + "scores" => NULL, + "title" => array( 0 => array( + "lang" => "en-US", + "text" => "Example AU") + ), + "moveOn"=> NULL, + "auIndex" => NULL, + "parents" => array(), + "objectives" => NULL, + "description" => array( 0 => array( + "lang" => "en-US", + "text" => "Example AU lesson description") + ), + 'activitytype' => NULL, + 'launchmethod' => NULL, + 'masteryscore' => NULL, + 'satisfied' => NULL, + 'launchurl' => NULL, + 'sessions' => NULL, + 'progress' => NULL, + 'noattempt' => NULL, + 'completed' => NULL, + 'passed' => NULL, + 'inprogress' => NULL, + 'launchMethod' => "AnyWindow", + 'lmsId' => "https://exampleau/ranomnum/au0", + 'moveOn' => "CompletedOrPassed", + 'auIndex' => 0, + 'activityType' => NULL, + 'masteryScore' => NULL + ); + // Perhaps a good test would be to test the constructor with a statement that has all the properties set. $this->mockStatementValues = array( 'id' => 'id', + 'attempt' => 'attempt', 'url' => 'url', 'type' => 'type', 'lmsid' => 'lmsid', @@ -68,7 +110,6 @@ protected function setUp(): void 'masteryscore' => 'masteryscore', 'satisfied' => 'satisfied', 'launchurl' => 'launchurl', - 'sessionid' => 'sessionid', 'sessions' => 'sessions', 'progress' => 'progress', 'noattempt' => 'noattempt', @@ -76,6 +117,7 @@ protected function setUp(): void 'passed' => 'passed', 'inprogress' => 'inprogress', ); + } protected function tearDown(): void @@ -84,10 +126,10 @@ protected function tearDown(): void } - //Retrieve Aus parses and returns AUs from large statements from the CMI5 player - //So to test, maybe make a statement and ensure the test value is returned? - //Arbitrarily pick a word and put in right place? See if it is returned? - public function testRetrieveAus() + // Retrieve Aus parses and returns AUs from large statements from the CMI5 player + // So to test, maybe make a statement and ensure the test value is returned? + // Arbitrarily pick a word and put in right place? See if it is returned? + public function testcmi5launch_retrieve_aus() { //It's not just returning it, it's splitting it into chuncks~! @@ -197,12 +239,12 @@ public function testRetrieveAus() ); - $helper = new Au_Helpers(); + $helper = new au_helpers(); //So now with this fake 'statement', lets ensure it pulls the correct value which is "correct Retrieval" - $retrieved = $helper->retrieveAus($mockStatement); + $retrieved = $helper->cmi5launch_retrieve_aus($mockStatement); - //It should retrieve the mock aus + // It should retrieve the mock aus $this->assertEquals($shouldBeReturned, $retrieved, "Expected retrieved statement to be equal to mock statement"); //This is being flaged as risky? //Is there a different way to test this? @@ -217,10 +259,10 @@ public function testRetrieveAus() //Those seem to pass, so take away line 206? } - //Test function that is fed an array of statments and returns an array of aus - public function testCreateAus() + //Test function that is fed an array of statments and returns an array of aus onjects + public function testcmi5launch_create_aus() { - //Should be enough to pass the mock statement values here, make an array of them first + // Should be enough to pass the mock statement values here, make an array of them first $testStatements = array(); //Lets create 4 aus statement @@ -228,15 +270,15 @@ public function testCreateAus() $testStatements[$i][] = $this->mockStatementValues; } - $helper = new Au_Helpers(); + $helper = new au_helpers(); //So now with this fake 'statement', lets ensure it pulls the correct value which is "correct Retrieval" - $auList = $helper->createAus($testStatements); + $auList = $helper->cmi5launch_create_aus($testStatements); //There should be a total of 4 Aus in this array $this->assertCount(4, $auList, "Expected retrieved statement to have four aus"); //And they should all be au objects foreach ($auList as $au) { - $this->assertInstanceOf(Au::class, $au, "Expected retrieved statement to be an array of aus"); + $this->assertInstanceOf(au::class, $au, "Expected retrieved statement to be an array of aus"); } } @@ -246,9 +288,65 @@ public function testCreateAus() //We just need tothat it saves the correct values and CALLS insert_record //Technically this function returns ids, so we can make a stub which just returns ids //This will test it is called without messing with the DB - public function testSaveAus() + public function testcmi5launch_save_aus() { + // The func should return auids created by the DB when AU's were saved in array format. + $helper = new au_helpers(); + + //Lets create 4 aus statement + for ($i = 0; $i < 3; $i++) { + $testAus[$i][] = $this->mockStatement2; + } + + //So now with this fake 'statement', lets ensure it pulls the correct value which is "correct Retrieval" + $returnedAUids = $helper->cmi5launch_save_aus($helper->cmi5launch_create_aus($testAus)); + + // First make sure array is returned + $this->assertIsArray($returnedAUids, "Expected retrieved statement to be an array"); + + // The array should have the same count of ids as AU's passed in + $this->assertCount(3, $returnedAUids, "Expected retrieved statement to have three aus"); + // Now iterate through the returned array and ensure ids were passed back, numeric ids + foreach ($returnedAUids as $auId) { + + // what is id? + // echo"auId: $auId"; + $this->assertIsNumeric($auId, "Expected array to have numeric values"); + } + global $auidForTest; + //Save to use in next test? + $auidForTest = $returnedAUids; + //Do I need to test fail? + + } + public function testcmi5launch_retrieve_aus_from_db() + { + // Access the global array of ids from above test + global $auidForTest; + + // global $auidForTest; + $helper = new au_helpers(); + + // It takes singular ids, so we will iterate through them + foreach ($auidForTest as $auId) { + + $returnedAu = $helper->cmi5launch_retrieve_aus_from_db($auId); + + // And the return should be an au object + $this->assertInstanceOf(au::class, $returnedAu, "Expected retrieved object to be an au object"); + } + + // And if it fails it should fail gracefully + $badid = 0; + $returnedAu = $helper->cmi5launch_retrieve_aus_from_db($badid); + + // And the return should be a false value + $this->assertNotTrue($returnedAu, "Expected retrieved object to be false"); + //And it should output this error message + $this->expectOutputString("
Error attempting to get AU data from DB. Check AU id. AU id is: " . $badid . "
"); + + } } \ No newline at end of file diff --git a/cmi5PHP/tests/cmi5ConnectorsTest.php b/cmi5PHP/tests/cmi5ConnectorsTest.php new file mode 100755 index 0000000..17bc9ab --- /dev/null +++ b/cmi5PHP/tests/cmi5ConnectorsTest.php @@ -0,0 +1,328 @@ +example = null; + } + + + // Create course sends data to the player. + // Now we dont actually want to talk to player or we will either make a bunch of unreal courses + // unless we then delete it? + // or have to make a fake player??? + // Does php have mocks or stubs? + // They do, and what is actually returned is a massive json encoded string. It is the return + // response when a course is created from player, but all we care about is a string return from stub? + //I understand what I worte here. IS it enouh to just retreive a strin? Or does it need to be in + // same shape it would be from the player? Like do I need to verify it has, these cpmonets? + // or is ti enouh to not.... Is my RPORTAM robusdt enouh to catch that? + // I don't think it i, it just throughs eneric errors, this may + // be a place to improve it. + + //Ok, so be that as it may THIS func gets a response and sends array back. If response is false it faisl + // robust enouh? + // I think since this job is just to take and pass on courseinfo, it doesn't have to validate the properties of returned array, that will fall into the testing of the functhat retrieves them? + + public function testcmi5launch_create_course_pass() + { + + // global $auidForTest; + global $auidForTest; + global $DB, $CFG, $filename; + + $id = 0; + $tenanttoken = "testtoken"; + + //If we make filename an object with it's own get_content method, we can stub it out + + + $filename = new class { + + public function get_content() { + return "testfilecontents"; + } + }; + // Wait this moiht work, we can just make this object a method, and stub it out ourselves, + // or would i be better to try and use their mocks + // Mock a cmi5 connector object but only stub ONE method, as we want to test the other + // Create a mock of the send_request class as we don't actually want + // to create a new course in the player. + $csc = $this->getMockBuilder('mod_cmi5launch\local\cmi5_connectors') + ->onlyMethods(array('cmi5launch_send_request_to_cmi5_player_post')) + ->getMock(); + + + + //$setting = $this->getMockBuilder(\stdclass::class)->addMethods(array('cmi5launch_settings'))->getMock(); + // $setting = $this->getFunctionMod(_mod_cmi5launch\local__, 'cmi5launch_settings'); + // $setting = $this->getMockBuilder(\stdclass::class) + // ->addMethods(array('cmi5launch_settings')) + // ->getMock(); + + // $setting = $this->createMock(cmi5launch__settings::class) + //->addMethods(array('cmi5launch_settings')) + //->getMock(); + +// mod_cmi5launch\local\cmi5launch_settings() +// Can I mock lib? Then stuff like settings could be mocked? + + + // We will have the mock return a basic string, as it's not under test + // the string just needs to be returned as is. We do expect create_course to only call this once. + $csc->expects($this->once()) + ->method('cmi5launch_send_request_to_cmi5_player_post') + // IT will call '/api/v1/course' nd not a whole url because that is accessed through "Settings" not reachable under test conditions, so it + // will only use the second part of concantation + ->with('testfilecontents', '/api/v1/course', 'testtoken') + ->willReturn('Request sent to player'); + + + // $setting->expects($this->any()) + // ->method('cmi5launch_settings') + // ->willReturn(array('cmi5launchplayerurl' => 'http://localhost:8000/launch.php')) + // ->with('Request sent to player') + ; + + // I think I need to say expect to be called with these, + // because for some reason it says paraaam 0 is not matching? + //Call the method under test. + $result =$csc->cmi5launch_create_course($id, $tenanttoken, $filename); + + // And the return should be a string (the original method returns what the player sends back or FALSE) + $this->assertIsString($result); + $this->assertEquals('Request sent to player', $result); + + + } + + public function testcmi5launch_create_course_fail() + { + // global $auidForTest; + global $auidForTest; + global $DB, $CFG; + + $id = 0; + $tenanttoken = "testtoken"; + // $filename = array ("testfilename" => "testfilecontents"); + //If we make filename an object with it's own get_content method, we can stub it out + + + $filename = new class { + + public function get_content() { + return "testfilecontents"; + } +}; + // Mock a cmi5 connector object but only stub ONE method, as we want to test the other + // Create a mock of the send_request class as we don't actually want + // to create a new course in the player. + $csc = $this->getMockBuilder('mod_cmi5launch\local\cmi5_connectors') + ->onlyMethods(array('cmi5launch_send_request_to_cmi5_player_post')) + ->getMock(); + + // We will have the mock return a FALSE value, this should enable us to test the + // method under failing conditions. We do expect create_course to only call this once. + $csc->expects($this->once()) + ->method('cmi5launch_send_request_to_cmi5_player_post') + ->with('testfilecontents', '/api/v1/course', 'testtoken') + ->willReturn(FALSE) + // ->with('Request sent to player') + ; + + //Call the method under test. + $result =$csc->cmi5launch_create_course($id, $tenanttoken, $filename); + + + // Result should be debug echo string and false + $this->assertNotTrue($result, "Expected retrieved object to be false"); + //And it should output this error message + $this->expectOutputString("Error attempting to get registration id querystring parameter.
"; - - die(); - } -} -//To hold launch url or whether launch url is new! -$location = ""; - -//If true, this is a NEW launch -if ($idAndStatus[0] == "true") { - - //Retrieve AUs - $au = $getAUs($id); - - //Retrieve AU index - $auIndex = $au->auindex; - - //Pass in the au index to retrieve a launchurl and session id - $urlDecoded = $retrieveUrl($cmi5launch->id, $auIndex); - - //Retrieve and store session id in the aus table - $sessionID = intval($urlDecoded['id']); - - //Check if there are previous sessions - if(!$au->sessions == NULL){ - //We don't want to overwrite so retrieve the sessions - $sessionList = json_decode($au->sessions); - //now add the new session number - $sessionList[] = $sessionID; - - }else{ - //It is null so just start fresh - $sessionList = array(); - $sessionList[] = $sessionID; - } - - //Save sessions - $au->sessions =json_encode($sessionList ); - - //The record needs to updated in db - $updated = $DB->update_record('cmi5launch_aus', $au, true); - - //Retrieve the launch url - $location = $urlDecoded['url']; - //And launch method - $launchMethod = $urlDecoded['launchMethod']; - - //Create and save session object to session table - $saveSession($sessionID, $location, $launchMethod); - -} else { - //This is a new session, we want to get the launch url from the sessions - $session = $DB->get_record('cmi5launch_sessions', array('sessionid' => $id)); - - //Launch url isss in old session record - $location = $session->launchurl; -} - -header("Location: ". $location); - -exit; \ No newline at end of file +. + +/** + * Launches the experience with the requested registration + * + * @copyright 2023 Megan Bohland + * @copyright Based on work by 2013 Andrew Downes + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_cmi5launch\local\cmi5_connectors; +use mod_cmi5launch\local\au_helpers; +use mod_cmi5launch\local\session_helpers; + +require_once(dirname(dirname(dirname(__FILE__))) . '/config.php'); +require('header.php'); +require_once("$CFG->dirroot/lib/outputcomponents.php"); +require_once(dirname(dirname(dirname(__FILE__))).'/config.php'); +require_once('header.php'); + +require_login($course, false, $cm); + +global $CFG, $cmi5launch, $USER, $DB; + +// MB - currently not utilizing events, but may in future. +/* +// Trigger Activity launched event. +$event = \mod_cmi5launch\event\activity_launched::create(array( + 'objectid' => $cmi5launch->id, + 'context' => $context, +)); +$event->add_record_snapshot('course_modules', $cm); +$event->add_record_snapshot('cmi5launch', $cmi5launch); +$event->trigger(); +*/ + +// External class and funcs to use. +$auhelper = new au_helpers; +$connectors = new cmi5_connectors; +$sessionhelper = new session_helpers; + +$savesession = $sessionhelper->cmi5launch_get_create_session(); +$cmi5launchretrieveurl = $connectors->cmi5launch_get_retrieve_url(); +$retrieveaus = $auhelper->get_cmi5launch_retrieve_aus_from_db(); + +// Retrieve registration id and au index (from AUview.php). +$fromauview = required_param('launchform_registration', PARAM_TEXT); + +// Break it into array (AU or session id is first index). +$idandstatus = explode(",", $fromauview); + +// Retrieve AU OR session id. +$id = array_shift($idandstatus); + +// Reload cmi5 instance. +$record = $DB->get_record('cmi5launch', array('id' => $cmi5launch->id)); + +// Retrieve user's course record. +$userscourse = $DB->get_record('cmi5launch_usercourse', ['courseid' => $record->courseid, 'userid' => $USER->id]); + +// Retrieve registration id. +$registrationid = $userscourse->registrationid; + +if (empty($registrationid)) { + echo "Error attempting to get registration id querystring parameter.
"; + + die(); + } +} + +// To hold launch url. +$location = ""; + +// Retrieve AUs. +$au = $retrieveaus($id); + +// Retrieve the au index. +$auindex = $au->auindex; + +// Pass in the au index to retrieve a launchurl and session id. +$urldecoded = $cmi5launchretrieveurl($cmi5launch->id, $auindex); + +// Retrieve and store session id in the aus table. +$sessionid = intval($urldecoded['id']); + +// Check if there are previous sessions. +if (!$au->sessions == null) { + // We don't want to overwrite so retrieve the sessions before changing them. + $sessionlist = json_decode($au->sessions); + // Now add the new session number. + $sessionlist[] = $sessionid; + +} else { + // If it is null just start fresh. + $sessionlist = array(); + $sessionlist[] = $sessionid; +} + +// Save sessions. +$au->sessions = json_encode($sessionlist); + +// The record needs to updated in DB. +$updated = $DB->update_record('cmi5launch_aus', $au, true); + +// Retrieve the launch url. +$location = $urldecoded['url']; +// And launch method. +$launchmethod = $urldecoded['launchMethod']; + +// Create and save session object to session table. +$savesession($sessionid, $location, $launchmethod); + +// Last thing check for updates. +cmi5launch_update_grades($cmi5launch, $USER->id); + +header("Location: ". $location); + +exit; diff --git a/lib.php b/lib.php index 9242f09..3bd8ad1 100755 --- a/lib.php +++ b/lib.php @@ -1,1161 +1,1114 @@ -. - -/** - * Library of interface functions and constants for module cmi5launch - * - * All the core Moodle functions, neeeded to allow the module to work - * integrated in Moodle should be placed here. - * All the cmi5launch specific functions, needed to implement all the module - * logic, should go to locallib.php. This will help to save some memory when - * Moodle is performing actions across all modules. - * - * @package mod_cmi5launch - * @copyright 2013 Andrew Downes - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -// cmi5PHP - required for interacting with the LRS in cmi5launch_get_statements. -require_once("$CFG->dirroot/mod/cmi5launch/cmi5PHP/autoload.php"); - -// SCORM library from the SCORM module. Required for its xml2Array class by cmi5launch_process_new_package. -require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php"); -//Classes for connecting to CMI5 player -require_once("$CFG->dirroot/mod/cmi5launch/cmi5PHP/src/cmi5Connector.php"); -require_once("$CFG->dirroot/mod/cmi5launch/cmi5PHP/src/ausHelpers.php"); -require_once("$CFG->dirroot/mod/cmi5launch/cmi5PHP/src/aus.php"); -require_once("$CFG->dirroot/mod/cmi5launch/cmi5PHP/src/sessions.php"); - -global $cmi5launchsettings; -$cmi5launchsettings = null; - -// Moodle Core API. - -//TODO- this is where we add functions for grades we want suchs as FEATURE_GRADE_HAS_GRADE -//For now I added those SCORM had, we can whittle this down -/** - * Returns the information on whether the module supports a feature - * - * @see plugin_supports() in lib/moodlelib.php - * @param string $feature FEATURE_xx constant for requested feature - * @return mixed true if the feature is supported, null if unknown - */ -function cmi5launch_supports($feature) { - switch($feature) { - case FEATURE_MOD_INTRO: - return true; - case FEATURE_COMPLETION_TRACKS_VIEWS: - return true; - case FEATURE_COMPLETION_HAS_RULES: - return true; - case FEATURE_BACKUP_MOODLE2: - return true; - ///////////////// - case FEATURE_GROUPS: - return true; - case FEATURE_GROUPINGS: - return true; - case FEATURE_GRADE_HAS_GRADE: - return true; - case FEATURE_GRADE_OUTCOMES: - return true; - case FEATURE_SHOW_DESCRIPTION: - return true; - case FEATURE_MOD_PURPOSE: - return MOD_PURPOSE_CONTENT; - ////////////////////// - default: - return null; - } -} - -/** - * Saves a new instance of the cmi5launch into the database - * - * Given an object containing all the necessary data, - * (defined by the form in mod_form.php) this function - * will create a new instance and return the id number - * of the new instance. - * - * @param object $cmi5launch An object from the form in mod_form.php - * @param mod_cmi5launch_mod_form $mform - * @return int The id of the newly inserted cmi5launch record - */ - -function cmi5launch_add_instance(stdClass $cmi5launch, mod_cmi5launch_mod_form $mform = null) { - global $DB, $CFG; - - $cmi5launch->timecreated = time(); - - // Need the id of the newly created instance to return (and use if override defaults checkbox is checked). - $cmi5launch->id = $DB->insert_record('cmi5launch', $cmi5launch); - - $cmi5launchlrs = cmi5launch_build_lrs_settings($cmi5launch); - - // Determine if override defaults checkbox is checked or we need to save watershed creds. - if ($cmi5launch->overridedefaults == '1' || $cmi5launchlrs->lrsauthentication == '2') { - $cmi5launchlrs->cmi5launchid = $cmi5launch->id; - - // Insert data into cmi5launch_lrs table. - if (!$DB->insert_record('cmi5launch_lrs', $cmi5launchlrs)) { - return false; - } - } - - // Process uploaded file. - if (!empty($cmi5launch->packagefile)) { - cmi5launch_process_new_package($cmi5launch); - } - - return $cmi5launch->id; -} - -/** - * Updates an instance of the cmi5launch in the database - * - * Given an object containing all the necessary data, - * (defined by the form in mod_form.php) this function - * will update an existing instance with new data. - * - * @param object $cmi5launch An object from the form in mod_form.php - * @param mod_cmi5launch_mod_form $mform - * @return boolean Success/Fail - */ -function cmi5launch_update_instance(stdClass $cmi5launch, mod_cmi5launch_mod_form $mform = null) { - global $DB, $CFG; - - $cmi5launch->timemodified = time(); - $cmi5launch->id = $cmi5launch->instance; - - $cmi5launchlrs = cmi5launch_build_lrs_settings($cmi5launch); - - // Determine if override defaults checkbox is checked. - if ($cmi5launch->overridedefaults == '1') { - // Check to see if there is a record of this instance in the table. - $cmi5launchlrsid = $DB->get_field( - 'cmi5launch_lrs', - 'id', - array('cmi5launchid' => $cmi5launch->instance), - IGNORE_MISSING - ); - // If not, will need to insert_record. - if (!$cmi5launchlrsid) { - if (!$DB->insert_record('cmi5launch_lrs', $cmi5launchlrs)) { - return false; - } - } else { // If it does exist, update it. - $cmi5launchlrs->id = $cmi5launchlrsid; - - if (!$DB->update_record('cmi5launch_lrs', $cmi5launchlrs)) { - return false; - } - } - } - - if (!$DB->update_record('cmi5launch', $cmi5launch)) { - return false; - } - - // Process uploaded file. - if (!empty($cmi5launch->packagefile)) { - cmi5launch_process_new_package($cmi5launch); - } - - return true; -} - -function cmi5launch_build_lrs_settings(stdClass $cmi5launch) { - global $DB, $CFG; - - // Data for cmi5launch_lrs table. - $cmi5launchlrs = new stdClass(); - $cmi5launchlrs->lrsendpoint = $cmi5launch->cmi5launchlrsendpoint; - $cmi5launchlrs->lrsauthentication = $cmi5launch->cmi5launchlrsauthentication; - $cmi5launchlrs->customacchp = $cmi5launch->cmi5launchcustomacchp; - $cmi5launchlrs->useactoremail = $cmi5launch->cmi5launchuseactoremail; - $cmi5launchlrs->lrsduration = $cmi5launch->cmi5launchlrsduration; - $cmi5launchlrs->cmi5launchid = $cmi5launch->instance; - $cmi5launchlrs->lrslogin = $cmi5launch->cmi5launchlrslogin; - $cmi5launchlrs->lrspass = $cmi5launch->cmi5launchlrspass; - - return $cmi5launchlrs; -} - -/** - * Removes an instance of the cmi5launch from the database - * - * Given an ID of an instance of this module, - * this function will permanently delete the instance - * and any data that depends on it. - * - * @param int $id Id of the module instance - * @return boolean Success/Failure - */ -function cmi5launch_delete_instance($id) { - global $DB; - - if (! $cmi5launch = $DB->get_record('cmi5launch', array('id' => $id))) { - return false; - } - - // Determine if there is a record of this (ever) in the cmi5launch_lrs table. - $cmi5launchlrsid = $DB->get_field('cmi5launch_lrs', 'id', array('cmi5launchid' => $id), $strictness = IGNORE_MISSING); - if ($cmi5launchlrsid) { - // If there is, delete it. - $DB->delete_records('cmi5launch_lrs', array('id' => $cmi5launchlrsid)); - } - - $DB->delete_records('cmi5launch', array('id' => $cmi5launch->id)); - - return true; -} - -/** - * Returns a small object with summary information about what a - * user has done with a given particular instance of this module - * Used for user activity reports. - * $return->time = the time they did it - * $return->info = a short text description - * - * @return stdClass|null - */ -function cmi5launch_user_outline($course, $user, $mod, $cmi5launch) { - $return = new stdClass(); - $return->time = 0; - $return->info = ''; - return $return; -} - -/** - * Prints a detailed representation of what a user has done with - * a given particular instance of this module, for user activity reports. - * - * @param stdClass $course the current course record - * @param stdClass $user the record of the user we are generating report for - * @param cm_info $mod course module info - * @param stdClass $cmi5launch the module instance record - * @return void, is supposed to echp directly - */ -function cmi5launch_user_complete($course, $user, $mod, $cmi5launch) { -} - -/** - * Given a course and a time, this module should find recent activity - * that has occurred in cmi5launch activities and print it out. - * Return true if there was output, or false is there was none. - * - * @return boolean - */ -function cmi5launch_print_recent_activity($course, $viewfullnames, $timestart) { - return false; // True if anything was printed, otherwise false. -} - -/** - * Prepares the recent activity data - * - * This callback function is supposed to populate the passed array with - * custom activity records. These records are then rendered into HTML via - * {@link cmi5launch_print_recent_mod_activity()}. - * - * @param array $activities sequentially indexed array of objects with the 'cmid' property - * @param int $index the index in the $activities to use for the next record - * @param int $timestart append activity since this time - * @param int $courseid the id of the course we produce the report for - * @param int $cmid course module id - * @param int $userid check for a particular user's activity only, defaults to 0 (all users) - * @param int $groupid check for a particular group's activity only, defaults to 0 (all groups) - * @return void adds items into $activities and increases $index - */ -function cmi5launch_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid = 0, $groupid = 0) { -} - -/** - * Prints single activity item prepared by {@see cmi5launch_get_recent_mod_activity()} - * @return void - */ -function cmi5launch_print_recent_mod_activity($activity, $courseid, $detail, $modnames, $viewfullnames) { -} - -/** - * Function to be run periodically according to the moodle cron - * This function searches for things that need to be done, such - * as sending out mail, toggling flags etc ... - * - * @return boolean - * @todo Finish documenting this function - **/ -function cmi5launch_cron() { - return true; -} - -/** - * Returns all other caps used in the module - * - * @example return array('moodle/site:accessallgroups'); - * @return array - */ -function cmi5launch_get_extra_capabilities() { - return array(); -} - -// File API. - -/** - * Returns the lists of all browsable file areas within the given module context - * - * The file area 'intro' for the activity introduction field is added automatically - * by {@link file_browser::get_file_info_context_module()} - * - * @param stdClass $course - * @param stdClass $cm - * @param stdClass $context - * @return array of [(string)filearea] => (string)description - */ -function cmi5launch_get_file_areas($course, $cm, $context) { - $areas = array(); - $areas['content'] = get_string('areacontent', 'scorm'); - $areas['package'] = get_string('areapackage', 'scorm'); - return $areas; -} - -/** - * File browsing support for cmi5launch file areas - * - * @package mod_cmi5launch - * @category files - * - * @param file_browser $browser - * @param array $areas - * @param stdClass $course - * @param stdClass $cm - * @param stdClass $context - * @param string $filearea - * @param int $itemid - * @param string $filepath - * @param string $filename - * @return file_info instance or null if not found - */ -function cmi5launch_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { - global $CFG; - - if (!has_capability('moodle/course:managefiles', $context)) { - return null; - } - - $fs = get_file_storage(); - - if ($filearea === 'package') { - $filepath = is_null($filepath) ? '/' : $filepath; - $filename = is_null($filename) ? '.' : $filename; - - $urlbase = $CFG->wwwroot.'/pluginfile.php'; - if (!$storedfile = $fs->get_file($context->id, 'mod_cmi5launch', 'package', 0, $filepath, $filename)) { - if ($filepath === '/' and $filename === '.') { - $storedfile = new virtual_root_file($context->id, 'mod_cmi5launch', 'package', 0); - } else { - // Not found. - return null; - } - } - return new file_info_stored($browser, $context, $storedfile, $urlbase, $areas[$filearea], false, true, false, false); - } - - return false; -} - -/** - * Serves cmi5 content, introduction images and packages. Implements needed access control ;-) - * - * @package mod_cmi5launch - * @category files - * @param stdClass $course course object - * @param stdClass $cm course module object - * @param stdClass $context context object - * @param string $filearea file area - * @param array $args extra arguments - * @param bool $forcedownload whether or not force download - * @param array $options additional options affecting the file serving - * @return bool false if file not found, does not return if found - just send the file - */ -function cmi5launch_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { - global $CFG, $DB; - - if ($context->contextlevel != CONTEXT_MODULE) { - return false; - } - - require_login($course, true, $cm); - $canmanageactivity = has_capability('moodle/course:manageactivities', $context); - - $filename = array_pop($args); - $filepath = implode('/', $args); - if ($filearea === 'content') { - $lifetime = null; - } else if ($filearea === 'package') { - $lifetime = 0; // No caching here. - } else { - return false; - } - - $fs = get_file_storage(); - - if ( - !$file = $fs->get_file($context->id, 'mod_cmi5launch', $filearea, 0, '/'.$filepath.'/', $filename) - or $file->is_directory() - ) { - if ($filearea === 'content') { // Return file not found straight away to improve performance. - send_header_404(); - die; - } - return false; - } - - // Finally send the file. - send_stored_file($file, $lifetime, 0, false, $options); -} - -/** - * Export file resource contents for web service access. - * - * @param cm_info $cm Course module object. - * @param string $baseurl Base URL for Moodle. - * @return array array of file content - */ -function cmi5launch_export_contents($cm, $baseurl) { - global $CFG; - $contents = array(); - $context = context_module::instance($cm->id); - - $fs = get_file_storage(); - $files = $fs->get_area_files($context->id, 'mod_cmi5launch', 'package', 0, 'sortorder DESC, id ASC', false); - - foreach ($files as $fileinfo) { - $file = array(); - $file['type'] = 'file'; - $file['filename'] = $fileinfo->get_filename(); - $file['filepath'] = $fileinfo->get_filepath(); - $file['filesize'] = $fileinfo->get_filesize(); - $file['fileurl'] = file_encode_url("$CFG->wwwroot/" . $baseurl, '/'.$context->id.'/mod_cmi5launch/package'. - $fileinfo->get_filepath().$fileinfo->get_filename(), true); - $file['timecreated'] = $fileinfo->get_timecreated(); - $file['timemodified'] = $fileinfo->get_timemodified(); - $file['sortorder'] = $fileinfo->get_sortorder(); - $file['userid'] = $fileinfo->get_userid(); - $file['author'] = $fileinfo->get_author(); - $file['license'] = $fileinfo->get_license(); - $contents[] = $file; - } - - return $contents; -} - -// Navigation API. - -/** - * Extends the global navigation tree by adding cmi5launch nodes if there is a relevant content - * - * This can be called by an AJAX request so do not rely on $PAGE as it might not be set up properly. - * - * @param navigation_node $navref An object representing the navigation tree node of the cmi5launch module instance - * @param stdClass $course - * @param stdClass $module - * @param cm_info $cm - */ -function cmi5launch_extend_navigation(navigation_node $navref, stdclass $course, stdclass $module, cm_info $cm) { -} - -/** - * Extends the settings navigation with the cmi5launch settings - * - * This function is called when the context for the page is a cmi5launch module. This is not called by AJAX - * so it is safe to rely on the $PAGE. - * - * @param settings_navigation $settingsnav {@link settings_navigation} - * @param navigation_node $cmi5launchnode {@link navigation_node} - */ -function cmi5launch_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $cmi5launchnode = null) { -} - -// Called by Moodle core. -function cmi5launch_get_completion_state($course, $cm, $userid, $type) { - global $CFG, $DB; - $result = $type; // Default return value. - - // Get cmi5launch. - if (!$cmi5launch = $DB->get_record('cmi5launch', array('id' => $cm->instance))) { - throw new Exception("Can't find activity {$cm->instance}"); // TODO: localise this. - } - - $cmi5launchsettings = cmi5launch_settings($cm->instance); - - $expirydate = null; - $expirydays = $cmi5launch->cmi5expiry; - if ($expirydays > 0) { - $expirydatetime = new DateTime(); - $expirydatetime->sub(new DateInterval('P'.$expirydays.'D')); - $expirydate = $expirydatetime->format('c'); - } - - if (!empty($cmi5launch->cmi5verbid)) { - // Try to get a statement matching actor, verb and object specified in module settings. - $statementquery = cmi5launch_get_statements( - $cmi5launchsettings['cmi5launchlrsendpoint'], - $cmi5launchsettings['cmi5launchlrslogin'], - $cmi5launchsettings['cmi5launchlrspass'], - $cmi5launchsettings['cmi5launchlrsversion'], - $cmi5launch->cmi5activityid, - cmi5launch_getactor($cm->instance), - $cmi5launch->cmi5verbid, - $expirydate - ); - - // If the statement exists, return true else return false. - if (!empty($statementquery->content) && $statementquery->success) { - $result = true; - } else { - $result = false; - } - } - - return $result; -} - -// cmi5Launch specific functions. - -/* -The functions below should really be in locallib, however they are required for one -or more of the functions above so need to be here. -It looks like the standard Quiz module does that same thing, so I don't feel so bad. -*/ - -/** - * Handles uploaded zip packages when a module is added or updated. Unpacks the zip contents - * and extracts the launch url and activity id from the cmi5.xml file. - * Note: This takes the *first* activity from the cmi5.xml file to be the activity intended - * to be launched. It will not go hunting for launch URLs any activities listed below. - * Based closely on code from the SCORM and (to a lesser extent) Resource modules. - * @package mod_cmi5launch - * @category cmi5 - * @param object $cmi5launch An object from the form in mod_form.php - * @return array empty if no issue is found. Array of error message otherwise - */ - -function cmi5launch_process_new_package($cmi5launch) { - global $DB, $CFG; - $cmid = $cmi5launch->coursemodule; - $context = context_module::instance($cmid); - - //bring in functions from classes cmi5Connector/ - $connectors = new cmi5Connectors; - $aus_helpers = new Au_Helpers; - - //bring in functions from class cmi5_table_connectors and AU helpers - $createCourse = $connectors->getCreateCourse(); - $retrieveAus = $aus_helpers-> getRetrieveAus(); - $saveAUs = $aus_helpers->getSaveAUs(); - $createAUs = $aus_helpers->getCreateAUs(); - - // Reload cmi5 instance. - $record = $DB->get_record('cmi5launch', array('id' => $cmi5launch->id)); - - $fs = get_file_storage(); - - $fs->delete_area_files($context->id, 'mod_cmi5launch', 'package'); - file_save_draft_area_files( - $cmi5launch->packagefile, - $context->id, - 'mod_cmi5launch', - 'package', - 0, - array('subdirs' => 0, 'maxfiles' => 1) - ); - - // Get filename of zip that was uploaded. - $files = $fs->get_area_files($context->id, 'mod_cmi5launch', 'package', 0, '', false); - if (count($files) < 1) { - return false; - } - - $zipfile = reset($files); - $zipfilename = $zipfile->get_filename(); - - $packagefile = false; - - $packagefile = $fs->get_file($context->id, 'mod_cmi5launch', 'package', 0, '/', $zipfilename); - - //Retrieve user settings to apply to newly created record - $settings = cmi5launch_settings($cmi5launch->id); - $token = $settings['cmi5launchtenanttoken']; - //Create the course and retrieve info for saving to DB - - //Lets wrap this in a try catch statement - $courseResults = $createCourse($context->id, $token, $packagefile ); - - //Take the results of created course and save new course id to table - $record->courseinfo = $courseResults; - - $returnedInfo = json_decode($courseResults, true); - - //Retrieve the lmsId of course - $lmsId = $returnedInfo["lmsId"]; - $record->cmi5activityid = $lmsId; - - $record->courseid = $returnedInfo["id"]; - - //MB - //Here, does the course need registration at all? - //retrieve reg - //$registration = $getRegistration($returnedInfo["id"], $cmi5launch->id); - //$record->registrationid = $registration; - - - //create url for sending to when requesting launch url for course - $playerUrl = $settings['cmi5launchplayerurl']; - - //TODO - another ref to tenant name! Maybe here is a good place to change user? - //MB - no, this needs to be tenant name cause this is course creation - $tenantname = $settings['cmi5launchtenantname']; - - //Build and save launchurl - $url = $playerUrl . "/api/v1/". $record->courseid. "/launch-url/"; - $record->launchurl = $url; - - //Retrieve the courses AUs and save to record - //Here it is saving to MASTER record, we need each student to have their own - - - //Maybe in view.php it does this same thing, saving to the student record instead. - //so these stay as a 'master' record and the students tweak their own - $aus = ($retrieveAus($returnedInfo)); - - //Maybe better to save AUs here and feed it the array returned by retreieveAUS - //$auIDs = $saveAUs($createAUs($aus)); - $record->aus = (json_encode($aus)); - - - //Populate player table with new course info - $DB->update_record('cmi5launch', $record, true); - - $fs->delete_area_files($context->id, 'mod_cmi5launch', 'content'); - - $packer = get_file_packer('application/zip'); - $packagefile->extract_to_storage($packer, $context->id, 'mod_cmi5launch', 'content', 0, '/'); - - // If the cmi5.xml file isn't there, don't do try to use it. - // This is unlikely as it should have been checked when the file was validated. - if ($manifestfile = $fs->get_file($context->id, 'mod_cmi5launch', 'content', 0, '/', 'cmi5.xml')) { - $xmltext = $manifestfile->get_content(); - - $defaultorgid = 0; - $firstinorg = 0; - - $pattern = '/&(?!\w{2,6};)/'; - $replacement = '&'; - $xmltext = preg_replace($pattern, $replacement, $xmltext); - - $objxml = new xml2Array(); - $manifest = $objxml->parse($xmltext); - - // Update activity id from the first activity in cmi5.xml, if it is found. - // Skip without error if not. (The Moodle admin will need to enter the id manually). - if (isset($manifest[0]["children"][0]["children"][0]["attrs"]["ID"])) { - $record->cmi5activityid = $manifest[0]["children"][0]["children"][0]["attrs"]["ID"]; - } - - // Update launch from the first activity in cmi5.xml, if it is found. - // Skip if not. (The Moodle admin will need to enter the url manually). - foreach ($manifest[0]["children"][0]["children"][0]["children"] as $property) { - if ($property["name"] === "LAUNCH") { - $record->cmi5launchurl = $CFG->wwwroot."/pluginfile.php/".$context->id."/mod_cmi5launch/" - .$manifestfile->get_filearea()."/".$property["tagData"]; - } - } - } - // Save reference. - ///turn off to trigger echo - return $DB->update_record('cmi5launch', $record); -} - -/** - * Check that a Zip file contains a cmi5.xml file in the right place. Used in mod_form.php. - * Heavily based on scorm_validate_package in /mod/scorm/lib.php - * @package mod_cmi5launch - * @category cmi5 - * @param stored_file $file a Zip file. - * @return array empty if no issue is found. Array of error message otherwise - */ -function cmi5launch_validate_package($file) { - $packer = get_file_packer('application/zip'); - $errors = array(); - $filelist = $file->list_files($packer); - if (!is_array($filelist)) { - $errors['packagefile'] = get_string('badarchive', 'cmi5launch'); - } else { - $badmanifestpresent = false; - foreach ($filelist as $info) { - if ($info->pathname == 'cmi5.xml') { - return array(); - } else if (strpos($info->pathname, 'cmi5.xml') !== false) { - // This package has cmi5 xml file inside a folder of the package. - $badmanifestpresent = true; - } - if (preg_match('/\.cst$/', $info->pathname)) { - return array(); - } - } - if ($badmanifestpresent) { - $errors['packagefile'] = get_string('badimsmanifestlocation', 'cmi5launch'); - } else { - $errors['packagefile'] = get_string('nomanifest', 'cmi5launch'); - } - } - return $errors; -} - -/** - * Fetches Statements from the LRS. This is used for completion tracking - - * we check for a statement matching certain criteria for each learner. - * - * @package mod_cmi5launch - * @category cmi5 - * @param string $url LRS endpoint URL - * @param string $basiclogin login/key for the LRS - * @param string $basicpass pass/secret for the LRS - * @param string $version version of xAPI to use - * @param string $activityid Activity Id to filter by - * @param cmi5 Agent $agent Agent to filter by - * @param string $verb Verb Id to filter by - * @param string $since Since date to filter by - * @return cmi5 LRS Response - */ -function cmi5launch_get_statements($url, $basiclogin, $basicpass, $version, $activityid, $agent, $verb, $since = null) { - - $lrs = new \cmi5\RemoteLRS($url, $version, $basiclogin, $basicpass); - - $statementsquery = array( - "agent" => $agent, - "verb" => new \cmi5\Verb(array("id" => trim($verb))), - "activity" => new \cmi5\Activity(array("id" => trim($activityid))), - "related_activities" => "false", - "format" => "ids" - ); - - if (!is_null($since)) { - $statementsquery["since"] = $since; - } - - // Get all the statements from the LRS. - $statementsresponse = $lrs->queryStatements($statementsquery); - - if ($statementsresponse->success == false) { - return $statementsresponse; - } - - $allthestatements = $statementsresponse->content->getStatements(); - $morestatementsurl = $statementsresponse->content->getMore(); - while (!empty($morestatementsurl)) { - $morestmtsresponse = $lrs->moreStatements($morestatementsurl); - if ($morestmtsresponse->success == false) { - return $morestmtsresponse; - } - $morestatements = $morestmtsresponse->content->getStatements(); - $morestatementsurl = $morestmtsresponse->content->getMore(); - // Note: due to the structure of the arrays, array_merge does not work as expected. - foreach ($morestatements as $morestatement) { - array_push($allthestatements, $morestatement); - } - } - - return new \cmi5\LRSResponse( - $statementsresponse->success, - $allthestatements, - $statementsresponse->httpResponse - ); -} - -/** - * Build a cmi5 Agent based on the current user - * - * @package mod_cmi5launch - * @category cmi5 - * @return cmi5 Agent $agent Agent - */ - -function cmi5launch_getactor($instance) { - global $USER, $CFG; - - $settings = cmi5launch_settings($instance); - - if ($USER->idnumber && $settings['cmi5launchcustomacchp']) { - $agent = array( - "name" => fullname($USER), - "account" => array( - "homePage" => $settings['cmi5launchcustomacchp'], - "name" => $USER->idnumber - ), - "objectType" => "Agent" - ); - } else if ($USER->email && $settings['cmi5launchuseactoremail']) { - $agent = array( - "name" => fullname($USER), - "mbox" => "mailto:".$USER->email, - "objectType" => "Agent" - ); - } else { - $agent = array( - "name" => fullname($USER), - "account" => array( - "homePage" => $CFG->wwwroot, - "name" => $USER->username - ), - "objectType" => "Agent" - ); - } - - return new \cmi5\Agent($agent); -} - - - -/** - * Returns the LRS settings relating to a cmi5 Launch module instance - * - * @package mod_cmi5launch - * @category cmi5 - * @param string $instance The Moodle id for the cmi5 module instance. - * @return array LRS settings to use - */ - - function cmi5launch_settings($instance) { - global $DB, $CFG, $cmi5launchsettings; - - if (!is_null($cmi5launchsettings)) { - return $cmi5launchsettings; - } - - $expresult = array(); - $activitysettings = $DB->get_record( - 'cmi5launch_lrs', - array('cmi5launchid' => $instance), - $fields = '*', - $strictness = IGNORE_MISSING - ); - - - // If global settings are not used, retrieve activity settings. - if (!use_global_cmi5_lrs_settings($instance)) { - $expresult['cmi5launchlrsendpoint'] = $activitysettings->lrsendpoint; - $expresult['cmi5launchlrsauthentication'] = $activitysettings->lrsauthentication; - $expresult['cmi5launchlrslogin'] = $activitysettings->lrslogin; - $expresult['cmi5launchlrspass'] = $activitysettings->lrspass; - $expresult['cmi5launchcustomacchp'] = $activitysettings->customacchp; - $expresult['cmi5launchuseactoremail'] = $activitysettings->useactoremail; - $expresult['cmi5launchlrsduration'] = $activitysettings->lrsduration; - } else { // Use global lrs settings. - $result = $DB->get_records('config_plugins', array('plugin' => 'cmi5launch')); - foreach ($result as $value) { - $expresult[$value->name] = $value->value; - } - } - - $expresult['cmi5launchlrsversion'] = '1.0.0'; - - $cmi5launchsettings = $expresult; - - return $expresult; -} - - -/** - * Should the global LRS settings be used instead of the instance specific ones? - * - * @package mod_cmi5launch - * @category cmi5 - * @param string $instance The Moodle id for the cmi5 module instance. - * @return bool - */ - -function use_global_cmi5_lrs_settings($instance) { - global $DB; - // Determine if there is a row in cmi5launch_lrs matching the current activity id. - $activitysettings = $DB->get_record('cmi5launch', array('id' => $instance)); - if ($activitysettings->overridedefaults == 1) { - return false; - } - return true; -} - -/* - -//Grade functions -//TODO - this is a copy of how scorm handles grades. I got these from SCORM's lib.php -//We will need to tweak -/** - * Return grade for given user or all users. - * - * @global stdClass - * @global object - * @param int $cmi5id id of scorm - * @param int $userid optional user id, 0 means all users - * @return array array of grades, false if none - */ - -function cmi5_get_user_grades($cmi5launch, $userid=0) { - global $CFG, $DB; - //What aer they pulling rom locallib? - //Do we need ot pul it as well? - // require_once($CFG->dirroot.'/mod/scorm/locallib.php'); - - $grades = array(); - - //They are pulling mutliple beacue this may be ALL grades! Like x student had this and y student had that - //Because if the userid is empty they want all as opposed to a particulr student - if (empty($userid)) { - - //We are going to need to make CMI5_tables - //They retrieve the list of users - $au_users = $DB->get_records_select('scorm_scoes_track', "cmi5id=? GROUP BY userid", - array($cmi5launch->id), "", "userid,null"); - - //if there is a list of users then they iterate through it and make user objects with the individual users - if ($au_users) { - foreach ($au_users as $au_user) { - $grades[$au_user->userid] = new stdClass(); - $grades[$au_user->userid]->id = $au_user->userid; - $grades[$au_user->userid]->userid = $au_user->userid; - $grades[$au_user->userid]->rawgrade = scorm_grade_user($cmi5launch, $au_user->userid); - } - } else { - return false; - } - - } else { - $preattempt = $DB->get_records_select('scorm_scoes_track', "cmi5id=? AND userid=? GROUP BY userid", - array($cmi5launch->id, $userid), "", "userid,null"); - if (!$preattempt) { - return false; // No attempt yet. - } - $grades[$userid] = new stdClass(); - $grades[$userid]->id = $userid; - $grades[$userid]->userid = $userid; - $grades[$userid]->rawgrade = scorm_grade_user($cmi5launch, $userid); - } - - return $grades; -} - - -/** - * For example, this callback for the assignment module is assignment_update_grades(). -*This callback should update the grade(s) for the supplied user. -* This may be as simple as retrieving the grades for the user from the activity module's own tables -* then calling {$modname}_grade_item_update(). - -*Its parameters are: - -*stdClass $modinstance the activity module settings. -*int $userid A user ID or 0 for all users. -*bool $nullifnone If a single user is specified, $nullifnone is true and the user has no grade then a grade item with a null rawgrade should be inserted - * Update grades in central gradebook - * - * @category grade - * @param object $cmi5launch - * @param int $userid specific user only, 0 mean all - * @param bool $nullifnone - */ -//This can call grade_item_update which helps interact with Moodle gradebook -//it can pull from DB and then call grade_item_update -//We could also later use this to change how the array is massaged -//in case they want highest or lowest -//TODO MB -function cmi5_update_grades($cmi5launch, $userid=0, $nullifnone=true) { - global $CFG, $DB; - //can our mod see these? - //I can make my own gradelib! to figuree stuff and things - - require_once($CFG->libdir.'/gradelib.php'); - require_once($CFG->libdir.'/completionlib.php'); - - // Reload cmi5 instance (to retrieve the aus). - $record = $DB->get_record('cmi5launch', array('id' => $cmi5launch->id)); - - //Make array to hold aus grades - $auGrades = array(); - $courseGrade = 0;//to hold overall grade - - //Lets now retrieve our list of AUs from cmi5launch - $auIDs = json_decode($record->aus); - //Cycle through and get each au ID - foreach ($auIDs as $key => $auID) { - - //get the au - $au = $DB->get_record('cmi5launch_aus', array('id' => $auID)); - // Retrieve the aus scores - - $auGrades[] = $au->grade; - } - //Lets prevent his - if (!$auGrades == null) { - $grade = array_sum($auGrades) / count($auGrades); - //Ok what is grade here? - } - $record->grade = $grade; - - //Call update grade?? - cmi5_grade_item_update($record); - - //This is to get multiple users? - /* - if ($grades = cmi5_get_user_grades($cmi5launch, $userid)) { - cmi5_grade_item_update($scorm, $grades); - // Set complete. - scorm_set_completion($scorm, $userid, COMPLETION_COMPLETE, $grades); - } else if ($userid and $nullifnone) { - $grade = new stdClass(); - $grade->userid = $userid; - $grade->rawgrade = null; - cmi5_grade_item_update($scorm, $grade); - // Set incomplete. - scorm_set_completion($scorm, $userid, COMPLETION_INCOMPLETE); - } else { - cmi5_grade_item_update($scorm); - }*/ -} - - -/** - * Update/create grade item for given cmi5 - * calls grade_update from moodle gradelib.php - * @category grade - * @uses GRADE_TYPE_VALUE - * @uses GRADE_TYPE_NONE - * @param object $cmi5launch object with extra cmidnumber //now cmi5launch - * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook - * @return object grade_item - */ -//TODO MB -//Return to this for grades -/* -//Whereever 'scorm' is replace with 'cmi5launch' -function cmi5_grade_item_update($record, $grades=null) { - global $CFG, $DB, $cmi5launch; - //Note the SCORM version called it's locallib, we may need to get funcs from there as well and place into - //OUR locallib - //May be where those constants were kept - require_once($CFG->dirroot.'/mod/cmi5launch/locallib.php'); - - //gradelib?? We need to see wha this is - //I beleive they are referencing moodle/lib/gradelib - //In case they need their base grade_update? - if (!function_exists('grade_update')) { // Workaround for buggy PHP versions. - require_once($CFG->libdir.'/gradelib.php'); - } - - //so this is getting the scorm id right? Well cmi5 id should work to? - //But a cmi5 id isntusually gotten with cmidnumber I don't - //yeah the cmi5 launch table has a 'courseid' and just a plain 'id' - //I think in this context its 'id' they want? - //If not it has to be 'courseid' - $params = array('itemname' => $cmi5launch->name); - if (isset($cmi5launch->id)) { - $params['idnumber'] = $cmi5launch->id; - } - - //HEre scorm is getting the grading method. - //Scorm originally pulled from scorm_scoes - SCO being a Sharable Object Model - - //Here the grading method is checking if it is a SCO, and then pulls from table - - //BUT it also pulls from PARAM, where are these values set in param? - //From searching 'gradetype' it seems these may be admin settings (admin>settings>grades.php). Where can we set theeesE? - - //Ok, these are probably the plugin seettings we added. and looks like we need to add arow - //to cmi5launch main table to HOLD the gradetype? - //Which is better than constants right? - //Looks loike grade_tyope and others were from scoes tables, which means they should be in our au table?? - - //Ok this is passing as arrays the grades to be updated and the params to do so - //GRADE_TYPE_VALUE appears to be a moodle constant, so should be found when proggram, runs lovally -/* if ($cmi5launch->grademethod == GRADE_AUS_CMI5) { - $maxgrade = $DB->count_records_select('scorm_scoes', 'scorm = ? AND '. - $DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id)); - if ($maxgrade) { - $params['gradetype'] = GRADE_TYPE_VALUE; - $params['grademax'] = $maxgrade; - $params['grademin'] = 0; - } else { - $params['gradetype'] = GRADE_TYPE_NONE; - } - } else { - $params['gradetype'] = GRADE_TYPE_VALUE; - $params['grademax'] = $cmi5launch->maxgrade; - $params['grademin'] = 0; - } - - if ($grades === 'reset') { - $params['reset'] = true; - $grades = null; - } - - - $grades = $record->grade; - //This is calling grade_update from lib>gradelib.php - /* - /** - * Submit new or update grade; update/create grade_item definition. Grade must have userid specified, - * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded'. - * Missing property or key means does not change the existing value. - * - * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax', - * 'gra/demin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones. - * - * Manual, course or category items can not be updated by this function. - * - * @category grade - * @param string $source Source of the grade such as 'mod/assignment' - * @param int $courseid ID of course - * @param string $itemtype Type of grade item. For example, mod or block - * @param string $itemmodule More specific then $itemtype. For example, assignment or forum. May be NULL for some item types - * @param int $iteminstance Instance ID of graded item - * @param int $itemnumber Most probably 0. Modules can use other numbers when having more than one grade for each user - * @param mixed $grades Grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only - * @param mixed $itemdetails Object or array describing the grading item, NULL if no change - * @param bool $isbulkupdate If bulk grade update is happening. - * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED - - return grade_update('mod/cmi5launch', $cmi5launch->courseid, 'mod', 'cmi5', $cmi5launch->id, 0, $grades, $params); -} -*/ - -/** - * Delete grade item for given scorm - * - * @category grade - * @param object $scorm object - * @return object grade_item - */ -/* - function scorm_grade_item_delete($scorm) { - global $CFG; - require_once($CFG->libdir.'/gradelib.php'); - - return grade_update('mod/scorm', $scorm->course, 'mod', 'scorm', $scorm->id, 0, null, array('deleted' => 1)); -} -*/ \ No newline at end of file +. + +/** + * Library of interface functions and constants for module cmi5launch + * + * All the core Moodle functions, neeeded to allow the module to work + * integrated in Moodle should be placed here. + * All the cmi5launch specific functions, needed to implement all the module + * logic, should go to locallib.php. This will help to save some memory when + * Moodle is performing actions across all modules. + * + * @package mod_cmi5launch + * @copyright 2013 Andrew Downes + * @copyright 2024 Megan Bohland - added functions for cmi5launch + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// Cmi5PHP - required for interacting with the LRS in cmi5launch_get_statements. +require_once("$CFG->dirroot/mod/cmi5launch/cmi5PHP/autoload.php"); + +// SCORM library from the SCORM module. Required for its xml2Array class by cmi5launch_process_new_package. +require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php"); + +use mod_cmi5launch\local\cmi5_connectors; +use mod_cmi5launch\local\au_helpers; +use mod_cmi5launch\local\grade_helpers; + +global $cmi5launchsettings; +$cmi5launchsettings = null; + +// Moodle Core API. + +/** + * Returns the information on whether the module supports a feature + * + * @see plugin_supports() in lib/moodlelib.php + * @param string $feature FEATURE_xx constant for requested feature + * @return mixed true if the feature is supported, null if unknown + */ +function cmi5launch_supports($feature) { + switch($feature) { + // True if module supports intro editor. + case FEATURE_MOD_INTRO: + return true; + // MB - we do not currently support the next two, but leaving in, in case we start to. + // True if module has code to track whether somebody viewed it + // case FEATURE_COMPLETION_TRACKS_VIEWS: + // return true; + // True if module has custom completion rules + // case FEATURE_COMPLETION_HAS_RULES: + // return true; + // True if module supports backup/restore of moodle2 format. + case FEATURE_BACKUP_MOODLE2: + return true; + // True if module supports groups. + case FEATURE_GROUPS: + return true; + // True if module supports groupings. + case FEATURE_GROUPINGS: + return true; + // True if module can provide a grade. + case FEATURE_GRADE_HAS_GRADE: + return true; + // True if module supports outcomes. + case FEATURE_GRADE_OUTCOMES: + return true; + // True if module can show description on course main page. + case FEATURE_SHOW_DESCRIPTION: + return true; + // Type of module. + case FEATURE_MOD_PURPOSE: + return MOD_PURPOSE_CONTENT; + default: + return null; + } +} + +/** + * Saves a new instance of the cmi5launch into the database + * + * Given an object containing all the necessary data, + * (defined by the form in mod_form.php) this function + * will create a new instance and return the id number + * of the new instance. + * + * @param object $cmi5launch An object from the form in mod_form.php + * @param mod_cmi5launch_mod_form $mform + * @return int The id of the newly inserted cmi5launch record + */ +function cmi5launch_add_instance(stdClass $cmi5launch, mod_cmi5launch_mod_form $mform = null) { + + global $DB, $CFG; + + $cmi5launch->timecreated = time(); + + // Need the id of the newly created instance to return (and use if override defaults checkbox is checked). + $cmi5launch->id = $DB->insert_record('cmi5launch', $cmi5launch); + + // Process uploaded file. + if (!empty($cmi5launch->packagefile)) { + + cmi5launch_process_new_package($cmi5launch); + } + + return $cmi5launch->id; +} + +/** + * Updates an instance of the cmi5launch in the database + * + * Given an object containing all the necessary data, + * (defined by the form in mod_form.php) this function + * will update an existing instance with new data. + * + * @param object $cmi5launch An object from the form in mod_form.php + * @param mod_cmi5launch_mod_form $mform + * @return boolean Success/Fail + */ +function cmi5launch_update_instance(stdClass $cmi5launch, mod_cmi5launch_mod_form $mform = null) { + global $DB, $CFG; + + $cmi5launch->timemodified = time(); + $cmi5launch->id = $cmi5launch->instance; + + // We removed this part of lrs box -MB + // $cmi5launchlrs = cmi5launch_build_lrs_settings($cmi5launch); + /* + // Determine if override defaults checkbox is checked. + if ($cmi5launch->overridedefaults == '1') { + // Check to see if there is a record of this instance in the table. + $cmi5launchlrsid = $DB->get_field( + 'cmi5launch_lrs', + 'id', + array('cmi5launchid' => $cmi5launch->instance), + IGNORE_MISSING + ); + // If not, will need to insert_record. + if (!$cmi5launchlrsid) { + if (!$DB->insert_record('cmi5launch_lrs', $cmi5launchlrs)) { + return false; + } + } else { // If it does exist, update it. + $cmi5launchlrs->id = $cmi5launchlrsid; + + if (!$DB->update_record('cmi5launch_lrs', $cmi5launchlrs)) { + return false; + } + } + } + */ + + if (!$DB->update_record('cmi5launch', $cmi5launch)) { + return false; + } + + // Process uploaded file. + if (!empty($cmi5launch->packagefile)) { + cmi5launch_process_new_package($cmi5launch); + } + + return true; +} + +function cmi5launch_build_lrs_settings(stdClass $cmi5launch) { + global $DB, $CFG; + + // Data for cmi5launch_lrs table. + $cmi5launchlrs = new stdClass(); + $cmi5launchlrs->customacchp = $cmi5launch->cmi5launchcustomacchp; + $cmi5launchlrs->useactoremail = $cmi5launch->cmi5launchuseactoremail; + $cmi5launchlrs->cmi5launchid = $cmi5launch->instance; + + return $cmi5launchlrs; +} + +/** + * Removes an instance of the cmi5launch from the database + * + * Given an ID of an instance of this module, + * this function will permanently delete the instance + * and any data that depends on it. + * + * @param int $id Id of the module instance + * @return boolean Success/Failure + */ +function cmi5launch_delete_instance($id) { + global $DB; + + if (! $cmi5launch = $DB->get_record('cmi5launch', array('id' => $id))) { + return false; + } + + // Determine if there is a record of this (ever) in the cmi5launch_lrs table. + $cmi5launchlrsid = $DB->get_field('cmi5launch_lrs', 'id', array('cmi5launchid' => $id), $strictness = IGNORE_MISSING); + if ($cmi5launchlrsid) { + // If there is, delete it. + $DB->delete_records('cmi5launch_lrs', array('id' => $cmi5launchlrsid)); + } + + $DB->delete_records('cmi5launch', array('id' => $cmi5launch->id)); + + return true; +} + +/** + * Returns a small object with summary information about what a + * user has done with a given particular instance of this module + * Used for user activity reports. + * $return->time = the time they did it + * $return->info = a short text description + * + * @return stdClass|null + */ +function cmi5launch_user_outline($course, $user, $mod, $cmi5launch) { + $return = new stdClass(); + $return->time = 0; + $return->info = ''; + return $return; +} + +/** + * Prints a detailed representation of what a user has done with + * a given particular instance of this module, for user activity reports. + * + * @param stdClass $course the current course record + * @param stdClass $user the record of the user we are generating report for + * @param cm_info $mod course module info + * @param stdClass $cmi5launch the module instance record + * @return void, is supposed to echp directly + */ +function cmi5launch_user_complete($course, $user, $mod, $cmi5launch) { +} + +/** + * Given a course and a time, this module should find recent activity + * that has occurred in cmi5launch activities and print it out. + * Return true if there was output, or false is there was none. + * + * @return boolean + */ +function cmi5launch_print_recent_activity($course, $viewfullnames, $timestart) { + return false; // True if anything was printed, otherwise false. +} + +/** + * Prepares the recent activity data + * + * This callback function is supposed to populate the passed array with + * custom activity records. These records are then rendered into HTML via + * {@link cmi5launch_print_recent_mod_activity()}. + * + * @param array $activities sequentially indexed array of objects with the 'cmid' property + * @param int $index the index in the $activities to use for the next record + * @param int $timestart append activity since this time + * @param int $courseid the id of the course we produce the report for + * @param int $cmid course module id + * @param int $userid check for a particular user's activity only, defaults to 0 (all users) + * @param int $groupid check for a particular group's activity only, defaults to 0 (all groups) + * @return void adds items into $activities and increases $index + */ +function cmi5launch_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid = 0, $groupid = 0) { +} + +/** + * Prints single activity item prepared by {@see cmi5launch_get_recent_mod_activity()} + * @return void + */ +function cmi5launch_print_recent_mod_activity($activity, $courseid, $detail, $modnames, $viewfullnames) { +} + +/** + * Function to be run periodically according to the moodle cron + * This function searches for things that need to be done, such + * as sending out mail, toggling flags etc ... + * + * @return boolean + * @todo Finish documenting this function + **/ +function cmi5launch_cron() { + return true; +} + +/** + * Returns all other caps used in the module + * + * @example return array('moodle/site:accessallgroups'); + * @return array + */ +function cmi5launch_get_extra_capabilities() { + return array(); +} + +// File API. + +/** + * Returns the lists of all browsable file areas within the given module context + * + * The file area 'intro' for the activity introduction field is added automatically + * by {@link file_browser::get_file_info_context_module()} + * + * @param stdClass $course + * @param stdClass $cm + * @param stdClass $context + * @return array of [(string)filearea] => (string)description + */ +function cmi5launch_get_file_areas($course, $cm, $context) { + $areas = array(); + $areas['content'] = get_string('areacontent', 'scorm'); + $areas['package'] = get_string('areapackage', 'scorm'); + return $areas; +} + +/** + * File browsing support for cmi5launch file areas + * + * @package mod_cmi5launch + * @category files + * + * @param file_browser $browser + * @param array $areas + * @param stdClass $course + * @param stdClass $cm + * @param stdClass $context + * @param string $filearea + * @param int $itemid + * @param string $filepath + * @param string $filename + * @return file_info instance or null if not found + */ +function cmi5launch_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { + global $CFG; + + if (!has_capability('moodle/course:managefiles', $context)) { + return null; + } + + $fs = get_file_storage(); + + if ($filearea === 'package') { + $filepath = is_null($filepath) ? '/' : $filepath; + $filename = is_null($filename) ? '.' : $filename; + + $urlbase = $CFG->wwwroot.'/pluginfile.php'; + if (!$storedfile = $fs->get_file($context->id, 'mod_cmi5launch', 'package', 0, $filepath, $filename)) { + if ($filepath === '/' && $filename === '.') { + $storedfile = new virtual_root_file($context->id, 'mod_cmi5launch', 'package', 0); + } else { + // Not found. + return null; + } + } + return new file_info_stored($browser, $context, $storedfile, $urlbase, $areas[$filearea], false, true, false, false); + } + + return false; +} + +/** + * Serves cmi5 content, introduction images and packages. Implements needed access control ;-) + * + * @package mod_cmi5launch + * @category files + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClass $context context object + * @param string $filearea file area + * @param array $args extra arguments + * @param bool $forcedownload whether or not force download + * @param array $options additional options affecting the file serving + * @return bool false if file not found, does not return if found - just send the file + */ +function cmi5launch_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { + global $CFG, $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + return false; + } + + require_login($course, true, $cm); + $canmanageactivity = has_capability('moodle/course:manageactivities', $context); + + $filename = array_pop($args); + $filepath = implode('/', $args); + if ($filearea === 'content') { + $lifetime = null; + } else if ($filearea === 'package') { + $lifetime = 0; // No caching here. + } else { + return false; + } + + $fs = get_file_storage(); + + if ( + (!$file = $fs->get_file($context->id, 'mod_cmi5launch', $filearea, 0, '/'.$filepath.'/', $filename)) + || ($file->is_directory()) + ) { + if ($filearea === 'content') { // Return file not found straight away to improve performance. + send_header_404(); + die; + } + return false; + } + + // Finally send the file. + send_stored_file($file, $lifetime, 0, false, $options); +} + +/** + * Export file resource contents for web service access. + * + * @param cm_info $cm Course module object. + * @param string $baseurl Base URL for Moodle. + * @return array array of file content + */ +function cmi5launch_export_contents($cm, $baseurl) { + global $CFG; + $contents = array(); + $context = context_module::instance($cm->id); + + $fs = get_file_storage(); + $files = $fs->get_area_files($context->id, 'mod_cmi5launch', 'package', 0, 'sortorder DESC, id ASC', false); + + foreach ($files as $fileinfo) { + $file = array(); + $file['type'] = 'file'; + $file['filename'] = $fileinfo->get_filename(); + $file['filepath'] = $fileinfo->get_filepath(); + $file['filesize'] = $fileinfo->get_filesize(); + $file['fileurl'] = file_encode_url("$CFG->wwwroot/" . $baseurl, '/'.$context->id.'/mod_cmi5launch/package'. + $fileinfo->get_filepath().$fileinfo->get_filename(), true); + $file['timecreated'] = $fileinfo->get_timecreated(); + $file['timemodified'] = $fileinfo->get_timemodified(); + $file['sortorder'] = $fileinfo->get_sortorder(); + $file['userid'] = $fileinfo->get_userid(); + $file['author'] = $fileinfo->get_author(); + $file['license'] = $fileinfo->get_license(); + $contents[] = $file; + } + + return $contents; +} + +// Navigation API. + +/** + * Extends the global navigation tree by adding cmi5launch nodes if there is a relevant content + * + * This can be called by an AJAX request so do not rely on $PAGE as it might not be set up properly. + * + * @param navigation_node $navref An object representing the navigation tree node of the cmi5launch module instance + * @param stdClass $course + * @param stdClass $module + * @param cm_info $cm + */ +function cmi5launch_extend_navigation(navigation_node $navref, stdclass $course, stdclass $module, cm_info $cm) { +} + +/** + * Extends the settings navigation with the cmi5launch settings + * + * This function is called when the context for the page is a cmi5launch module. This is not called by AJAX + * so it is safe to rely on the $PAGE. + * + * @param settings_navigation $settingsnav {@link settings_navigation} + * @param navigation_node $cmi5launchnode {@link navigation_node} + */ +function cmi5launch_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $cmi5launchnode = null) { +} + +// Called by Moodle core. +function cmi5launch_get_completion_state($course, $cm, $userid, $type) { + global $CFG, $DB; + $result = $type; // Default return value. + + // Get cmi5launch. + if (!$cmi5launch = $DB->get_record('cmi5launch', array('id' => $cm->instance))) { + throw new Exception("Can't find activity {$cm->instance}"); // TODO: localise this. + } + + $cmi5launchsettings = cmi5launch_settings($cm->instance); + + $expirydate = null; + $expirydays = $cmi5launch->cmi5expiry; + if ($expirydays > 0) { + $expirydatetime = new DateTime(); + $expirydatetime->sub(new DateInterval('P'.$expirydays.'D')); + $expirydate = $expirydatetime->format('c'); + } + + if (!empty($cmi5launch->cmi5verbid)) { + // Try to get a statement matching actor, verb and object specified in module settings. + $statementquery = cmi5launch_get_statements( + $cmi5launchsettings['cmi5launchlrsendpoint'], + $cmi5launchsettings['cmi5launchlrslogin'], + $cmi5launchsettings['cmi5launchlrspass'], + $cmi5launchsettings['cmi5launchlrsversion'], + $cmi5launch->cmi5activityid, + cmi5launch_getactor($cm->instance), + $cmi5launch->cmi5verbid, + $expirydate + ); + + // If the statement exists, return true else return false. + if (!empty($statementquery->content) && $statementquery->success) { + $result = true; + } else { + $result = false; + } + } + + return $result; +} + +// Cmi5Launch specific functions. +/* +The functions below should really be in locallib, however they are required for one +or more of the functions above so need to be here. +It looks like the standard Quiz module does that same thing, so I don't feel so bad. +*/ + +/** + * Handles uploaded zip packages when a module is added or updated. Unpacks the zip contents + * and extracts the launch url and activity id from the cmi5.xml file. + * Note: This takes the *first* activity from the cmi5.xml file to be the activity intended + * to be launched. It will not go hunting for launch URLs any activities listed below. + * Based closely on code from the SCORM and (to a lesser extent) Resource modules. + * @package mod_cmi5launch + * @category cmi5 + * @param object $cmi5launch An object from the form in mod_form.php + * @return array empty if no issue is found. Array of error message otherwise + */ + +function cmi5launch_process_new_package($cmi5launch) { + + global $DB, $CFG; + $cmid = $cmi5launch->coursemodule; + $context = context_module::instance($cmid); + + // Bring in functions from classes cmi5Connector and AU helpers. + $connectors = new cmi5_connectors; + $auhelper = new au_helpers; + + // Bring in functions from class cmi5_table_connectors and AU helpers. + $createcourse = $connectors->cmi5launch_get_create_course(); + $retrieveaus = $auhelper->get_cmi5launch_retrieve_aus(); + // Reload cmi5 instance. + $record = $DB->get_record('cmi5launch', array('id' => $cmi5launch->id)); + + $fs = get_file_storage(); + + $fs->delete_area_files($context->id, 'mod_cmi5launch', 'package'); + file_save_draft_area_files( + $cmi5launch->packagefile, + $context->id, + 'mod_cmi5launch', + 'package', + 0, + array('subdirs' => 0, 'maxfiles' => 1) + ); + // Get filename of zip that was uploaded. + $files = $fs->get_area_files($context->id, 'mod_cmi5launch', 'package', 0, '', false); + if (count($files) < 1) { + return false; + } + + $zipfile = reset($files); + $zipfilename = $zipfile->get_filename(); + $packagefile = false; + + $packagefile = $fs->get_file($context->id, 'mod_cmi5launch', 'package', 0, '/', $zipfilename); + + // Retrieve user settings to apply to newly created record. + $settings = cmi5launch_settings($cmi5launch->id); + $token = $settings['cmi5launchtenanttoken']; + // Create the course and retrieve info for saving to DB. + $courseresults = $createcourse($context->id, $token, $packagefile); + + // Take the results of created course and save new course id to table. + $record->courseinfo = $courseresults; + + $returnedinfo = json_decode($courseresults, true); + // Retrieve the lmsId of course. + $lmsid = $returnedinfo["lmsId"]; + $record->cmi5activityid = $lmsid; + + $record->courseid = $returnedinfo["id"]; + + // Create url for sending to when requesting launch url for course. + $playerurl = $settings['cmi5launchplayerurl']; + + // Build and save launchurl. + $url = $playerurl . "/api/v1/". $record->courseid. "/launch-url/"; + $record->launchurl = $url; + + // Retrieve AUs and save to table. + $aus = ($retrieveaus($returnedinfo)); + $record->aus = (json_encode($aus)); + + $fs->delete_area_files($context->id, 'mod_cmi5launch', 'content'); + + $packer = get_file_packer('application/zip'); + $packagefile->extract_to_storage($packer, $context->id, 'mod_cmi5launch', 'content', 0, '/'); + + // If the cmi5.xml file isn't there, don't do try to use it. + // This is unlikely as it should have been checked when the file was validated. + if ($manifestfile = $fs->get_file($context->id, 'mod_cmi5launch', 'content', 0, '/', 'cmi5.xml')) { + $xmltext = $manifestfile->get_content(); + + $defaultorgid = 0; + $firstinorg = 0; + + $pattern = '/&(?!\w{2,6};)/'; + $replacement = '&'; + $xmltext = preg_replace($pattern, $replacement, $xmltext); + + $objxml = new xml2Array(); + $manifest = $objxml->parse($xmltext); + + // Update activity id from the first activity in cmi5.xml, if it is found. + // Skip without error if not. (The Moodle admin will need to enter the id manually). + if (isset($manifest[0]["children"][0]["children"][0]["attrs"]["ID"])) { + $record->cmi5activityid = $manifest[0]["children"][0]["children"][0]["attrs"]["ID"]; + } + + // Update launch from the first activity in cmi5.xml, if it is found. + // Skip if not. (The Moodle admin will need to enter the url manually). + foreach ($manifest[0]["children"][0]["children"][0]["children"] as $property) { + if ($property["name"] === "LAUNCH") { + $record->cmi5launchurl = $CFG->wwwroot."/pluginfile.php/".$context->id."/mod_cmi5launch/" + .$manifestfile->get_filearea()."/".$property["tagData"]; + } + } + } + // Save reference. + // Turn off to trigger echo. + return $DB->update_record('cmi5launch', $record); +} + +/** + * Check that a Zip file contains a cmi5.xml file in the right place. Used in mod_form.php. + * Heavily based on scorm_validate_package in /mod/scorm/lib.php + * @package mod_cmi5launch + * @category cmi5 + * @param stored_file $file a Zip file. + * @return array empty if no issue is found. Array of error message otherwise + */ +function cmi5launch_validate_package($file) { + $packer = get_file_packer('application/zip'); + $errors = array(); + $filelist = $file->list_files($packer); + if (!is_array($filelist)) { + $errors['packagefile'] = get_string('badarchive', 'cmi5launch'); + } else { + $badmanifestpresent = false; + foreach ($filelist as $info) { + if ($info->pathname == 'cmi5.xml') { + return array(); + } else if (strpos($info->pathname, 'cmi5.xml') !== false) { + // This package has cmi5 xml file inside a folder of the package. + $badmanifestpresent = true; + } + if (preg_match('/\.cst$/', $info->pathname)) { + return array(); + } + } + if ($badmanifestpresent) { + $errors['packagefile'] = get_string('badimsmanifestlocation', 'cmi5launch'); + } else { + $errors['packagefile'] = get_string('nomanifest', 'cmi5launch'); + } + } + return $errors; +} + +/** + * Check for AUs and their satisifed status in a block. Recursive to handle nested blocks. + * + * @package mod_cmi5launch + * @category cmi5 + * @param mixed bool|array - $auinfoin - the info containing a block, au, or true/false au satisfied value + * @param string $aulmsid - the lms id of the au we are looking for. + * @return mixed - returns an array of aus or au satisfied value. + */ + +function cmi5launch_find_au_satisfied($auinfoin, $aulmsid) { + + $ausatisfied = ""; + + // Check if auinfoin is a boolean or an array. + // It will be an array if an AU was found on recursive call. + if (is_bool($auinfoin)) { + + // Return value to func that called it. + return $auinfoin; + + // If it's an array it is either a block we still need to break down, or an AU we need to find satisfied value for. + } else if (is_array($auinfoin)) { + + // Check AU's satisifeid value and display accordingly. + foreach ($auinfoin as $key => $auinfo) { + + $ausatisfied = "false"; + + // If it's a block, we need to keep breaking it down. + if ($auinfo["type"] == "block" ) { + + // Grab its children, this is what other blocks or AU's will be nested in. + $auchildren = $auinfo["children"]; + + // Now recursively call function again. + $ausatisfied = cmi5launch_find_au_satisfied($auchildren, $aulmsid); + + // If it's an AU, we need to check if it's the one we are looking for. + } else if ($auinfo["type"] == "au") { + + // Search for the correct lms id and take only the AU that matches. + if ( $auinfo["lmsId"] == $aulmsid) { + // If it is, retrieve the satisfied value. + $ausatisfied = $auinfo["satisfied"]; + } else { + + // If no ids match we have a problem, and need to return. + $ausatisfied = "No ids match"; + } + } else { + // This shouldn't be reachable, but in case add error message. + echo "Type from statement does not equal either block or AU."; + } + } + } else { + + echo"Incorrect value passed to function cmi5launch_find_au_satisfied. Correct values are a boolean or array"; + + } + + return $ausatisfied; +} + +/** + * Fetches Statements from the LRS. This is used for completion tracking - + * we check for a statement matching certain criteria for each learner. + * + * @package mod_cmi5launch + * @category cmi5 + * @param string $url LRS endpoint URL + * @param string $basiclogin login/key for the LRS + * @param string $basicpass pass/secret for the LRS + * @param string $version version of xAPI to use + * @param string $activityid Activity Id to filter by + * @param cmi5 Agent $agent Agent to filter by + * @param string $verb Verb Id to filter by + * @param string $since Since date to filter by + * @return cmi5 LRS Response + */ +function cmi5launch_get_statements($url, $basiclogin, $basicpass, $version, $activityid, $agent, $verb, $since = null) { + + $lrs = new \cmi5\RemoteLRS($url, $version, $basiclogin, $basicpass); + + $statementsquery = array( + "agent" => $agent, + "verb" => new \cmi5\Verb(array("id" => trim($verb))), + "activity" => new \cmi5\Activity(array("id" => trim($activityid))), + "related_activities" => "false", + "format" => "ids", + ); + + if (!is_null($since)) { + $statementsquery["since"] = $since; + } + + // Get all the statements from the LRS. + $statementsresponse = $lrs->queryStatements($statementsquery); + + if ($statementsresponse->success == false) { + return $statementsresponse; + } + + $allthestatements = $statementsresponse->content->getStatements(); + $morestatementsurl = $statementsresponse->content->getMore(); + while (!empty($morestatementsurl)) { + $morestmtsresponse = $lrs->moreStatements($morestatementsurl); + if ($morestmtsresponse->success == false) { + return $morestmtsresponse; + } + $morestatements = $morestmtsresponse->content->getStatements(); + $morestatementsurl = $morestmtsresponse->content->getMore(); + // Note: due to the structure of the arrays, array_merge does not work as expected. + foreach ($morestatements as $morestatement) { + array_push($allthestatements, $morestatement); + } + } + + return new \cmi5\LRSResponse( + $statementsresponse->success, + $allthestatements, + $statementsresponse->httpResponse + ); +} + +/** + * Build a cmi5 Agent based on the current user + * + * @package mod_cmi5launch + * @category cmi5 + * @return cmi5 Agent $agent Agent + */ + +function cmi5launch_getactor($instance) { + global $USER, $CFG; + + $settings = cmi5launch_settings($instance); + + if ($USER->idnumber && $settings['cmi5launchcustomacchp']) { + $agent = array( + "name" => fullname($USER), + "account" => array( + "homePage" => $settings['cmi5launchcustomacchp'], + "name" => $USER->idnumber, + ), + "objectType" => "Agent", + ); + } else if ($USER->email && $settings['cmi5launchuseactoremail']) { + $agent = array( + "name" => fullname($USER), + "mbox" => "mailto:".$USER->email, + "objectType" => "Agent", + ); + } else { + $agent = array( + "name" => fullname($USER), + "account" => array( + "homePage" => $CFG->wwwroot, + "name" => $USER->username, + ), + "objectType" => "Agent", + ); + } + + return new \cmi5\Agent($agent); +} + + +/** + * Returns the LRS settings relating to a cmi5 Launch module instance + * + * @package mod_cmi5launch + * @category cmi5 + * @param string $instance The Moodle id for the cmi5 module instance. + * @return array LRS settings to use + */ +function cmi5launch_settings($instance) { + + global $DB, $CFG, $cmi5launchsettings; + + if (!is_null($cmi5launchsettings)) { + return $cmi5launchsettings; + } + + $expresult = array(); + + $result = $DB->get_records('config_plugins', array('plugin' => 'cmi5launch')); + + foreach ($result as $value) { + $expresult[$value->name] = $value->value; + } + + $expresult['cmi5launchlrsversion'] = '1.0.0'; + + $cmi5launchsettings = $expresult; + + return $expresult; +} + + +/** + * Should the global LRS settings be used instead of the instance specific ones? + * + * @package mod_cmi5launch + * @category cmi5 + * @param string $instance The Moodle id for the cmi5 module instance. + * @return bool + */ +function cmi5launch_use_global_cmi5_lrs_settings($instance) { + global $DB; + // Determine if there is a row in cmi5launch_lrs matching the current activity id. + $activitysettings = $DB->get_record('cmi5launch', array('id' => $instance)); + + /* Removed override defaults from db + if ($activitysettings->overridedefaults == 1) { + return false; + } + */ + return true; +} + +// Grade functions. + +/** + * Return grade for given user or all users. + * + * @global stdClass + * @global object + * @param int $cmi5id id of scorm + * @param int $userid optional user id, 0 means all users + * @return array array of grades, false if none + */ +function cmi5launch_get_user_grades($cmi5launch, $userid=0) { + + // External class and functions. + $gradehelpers = new grade_helpers; + + $updategrades = $gradehelpers->get_cmi5launch_check_user_grades_for_updates(); + + global $CFG, $DB; + + $id = required_param('id', PARAM_INT); + $contextmodule = context_module::instance($id); + + $grades = array(); + + // If userid is empty it means we want all users. + if (empty($userid)) { + + // If the userid is empty, use get_enrolled_users for this course then update all their grades. + $users = get_enrolled_users($contextmodule); + + // If there is a list of users then iterate through it and make grade objects with them and their updated grades. + if ($users) { + + foreach ($users as $user) { + + $grades[$user->id] = new stdClass(); + $grades[$user->id]->id = $user->id; + $grades[$user->id]->userid = $user->id; + $grades[$user->id]->rawgrade = $updategrades($user); + } + } else { + // Return false to avoid null values if no users. + return false; + } + } else { + + // This is if we have a specific user, so we need to retrieve their information. + $user = $DB->get_record('user', ['id' => $userid]); + + // Make grade objects with the individual user and their updated grades. + $grades[$userid] = new stdClass(); + $grades[$userid]->id = $userid; + $grades[$userid]->userid = $userid; + $grades[$userid]->rawgrade = $updategrades($user); + } + + return $grades; +} + +/** + * Update grades in central gradebook. + * @category grade + * @param object $cmi5launch - mod object + * @param int $userid - A user ID or 0 for all users. + * @param bool $nullifnone - If true and a single user is specified with no grade, a grade item with a null rawgrade is inserted. + */ +// This function is called automatically by moodle if it needs the users grades updated. +// It can also be called manually when you want to push a new grade to gradebook. +// This function should do whatever is needed to generate the relevant grades to push into gradebook +// then call 'myplugin_grade_item_update with the grades to write. + +function cmi5launch_update_grades($cmi5launch, $userid = 0, $nullifnone = true) { + + global $CFG, $DB; + + require_once($CFG->libdir . '/gradelib.php'); + require_once($CFG->libdir . '/completionlib.php'); + $id = required_param('id', PARAM_INT); + + // Reload cmi5 course instance. + $record = $DB->get_record('cmi5launch', array('id' => $cmi5launch->id)); + $cm = get_coursemodule_from_id('cmi5launch', $id, 0, false, MUST_EXIST); + $contextmodule = context_module::instance($cm->id); + $users = get_enrolled_users($contextmodule); + + // Retrieve user grades and update gradebook. + if ($userid) { + + $grades = cmi5launch_get_user_grades($cmi5launch, $userid); + + // Grades come back nested in array, with keys being the user id. + $grades = $grades[$userid]; + + cmi5launch_grade_item_update($cmi5launch, $grades); + + } else if ($userid && $nullifnone) { + + // User has no grades so assign null. + $grade = new stdClass(); + $grade->userid = $userid; + $grade->rawgrade = null; + + cmi5launch_grade_item_update($cmi5launch, $grade); + + } else { + + cmi5launch_grade_item_update($cmi5launch); + } +} + +/** + * Update/create grade item for given cmi5 activity. + * Calls grade_update from moodle gradelib.php + * @category grade + * @param object $cmi5launch - mod object + * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook + * @return object grade_item + */ +// This is the only place that grade_update should be called. +// And this function should be called from cmi5launch_add_instance, cmi5launch_update_instance and cmi5launch_update_grades. +// It should look at settings in the activity $activitydbrecord to determine grading type, max and min +// values, etc. Then setup gradeinfo to pass to grade_update, it should also pass on the optional grades value. +// $gradeinfo is an array containing: ['itemname' => $activityname, 'idnumber' => $activityidnumber, +// 'gradetype' => GRADE_TYPE_VALUE, 'grademax' => 100, 'grademin' => 0]. +function cmi5launch_grade_item_update($cmi5launch, $grades = null) { + + global $CFG, $DB, $USER, $cmi5launchsettings; + + // Bring in grade helpers. + $gradehelpers = new grade_helpers; + + // Functions from other classes. + $highestgrade = $gradehelpers->get_cmi5launch_highest_grade(); + $averagegrade = $gradehelpers->get_cmi5launch_average_grade(); + $settings = cmi5launch_settings($cmi5launch->id); + + if (!function_exists('grade_update')) { // Workaround for buggy PHP versions. + require_once($CFG->libdir . '/gradelib.php'); + } + // Reload cmi5 course instance. + $record = $DB->get_record('cmi5launch', array('id' => $cmi5launch->id)); + + // Assign course instance info to grade params. + $params = array('itemname' => $cmi5launch->name); + if (isset($cmi5launch->id)) { + $params['idnumber'] = $cmi5launch->id; + } + + // Retrieve the settings for course grading. + $gradetype = $cmi5launchsettings["grademethod"]; + $maxgrade = $settings['maxgrade']; + + // Assign settings to grade item param. + $params['grademax'] = $maxgrade; + $params['grademin'] = 0; + + // If there's a max grade, set it. + if ($maxgrade) { + $params['gradetype'] = $gradetype; + $params['grademax'] = $maxgrade; + $params['grademin'] = 0; + } else { + + $params['gradetype'] = $gradetype; + } + + // Check if it's call to reset. + if ($grades === 'reset') { + $params['reset'] = true; + $grades = null; + } else { + + // Calculate grade based on grade type, and update rawgrade (a param of grade item). + switch($gradetype){ + + // 'MOD_CMI5LAUNCH_AUS_GRADE' = '0'. + // 'MOD_CMI5LAUNCH_GRADE_HIGHEST' = '1'. + // 'MOD_CMI5LAUNCH_GRADE_AVERAGE', = '2'. + // 'MOD_CMI5LAUNCH_GRADE_SUM', = '3'. + + case 1: + foreach ($grades as $key => $grade) { + + $grades->rawgrade = $highestgrade($grades->rawgrade); + } + break; + + case 2: + foreach ($grades as $key => $grade) { + + $grades->rawgrade = $averagegrade($grades->rawgrade); + + } + break; + } + } + + // Call grade_update to update gradebook. + return grade_update('mod/cmi5launch', $cmi5launch->course, 'mod', 'cmi5launch', $cmi5launch->id, 0, $grades, $params); +} + diff --git a/locallib.php b/locallib.php index 0a68c60..048afb8 100755 --- a/locallib.php +++ b/locallib.php @@ -1,599 +1,505 @@ -. - -/** - * Internal library of functions for module cmi5launch - * - * All the cmi5launch specific functions, needed to implement the module - * logic, should go here. Never include this file from your lib.php! - * - * @package mod_cmi5launch - * @copyright 2013 Andrew Downes - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); -require_once("$CFG->dirroot/mod/cmi5launch/lib.php"); - -//Classes for connecting to CMI5 player -require_once("$CFG->dirroot/mod/cmi5launch/cmi5PHP/src/cmi5Connector.php"); - - -/** - * Send a statement that the activity was launched. - * This is useful for debugging - if the 'launched' statement is present in the LRS, you know the activity was at least launched. - * - * @package mod_cmi5launch - * @category cmi5 - * @param string/UUID $registrationid The cmi5 Registration UUID associated with the launch. - * @return cmi5 LRS Response - */ -function cmi5_launched_statement($registrationid) { - global $cmi5launch, $course, $CFG; - $cmi5launchsettings = cmi5launch_settings($cmi5launch->id); - - $version = $cmi5launchsettings['cmi5launchlrsversion']; - $url = $cmi5launchsettings['cmi5launchlrsendpoint']; - $basiclogin = $cmi5launchsettings['cmi5launchlrslogin']; - $basicpass = $cmi5launchsettings['cmi5launchlrspass']; - - $cmi5phputil = new \cmi5\Util(); - $statementid = $cmi5phputil->getUUID(); - - $lrs = new \cmi5\RemoteLRS($url, $version, $basiclogin, $basicpass); - - $parentdefinition = array(); - if (isset($course->summary) && $course->summary !== "") { - $parentdefinition["description"] = array( - "en-US" => $course->summary - ); - } - - if (isset($course->fullname) && $course->fullname !== "") { - $parentdefinition["name"] = array( - "en-US" => $course->fullname - ); - } - - $statement = new \cmi5\Statement( - array( - 'id' => $statementid, - 'actor' => cmi5launch_getactor($cmi5launch->id), - 'verb' => array( - 'id' => 'http://adlnet.gov/expapi/verbs/launched', - 'display' => array( - 'en-US' => 'launched' - ) - ), - - 'object' => array( - 'id' => $cmi5launch->cmi5activityid, - 'objectType' => "Activity" - ), - - "context" => array( - "registration" => $registrationid, - "contextActivities" => array( - "parent" => array( - array( - "id" => $CFG->wwwroot.'/course/view.php?id='. $course->id, - "objectType" => "Activity", - "definition" => $parentdefinition - ) - ), - "grouping" => array( - array( - "id" => $CFG->wwwroot, - "objectType" => "Activity" - ) - ), - "category" => array( - array( - "id" => "https://moodle.org", - "objectType" => "Activity", - "definition" => array ( - "type" => "http://id.cmi5api.com/activitytype/source" - ) - ) - ) - ), - "language" => cmi5launch_get_moodle_langauge() - ), - "timestamp" => date(DATE_ATOM) - ) - ); - - $response = $lrs->saveStatement($statement); - return $response; -} - -/** - * Builds a cmi5 launch link for the current module and a given registration - * - * @package mod_cmi5launch - * @category cmi5 - * @param string/UUID $registrationid The cmi5 Registration UUID associated with the launch. - * @return string launch link including querystring. - */ -function cmi5launch_get_launch_url($registrationuuid, $auID) { - global $cmi5launch, $CFG, $DB; - $cmi5launchsettings = cmi5launch_settings($cmi5launch->id); - $expiry = new DateTime('NOW'); - $xapiduration = $cmi5launchsettings['cmi5launchlrsduration']; - $expiry->add(new DateInterval('PT'.$xapiduration.'M')); - - $url = trim($cmi5launchsettings['cmi5launchlrsendpoint']); - - // Call the function to get the credentials from the LRS. - $basiclogin = trim($cmi5launchsettings['cmi5launchlrslogin']); - $basicpass = trim($cmi5launchsettings['cmi5launchlrspass']); - - switch ($cmi5launchsettings['cmi5launchlrsauthentication']) { - - // Learning Locker 1. - case "0": - $creds = cmi5launch_get_creds_learninglocker($cmi5launchsettings['cmi5launchlrslogin'], - $cmi5launchsettings['cmi5launchlrspass'], - $url, - $expiry, - $registrationuuid - ); - $basicauth = base64_encode($creds["contents"]["key"].":".$creds["contents"]["secret"]); - break; - - // Watershed. - case "2": - $creds = cmi5launch_get_creds_watershed ( - $basiclogin, - $basicpass, - $url, - $xapiduration * 60 - ); - $basicauth = base64_encode($creds["key"].":".$creds["secret"]); - break; - - default: - $basicauth = base64_encode($basiclogin.":".$basicpass); - break; - } -//to bring in functions from class cmi5Connector -$connectors = new cmi5Connectors; -//Get retrieve URL function -$retrieveUrl = $connectors->getRetrieveUrl(); -//See here we are passing the auid. If we have session ids will we pass those instead -//or is that a whole new func, I think it may be -//$rtnstring = $retrieveUrl($cmi5launch->id, $auID); - -//return $rtnstring; -} - -/** - * Used with Learning Locker integration to fetch credentials from the LRS. - * This process is not part of the xAPI specification or the cmi5 launch spec. - * - * @package mod_cmi5launch - * @category cmi5 - * @param string $basiclogin login/key for the LRS - * @param string $basicpass pass/secret for the LRS - * @param string $url LRS endpoint URL - * @return array the response of the LRS (Note: not a cmi5 LRS Response object) - */ -function cmi5launch_get_creds_learninglocker($basiclogin, $basicpass, $url, $expiry, $registrationuuid) { - global $cmi5launch; - $actor = cmi5launch_getactor($cmi5launch->id); - $data = array( - 'scope' => array ('all'), - 'expiry' => $expiry->format(DATE_ATOM), - 'historical' => false, - 'actors' => array( - "objectType" => 'Person', - "name" => array($actor->getName()) - ), - 'auth' => $actor, - 'activity' => array( - $cmi5launch->cmi5activityid, - ), - 'registration' => $registrationuuid - ); - - if (null !== $actor->getMbox()) { - $data['actors']['mbox'] = array($actor->getMbox()); - } else if (null !== $actor->getMbox_sha1sum()) { - $data['actors']['mbox_sha1sum'] = array($actor->getMbox_sha1sum()); - } else if (null !== $actor->getOpenid()) { - $data['actors']['openid'] = array($actor->getOpenid()); - } else if (null !== $actor->getAccount()) { - $data['actors']['account'] = array($actor->getAccount()); - } - - $streamopt = array( - 'ssl' => array( - 'verify-peer' => false, - ), - 'http' => array( - 'method' => 'POST', - 'ignore_errors' => false, - 'header' => array( - 'Authorization: Basic ' . base64_encode(trim($basiclogin) . ':' .trim($basicpass)), - 'Content-Type: application/json', - 'Accept: application/json, */*; q=0.01', - ), - 'content' => cmi5launch_myjson_encode($data), - ), - ); - - $streamparams = array(); - - $context = stream_context_create($streamopt); - - $stream = fopen(trim($url) . 'Basic/request'.'?'.http_build_query($streamparams, '', '&'), 'rb', false, $context); - - $returncode = explode(' ', $http_response_header[0]); - $returncode = (int)$returncode[1]; - - switch($returncode){ - case 200: - $ret = stream_get_contents($stream); - $meta = stream_get_meta_data($stream); - - if ($ret) { - $ret = json_decode($ret, true); - } - break; - default: // Error! - $ret = null; - $meta = $returncode; - break; - } - - return array( - 'contents' => $ret, - 'metadata' => $meta - ); -} - -/** - * By default, PHP escapes slashes when encoding into JSON. This cause problems for cmi5, - * so this function unescapes the slashes after encoding. - * - * @package mod_cmi5launch - * @category cmi5 - * @param object or array $obj object or array encode to JSON - * @return string/JSON JSON encoded object or array - */ -function cmi5launch_myjson_encode($obj) { - return str_replace('\\/', '/', json_encode($obj)); -} - -/** - * Save data to the state. Note: registration is not used as this is a general bucket of data against the activity/learner. - * - * @package mod_cmi5launch - * @category cmi5 - * @param string $data data to store as document - * @param string $key id to store the document against - * @param string $etag etag associated with the document last time it was fetched (may be Null if document is new) - * @return cmi5 LRS Response - */ -function cmi5launch_get_global_parameters_and_save_state($data, $key, $etag) { - global $cmi5launch; - $cmi5launchsettings = cmi5launch_settings($cmi5launch->id); - $lrs = new \cmi5\RemoteLRS( - $cmi5launchsettings['cmi5launchlrsendpoint'], - $cmi5launchsettings['cmi5launchlrsversion'], - $cmi5launchsettings['cmi5launchlrslogin'], - $cmi5launchsettings['cmi5launchlrspass'] - ); - - return $lrs->saveState( - new \cmi5\Activity(array("id" => trim($cmi5launch->cmi5activityid))), - cmi5launch_getactor($cmi5launch->id), - $key, - cmi5launch_myjson_encode($data), - array( - 'etag' => $etag, - 'contentType' => 'application/json' - ) - ); -} - -/** - * Save data to the agent profile. - * Note: registration is not used as this is a general bucket of data against the activity/learner. - * Note: fetches a new etag before storing. Will ALWAYS overwrite existing contents of the document. - * - * @package mod_cmi5launch - * @category cmi5 - * @param string $data data to store as document - * @param string $key id to store the document against - * @return cmi5 LRS Response - */ -function cmi5launch_get_global_parameters_and_save_agentprofile($data, $key) { - global $cmi5launch; - $cmi5launchsettings = cmi5launch_settings($cmi5launch->id); - - $lrs = new \cmi5\RemoteLRS( - $cmi5launchsettings['cmi5launchlrsendpoint'], - $cmi5launchsettings['cmi5launchlrsversion'], - $cmi5launchsettings['cmi5launchlrslogin'], - $cmi5launchsettings['cmi5launchlrspass'] - ); - - $getresponse = $lrs->retrieveAgentProfile(cmi5launch_getactor($cmi5launch->id), $key); - - $opts = array( - 'contentType' => 'application/json' - ); - if ($getresponse->success) { - $opts['etag'] = $getresponse->content->getEtag(); - } - - return $lrs->saveAgentProfile( - cmi5launch_getactor($cmi5launch->id), - $key, - cmi5launch_myjson_encode($data), - $opts - ); -} - -/** - * Get data from the state. Note: registration is not used as this is a general bucket of data against the activity/learner. - * - * @package mod_cmi5launch - * @category cmi5 - * @param string $key id to store the document against - * @return cmi5 LRS Response containing the response code and data or error message - */ -function cmi5launch_get_global_parameters_and_get_state($key) { - global $cmi5launch; - $cmi5launchsettings = cmi5launch_settings($cmi5launch->id); - - $lrs = new \cmi5\RemoteLRS( - $cmi5launchsettings['cmi5launchlrsendpoint'], - $cmi5launchsettings['cmi5launchlrsversion'], - $cmi5launchsettings['cmi5launchlrslogin'], - $cmi5launchsettings['cmi5launchlrspass'] - ); - - - return $lrs->retrieveState( - new \cmi5\Activity(array("id" => trim($cmi5launch->cmi5activityid))), - cmi5launch_getactor($cmi5launch->id), - $key - ); -} - - -/** - * Get the current lanaguage of the current user and return it as an RFC 5646 language tag - * - * @package mod_cmi5launch - * @category cmi5 - * @return string RFC 5646 language tag - */ - -function cmi5launch_get_moodle_langauge() { - $lang = current_language(); - $langarr = explode('_', $lang); - if (count($langarr) == 2) { - return $langarr[0].'-'.strtoupper($langarr[1]); - } else { - return $lang; - } -} - - -/** - * Used with Watershed integration to fetch credentials from the LRS. - * This process is not part of the xAPI specification or the cmi5 launch spec. - * - * @package mod_cmi5launch - * @category cmi5 - * @param string $login login for Watershed - * @param string $pass pass for Watershed - * @param string $endpoint LRS endpoint URL - * @param int $expiry number of seconds the credentials are required for - * @return array the response of the LRS (Note: not a cmi5 LRS Response object) - */ -function cmi5launch_get_creds_watershed($login, $pass, $endpoint, $expiry) { - global $CFG, $DB; - - // Process input parameters. - $auth = 'Basic '.base64_encode($login.':'.$pass); - - $explodedendpoint = explode ('/', $endpoint); - $wsserver = $explodedendpoint[0].'//'.$explodedendpoint[2]; - $orgid = $explodedendpoint[5]; - - // Create a session. - $createsessionresponse = cmi5launch_send_api_request( - $auth, - "POST", - $wsserver . "/api/organizations/" . $orgid . "/activity-providers/self/sessions", - [ - "content" => json_encode([ - "expireSeconds" => $expiry, - "scope" => "xapi:all" - ]) - ] - ); - - if ($createsessionresponse["status"] === 200) { - return [ - "key" => $createsessionresponse["content"]->key, - "secret" => $createsessionresponse["content"]->secret - ]; - } else { - $reason = get_string('apCreationFailed', 'cmi5launch') - ." Status: ". $createsessionresponse["status"].". Response: ".$createsessionresponse["content"]->message; - throw new moodle_exception($reason, 'cmi5launch', ''); - } -} - -/* -@method sendAPIRequest Sends a request to the API. -@param {String} [$auth] Auth string -@param {String} [$method] Method of the request e.g. POST. -@param {String} [$url] URL to request -@param {Array} [$options] Array of optional properties. - @param {String} [content] Content of the request (should be JSON). -@return {Array} Details of the response - @return {String} [metadata] Raw metadata of the response - @return {String} [content] Raw content of the response - @return {Integer} [status] HTTP status code of the response e.g. 201 -*/ -function cmi5launch_send_api_request($auth, $method, $url) { - $options = func_num_args() === 4 ? func_get_arg(3) : array(); - - if (!isset($options['contentType'])) { - $options['contentType'] = 'application/json'; - } - - $http = array( - // We don't expect redirects. - 'max_redirects' => 0, - // This is here for some proxy handling. - 'request_fulluri' => 1, - // Switching this to false causes non-2xx/3xx status codes to throw exceptions. - // but we need to handle the "error" status codes ourselves in some cases. - 'ignore_errors' => true, - 'method' => $method, - 'header' => array() - ); - - array_push($http['header'], 'Authorization: ' . $auth); - - if (($method === 'PUT' || $method === 'POST') && isset($options['content'])) { - $http['content'] = $options['content']; - array_push($http['header'], 'Content-length: ' . strlen($options['content'])); - array_push($http['header'], 'Content-Type: ' . $options['contentType']); - } - - $context = stream_context_create(array( 'http' => $http )); - - $fp = fopen($url, 'rb', false, $context); - - $content = ""; - - if (! $fp) { - return array ( - "metadata" => null, - "content" => $content, - "status" => 0 - ); - } - $metadata = stream_get_meta_data($fp); - $content = stream_get_contents($fp); - $responsecode = (int)explode(' ', $metadata["wrapper_data"][0])[1]; - - fclose($fp); - - if ($options['contentType'] == 'application/json') { - $content = json_decode($content); - } - - return array ( - "metadata" => $metadata, - "content" => $content, - "status" => $responsecode - ); -} - -//Grade stuff from SCORM - -//Move these to top where they belong if they are what we need -define('GRADE_AUS_CMI5', '0'); -define('GRADE_HIGHEST_CMI5', '1'); -define('GRADE_AVERAGE_CMI5', '2'); -define('GRADE_SUM_CMI5', '3'); - -define('HIGHEST_ATTEMPT_CMI5', '0'); -define('AVERAGE_ATTEMPT_CMI5', '1'); -define('FIRST_ATTEMPT_CMI5', '2'); -define('LAST_ATTEMPT_CMI5', '3'); - -define('CMI5_FORCEATTEMPT_NO', 0); -define('CMI5_FORCEATTEMPT_ONCOMPLETE', 1); -define('CMI5_FORCEATTEMPT_ALWAYS', 2); - -define('CMI5_UPDATE_NEVER', '0'); -define('CMI5_UPDATE_EVERYDAY', '2'); -define('CMI5_UPDATE_EVERYTIME', '3'); - -/** - * Returns an array of the array of update frequency options - * - * @return array an array of update frequency options - */ -function cmi5_get_updatefreq_array() { - return array(CMI5_UPDATE_NEVER => get_string('never'), - CMI5_UPDATE_EVERYDAY => get_string('everyday', 'cmi5launch'), - CMI5_UPDATE_EVERYTIME => get_string('everytime', 'cmi5launch')); -} - -/** - * Returns an array of the array of what grade options - * - * @return array an array of what grade options - */ -function cmi5_get_grade_method_array() { - return array (GRADE_AUS_CMI5 => get_string('GRADE_CMI5_AUS', 'cmi5launch'), - GRADE_HIGHEST_CMI5 => get_string('GRADE_HIGHEST_CMI5', 'cmi5launch'), - GRADE_AVERAGE_CMI5 => get_string('GRADE_AVERAGE_CMI5', 'cmi5launch'), - GRADE_SUM_CMI5 => get_string('GRADE_SUM_CMI5', 'cmi5launch')); -} - - - -/** - * Returns an array of the array of attempt options - * - * @return array an array of attempt options - */ -function cmi5_get_attempts_array() { - $attempts = array(0 => get_string('nolimit', 'cmi5launch'), - 1 => get_string('attempt1', 'cmi5launch')); - - for ($i = 2; $i <= 6; $i++) { - $attempts[$i] = get_string('attemptsx', 'cmi5launch', $i); - } - - return $attempts; -} - -/** - * Returns an array of the array of what grade options - * - * @return array an array of what grade options - */ -function cmi5_get_what_grade_array() { - return array (HIGHEST_ATTEMPT_CMI5 => get_string('HIGHEST_ATTEMPT_CMI5', 'cmi5launch'), - AVERAGE_ATTEMPT_CMI5 => get_string('AVERAGE_ATTEMPT_CMI5', 'cmi5launch'), - FIRST_ATTEMPT_CMI5 => get_string('FIRST_ATTEMPT_CMI5', 'cmi5launch'), - LAST_ATTEMPT_CMI5 => get_string('last_attempt_cmi5', 'cmi5launch')); -} - -/** - * Returns an array of the force attempt options - * - * @return array an array of attempt options - */ -function cmi5_get_forceattempt_array() { - return array(CMI5_FORCEATTEMPT_NO => get_string('no'), - CMI5_FORCEATTEMPT_ONCOMPLETE => get_string('forceattemptoncomplete', 'cmi5launch'), - CMI5_FORCEATTEMPT_ALWAYS => get_string('forceattemptalways', 'cmi5launch')); -} - +. + +/** + * Internal library of functions for module cmi5launch + * + * All the cmi5launch specific functions, needed to implement the module + * logic, should go here. Never include this file from your lib.php! + * + * @package mod_cmi5launch + * @copyright 2013 Andrew Downes + * @copyright 2024 Megan Bohland - added functions + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once("$CFG->dirroot/mod/cmi5launch/lib.php"); + +// Class for connecting to CMI5 player. +use mod_cmi5launch\local\cmi5_connectors; + +// Grade stuff. +define('MOD_CMI5LAUNCH_AUS_GRADE', '0'); +define('MOD_CMI5LAUNCH_GRADE_HIGHEST', '1'); +define('MOD_CMI5LAUNCH_GRADE_AVERAGE', '2'); +define('MOD_CMI5LAUNCH_GRADE_SUM', '3'); + +define('MOD_CMI5LAUNCH_HIGHEST_ATTEMPT', '0'); +define('MOD_CMI5LAUNCH_AVERAGE_ATTEMPT', '1'); +define('MOD_CMI5LAUNCH_FIRST_ATTEMPT', '2'); +define('MOD_CMI5LAUNCH_LAST_ATTEMPT', '3'); + +define('MOD_CMI5LAUNCH_FORCEATTEMPT_NO', 0); +define('MOD_CMI5LAUNCH_FORCEATTEMPT_ONCOMPLETE', 1); +define('MOD_CMI5LAUNCH_FORCEATTEMPT_ALWAYS', 2); + +define('MOD_CMI5LAUNCH_UPDATE_NEVER', '0'); +define('MOD_CMI5LAUNCH_UPDATE_EVERYDAY', '2'); +define('MOD_CMI5LAUNCH_UPDATE_EVERYTIME', '3'); + + + +/** + * Builds a cmi5 launch link for the current module and a given registration + * + * @package mod_cmi5launch + * @category cmi5 + * @param string/UUID $registrationid The cmi5 Registration UUID associated with the launch. + * @return string launch link including querystring. + */ +function cmi5launch_get_launch_url($registrationuuid, $auid) { + + global $cmi5launch, $CFG, $DB; + $cmi5launchsettings = cmi5launch_settings($cmi5launch->id); + $expiry = new DateTime('NOW'); + $xapiduration = $cmi5launchsettings['cmi5launchlrsduration']; + $expiry->add(new DateInterval('PT'.$xapiduration.'M')); + + $url = trim($cmi5launchsettings['cmi5launchlrsendpoint']); + + // Call the function to get the credentials from the LRS. + $basiclogin = trim($cmi5launchsettings['cmi5launchlrslogin']); + $basicpass = trim($cmi5launchsettings['cmi5launchlrspass']); + + switch ($cmi5launchsettings['cmi5launchlrsauthentication']) { + + // Learning Locker 1. + case "0": + $creds = cmi5launch_get_creds_learninglocker($cmi5launchsettings['cmi5launchlrslogin'], + $cmi5launchsettings['cmi5launchlrspass'], + $url, + $expiry, + $registrationuuid + ); + $basicauth = base64_encode($creds["contents"]["key"].":".$creds["contents"]["secret"]); + break; + + // Watershed. + case "2": + $creds = cmi5launch_get_creds_watershed ( + $basiclogin, + $basicpass, + $url, + $xapiduration * 60 + ); + $basicauth = base64_encode($creds["key"].":".$creds["secret"]); + break; + + default: + $basicauth = base64_encode($basiclogin.":".$basicpass); + break; + } + // Bring in functions from class cmi5Connector. + $connectors = new cmi5_connectors; + // Get retrieve URL function. + $cmi5launchretrieveurl = $connectors->cmi5launch_get_retrieve_url(); + + // return $rtnstring; +} + +/** + * Used with Learning Locker integration to fetch credentials from the LRS. + * This process is not part of the xAPI specification or the cmi5 launch spec. + * + * @package mod_cmi5launch + * @category cmi5 + * @param string $basiclogin login/key for the LRS + * @param string $basicpass pass/secret for the LRS + * @param string $url LRS endpoint URL + * @return array the response of the LRS (Note: not a cmi5 LRS Response object) + */ +function cmi5launch_get_creds_learninglocker($basiclogin, $basicpass, $url, $expiry, $registrationuuid) { + global $cmi5launch; + $actor = cmi5launch_getactor($cmi5launch->id); + $data = array( + 'scope' => array ('all'), + 'expiry' => $expiry->format(DATE_ATOM), + 'historical' => false, + 'actors' => array( + "objectType" => 'Person', + "name" => array($actor->getName()), + ), + 'auth' => $actor, + 'activity' => array( + $cmi5launch->cmi5activityid, + ), + 'registration' => $registrationuuid, + ); + + if (null !== $actor->getMbox()) { + $data['actors']['mbox'] = array($actor->getMbox()); + } else if (null !== $actor->getMbox_sha1sum()) { + $data['actors']['mbox_sha1sum'] = array($actor->getMbox_sha1sum()); + } else if (null !== $actor->getOpenid()) { + $data['actors']['openid'] = array($actor->getOpenid()); + } else if (null !== $actor->getAccount()) { + $data['actors']['account'] = array($actor->getAccount()); + } + + $streamopt = array( + 'ssl' => array( + 'verify-peer' => false, + ), + 'http' => array( + 'method' => 'POST', + 'ignore_errors' => false, + 'header' => array( + 'Authorization: Basic ' . base64_encode(trim($basiclogin) . ':' .trim($basicpass)), + 'Content-Type: application/json', + 'Accept: application/json, */*; q=0.01', + ), + 'content' => cmi5launch_myjson_encode($data), + ), + ); + + $streamparams = array(); + + $context = stream_context_create($streamopt); + + $stream = fopen(trim($url) . 'Basic/request'.'?'.http_build_query($streamparams, '', '&'), 'rb', false, $context); + + $returncode = explode(' ', $http_response_header[0]); + $returncode = (int)$returncode[1]; + + switch($returncode){ + case 200: + $ret = stream_get_contents($stream); + $meta = stream_get_meta_data($stream); + + if ($ret) { + $ret = json_decode($ret, true); + } + break; + default: // Error! + $ret = null; + $meta = $returncode; + break; + } + + return array( + 'contents' => $ret, + 'metadata' => $meta, + ); +} + +/** + * By default, PHP escapes slashes when encoding into JSON. This cause problems for cmi5, + * so this function unescapes the slashes after encoding. + * + * @package mod_cmi5launch + * @category cmi5 + * @param object or array $obj object or array encode to JSON + * @return string/JSON JSON encoded object or array + */ +function cmi5launch_myjson_encode($obj) { + return str_replace('\\/', '/', json_encode($obj)); +} + +/** + * Save data to the state. Note: registration is not used as this is a general bucket of data against the activity/learner. + * + * @package mod_cmi5launch + * @category cmi5 + * @param string $data data to store as document + * @param string $key id to store the document against + * @param string $etag etag associated with the document last time it was fetched (may be Null if document is new) + * @return cmi5 LRS Response + */ +function cmi5launch_get_global_parameters_and_save_state($data, $key, $etag) { + global $cmi5launch; + $cmi5launchsettings = cmi5launch_settings($cmi5launch->id); + $lrs = new \cmi5\RemoteLRS( + $cmi5launchsettings['cmi5launchlrsendpoint'], + $cmi5launchsettings['cmi5launchlrsversion'], + $cmi5launchsettings['cmi5launchlrslogin'], + $cmi5launchsettings['cmi5launchlrspass'] + ); + + return $lrs->saveState( + new \cmi5\Activity(array("id" => trim($cmi5launch->cmi5activityid))), + cmi5launch_getactor($cmi5launch->id), + $key, + cmi5launch_myjson_encode($data), + array( + 'etag' => $etag, + 'contentType' => 'application/json', + ) + ); +} + +/** + * Save data to the agent profile. + * Note: registration is not used as this is a general bucket of data against the activity/learner. + * Note: fetches a new etag before storing. Will ALWAYS overwrite existing contents of the document. + * + * @package mod_cmi5launch + * @category cmi5 + * @param string $data data to store as document + * @param string $key id to store the document against + * @return cmi5 LRS Response + */ +function cmi5launch_get_global_parameters_and_save_agentprofile($data, $key) { + global $cmi5launch; + $cmi5launchsettings = cmi5launch_settings($cmi5launch->id); + + $lrs = new \cmi5\RemoteLRS( + $cmi5launchsettings['cmi5launchlrsendpoint'], + $cmi5launchsettings['cmi5launchlrsversion'], + $cmi5launchsettings['cmi5launchlrslogin'], + $cmi5launchsettings['cmi5launchlrspass'] + ); + + $getresponse = $lrs->retrieveAgentProfile(cmi5launch_getactor($cmi5launch->id), $key); + + $opts = array( + 'contentType' => 'application/json', + ); + if ($getresponse->success) { + $opts['etag'] = $getresponse->content->getEtag(); + } + + return $lrs->saveAgentProfile( + cmi5launch_getactor($cmi5launch->id), + $key, + cmi5launch_myjson_encode($data), + $opts + ); +} + +/** + * Get data from the state. Note: registration is not used as this is a general bucket of data against the activity/learner. + * + * @package mod_cmi5launch + * @category cmi5 + * @param string $key id to store the document against + * @return cmi5 LRS Response containing the response code and data or error message + */ +function cmi5launch_get_global_parameters_and_get_state($key) { + global $cmi5launch; + $cmi5launchsettings = cmi5launch_settings($cmi5launch->id); + + $lrs = new \cmi5\RemoteLRS( + $cmi5launchsettings['cmi5launchlrsendpoint'], + $cmi5launchsettings['cmi5launchlrsversion'], + $cmi5launchsettings['cmi5launchlrslogin'], + $cmi5launchsettings['cmi5launchlrspass'] + ); + + return $lrs->retrieveState( + new \cmi5\Activity(array("id" => trim($cmi5launch->cmi5activityid))), + cmi5launch_getactor($cmi5launch->id), + $key + ); +} + + +/** + * Get the current lanaguage of the current user and return it as an RFC 5646 language tag + * + * @package mod_cmi5launch + * @category cmi5 + * @return string RFC 5646 language tag + */ + +function cmi5launch_get_moodle_langauge() { + $lang = current_language(); + $langarr = explode('_', $lang); + if (count($langarr) == 2) { + return $langarr[0].'-'.strtoupper($langarr[1]); + } else { + return $lang; + } +} + + +/** + * Used with Watershed integration to fetch credentials from the LRS. + * This process is not part of the xAPI specification or the cmi5 launch spec. + * + * @package mod_cmi5launch + * @category cmi5 + * @param string $login login for Watershed + * @param string $pass pass for Watershed + * @param string $endpoint LRS endpoint URL + * @param int $expiry number of seconds the credentials are required for + * @return array the response of the LRS (Note: not a cmi5 LRS Response object) + */ +function cmi5launch_get_creds_watershed($login, $pass, $endpoint, $expiry) { + global $CFG, $DB; + + // Process input parameters. + $auth = 'Basic '.base64_encode($login.':'.$pass); + + $explodedendpoint = explode ('/', $endpoint); + $wsserver = $explodedendpoint[0].'//'.$explodedendpoint[2]; + $orgid = $explodedendpoint[5]; + + // Create a session. + $createsessionresponse = cmi5launch_send_api_request( + $auth, + "POST", + $wsserver . "/api/organizations/" . $orgid . "/activity-providers/self/sessions", + [ + "content" => json_encode([ + "expireSeconds" => $expiry, + "scope" => "xapi:all", + ]), + ] + ); + + if ($createsessionresponse["status"] === 200) { + return [ + "key" => $createsessionresponse["content"]->key, + "secret" => $createsessionresponse["content"]->secret, + ]; + } else { + $reason = get_string('apCreationFailed', 'cmi5launch') + ." Status: ". $createsessionresponse["status"].". Response: ".$createsessionresponse["content"]->message; + throw new moodle_exception($reason, 'cmi5launch', ''); + } +} + +/* +@method sendAPIRequest Sends a request to the API. +@param {String} [$auth] Auth string +@param {String} [$method] Method of the request e.g. POST. +@param {String} [$url] URL to request +@param {Array} [$options] Array of optional properties. + @param {String} [content] Content of the request (should be JSON). +@return {Array} Details of the response + @return {String} [metadata] Raw metadata of the response + @return {String} [content] Raw content of the response + @return {Integer} [status] HTTP status code of the response e.g. 201 +*/ +function cmi5launch_send_api_request($auth, $method, $url) { + $options = func_num_args() === 4 ? func_get_arg(3) : array(); + + if (!isset($options['contentType'])) { + $options['contentType'] = 'application/json'; + } + + $http = array( + // We don't expect redirects. + 'max_redirects' => 0, + // This is here for some proxy handling. + 'request_fulluri' => 1, + // Switching this to false causes non-2xx/3xx status codes to throw exceptions. + // but we need to handle the "error" status codes ourselves in some cases. + 'ignore_errors' => true, + 'method' => $method, + 'header' => array(), + ); + + array_push($http['header'], 'Authorization: ' . $auth); + + if (($method === 'PUT' || $method === 'POST') && isset($options['content'])) { + $http['content'] = $options['content']; + array_push($http['header'], 'Content-length: ' . strlen($options['content'])); + array_push($http['header'], 'Content-Type: ' . $options['contentType']); + } + + $context = stream_context_create(array( 'http' => $http )); + + $fp = fopen($url, 'rb', false, $context); + + $content = ""; + + if (! $fp) { + return array ( + "metadata" => null, + "content" => $content, + "status" => 0, + ); + } + $metadata = stream_get_meta_data($fp); + $content = stream_get_contents($fp); + $responsecode = (int)explode(' ', $metadata["wrapper_data"][0])[1]; + + fclose($fp); + + if ($options['contentType'] == 'application/json') { + $content = json_decode($content); + } + + return array ( + "metadata" => $metadata, + "content" => $content, + "status" => $responsecode, + ); +} + +/** + * Returns an array of the array of update frequency options + * + * @return array an array of update frequency options + */ +function cmi5launch_get_updatefreq_array() { + return array(MOD_CMI5LAUNCH_UPDATE_NEVER => get_string('never'), + MOD_CMI5LAUNCH_UPDATE_EVERYDAY => get_string('everyday', 'cmi5launch'), + MOD_CMI5LAUNCH_UPDATE_EVERYTIME => get_string('everytime', 'cmi5launch')); +} + +/** + * Returns an array of the array of what grade options + * + * @return array an array of what grade options + */ +function cmi5launch_get_grade_method_array() { + return array ( + MOD_CMI5LAUNCH_GRADE_HIGHEST => get_string('mod_cmi5launch_grade_highest', 'cmi5launch'), + MOD_CMI5LAUNCH_GRADE_AVERAGE => get_string('mod_cmi5launch_grade_average', 'cmi5launch'), + ); +} + +/** + * Returns an array of the array of attempt options + * + * @return array an array of attempt options + */ +function cmi5launch_get_attempts_array() { + $attempts = array(0 => get_string('nolimit', 'cmi5launch'), + 1 => get_string('attempt1', 'cmi5launch')); + + for ($i = 2; $i <= 6; $i++) { + $attempts[$i] = get_string('attemptsx', 'cmi5launch', $i); + } + + return $attempts; +} + +/** + * Returns an array of the array of what grade options + * + * @return array an array of what grade options + */ +function cmi5launch_get_what_grade_array() { + return array (MOD_CMI5LAUNCH_HIGHEST_ATTEMPT => get_string('mod_cmi5launch_highest_attempt', 'cmi5launch'), + MOD_CMI5LAUNCH_AVERAGE_ATTEMPT => get_string('mod_cmi5launch_average_attempt', 'cmi5launch'), + MOD_CMI5LAUNCH_FIRST_ATTEMPT => get_string('mod_cmi5launch_first_attempt', 'cmi5launch'), + MOD_CMI5LAUNCH_LAST_ATTEMPT => get_string('mod_cmi5launch_last_attempt', 'cmi5launch')); +} + +/** + * Returns an array of the force attempt options + * + * @return array an array of attempt options + */ +function cmi5launch_get_forceattempt_array() { + return array(MOD_CMI5LAUNCH_FORCEATTEMPT_NO => get_string('no'), + MOD_CMI5LAUNCH_FORCEATTEMPT_ONCOMPLETE => get_string('forceattemptoncomplete', 'cmi5launch'), + MOD_CMI5LAUNCH_FORCEATTEMPT_ALWAYS => get_string('forceattemptalways', 'cmi5launch')); +} diff --git a/mod_form.php b/mod_form.php index fb9cf80..91c8e40 100755 --- a/mod_form.php +++ b/mod_form.php @@ -1,403 +1,319 @@ -. - -/** - * The main cmi5launch configuration form - * - * It uses the standard core Moodle formslib. For more info about them, please - * visit: http://docs.moodle.org/en/Development:lib/formslib.php - * - * @package mod_cmi5launch - * @copyright 2013 Andrew Downes - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot.'/course/moodleform_mod.php'); - -/** - * Module instance settings form - */ -class mod_cmi5launch_mod_form extends moodleform_mod { - - /** - * Defines forms elements - */ - public function definition() { - - global $CFG; - $cfgcmi5launch = get_config('cmi5launch'); - - $mform = $this->_form; - - // Adding the "general" fieldset, where all the common settings are showed. - $mform->addElement('header', 'general', get_string('general', 'form')); - - // Adding the standard "name" field. - $mform->addElement('text', 'name', get_string('cmi5launchname', 'cmi5launch'), array('size' => '64')); - if (!empty($CFG->formatstringstriptags)) { - $mform->setType('name', PARAM_TEXT); - } else { - $mform->setType('name', PARAM_CLEANHTML); - } - $mform->addRule('name', null, 'required', null, 'client'); - $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); - $mform->addHelpButton('name', 'cmi5launchname', 'cmi5launch'); - // Adding the standard "intro" and "introformat" fields. - $this->standard_intro_elements(); - - $mform->addElement('header', 'packageheading', get_string('cmi5packagetitle', 'cmi5launch')); - $mform->addElement( - 'static', - 'packagesettingsdescription', - get_string('cmi5packagetitle', 'cmi5launch'), - get_string('cmi5packagetext', 'cmi5launch') - ); - - // Start required Fields for Activity. - $mform->addElement('text', 'cmi5launchurl', get_string('cmi5launchurl', 'cmi5launch'), array('size' => '64')); - $mform->setType('cmi5launchurl', PARAM_TEXT); - $mform->addRule('cmi5launchurl', null, 'required', null, 'client'); - $mform->addRule('cmi5launchurl', get_string('maximumchars', '', 1333), 'maxlength', 1333, 'client'); - $mform->addHelpButton('cmi5launchurl', 'cmi5launchurl', 'cmi5launch'); - $mform->setDefault('cmi5launchurl', 'https://example.com/example-activity/index.html'); - - - $mform->addElement('text', 'cmi5activityid', get_string('cmi5activityid', 'cmi5launch'), array('size' => '64')); - $mform->setType('cmi5activityid', PARAM_TEXT); - $mform->addRule('cmi5activityid', null, 'required', null, 'client'); - $mform->addRule('cmi5activityid', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); - $mform->addHelpButton('cmi5activityid', 'cmi5activityid', 'cmi5launch'); - $mform->setDefault('cmi5activityid', 'https://example.com/example-activity'); - // End required Fields for Activity. - - // New local package upload. - - //Ok, this is making an array of filemanager - $filemanageroptions = array(); - $filemanageroptions['accepted_types'] = array('.zip'); - $filemanageroptions['maxbytes'] = 0; - $filemanageroptions['maxfiles'] = 1; - $filemanageroptions['subdirs'] = 0; - $mform->addElement( - 'filemanager', - 'packagefile', - get_string('cmi5package', - 'cmi5launch'), - null, - $filemanageroptions - ); - $mform->addHelpButton('packagefile', 'cmi5package', 'cmi5launch'); - - // Start advanced settings. - $mform->addElement('header', 'lrsheading', get_string('lrsheading', 'cmi5launch')); - - $mform->addElement( - 'static', - 'description', - get_string('lrsdefaults', 'cmi5launch'), - get_string('lrssettingdescription', 'cmi5launch') - ); - - // Override default LRS settings. - $mform->addElement('advcheckbox', 'overridedefaults', get_string('overridedefaults', 'cmi5launch')); - $mform->addHelpButton('overridedefaults', 'overridedefaults', 'cmi5launch'); - - // Add LRS endpoint. - $mform->addElement( - 'text', - 'cmi5launchlrsendpoint', - get_string('cmi5launchlrsendpoint', 'cmi5launch'), - array('size' => '64') - ); - $mform->setType('cmi5launchlrsendpoint', PARAM_TEXT); - $mform->addRule('cmi5launchlrsendpoint', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); - $mform->addHelpButton('cmi5launchlrsendpoint', 'cmi5launchlrsendpoint', 'cmi5launch'); - $mform->setDefault('cmi5launchlrsendpoint', $cfgcmi5launch->cmi5launchlrsendpoint); - $mform->disabledIf('cmi5launchlrsendpoint', 'overridedefaults'); - - // Add LRS Authentication. - $authoptions = array( - 1 => get_string('cmi5launchlrsauthentication_option_0', 'cmi5launch'), - 2 => get_string('cmi5launchlrsauthentication_option_1', 'cmi5launch'), - 0 => get_string('cmi5launchlrsauthentication_option_2', 'cmi5launch') - ); - $mform->addElement( - 'select', - 'cmi5launchlrsauthentication', - get_string('cmi5launchlrsauthentication', 'cmi5launch'), - $authoptions - ); - $mform->disabledIf('cmi5launchlrsauthentication', 'overridedefaults'); - $mform->addHelpButton('cmi5launchlrsauthentication', 'cmi5launchlrsauthentication', 'cmi5launch'); - $mform->getElement('cmi5launchlrsauthentication')->setSelected($cfgcmi5launch->cmi5launchlrsauthentication); - - $mform->addElement( - 'static', - 'description', - get_string('cmi5launchlrsauthentication_watershedhelp_label', 'cmi5launch'), - get_string('cmi5launchlrsauthentication_watershedhelp', 'cmi5launch') - ); - - // Add basic authorisation login. - $mform->addElement( - 'text', - 'cmi5launchlrslogin', - get_string('cmi5launchlrslogin', 'cmi5launch'), - array('size' => '64') - ); - $mform->setType('cmi5launchlrslogin', PARAM_TEXT); - $mform->addRule('cmi5launchlrslogin', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); - $mform->addHelpButton('cmi5launchlrslogin', 'cmi5launchlrslogin', 'cmi5launch'); - $mform->setDefault('cmi5launchlrslogin', $cfgcmi5launch->cmi5launchlrslogin); - $mform->disabledIf('cmi5launchlrslogin', 'overridedefaults'); - - // Add basic authorisation pass. - $mform->addElement( - 'password', - 'cmi5launchlrspass', - get_string('cmi5launchlrspass', 'cmi5launch'), - array('size' => '64') - ); - $mform->setType('cmi5launchlrspass', PARAM_TEXT); - $mform->addRule('cmi5launchlrspass', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); - $mform->addHelpButton('cmi5launchlrspass', 'cmi5launchlrspass', 'cmi5launch'); - $mform->setDefault('cmi5launchlrspass', $cfgcmi5launch->cmi5launchlrspass); - $mform->disabledIf('cmi5launchlrspass', 'overridedefaults'); - - // Duration. - $mform->addElement( - 'text', - 'cmi5launchlrsduration', - get_string('cmi5launchlrsduration', 'cmi5launch'), - array('size' => '64') - ); - $mform->setType('cmi5launchlrsduration', PARAM_TEXT); - $mform->addRule('cmi5launchlrsduration', get_string('maximumchars', '', 5), 'maxlength', 5, 'client'); - $mform->addHelpButton('cmi5launchlrsduration', 'cmi5launchlrsduration', 'cmi5launch'); - $mform->setDefault('cmi5launchlrsduration', $cfgcmi5launch->cmi5launchlrsduration); - $mform->disabledIf('cmi5launchlrsduration', 'overridedefaults'); - - // Actor account homePage. - $mform->addElement( - 'text', - 'cmi5launchcustomacchp', - get_string('cmi5launchcustomacchp', 'cmi5launch'), - array('size' => '64') - ); - $mform->setType('cmi5launchcustomacchp', PARAM_TEXT); - $mform->addRule('cmi5launchcustomacchp', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); - $mform->addHelpButton('cmi5launchcustomacchp', 'cmi5launchcustomacchp', 'cmi5launch'); - $mform->setDefault('cmi5launchcustomacchp', $cfgcmi5launch->cmi5launchcustomacchp); - $mform->disabledIf('cmi5launchcustomacchp', 'overridedefaults'); - - // Don't use email. - $mform->addElement( - 'advcheckbox', - 'cmi5launchuseactoremail', - get_string('cmi5launchuseactoremail', 'cmi5launch') - ); - $mform->addHelpButton('cmi5launchuseactoremail', 'cmi5launchuseactoremail', 'cmi5launch'); - $mform->setDefault('cmi5launchuseactoremail', $cfgcmi5launch->cmi5launchuseactoremail); - $mform->disabledIf('cmi5launchuseactoremail', 'overridedefaults'); - // End advanced settings. - - // Behavior settings. - $mform->addElement('header', 'behaviorheading', get_string('behaviorheading', 'cmi5launch')); - - // Allow multiple ongoing registrations. - $mform->addElement('advcheckbox', 'cmi5multipleregs', get_string('cmi5multipleregs', 'cmi5launch')); - $mform->addHelpButton('cmi5multipleregs', 'cmi5multipleregs', 'cmi5launch'); - $mform->setDefault('cmi5multipleregs', 1); - - // Add standard elements, common to all modules. - $this->standard_coursemodule_elements(); - // Add standard buttons, common to all modules. - $this->add_action_buttons(); - } - - public function add_completion_rules() { - $mform =& $this->_form; - - // Add Verb Id setting. - $verbgroup = array(); - $verbgroup[] =& $mform->createElement( - 'checkbox', - 'completionverbenabled', - ' ', - get_string('completionverb', 'cmi5launch') - ); - $verbgroup[] =& $mform->createElement('text', 'cmi5verbid', ' ', array('size' => '64')); - $mform->setType('cmi5verbid', PARAM_TEXT); - - $mform->addGroup($verbgroup, 'completionverbgroup', get_string('completionverbgroup', 'cmi5launch'), array(' '), false); - $mform->addGroupRule( - 'completionverbgroup', array( - 'cmi5verbid' => array( - array(get_string('maximumchars', '', 255), 'maxlength', 255, 'client') - ) - ) - ); - - $mform->addHelpButton('completionverbgroup', 'completionverbgroup', 'cmi5launch'); - $mform->disabledIf('cmi5verbid', 'completionverbenabled', 'notchecked'); - $mform->setDefault('cmi5verbid', 'http://adlnet.gov/expapi/verbs/completed'); - - // Add Completion Expiry Date setting. - $completiongroup = array(); - $completiongroup[] =& $mform->createElement( - 'checkbox', - 'completionexpiryenabled', - ' ', - get_string('completionexpiry', 'cmi5launch') - ); - $completiongroup[] =& $mform->createElement('text', 'cmi5expiry', ' ', array('size' => '64')); - $mform->setType('cmi5expiry', PARAM_TEXT); - - $mform->addGroup( - $completiongroup, - 'completionexpirygroup', - get_string('completionexpirygroup', 'cmi5launch'), - array(' '), - false - ); - $mform->addGroupRule( - 'completionexpirygroup', array( - 'cmi5expiry' => array( - array(get_string('maximumchars', '', 10), 'maxlength', 10, 'client') - ) - ) - ); - - $mform->addHelpButton('completionexpirygroup', 'completionexpirygroup', 'cmi5launch'); - $mform->disabledIf('cmi5expiry', 'completionexpiryenabled', 'notchecked'); - $mform->setDefault('cmi5expiry', '365'); - - return array('completionverbgroup', 'completionexpirygroup'); - } - - public function completion_rule_enabled($data) { - if (!empty($data['completionverbenabled']) && !empty($data['cmi5verbid'])) { - return true; - } - if (!empty($data['completionexpiryenabled']) && !empty($data['cmi5expiry'])) { - return true; - } - return false; - } - - public function get_data() { - $data = parent::get_data(); - if (!$data) { - return $data; - } - if (!empty($data->completionunlocked)) { - // Turn off completion settings if the checkboxes aren't ticked. - $autocompletion = !empty($data->completion) && $data->completion == COMPLETION_TRACKING_AUTOMATIC; - if (empty($data->completionverbenabled) || !$autocompletion) { - $data->cmi5verbid = ''; - } - if (empty($data->completionexpiryenabled) || !$autocompletion) { - $data->cmi5expiry = ''; - } - } - return $data; - } - - public function data_preprocessing(&$defaultvalues) { - parent::data_preprocessing($defaultvalues); - - global $DB; - - // Determine if default lrs settings were overriden. - if (!empty($defaultvalues['overridedefaults'])) { - if ($defaultvalues['overridedefaults'] == '1') { - // Retrieve activity lrs settings from DB. - $cmi5launchlrs = $DB->get_record( - 'cmi5launch_lrs', - array('cmi5launchid' => $defaultvalues['instance']), - $fields = '*', - IGNORE_MISSING - ); - $defaultvalues['cmi5launchlrsendpoint'] = $cmi5launchlrs->lrsendpoint; - $defaultvalues['cmi5launchlrsauthentication'] = $cmi5launchlrs->lrsauthentication; - $defaultvalues['cmi5launchcustomacchp'] = $cmi5launchlrs->customacchp; - $defaultvalues['cmi5launchuseactoremail'] = $cmi5launchlrs->useactoremail; - $defaultvalues['cmi5launchlrsduration'] = $cmi5launchlrs->lrsduration; - $defaultvalues['cmi5launchlrslogin'] = $cmi5launchlrs->lrslogin; - $defaultvalues['cmi5launchlrspass'] = $cmi5launchlrs->lrspass; - - } - } - - $draftitemid = file_get_submitted_draft_itemid('packagefile'); - file_prepare_draft_area( - $draftitemid, - $this->context->id, - 'mod_cmi5launch', - 'package', - 0, - array('subdirs' => 0, 'maxfiles' => 1) - ); - $defaultvalues['packagefile'] = $draftitemid; - - // Set up the completion checkboxes which aren't part of standard data. - // We also make the default value (if you turn on the checkbox) for those - // numbers to be 1, this will not apply unless checkbox is ticked. - if (!empty($defaultvalues['cmi5verbid'])) { - $defaultvalues['completionverbenabled'] = 1; - } else { - $defaultvalues['cmi5verbid'] = 'http://adlnet.gov/expapi/verbs/completed'; - } - if (!empty($defaultvalues['cmi5expiry'])) { - $defaultvalues['completionexpiryenabled'] = 1; - } else { - $defaultvalues['cmi5expiry'] = 365; - } - - } - // Validate the form elements after submitting (server-side). - public function validation($data, $files) { - global $CFG, $USER; - $errors = parent::validation($data, $files); - if (!empty($data['packagefile'])) { - $draftitemid = file_get_submitted_draft_itemid('packagefile'); - - file_prepare_draft_area( - $draftitemid, - $this->context->id, - 'mod_cmi5launch', - 'packagefilecheck', - null, - array('subdirs' => 0, 'maxfiles' => 1) - ); - - // Get file from users draft area. - $usercontext = context_user::instance($USER->id); - $fs = get_file_storage(); - $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id', false); - - if (count($files) < 1) { - return $errors; - } - $file = reset($files); - // Validate this cmi5 package. - $errors = array_merge($errors, cmi5launch_validate_package($file)); - } - return $errors; - } -} +. + +/** + * The main cmi5launch configuration form + * + * It uses the standard core Moodle formslib. For more info about them, please + * visit: http://docs.moodle.org/en/Development:lib/formslib.php + * + * @package mod_cmi5launch + * @copyright 2023 Bohland + * @copyright Based on work by 2013 Andrew Downes + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/course/moodleform_mod.php'); + +/** + * Module instance settings form + */ +class mod_cmi5launch_mod_form extends moodleform_mod { + + /** + * Defines forms elements + */ + public function definition() { + + global $CFG; + $cfgcmi5launch = get_config('cmi5launch'); + + $mform = $this->_form; + + // Adding the "general" fieldset, where all the common settings are showed. + $mform->addElement('header', 'general', get_string('general', 'form')); + + // Adding the standard "name" field. + $mform->addElement('text', 'name', get_string('cmi5launchname', 'cmi5launch'), array('size' => '64')); + if (!empty($CFG->formatstringstriptags)) { + $mform->setType('name', PARAM_TEXT); + } else { + $mform->setType('name', PARAM_CLEANHTML); + } + $mform->addRule('name', null, 'required', null, 'client'); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('name', 'cmi5launchname', 'cmi5launch'); + // Adding the standard "intro" and "introformat" fields. + $this->standard_intro_elements(); + + $mform->addElement('header', 'packageheading', get_string('cmi5packagetitle', 'cmi5launch')); + $mform->addElement( + 'static', + 'packagesettingsdescription', + get_string('cmi5packagetitle', 'cmi5launch'), + get_string('cmi5packagetext', 'cmi5launch') + ); + + // Start required Fields for Activity. + /* + $mform->addElement('text', 'cmi5launchurl', get_string('cmi5launchurl', 'cmi5launch'), array('size' => '64')); + $mform->setType('cmi5launchurl', PARAM_TEXT); + $mform->addRule('cmi5launchurl', null, 'required', null, 'client'); + $mform->addRule('cmi5launchurl', get_string('maximumchars', '', 1333), 'maxlength', 1333, 'client'); + $mform->addHelpButton('cmi5launchurl', 'cmi5launchurl', 'cmi5launch'); + $mform->setDefault('cmi5launchurl', 'https://example.com/example-activity/index.html'); + + $mform->addElement('text', 'cmi5activityid', get_string('cmi5activityid', 'cmi5launch'), array('size' => '64')); + $mform->setType('cmi5activityid', PARAM_TEXT); + $mform->addRule('cmi5activityid', null, 'required', null, 'client'); + $mform->addRule('cmi5activityid', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('cmi5activityid', 'cmi5activityid', 'cmi5launch'); + $mform->setDefault('cmi5activityid', 'https://example.com/example-activity'); + */ + // End required Fields for Activity. + + // New local package upload. + + // This is making an array of filemanager. + $filemanageroptions = array(); + $filemanageroptions['accepted_types'] = array('.zip'); + $filemanageroptions['maxbytes'] = 0; + $filemanageroptions['maxfiles'] = 1; + $filemanageroptions['subdirs'] = 0; + $mform->addElement( + 'filemanager', + 'packagefile', + get_string('cmi5package', + 'cmi5launch'), + null, + $filemanageroptions + ); + $mform->addHelpButton('packagefile', 'cmi5package', 'cmi5launch'); + + // Start advanced settings. + $mform->addElement('header', 'lrsheading', get_string('lrsheading', 'cmi5launch')); + + // Actor account homePage. + $mform->addElement( + 'text', + 'cmi5launchcustomacchp', + get_string('cmi5launchcustomacchp', 'cmi5launch'), + array('size' => '64') + ); + $mform->setType('cmi5launchcustomacchp', PARAM_TEXT); + $mform->addRule('cmi5launchcustomacchp', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('cmi5launchcustomacchp', 'cmi5launchcustomacchp', 'cmi5launch'); + $mform->setDefault('cmi5launchcustomacchp', $cfgcmi5launch->cmi5launchcustomacchp); + $mform->disabledIf('cmi5launchcustomacchp', 'overridedefaults'); + + // Don't use email. + $mform->addElement( + 'advcheckbox', + 'cmi5launchuseactoremail', + get_string('cmi5launchuseactoremail', 'cmi5launch') + ); + $mform->addHelpButton('cmi5launchuseactoremail', 'cmi5launchuseactoremail', 'cmi5launch'); + $mform->setDefault('cmi5launchuseactoremail', $cfgcmi5launch->cmi5launchuseactoremail); + $mform->disabledIf('cmi5launchuseactoremail', 'overridedefaults'); + // End advanced settings. + + // Behavior settings. + $mform->addElement('header', 'behaviorheading', get_string('behaviorheading', 'cmi5launch')); + + // Allow multiple ongoing registrations. + $mform->addElement('advcheckbox', 'cmi5multipleregs', get_string('cmi5multipleregs', 'cmi5launch')); + $mform->addHelpButton('cmi5multipleregs', 'cmi5multipleregs', 'cmi5launch'); + $mform->setDefault('cmi5multipleregs', 1); + + // Add standard elements, common to all modules. + $this->standard_coursemodule_elements(); + // Add standard buttons, common to all modules. + $this->add_action_buttons(); + } + + public function add_completion_rules() { + $mform =& $this->_form; + + // Add Verb Id setting. + $verbgroup = array(); + $verbgroup[] =& $mform->createElement( + 'checkbox', + 'completionverbenabled', + ' ', + get_string('completionverb', 'cmi5launch') + ); + $verbgroup[] =& $mform->createElement('text', 'cmi5verbid', ' ', array('size' => '64')); + $mform->setType('cmi5verbid', PARAM_TEXT); + + $mform->addGroup($verbgroup, 'completionverbgroup', get_string('completionverbgroup', 'cmi5launch'), array(' '), false); + $mform->addGroupRule( + 'completionverbgroup', array( + 'cmi5verbid' => array( + array(get_string('maximumchars', '', 255), 'maxlength', 255, 'client'), + ), + ) + ); + + $mform->addHelpButton('completionverbgroup', 'completionverbgroup', 'cmi5launch'); + $mform->disabledIf('cmi5verbid', 'completionverbenabled', 'notchecked'); + $mform->setDefault('cmi5verbid', 'http://adlnet.gov/expapi/verbs/completed'); + + // Add Completion Expiry Date setting. + $completiongroup = array(); + $completiongroup[] =& $mform->createElement( + 'checkbox', + 'completionexpiryenabled', + ' ', + get_string('completionexpiry', 'cmi5launch') + ); + $completiongroup[] =& $mform->createElement('text', 'cmi5expiry', ' ', array('size' => '64')); + $mform->setType('cmi5expiry', PARAM_TEXT); + + $mform->addGroup( + $completiongroup, + 'completionexpirygroup', + get_string('completionexpirygroup', 'cmi5launch'), + array(' '), + false + ); + $mform->addGroupRule( + 'completionexpirygroup', array( + 'cmi5expiry' => array( + array(get_string('maximumchars', '', 10), 'maxlength', 10, 'client'), + ), + ) + ); + + $mform->addHelpButton('completionexpirygroup', 'completionexpirygroup', 'cmi5launch'); + $mform->disabledIf('cmi5expiry', 'completionexpiryenabled', 'notchecked'); + $mform->setDefault('cmi5expiry', '365'); + + return array('completionverbgroup', 'completionexpirygroup'); + } + + public function completion_rule_enabled($data) { + if (!empty($data['completionverbenabled']) && !empty($data['cmi5verbid'])) { + return true; + } + if (!empty($data['completionexpiryenabled']) && !empty($data['cmi5expiry'])) { + return true; + } + return false; + } + + public function get_data() { + $data = parent::get_data(); + if (!$data) { + return $data; + } + if (!empty($data->completionunlocked)) { + // Turn off completion settings if the checkboxes aren't ticked. + $autocompletion = !empty($data->completion) && $data->completion == COMPLETION_TRACKING_AUTOMATIC; + if (empty($data->completionverbenabled) || !$autocompletion) { + $data->cmi5verbid = ''; + } + if (empty($data->completionexpiryenabled) || !$autocompletion) { + $data->cmi5expiry = ''; + } + } + return $data; + } + + public function data_preprocessing(&$defaultvalues) { + parent::data_preprocessing($defaultvalues); + + global $DB; + + // Determine if default lrs settings were overriden. + if (!empty($defaultvalues['overridedefaults'])) { + if ($defaultvalues['overridedefaults'] == '1') { + // Retrieve activity lrs settings from DB. + $cmi5launchlrs = $DB->get_record( + 'cmi5launch_lrs', + array('cmi5launchid' => $defaultvalues['instance']), + $fields = '*', + IGNORE_MISSING + ); + $defaultvalues['cmi5launchlrsendpoint'] = $cmi5launchlrs->lrsendpoint; + $defaultvalues['cmi5launchlrsauthentication'] = $cmi5launchlrs->lrsauthentication; + $defaultvalues['cmi5launchcustomacchp'] = $cmi5launchlrs->customacchp; + $defaultvalues['cmi5launchuseactoremail'] = $cmi5launchlrs->useactoremail; + $defaultvalues['cmi5launchlrsduration'] = $cmi5launchlrs->lrsduration; + $defaultvalues['cmi5launchlrslogin'] = $cmi5launchlrs->lrslogin; + $defaultvalues['cmi5launchlrspass'] = $cmi5launchlrs->lrspass; + + } + } + + $draftitemid = file_get_submitted_draft_itemid('packagefile'); + file_prepare_draft_area( + $draftitemid, + $this->context->id, + 'mod_cmi5launch', + 'package', + 0, + array('subdirs' => 0, 'maxfiles' => 1) + ); + $defaultvalues['packagefile'] = $draftitemid; + + // Set up the completion checkboxes which aren't part of standard data. + // We also make the default value (if you turn on the checkbox) for those + // numbers to be 1, this will not apply unless checkbox is ticked. + if (!empty($defaultvalues['cmi5verbid'])) { + $defaultvalues['completionverbenabled'] = 1; + } else { + $defaultvalues['cmi5verbid'] = 'http://adlnet.gov/expapi/verbs/completed'; + } + if (!empty($defaultvalues['cmi5expiry'])) { + $defaultvalues['completionexpiryenabled'] = 1; + } else { + $defaultvalues['cmi5expiry'] = 365; + } + + } + // Validate the form elements after submitting (server-side). + public function validation($data, $files) { + global $CFG, $USER; + $errors = parent::validation($data, $files); + if (!empty($data['packagefile'])) { + $draftitemid = file_get_submitted_draft_itemid('packagefile'); + + file_prepare_draft_area( + $draftitemid, + $this->context->id, + 'mod_cmi5launch', + 'packagefilecheck', + null, + array('subdirs' => 0, 'maxfiles' => 1) + ); + + // Get file from users draft area. + $usercontext = context_user::instance($USER->id); + $fs = get_file_storage(); + $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id', false); + + if (count($files) < 1) { + return $errors; + } + $file = reset($files); + // Validate this cmi5 package. + $errors = array_merge($errors, cmi5launch_validate_package($file)); + } + return $errors; + } +} diff --git a/report.php b/report.php old mode 100644 new mode 100755 index 6f119a9..fc768cc --- a/report.php +++ b/report.php @@ -1,6 +1,346 @@ . -echo $OUTPUT->heading("This is the report page."); -?> \ No newline at end of file +/** + * The report page. Displays either the course grades for teacher, or user grades for student. + * + * @copyright 2023 Megan Bohland + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +use mod_cmi5launch\local\grade_helpers; + +require_once(dirname(dirname(dirname(__FILE__))) . '/config.php'); +require('header.php'); +require_once($CFG->libdir.'/tablelib.php'); +require_once($CFG->dirroot.'/mod/cmi5launch/locallib.php'); +require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->dirroot. '/reportbuilder/classes/local/report/column.php'); + +require_login($course, false, $cm); + +define('CMI5LAUNCH_REPORT_DEFAULT_PAGE_SIZE', 20); +define('CMI5LAUNCH_REPORT_ATTEMPTS_ALL_STUDENTS', 0); +define('CMI5LAUNCH_REPORT_ATTEMPTS_STUDENTS_WITH', 1); +define('CMI5LAUNCH_REPORT_ATTEMPTS_STUDENTS_WITH_NO', 2); + +global $cmi5launch, $cmi5launchsettings, $USER, $DB; +// Reload course information. +$cm = get_coursemodule_from_id('cmi5launch', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + +// Activity Module ID. +$id = required_param('id', PARAM_INT); +$download = optional_param('download', '', PARAM_RAW); +$mode = optional_param('mode', '', PARAM_ALPHA); // Report mode. +// Item number, may be != 0 for activities that allow more than one grade per user. +// Itemnumber is from the moodle grade_items table, which holds info on the grade item +// itself such as course, mod type, activity title, etc. +$itemnumber = optional_param('itemnumber', 0, PARAM_INT); +// Currently logged in user. +$userid = optional_param('userid', 0, PARAM_INT); +// The itemid is from the Moodle grade_grades table, corresponds to a grade column (such as +// one cmi5launch or other activity part of a course). +$itemid = optional_param('itemid', 0, PARAM_INT); +// This is the gradeid, which is the id in the same grade_grades table. A row entry, a particular users info. +$gradeid = optional_param('gradeid', 0, PARAM_INT); +// Active page. +$page = optional_param('page', 0, PARAM_INT); +$showall = optional_param('showall', null, PARAM_BOOL); +$cmid = optional_param('cmid', null, PARAM_INT); + +$url = new moodle_url('/mod/cmi5launch/report.php'); +if ($page !== 0) { + $url->param('page', $page); +} else if ($showall) { + $url->param('showall', $showall); +} +$requestbody = file_get_contents('php://input'); + +$contextmodule = context_module::instance($cm->id); + +// Setup page url. +$PAGE->set_url($url); +$PAGE->set_pagelayout('report'); +$PAGE->requires->jquery(); + +// Functions from other classes. +$gradehelpers = new grade_helpers; + +$updategrades = $gradehelpers->get_cmi5launch_check_user_grades_for_updates(); +$highestgrade = $gradehelpers->get_cmi5launch_highest_grade(); +$averagegrade = $gradehelpers->get_cmi5launch_average_grade(); + + +$cmi5launchsettings = cmi5launch_settings($cmi5launch->id); + +// Activate the secondary nav tab. +navigation_node::override_active_url(new moodle_url('/mod/cmi5launch/report.php', ['id' => $id])); + +?> + + $contextmodule, + 'other' => array( + 'scormid' => $scorm->id, + 'mode' => $mode + ) +)); + +$event->add_record_snapshot('course_modules', $cm); +$event->add_record_snapshot('scorm', $scorm); +$event->trigger(); +*/ + +// Print the page header. +if (empty($noheader)) { + + $strreport = get_string('report', 'cmi5launch'); + $strattempt = get_string('attempt', 'cmi5launch'); + + // Setup the page + $PAGE->set_title("$course->shortname: ".format_string($course->id)); + $PAGE->set_heading($course->fullname); + $PAGE->activityheader->set_attrs([ + 'hidecompletion' => true, + 'description' => '', + ]); + $PAGE->navbar->add($strreport, new moodle_url('/mod/cmi5launch/report.php', array('id' => $cm->id))); + + echo $OUTPUT->header(); +} + +// Create table to display on page. +$reporttable = new \flexible_table('mod-cmi5launch-report'); + +$columns[] = 'AU Title'; +$headers[] = get_string('autitle', 'cmi5launch'); + +// The table is always the same, but the amount of users shown varies. +// If user has capability, they can see all users. +if (has_capability('mod/cmi5launch:viewgrades', $context)) { + + // Get enrolled users for this course. + $users = get_enrolled_users($contextmodule); + + + foreach ($users as $user) { + + // Call updategrades to ensure all grades are up to date before view. + $updategrades($user); + + // Each user needs their own column. + $headers[] = $user->username; + $columns[] = $user->username; + + // Backbutton goes to grader. + $backurl = $CFG->wwwroot . '/grade/report/grader/index.php' . '?id=' .$cmi5launch->course; + } +} else { + + // If the user does not have the correct capability then we are looking at a specific user, + // who is not a teacher and needs to see only their grades. + // Retrieve that user from DB. + $user = $DB->get_record('user', array('id' => $USER->id)); + + // Make sure their grades are up to date. + $updategrades($user); + // Add user to array for processing, the table build expects an array of users with their id as their index. + $users[$user->id] = $user; + + // Each user needs their own column. + $headers[] = $user->username; + $columns[] = $user->username; + + // Where back button goes. + $backurl = $CFG->wwwroot . '/grade/report/user/index.php' . '?id=' .$cmi5launch->course; +} + + // Create back button. + ?> + + get_record('cmi5launch', array('id' => $cmi5launch->id)); + + // Retrieve AU ids for this course. + $aus = json_decode($record->aus, true); + + // Separate AUS into array on id. + $auschunked = array_chunk($aus, 13, true); + + // Add the columns and headers to the table. + $reporttable->define_columns($columns); + $reporttable->define_headers($headers); + $reporttable->define_baseurl($PAGE->url); + // Setup table (this needs to be done before data is added). + $reporttable->setup(); + + // Unfortunately, array_chunk nests our AU's, we need to use an index to grab them. + // I have not found a way to reliably separate AUs from DB without nesting -MB. + foreach ($auschunked[0] as $au) { + + // Array to hold data for rows. + $rowdata = array(); + + // For each AU, iterate through each user. + foreach ($users as $user) { + + // Array to hold info for next page, that will be placed into buttons for user to click. + $infofornextpage = array(); + + // Retrieve the current au id, this is always unique and will help with retrieving the + // student grades. It is the uniquie id cmi5 spec id. + $aulmsid = $au[0]['lmsId']; + $infofornextpage[] = $aulmsid; + // Grab the current title of the AU for the row header, also to be sent to next page. + $currenttitle = $au[0]['title'][0]['text']; + $infofornextpage[] = $currenttitle; + $rowdata["AU Title"] = ($currenttitle); + + $username = $user->username; + + // Retrieve users specific info for this course. + $userrecord = $DB->get_record('cmi5launch_usercourse', ['courseid' => $record->courseid, 'userid' => $user->id]); + + // Retrieve grade type from settings. + $gradetype = $cmi5launchsettings["grademethod"]; + + // User record may be null if user has not participated in course yet. + if ($userrecord == null) { + + $userscore = " "; + $infofornextpage[] = null; + } else { + + // Retrieve the users grades for this course. + $usergrades = json_decode($userrecord->ausgrades, true); + + // These are the AUS we want to send on if clicked, the more specific ids (THIS users AU ids). + $currentauids = $userrecord->aus; + $infofornextpage[] = $currentauids; + + $userscore = ""; + + if (!$usergrades == null) { + + // Now compare the usergrades array keys to lmsid of current au, if + // it matches then we want to display, that's what userscore is. + if (array_key_exists($aulmsid, $usergrades)) { + + // If it is, we want it's info which should be title => grade(s). + $auinfo = array(); + $auinfo = $usergrades[$aulmsid]; + $augrades = $auinfo[$currenttitle]; + + // This is just to display, it calculates here so it doesn't effect the base array stored for AU. + switch($gradetype){ + + // MOD_CMI5LAUNCH_AUS_GRADE' = '0'. + // MOD_CMI5LAUNCH_GRADE_HIGHEST' = '1'. + // MOD_CMI5LAUNCH_GRADE_AVERAGE', = '2'. + // MOD_CMI5LAUNCH_GRADE_SUM', = '3'. + case 1: + $userscore = strval($highestgrade($augrades)); + break; + case 2: + // We need to update rawgrade not all of grades, that wipes out the array format it needs. + $userscore = strval($averagegrade($augrades)); + break; + } + + // Remove [] from userscore if they are there. + $toremove = array("[", "]"); + if ($userscore != null && str_contains($userscore, "[")) { + $userscore = str_replace($toremove, "", $userscore); + } + } + + } else { + $userscore = "N/A"; + } + + } + // Add the userid to info for next page. + $infofornextpage[] = $user->id; + + // Convert their grade to string to be passed into html button. + $userscoreasstring = strval($userscore); + + // Encode to send to next page, because it has to go as a string and pass through the Javascript function. + $sendtopage = base64_encode(json_encode($infofornextpage, JSON_HEX_QUOT)); + + // Build the button to be displayed. It appears as the users score, but is also a link + // to session_report if user wants to break down the score. + $button = "" + . $userscoreasstring . ""; + + // Add the button to the row data under the correct user. + $rowdata[$username] = ($button); + } + + // Add the row data to the table. + $reporttable->add_data_keyed($rowdata); + } + + // Finish building table now that all data is passed in. + $reporttable->get_page_start(); + $reporttable->get_page_size(); + $reporttable->finish_output(); +?> + + +footer(); +} diff --git a/session_report.php b/session_report.php new file mode 100755 index 0000000..738afdd --- /dev/null +++ b/session_report.php @@ -0,0 +1,287 @@ +. + +/** + * Class to report on sessions grades. + * + * @copyright 2023 Megan Bohland + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_cmi5launch\local\au_helpers; +use mod_cmi5launch\local\session_helpers; + +require_once(dirname(dirname(dirname(__FILE__))) . '/config.php'); +require('header.php'); +require_once($CFG->libdir.'/tablelib.php'); +require_once($CFG->dirroot.'/mod/cmi5launch/locallib.php'); +require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->dirroot. '/reportbuilder/classes/local/report/column.php'); + +require_login($course, false, $cm); + +define('CMI5LAUNCH_REPORT_DEFAULT_PAGE_SIZE', 20); +define('CMI5LAUNCH_REPORT_ATTEMPTS_ALL_STUDENTS', 0); +define('CMI5LAUNCH_REPORT_ATTEMPTS_STUDENTS_WITH', 1); +define('CMI5LAUNCH_REPORT_ATTEMPTS_STUDENTS_WITH_NO', 2); +$PAGE->requires->jquery(); + +global $cmi5launch, $CFG; + +$cmi5launchsettings = cmi5launch_settings($cmi5launch->id); + +// Retrieve the grade type to use to calculate the overall score. +$gradetype = $cmi5launchsettings["grademethod"]; + +// External classes and functions. +$sessionhelper = new session_helpers; +$aushelpers = new au_helpers; + +$updatesession = $sessionhelper->cmi5launch_get_update_session(); +$getaus = $aushelpers->get_cmi5launch_retrieve_aus_from_db(); + +// Activity Module ID. +$id = required_param('id', PARAM_INT); + +// Retrieve the user and AU specific info from previous page. +$fromreportpage = base64_decode(required_param('session_report', PARAM_TEXT) ); +// Break it into array. +$fromreportpage = json_decode($fromreportpage, true); + +// The args from the previous page come through in this order: +// 0: cmi5 unique AU ID. +// 1: AU title. +// 2: AU IDs to retrieve AUs from DB for this user. +// 3: The user id, the one whose grades we need. +$cmi5idprevpage = $fromreportpage[0]; +$currenttitle = $fromreportpage[1]; +$auidprevpage = $fromreportpage[2]; +$userid = $fromreportpage[3]; + +// Retrieve the course module. +$cm = get_coursemodule_from_id('cmi5launch', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$contextmodule = context_module::instance($cm->id); + +// Set page url. +$url = new moodle_url('/mod/cmi5launch/session_report.php'); +$url->param('id', $id); + +// Activate the secondary nav tab. +navigation_node::override_active_url(new moodle_url('/mod/cmi5launch/classes/local/session_report.php', ['id' => $id])); + +if (!empty($download)) { + $noheader = true; +} + +// Print the page header. +if (empty($noheader)) { + + $strreport = get_string('report', 'cmi5launch'); + + // Setup the page. + $PAGE->set_url($url); + $PAGE->set_pagelayout('report'); + $PAGE->set_title("$course->shortname: ".format_string($cm->name)); + $PAGE->set_heading($course->fullname); + $PAGE->activityheader->set_attrs([ + 'hidecompletion' => true, + 'description' => '', + ]); + $PAGE->navbar->add($strreport, new moodle_url('/mod/cmi5launch/report.php', array('id' => $cm->id))); + + echo $OUTPUT->header(); +} + +// Back button. +?> + +get_record('user', array('id' => $userid)); + +// Create tables to display on page. +// This is the main table with session info. +$table = new \flexible_table('mod-cmi5launch-report'); +// This table holds the overall score, showing the grading type. +$scoretable = new \flexible_table('mod-cmi5launch-report'); + +$columns[] = 'Attempt'; +$headers[] = get_string('attempt', 'cmi5launch'); +$columns[] = 'Started'; +$headers[] = get_string('started', 'cmi5launch'); +$columns[] = 'Finished'; +$headers[] = get_string('last', 'cmi5launch'); +$columns[] = 'Status'; +$headers[] = "AU Satisfied Status"; +$columns[] = 'Score'; +$headers[] = get_string('score', 'cmi5launch'); + +$scorecolumns = array(); +$scoreheaders = array(); + +// Add the columns and headers to the table. +$table->define_columns($columns); +$table->define_headers($headers); +$table->define_baseurl($PAGE->url); + +// Decode and put AU ids in array. +$auids = (json_decode($auidprevpage, true) ); + +// For each AU id, find the one that matches our auid from previous page, this is the record we want. +foreach ($auids as $key => $auid) { + + // Retrieve record from table. + $au = $DB->get_record('cmi5launch_aus', ['id' => $auid]); + + if ($au->lmsid == $cmi5idprevpage) { + + // If the id matches it is the record we want. + $aurecord = $au; + } +} + +if ($aurecord->sessions != null || false) { + // Retrieve session ids for this course. + $sessions = json_decode($aurecord->sessions, true); + + // Start Attempts at one. + $attempt = 1; + + // Arrays to hold row info. + $rowdata = array(); + $scorerow = array(); + + // An array to hold grades for max or mean scoring. + $sessionscores = array(); + // Set table up, this needs to be done before rows added. + $table->setup(); + $austatus = ""; + + // There may be more than one session. + foreach ($sessions as $sessionid) { + + $session = $updatesession($sessionid, $cmi5launch->id, $user); + // Add score to array for AU. + $sessionscores[] = $session->score; + + // Retrieve createdAt and format. + $date = new DateTime($session->createdat, new DateTimeZone('US/Eastern')); + $date->setTimezone(new DateTimeZone('America/New_York')); + $datestart = $date->format('D d M Y H:i:s'); + + // Retrieve lastRequestTime and format. + $date = new DateTime($session->lastrequesttime, new DateTimeZone('US/Eastern')); + $date->setTimezone(new DateTimeZone('America/New_York')); + $datefinish = $date->format('D d M Y H:i:s'); + + // The users sessions. + $usersession = $DB->get_record('cmi5launch_sessions', array('sessionid' => $sessionid)); + + // Add row data. + $rowdata["Attempt"] = "Attempt " . $attempt; + $rowdata["Started"] = $datestart; + $rowdata["Finished"] = $datefinish; + + // AUs moveon specification. + $aumoveon = $aurecord->moveon; + + // 0 is no 1 is yes, these are from CMI5 player + $iscompleted = $session->iscompleted; + $ispassed = $session->ispassed; + $isfailed = $session->isfailed; + $isterminated = $session->isterminated; + $isabandoned = $session->isabandoned; + + // If it's been attempted but no moveon value. + if ($iscompleted == 1) { + + $austatus = "Completed"; + + if ($ispassed == 1) { + $austatus = "Completed and Passed"; + } + if ($isfailed == 1) { + $austatus = "Completed and Failed"; + } + } + // Update table. + $scorecolumns[] = "Attempt " . $attempt; + $scoreheaders[] = "Attempt " . $attempt; + $scorerow["Attempt " . $attempt] = $usersession->score; + + switch ($gradetype) { + + // 'MOD_CMI5LAUNCH_AUS_GRADE' = '0'). + // 'MOD_CMI5LAUNCH_GRADE_HIGHEST' = '1'. + // 'MOD_CMI5LAUNCH_GRADE_AVERAGE', = '2'. + // 'MOD_CMI5LAUNCH_GRADE_SUM', = '3'. + + case 1: + $grade = "Highest"; + $overall = max($sessionscores); + break; + case 2: + $grade = "Average"; + $overall = (array_sum($sessionscores) / count($sessionscores)); + break; + } + + $scorerow["Grading type"] = $grade; + + $attempt++; + + $rowdata["Status"] = $austatus; + + $rowdata["Score"] = $usersession->score; + + $table->add_data_keyed($rowdata); + } +} + +// Display the grading type, highest, avg, etc. +$scorecolumns[] = 'Grading type'; +$scoreheaders[] = 'Gradingtype'; +$scorecolumns[] = 'Overall Score'; +$scoreheaders[] = 'Overall Score'; + +// Session score may be null or empty. +if (!empty($sessionscores)) { + + $scorerow["Overall Score"] = $overall; +} + +// Setup score table. +$scoretable->define_columns($scorecolumns); +$scoretable->define_headers($scoreheaders); +$scoretable->define_baseurl($PAGE->url); +$scoretable->setup(); +$scoretable->add_data_keyed($scorerow); +$scoretable->add_data_keyed("SCORE"); + +$table->get_page_start(); +$table->get_page_size(); + +$scoretable->get_page_start(); +$scoretable->get_page_size(); + +$table->add_separator(); +$scoretable->finish_output(); +$table->finish_output(); diff --git a/settings.php b/settings.php index 93d70c0..8718ee5 100755 --- a/settings.php +++ b/settings.php @@ -1,139 +1,152 @@ -. - -/* For global cmi5 settings */ - -defined('MOODLE_INTERNAL') || die; - -if ($ADMIN->fulltree) { - require_once($CFG->dirroot . '/mod/cmi5launch/locallib.php'); - require_once($CFG->dirroot . '/mod/cmi5launch/settingslib.php'); - - //MB - //From scorm grading stuff - $yesno = array(0 => get_string('no'), - 1 => get_string('yes')); - - // Default display settings. - $settings->add(new admin_setting_heading('cmi5launch/cmi5launchlrsfieldset', - get_string('cmi5launchlrsfieldset', 'cmi5launch'), - get_string('cmi5launchlrsfieldset_help', 'cmi5launch'))); - - $settings->add(new admin_setting_configtext_mod_cmi5launch('cmi5launch/cmi5launchlrsendpoint', - get_string('cmi5launchlrsendpoint', 'cmi5launch'), - get_string('cmi5launchlrsendpoint_help', 'cmi5launch'), - get_string('cmi5launchlrsendpoint_default', 'cmi5launch'), PARAM_URL)); - - $options = array( - 1 => get_string('cmi5launchlrsauthentication_option_0', 'cmi5launch'), - 2 => get_string('cmi5launchlrsauthentication_option_1', 'cmi5launch'), - 0 => get_string('cmi5launchlrsauthentication_option_2', 'cmi5launch') - ); - // Note the numbers above are deliberately mis-ordered for reasons of backwards compatibility with older settings. - - $setting = new admin_setting_configselect('cmi5launch/cmi5launchlrsauthentication', - get_string('cmi5launchlrsauthentication', 'cmi5launch'), - get_string('cmi5launchlrsauthentication_help', 'cmi5launch').'