diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..aa9a047 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +# PHP PSR-2 Coding Standards +# http://www.php-fig.org/psr/psr-2/ + +root = true + +[*.php] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore index 841356e..9f87aac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # authentication credentials .htpasswd -secrets.xml +config.xml # dynamically-loaded and/or caches /logs/*.log @@ -10,6 +10,3 @@ secrets.xml /smarty/templates_c/* !/smarty/templates_c/README.md /admin/.htaccess - -# file system -.DS_* diff --git a/.htaccess b/.htaccess index d890306..ca5afec 100644 --- a/.htaccess +++ b/.htaccess @@ -1,21 +1,9 @@ -# block access to all Apache config files - - Order allow,deny - Deny from all - - -# block access by file extension - - Order allow,deny - Deny from all - +# Block access to things that should stay private + + Order allow,deny + Deny from all + # block verboten directories -RedirectMatch 404 /classes(/|$) +RedirectMatch 404 /src(/|$) RedirectMatch 404 /logs(/|$) - -# block access to secrets.xml - - Order allow,deny - Deny from all - diff --git a/README.md b/README.md index b978060..ebcb172 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This LTI is explicitly inspired by the University of Michigan presentation at In The reason that St. Mark's chose to use Canvas was to reduce the friction in communication about student progress and feedback, specifically with the trio of the student, the teacher and the student's advisor in mind. We want the advisor to be able to provide the student with the best possible counsel and advice, with minimal overhead to the teacher but _with_ high coordination with the teacher. -### The Setup +## The Setup Each of our advisors (who are mostly teaching faculty) advise a group of 3-7 advisees. Each advisor has an advisory course in which the advisor is the teacher and the advisees are the students. These courses are created within our Advisory Groups sub-account. @@ -14,20 +14,20 @@ This LTI is placed in the Advisory Groups sub-account, which causes it to: - Display an administrative dashboard to account administrators within the Advisory Groups sub-account. This provides a GUI for further configuration specific to our advisory setup (e.g. creating a matching observer user for each advisee) - Display a course-navigation entry to teachers of every course in the Advisory Groups sub-account. - -### Course-level Advisor Dashboard -![Course-level Advisor Dashboard](/docs/course-level-dashboard.png) +## Course-level Advisor Dashboard + +![Course-level Advisor Dashboard](/images/course-level-dashboard.png) At the course level, advisors are given several options: - - A "Relative Grades" view, that shows an advisee's performance in their classes relative to their classmates. This is drawn from the Analytics API, and normalizes all assignments to be presented as a percentage (without regard to total point value). Essentially, this is a variation of the Course Analytics view already available to teachers, but is (we think) a simpler, easier to "grok" presentation for advisors: is my advisee washed up on the beach, in the "river" with their peers or surfing the waves independently. ![Relative Grades](/docs/relative-grades.png) + - A "Relative Grades" view, that shows an advisee's performance in their classes relative to their classmates. This is drawn from the Analytics API, and normalizes all assignments to be presented as a percentage (without regard to total point value). Essentially, this is a variation of the Course Analytics view already available to teachers, but is (we think) a simpler, easier to "grok" presentation for advisors: is my advisee washed up on the beach, in the "river" with their peers or surfing the waves independently. ![Relative Grades](/images/relative-grades.png) - A listing of observer logins for their advisees. These observers are paired with the advisee via the User Observees API, which causes enrollment changes for the advisee to be synched with the observer (requiring no intervention from [our enrollment management script](https://github.com/smtech/canvas-blackbaud-enrollment-automation), which just handles student enrollments). - Quick access to the Faculty Journal for advisees, via our [Faculty Journal](https://github.com/smtech/canvas-faculty-journal) add-on, which allows teachers to browse through the faculty journal entries for entire classes a là SpeedGrader. - -### Account-level Administrative Dashboard -![Account-level Administrative Dashboard](/docs/account-level-dashboard.png) +## Account-level Administrative Dashboard + +![Account-level Administrative Dashboard](/images/account-level-dashboard.png) At the account level, administrators are able to: @@ -35,6 +35,60 @@ At the account level, administrators are able to: - Rename Advisory Groups. This may really be a one-off, but our advisory groups came out of our SIS this year with really dumb, non-transparent names. So this runs through the advisory group courses and names them with the teacher's last name: "Battis Advisory Group". - Download Observers CSV. Periodically, updates to the observer passwords fail. Not entirely clear why. This is a short-circuit around that problem: it generates a `users.csv` document with the stored passwords for all of the observers, for easy SIS CSV import. A really elegant developer would have just fed that straight into the API. -### Planned Improvements +## Install + + The _best_ way to install this on your own LAMP server would be: + +## Requirements + +###### Composer + +This LTI uses [Composer](http://getcomposer.org) to manage dependencies. Before starting, make sure that you have [installed the Composer command line tool](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx)! + +###### LAMP Stack + +This is designed to run on a LAMP stack (Linux, Apache, MySQL, PHP). Presumably a savvy user could munge it around to work in some other configuration without too much sweat. But… + +###### SSL Certificate (quasi-optional) + +Additionally, all of the LTI interactions will run more smoothly if you are hosting this tool on a web server with valid SSL credentials. Happily, [Let's Encrypt](https://letsencrypt.org/) provides a free service and tool to acquire those credentials! + +## Install + + 1. Clone the repository (command line below, but you can use your favorite GUI GitHub client instead). + ```BASH + git clone https://github.com/smtech/advisor-dashboard.git + ``` + 2. Install dependencies (the `--prefer-dist` flag indicates that you want the distribution version of the dependency packages, excluding tests, examples, documentation, etc.) + ```BASH + cd advisor-dashboard + composer install --prefer-dist + ``` + 3. Copy `config-example.xml` to `config.xml` and edit with your credentials. + + _You **must** include MySQL credentials, but you may choose between manually generating an API access token and providing your canvas instance `url` and the `token` **or** [acquiring your own Canvas Developer credentials](https://canvas.instructure.com/doc/api/file.oauth.html#oauth2-flow-0) and providing that `key` and `secret` to be used during installation to interactively request an API access token._ + + 4. If you've been doing all this on your web server, sweet! If not, upload the `advisor-dashboard` directory to your web server now. + + After uploading your files (or basking in the glow of not having to upload the files), double-check your file permissions to make sure that the web server user (usually `apache` or `www-data`) has read and write access to the the `advisor-dashboard` hierarchy. + + _Technically you only need **really** write access to the `/logs` directory and to `/vendor/battis/bootstrapsmarty/cache` and `/vendor/battis/bootstrapsmarty/templates_c` …but life is just simpler if you give write access to the whole directory._ + + 5. Point your browser at the install directory: + + ``` + https://your-domain.com/path/to/advisor-dashboard + ``` + + This will prompt the app to load the settings from `config.xml` into its cache and to configure itself to be ready to manage LTI Tool Consumers. You will be redirected to the consumer management page `admin/` (after interactively providing an API access token if `config.xml` has a `key` and `secret`). + + 6. Create a Tool Consumer. The name is human-readable and just needs to mean something to you. The key and secret are automagically generated, but you can change them to whatever you want. + + 7. Create an app placement in Canvas in your account of advisory courses. Use the "by URL" app configuration using the URL provided on the consumers page (and the key and secret for the consumer you just created). + + + You're all set! + +## Planned Improvements -In general, improvements to our tools come through direct feedback from our faculty and students. We track specific requests (and bugs) using [the GitHub issue tracker for this repository](https://github.com/smtech/advisor-dashboard/issues). \ No newline at end of file +In general, improvements to our tools come through direct feedback from our faculty and students. We track specific requests (and bugs) using [the GitHub issue tracker for this repository](https://github.com/smtech/advisor-dashboard/issues). diff --git a/account/common.inc.php b/account/common.inc.php index 4d598d6..6eae6fa 100644 --- a/account/common.inc.php +++ b/account/common.inc.php @@ -1,7 +1,5 @@ addTemplateDir(__DIR__ . '/templates', basename(__DIR__)); - -?> \ No newline at end of file +$toolbox->smarty_prependTemplateDir(__DIR__ . '/templates', basename(__DIR__)); diff --git a/account/create-advisor-observers.php b/account/create-advisor-observers.php index c9d0165..080cdf5 100644 --- a/account/create-advisor-observers.php +++ b/account/create-advisor-observers.php @@ -1,123 +1,130 @@ mysql_query("SHOW TABLES LIKE 'lti_%'")->num_rows == 0); { + $toolbox->mysql_query(" + CREATE TABLE `observers` ( + `id` int(11) unsigned NOT NULL, + `password` varchar(10) NOT NULL DEFAULT '', + `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=latin1; + "); +} + +$toolbox->cache_pushKey(basename(__FILE__, '.php')); { + + $pwgen = new PWGen( + PASSWORD_LENGTH, + PASSWORD_SECURE, + PASSWORD_NUMERALS, + PASSWORD_CAPITALS, + PASSWORD_AMBIGUOUS, + PASSWORD_NO_VOWELS, + PASSWORD_SYMBOLS + ); + + $step = (empty($_REQUEST['step']) ? STEP_INSTRUCTIONS : $_REQUEST['step']); + + switch ($step) { + case STEP_GENERATE: + + // TODO test for account and term -switch ($step) { - case STEP_GENERATE: - - // TODO test for account and term - - //try { /* walk through all of our advisory courses.. */ - $advisories = $api->get( - "accounts/{$_REQUEST['account']}/courses", - array( - 'with_enrollments' => 'true', - 'enrollment_term_id' => $_REQUEST['term'] - ) - ); - + $advisories = $toolbox->api_get("accounts/{$_REQUEST['account']}/courses", [ + 'with_enrollments' => 'true', + 'enrollment_term_id' => $_REQUEST['term'] + ]); + $courses = 0; $updated = 0; $created = 0; $reset = 0; - + foreach($advisories as $advisory) { /* cache the teacher */ - $advisors = $api->get("courses/{$advisory['id']}/users", - array( - 'enrollment_role' => 'TeacherEnrollment' - ) - ); + $advisors = $toolbox->api_get("courses/{$advisory['id']}/users", [ + 'enrollment_role' => 'TeacherEnrollment' + ]); if ($advisors->count()) { $advisor = $advisors[0]; $courses++; } else { - $smarty->addMessage( + $toolbox->smarty_addMessage( "{$advisory['name']}", - "No teacher was found in this advisory and it was skipped.", + "No teacher was found in this advisory and it was skipped.", NotificationMessage::ERROR ); break; } $advisorLastName = substr($advisor['sortable_name'], 0, strpos($advisor['sortable_name'], ',')); - + /* look at all the student enrollments... */ - $advisees = $api->get("courses/{$advisory['id']}/users", - array( - 'enrollment_role' => 'StudentEnrollment' - ) - ); - + $advisees = $toolbox->api_get("courses/{$advisory['id']}/users", [ + 'enrollment_role' => 'StudentEnrollment' + ]); + foreach($advisees as $advisee) { - - $observer = array(); - + /* generate what the advisor account info should be */ - $observer['sis_user_id'] = "{$advisee['sis_user_id']}-advisor"; - $observer['login'] = strtolower('advisor' . substr($advisee['login_id'], 0, strpos($advisee['login_id'], '@'))); - $observer['password'] = $pwgen->generate(); - $observer['name'] = "{$advisee['name']} ($advisorLastName Advisor)"; - $observer['sortable_name'] = "{$advisee['sortable_name']} ($advisorLastName Advisor)"; - $observer['short_name'] = "{$advisee['short_name']} ($advisorLastName Advisor)"; - - - /* this email format works for Google Apps domains -- it's the advisor's email address with a tag that identifies the email as relating to the advisee */ - $observer['email'] = strtolower(substr($advisor['sis_login_id'], 0, strpos($advisor['sis_login_id'], '@')) . '+' . substr($advisee['sis_login_id'], 0, strpos($advisee['sis_login_id'], '@')) . substr($advisor['sis_login_id'], strpos($advisor['sis_login_id'], '@'))); - + $observer = [ + 'sis_user_id' => "{$advisee['sis_user_id']}-advisor", + 'login' => strtolower('advisor' . substr($advisee['login_id'], 0, strpos($advisee['login_id'], '@'))), + 'password' => $pwgen->generate(), + 'name' => "{$advisee['name']} ($advisorLastName Advisor)", + 'sortable_name' => "{$advisee['sortable_name']} ($advisorLastName Advisor)", + 'short_name' => "{$advisee['short_name']} ($advisorLastName Advisor)", + + /* this email format works for Google Apps domains -- it's the advisor's email address with a tag that identifies the email as relating to the advisee */ + 'email' => strtolower(substr($advisor['sis_login_id'], 0, strpos($advisor['sis_login_id'], '@')) . '+' . substr($advisee['sis_login_id'], 0, strpos($advisee['sis_login_id'], '@')) . substr($advisor['sis_login_id'], strpos($advisor['sis_login_id'], '@'))) + ]; + /* check for an existing advisor account */ $existing = true; try { - $existing = $api->get("users/sis_user_id:{$observer['sis_user_id']}"); + $existing = $toolbox->api_get("users/sis_user_id:{$observer['sis_user_id']}"); } catch (Exception $e) { /* if the request generates an error... the observer does not exist */ - $existing = false; + $existing = false; } - + /* if there is already an advisor account, update it */ if ($existing) { - + /* update name */ - $api->put("users/{$existing['id']}", - array( - 'user[name]' => $observer['name'], - 'user[short_name]' => $observer['short_name'], - 'user[sortable_name]' => $observer['sortable_name'], - ) - ); - + $toolbox->api_put("users/{$existing['id']}", [ + 'user[name]' => $observer['name'], + 'user[short_name]' => $observer['short_name'], + 'user[sortable_name]' => $observer['sortable_name'], + ]); + /* update email */ - $communicationChannels = $api->get("users/{$existing['id']}/communication_channels"); + $communicationChannels = $toolbox->api_get("users/{$existing['id']}/communication_channels"); $emailExists = false; - $channelsToDelete = array(); + $channelsToDelete = []; foreach($communicationChannels as $communicationChannel) { if ($communicationChannel['address'] != $observer['email']) { $channelsToDelete[] = $communicationChannel['id']; @@ -126,23 +133,21 @@ } } if (!$emailExists) { - $api->post("users/{$existing['id']}/communication_channels", - array( - 'communication_channel[type]' => 'email', - 'communication_channel[address]' => $observer['email'], - 'skip_confirmation' => true, - 'position' => 1 - ) - ); + $toolbox->api_post("users/{$existing['id']}/communication_channels", [ + 'communication_channel[type]' => 'email', + 'communication_channel[address]' => $observer['email'], + 'skip_confirmation' => true, + 'position' => 1 + ]); } foreach($channelsToDelete as $channelToDelete) { - $api->delete("users/{$existing['id']}/communication_channels/{$channelToDelete}"); + $toolbox->api_delete("users/{$existing['id']}/communication_channels/{$channelToDelete}"); } - + /* turn off notifications */ - $communicationChannels = $api->get("users/{$existing['id']}/communication_channels"); - $notificationPreferences = $api->get("users/{$existing['id']}/communication_channels/{$communicationChannels[0]['id']}/notification_preferences"); - $newPrefs = array(); + $communicationChannels = $toolbox->api_get("users/{$existing['id']}/communication_channels"); + $notificationPreferences = $toolbox->api_get("users/{$existing['id']}/communication_channels/{$communicationChannels[0]['id']}/notification_preferences"); + $newPrefs = []; foreach ($notificationPreferences['notification_preferences'] as $pref) { if (($pref['frequency'] != 'never') && ($pref['notification'] != 'confirm_sms_communication_channel')) { $newPrefs["notification_preferences[{$pref['notification']}][frequency]"] = 'never'; @@ -150,22 +155,20 @@ } if (count($newPrefs)) { $newPrefs['as_user_id'] = $existing['id']; - $api->put("users/self/communication_channels/{$communicationChannels[0]['id']}/notification_preferences", $newPrefs); + $toolbox->api_put("users/self/communication_channels/{$communicationChannels[0]['id']}/notification_preferences", $newPrefs); } - + /* reset password */ - if (isset($_REQUEST['reset_passwords'])) { - $logins = $api->get("users/{$existing['id']}/logins"); - + if (!empty($_REQUEST['reset_passwords'])) { + $logins = $toolbox->api_get("users/{$existing['id']}/logins"); + // FIXME I'm totally just assuming that a user account has a login (and only one) - $api->put("accounts/1/logins/{$logins[0]['id']}", - array( - 'login[password]' => $observer['password'] - ) - ); - + $toolbox->api_put("accounts/1/logins/{$logins[0]['id']}", [ + 'login[password]' => $observer['password'] + ]); + // FIXME need some error-checking here - $sql->query(" + $toolbox->mysql_query(" UPDATE `observers` SET `password` = '{$observer['password']}' @@ -176,25 +179,23 @@ $reset++; } $updated++; - + /* otherwise, create one! */ } else { - $existing = $api->post('accounts/1/users', - array( - 'user[name]' => $observer['name'], - 'user[short_name]' => $observer['short_name'], - 'user[sortable_name]' => $observer['sortable_name'], - 'pseudonym[unique_id]' => $observer['login'], - 'psuedonym[password]' => $observer['password'], - 'pseudonym[sis_user_id]' => $observer['sis_user_id'], - 'communication_channel[type]' => 'email', - 'communication_channel[address]' => $observer['email'], - 'communication_channel[skip_confirmation]' => true - ) - ); - + $existing = $toolbox->api_post('accounts/1/users', [ + 'user[name]' => $observer['name'], + 'user[short_name]' => $observer['short_name'], + 'user[sortable_name]' => $observer['sortable_name'], + 'pseudonym[unique_id]' => $observer['login'], + 'psuedonym[password]' => $observer['password'], + 'pseudonym[sis_user_id]' => $observer['sis_user_id'], + 'communication_channel[type]' => 'email', + 'communication_channel[address]' => $observer['email'], + 'communication_channel[skip_confirmation]' => true + ]); + // FIXME need a little error-checking here - $sql->query(" + $toolbox->mysql_query(" INSERT INTO `observers` ( `id`, @@ -204,41 +205,32 @@ '{$observer['password']}' ) "); - + $created++; } - + /* set up observation pairing */ - $api->put("users/{$existing['id']}/observees/{$advisee['id']}"); + $toolbox->api_put("users/{$existing['id']}/observees/{$advisee['id']}"); } - - // TODO rename advisory courses } - - $smarty->addMessage( + + $toolbox->smarty_addMessage( 'Advisor-Observers', "$created new observers created, $updated observers updated ($reset passwords reset) in $courses advisory groups.", NotificationMessage::GOOD ); - - //} catch (Exception $e) { - // exceptionErrorMessage($e); - //} - - /* flows into STEP_INSTRUCTIONS */ - - case STEP_INSTRUCTIONS: - default: - $smarty->assign('terms', getTermList()); - $smarty->assign( - 'formHidden', - array( - 'step' => STEP_GENERATE, - 'account' => $_SESSION['accountId'] - ) - ); - $smarty->display(basename(__FILE__, '.php') . '/instructions.tpl'); -} + /* flows into STEP_INSTRUCTIONS */ -?> \ No newline at end of file + case STEP_INSTRUCTIONS: + default: + $toolbox->smarty_assign([ + 'terms' => $toolbox->getTermList(), + 'formHidden' => [ + 'step' => STEP_GENERATE, + 'account' => $_SESSION[ACCOUNT_ID] + ] + ]); + $toolbox->smarty_display(basename(__FILE__, '.php') . '/instructions.tpl'); + } +} $toolbox->cache_popKey(); diff --git a/account/download-observers-csv.php b/account/download-observers-csv.php index d288379..18374dc 100644 --- a/account/download-observers-csv.php +++ b/account/download-observers-csv.php @@ -1,6 +1,11 @@ cache_pushKey(basename(__FILE__, '.php')); { + $step = (empty($_REQUEST['step']) ? STEP_INSTRUCTIONS : $_REQUEST['step']); -define('STEP_INSTRUCTIONS', 1); -define('STEP_CSV', 2); + switch ($step) { -$step = (empty($_REQUEST['step']) ? STEP_INSTRUCTIONS : $_REQUEST['step']); + case STEP_CSV: + try { + $account = (empty($_REQUEST['account']) ? 1 : $_REQUEST['account']); + if (empty($_REQUEST['account'])) { + $toolbox->smarty_addMessage( + 'No Account', + 'No account specified, all users included in CSV file.', + NotificationMessage::WARNING + ); + } -switch ($step) { - - case STEP_CSV: - try { - $account = (empty($_REQUEST['account']) ? 1 : $_REQUEST['account']); - if (empty($_REQUEST['account'])) { - $smarty->addMessage( - 'No Account', - 'No account specified, all users included in CSV file.', - NotificationMessage::WARNING - ); - } - - $data = $cache->getCache("$account/users"); - if ($data === false) { - $users = $api->get( - "accounts/$account/users", - array( + $data = $toolbox->cache_get("$account/users"); + if ($data === false) { + $users = $toolbox->api_get("accounts/$account/users", [ 'search_term' => '-advisor' - ) - ); - $data[] = array( - 'id', 'user_id', 'login_id', 'password', 'full_name', 'sortable_name', 'short_name', - 'email', 'status' - ); - foreach ($users as $user) { - $response = $sql->query(" - SELECT * - FROM `observers` - WHERE - `id` = '{$user['id']}' - LIMIT 1 - "); - $row = $response->fetch_assoc(); - if ($row) { - $data[] = array( - blank($user, 'id'), - blank($user, 'sis_user_id'), - blank($user, 'login_id'), - blank($row, 'password'), - blank($user, 'name'), - blank($user, 'sortable_name'), - blank($user, 'short_name'), - blank($user, 'email'), - 'active' - ); + ]); + $data[] = ['id', 'user_id', 'login_id', 'password', 'full_name', 'sortable_name', 'short_name', 'email', 'status']; + foreach ($users as $user) { + $response = $toolbox->mysql_query(" + SELECT * + FROM `observers` + WHERE + `id` = '{$user['id']}' + LIMIT 1 + "); + $row = $response->fetch_assoc(); + if ($row) { + $data[] = [ + blank($user, 'id'), + blank($user, 'sis_user_id'), + blank($user, 'login_id'), + blank($row, 'password'), + blank($user, 'name'), + blank($user, 'sortable_name'), + blank($user, 'short_name'), + blank($user, 'email'), + 'active' + ]; + } } - } - $cache->setCache("$account/users", $data, 15 * 60); + $toolbox->cache_set("$account/users", $data); + } + + $toolbox->smarty_assign([ + 'csv' => basename(__FILE__, '.php') . "/$account/users", + 'filename' => date('Y-m-d_H-i-s') . "_account-{$account}_observers" + ]); + $toolbox->smarty_addMessage( + 'Ready for Download', + 'users.csv is ready and download should start automatically in a few seconds. Click the link below if the download does not start automatically.', + NotificationMessage::GOOD + ); + } catch (Exception $e) { + $toolbox->smarty_addMessage('Error ' . $e->getCode(), $e->getMessage(), NotificationMessage::ERROR); } - $smarty->assign('csv', basename(__FILE__, '.php') . "/$account/users"); - $smarty->assign('filename', date('Y-m-d_H-i-s') . "_account-{$account}_observers"); - $smarty->addMessage( - 'Ready for Download', - 'users.csv is ready and download should start automatically in a few seconds. Click the link below if the download does not start automatically.', - NotificationMessage::GOOD - ); - } catch (Exception $e) { - $smarty->addMessage('Error ' . $e->getCode(), $e->getMessage(), NotificationMessage::ERROR); - } - - /* flows into STEP_INSTRUCTIONS */ - - case STEP_INSTRUCTIONS: - default: - $smarty->assign('formHidden', array('step' => STEP_CSV, 'account' => $_SESSION['accountId'])); - $smarty->display(basename(__FILE__, '.php') . '/instructions.tpl'); -} - -?> \ No newline at end of file + + /* flows into STEP_INSTRUCTIONS */ + + case STEP_INSTRUCTIONS: + default: + $toolbox->smarty_assign('formHidden', [ + 'step' => STEP_CSV, + 'account' => $_SESSION[ACCOUNT_ID] + ]); + $toolbox->smarty_display(basename(__FILE__, '.php') . '/instructions.tpl'); + } +} $toolbox->cache_popKey(); diff --git a/account/index.php b/account/index.php index c802dda..006a071 100644 --- a/account/index.php +++ b/account/index.php @@ -1,7 +1,4 @@ display('subpage.tpl'); - -?> \ No newline at end of file +header('Location: create-advisor-observers.php'); +exit; diff --git a/account/rename-advisory-groups.php b/account/rename-advisory-groups.php index be18dc4..a9695b4 100644 --- a/account/rename-advisory-groups.php +++ b/account/rename-advisory-groups.php @@ -2,50 +2,40 @@ require_once('common.inc.php'); +use Battis\BootstrapSmarty\NotificationMessage; + define('STEP_INSTRUCTIONS', 1); define('STEP_RENAME', 2); $step = (empty($_REQUEST['step']) ? STEP_INSTRUCTIONS : $_REQUEST['step']); switch ($step) { - + case STEP_RENAME: try { $updated = 0; $unchanged = 0; - $courses = $api->get( - "accounts/{$_REQUEST['account']}/courses", - array( - 'enrollment_term_id' => $_REQUEST['term'], - 'with_enrollments' => 'true' - ) - ); + $courses = $toolbox->api_get("accounts/{$_REQUEST['account']}/courses", [ + 'enrollment_term_id' => $_REQUEST['term'], + 'with_enrollments' => 'true' + ]); foreach ($courses as $course) { - $teachers = $api->get( - "/courses/{$course['id']}/enrollments", - array( - 'type' => 'TeacherEnrollment' - ) - ); + $teachers = $toolbox->api_get("/courses/{$course['id']}/enrollments", [ + 'type' => 'TeacherEnrollment' + ]); if ($teacher = $teachers[0]['user']) { $nameParts = explode(',', $teacher['sortable_name']); $courseName = trim($nameParts[0]) . ' Advisory Group'; - $api->put( - "courses/{$course['id']}", - array( - 'course[name]' => $courseName, - 'course[course_code]' => $courseName - ) - ); - $sections = $api->get("courses/{$course['id']}/sections"); + $toolbox->api_put("courses/{$course['id']}", [ + 'course[name]' => $courseName, + 'course[course_code]' => $courseName + ]); + $sections = $toolbox->api_get("courses/{$course['id']}/sections"); foreach($sections as $section) { if ($section['name'] == $course['name']) { - $api->put( - "sections/{$sections[0]['id']}", - array( - 'course_section[name]' => $courseName - ) - ); + $toolbox->api_put("sections/{$sections[0]['id']}", [ + 'course_section[name]' => $courseName + ]); } } $updated++; @@ -54,34 +44,28 @@ } } } catch (Exception $e) { - exceptionErrorMessage($e); + $toolbox->smarty_addMessage('Error ' . $e->getCode(), $e->getMessage(), NotificationMessage::ERROR); } - $courses = $api->get( - "accounts/{$_REQUEST['account']}/courses", - array( - 'enrollment_term_id' => $_REQUEST['term'], - 'with_enrollments' => 'true', - 'published' => 'true' - ) - ); - - $smarty->addMessage( + $courses = $toolbox->api_get("accounts/{$_REQUEST['account']}/courses", [ + 'enrollment_term_id' => $_REQUEST['term'], + 'with_enrollments' => 'true', + 'published' => 'true' + ]); + + $toolbox->smarty_addMessage( 'Renamed advisory courses', "$updated courses were renamed, and $unchanged were left unchanged.", NotificationMessage::GOOD ); - + case STEP_INSTRUCTIONS: default: - $smarty->assign('terms', getTermList()); - $smarty->assign( - 'formHidden', - array( + $toolbox->smarty_assign([ + 'terms' => $toolbox->getTermList(), + 'formHidden' => [ 'step' => STEP_RENAME, 'account' => $_SESSION['accountId'] - ) - ); - $smarty->display(basename(__FILE__, '.php') . '/instructions.tpl'); + ] + ]); + $toolbox->smarty_display(basename(__FILE__, '.php') . '/instructions.tpl'); } - -?> \ No newline at end of file diff --git a/admin/README.md b/admin/README.md deleted file mode 100644 index aa1aefe..0000000 --- a/admin/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# admin/ - -Administrative interface for the service that backs the LTI Tool Provider (TP). \ No newline at end of file diff --git a/admin/common.inc.php b/admin/common.inc.php index cea323c..06ac97e 100644 --- a/admin/common.inc.php +++ b/admin/common.inc.php @@ -1,7 +1,7 @@ assign('category', 'Admin Panel'); +require_once __DIR__ . '/../common.inc.php'; -?> \ No newline at end of file +use Battis\DataUtilities; + +$toolbox->smarty_assign('category', DataUtilities::titleCase(basename(__DIR__))); diff --git a/admin/consumers-control-panel.php b/admin/consumers-control-panel.php new file mode 100644 index 0000000..826e84c --- /dev/null +++ b/admin/consumers-control-panel.php @@ -0,0 +1,79 @@ +getMySQL()), + true // wicked confusing _not_ to autoenable + ); + + /* pre-fill secret if not editing an existing consumer */ + if (empty($key)) { + $consumer->secret = LTI_Data_Connector::getRandomString(32); + } + + return $consumer; +} + +/* load requested consumer (or create new if none requested) */ +$consumer = createConsumer($toolbox, $key); + +/* what are we asked to do with this consumer? */ +switch ($action) { + case 'update': + case 'insert': { + $consumer->name = $name; + $consumer->secret = $secret; + $consumer->enabled = $enabled; + if (!$consumer->save()) { + $toolbox->smarty_addMessage( + 'Error saving consumer', + 'There was an error attempting to save your new or updated consumer information to the database.', + NotificationMessage::ERROR + ); + } + break; + } + case 'delete': { + $consumer->delete(); + break; + } + case 'select': { + $toolbox->smarty_assign('key', $key); + break; + } +} + +/* + * if action was anything other than 'select', create a new empty consumer to + * fill the form with + */ +if ($action && $action !== 'select') { + $consumer = createConsumer($toolbox); +} + +/* display a list of consumers */ +$consumers = $toolbox->lti_getConsumers(); +$toolbox->smarty_assign([ + 'consumers' => $consumers, + 'consumer' => $consumer, + 'formAction' => $_SERVER['PHP_SELF'], + 'appUrl' => $toolbox->config('APP_URL') +]); +$toolbox->smarty_display(basename(__FILE__, '.php') . '.tpl'); diff --git a/admin/consumers.php b/admin/consumers.php deleted file mode 100644 index 1d092a8..0000000 --- a/admin/consumers.php +++ /dev/null @@ -1,91 +0,0 @@ -name = $_name; - $consumer->secret = $_REQUEST['secret']; - $consumer->enabled = isset($_REQUEST['enabled']); - if (!$consumer->save()) { - $valid = false; - $message = "Consumer could not be saved. {$sql->error}"; - } - } - - if (!$valid) { - $smarty->addMessage( - 'Required information missing', - $message, - NotificationMessage::ERROR - ); - } - -/* look up consumer to edit, if requested */ -} elseif (isset($_REQUEST['consumer_key'])) { - $consumer = new LTI_Tool_Consumer($_REQUEST['consumer_key'], LTI_Data_Connector::getDataConnector($sql)); - if (isset($_REQUEST['action'])) - switch ($_REQUEST['action']) { - case 'delete': { - $consumer->delete(); - break; - } - case 'select': { - $name = $consumer->name; - $key = $consumer->getKey(); - $secret = $consumer->secret; - $enabled = $consumer->enabled; - break; - } - case 'update': - case 'insert': - default: { - // leave default form values set - } - } -} - -/* display a list of consumers */ -$response = $sql->query("SELECT * FROM `" . LTI_Data_Connector::CONSUMER_TABLE_NAME . "` ORDER BY `name` ASC, `consumer_key` ASC"); -$consumers = array(); -while ($consumer = $response->fetch_assoc()) { - $consumers[] = $consumer; -} -if (!empty($consumers)) { - $smarty->assign('fields', array_keys($consumers[0])); -} -$smarty->assign('consumers', $consumers); - -/* use current values */ -$smarty->assign('name', $name); -$smarty->assign('key', $key); -$smarty->assign('secret', $secret); -$smarty->assign('enabled', $enabled); - -$smarty->assign('formAction', $_SERVER['PHP_SELF']); -$smarty->assign('requestKey', (isset($_REQUEST['consumer_key']) ? $_REQUEST['consumer_key'] : null)); - -$smarty->display('lti-consumers.tpl'); \ No newline at end of file diff --git a/admin/index.php b/admin/index.php index a95010c..3726f45 100644 --- a/admin/index.php +++ b/admin/index.php @@ -1,11 +1,6 @@ \ No newline at end of file +header('Location: consumers-control-panel.php'); +exit; diff --git a/admin/install-app.inc.php b/admin/install-app.inc.php deleted file mode 100644 index 86a1332..0000000 --- a/admin/install-app.inc.php +++ /dev/null @@ -1,39 +0,0 @@ -addMessage( - 'App metadata updated', - 'Application metadata has been updated to create config.xml', - NotificationMessage::GOOD -); - -?> diff --git a/admin/install.php b/admin/install.php deleted file mode 100644 index a67487b..0000000 --- a/admin/install.php +++ /dev/null @@ -1,78 +0,0 @@ -addMessage( - 'App already installed', - 'It appears that the application has already been installed and is ready for - use.' - ); - -/* ...otherwise, let's start with the SECRETS_FILE */ -} else { - if(!file_exists(SECRETS_FILE)) { - if (isset($_REQUEST['step']) && $_REQUEST['step'] == CanvasAPIviaLTI_Installer::SECRETS_ENTERED_STEP) { - CanvasAPIviaLTI_Installer::createSecretsFile(CanvasAPIviaLTI_Installer::SECRETS_ENTERED_STEP); - } else { - CanvasAPIviaLTI_Installer::createSecretsFile(); - } - } -} - -/* establish our database connection */ -$secrets = initSecrets(); -$sql = initMySql(); - -try { - if (!isset($_REQUEST['step'])) { - /* load all of our various schema into the database... */ - CanvasAPIviaLTI_Installer::createLTIDatabaseTables(); - CanvasAPIviaLTI_Installer::createAppDatabaseTables(); - - /* ...and initialize the app metadata... */ - $metadata = CanvasAPIviaLTI_Installer::createAppMetadata(); - - /* ...optionally, acquire an API token for the app */ - CanvasAPIviaLTI_Installer::acquireAPIToken(CanvasAPIviaLTI_Installer::API_DECISION_NEEDED_STEP); - } else { - $metadata = new AppMetadata($sql, $secrets->app->id); - $skip = (isset($_REQUEST['skip']) ? $_REQUEST['skip'] : false); - CanvasAPIviaLTI_Installer::acquireAPIToken($_REQUEST['step'], $skip); - } -} catch (CanvasAPIviaLTI_Installer_Exception $e) { - $smarty->addMessage( - 'LTI Installer error', - $e->getMessage() . ' [Error ' . $e->getCode() . ']', - NotificationMessage::ERROR - ); - $smarty->display(); - exit; -} - -try { - /* any additional app-specific install steps */ - require_once('install-app.inc.php'); -} catch (CanvasAPIviaLTI_Installer_Exception $e) { - $smarty->addMessage( - 'App Installer error', - $e->getMessage() . ' [Error ' . $e->getCode() . ']', - NotificationMessage::ERROR - ); -} - -/* reset $metadata to get update any computed values */ -$metadata = initAppMetadata(); - -$smarty->assign('content', ' -

Installation complete

-

The application installation is complete. You may configure LTI Tool Consumer (TC) information by navigating to Consumers.

' -); - -$smarty->display(); - -?> \ No newline at end of file diff --git a/admin/oauth.php b/admin/oauth.php index b5033b4..833f040 100644 --- a/admin/oauth.php +++ b/admin/oauth.php @@ -1,56 +1,59 @@ oauth->id, - (string) $secrets->oauth->key, - "{$metadata['APP_URL']}/admin/install.php?step={$_REQUEST['step']}", - (string) $secrets->app->name - ); - } - - /* OAuthNegotiator will return here periodically and we will just keep re-instantiating it until it finishes */ - $oauth = new OAuthNegotiator(); -} catch (OAuthNegotiator_Exception $e) { - $smarty->addMessage( - 'OAuthNegotiator error', - $e->getMessage() . ' [Error ' . $e->getCode() . ']', - NotificationMessage::ERROR - ); - $smarty->assign( - 'content', - '

Install Interrupted

-

An error interrupted the installation process. To restart, click here.

'); - $smarty->display(); +/* do we have a Canvas instance URL yet? */ +if (empty($_SESSION[CANVAS_INSTANCE_URL]) && empty ($_REQUEST['url'])) { + $toolbox->smarty_assign('formAction', $_SERVER['PHP_SELF']); + $toolbox->smarty_display('oauth.tpl'); exit; +} elseif (empty($_SESSION[CANVAS_INSTANCE_URL]) && !empty($_REQUEST['url'])) { + $_SESSION[CANVAS_INSTANCE_URL] = $_REQUEST['url']; } -?> \ No newline at end of file +$canvas = $toolbox->config('TOOL_CANVAS_API'); +$provider = new CanvasLMS([ + 'clientId' => $canvas['key'], + 'clientSecret' => $canvas['secret'], + 'purpose' => $toolbox->config('TOOL_NAME'), + 'redirectUri' => DataUtilities::URLfromPath(__FILE__), + 'canvasInstanceUrl' => $_SESSION[CANVAS_INSTANCE_URL] +]); + +/* if we don't already have an authorization code, let's get one! */ +if (!isset($_GET['code'])) { + $authorizationUrl = $provider->getAuthorizationUrl(); + $_SESSION[OAUTH_STATE] = $provider->getState(); + header("Location: $authorizationUrl"); + exit; + +/* check that the passed state matches the stored state to mitigate cross-site request forgery attacks */ +} elseif (empty($_GET['state']) || $_GET['state'] !== $_SESSION[OAUTH_STATE]) { + unset($_SESSION[OAUTH_STATE]); + exit('Invalid state'); + +} else { + /* acquire and save our token (using our existing code) */ + $canvas = $toolbox->config('TOOL_CANVAS_API'); + $canvas['url'] = $_SESSION[CANVAS_INSTANCE_URL]; + $canvas['token'] = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']])->getToken(); + + /* pass back the newly-acquired token in session data */ + $_SESSION['TOOL_CANVAS_API'] = $canvas; + + /* return to what we were doing before we had to authenticate */ + header("Location: {$_SESSION['oauth-return']}"); + unset($_SESSION[OAUTH_STATE]); + unset($_SESSION['oauth-return']); + exit; +} diff --git a/admin/schema-app.sql b/admin/schema-app.sql deleted file mode 100644 index 104fec0..0000000 --- a/admin/schema-app.sql +++ /dev/null @@ -1,11 +0,0 @@ -/* the table to store user tokens (admin tokens are stored in the AppMetadata table) */ -CREATE TABLE IF NOT EXISTS `user_tokens` ( - `consumer_key` varchar(255) NOT NULL DEFAULT '', - `id` varchar(255) NOT NULL DEFAULT '', - `token` varchar(255) DEFAULT '', - `api_url` text, - `modified` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - -/* add any other tables to support the application logic here */ \ No newline at end of file diff --git a/app.php b/app.php deleted file mode 100644 index 040ee36..0000000 --- a/app.php +++ /dev/null @@ -1,15 +0,0 @@ -user->getResourceLink()->settings['custom_canvas_account_id'])) { - $_SESSION['accountId'] = $_SESSION['toolProvider']->user->getResourceLink()->settings['custom_canvas_account_id']; - header("Location: account/"); - exit; -} else { - $_SESSION['courseId'] = $_SESSION['toolProvider']->user->getResourceLink()->settings['custom_canvas_course_id']; - header('Location: course/'); -} - - -?> \ No newline at end of file diff --git a/classes/CanvasAPIviaLTI.php b/classes/CanvasAPIviaLTI.php deleted file mode 100644 index bbe652f..0000000 --- a/classes/CanvasAPIviaLTI.php +++ /dev/null @@ -1,116 +0,0 @@ - - **/ -class CanvasAPIviaLTI extends LTI_Tool_Provider { - - /** - * Handle launch requests, which start the application running - **/ - public function onLaunch() { - global $metadata; // FIXME grown-ups don't program like this - global $sql; // FIXME grown-ups don't program like this - - /* is this user in a role that can use this app? */ - if ($this->user->isAdmin() || $this->user->isStaff()) { - - /* set up any needed session variables */ - $_SESSION['consumer_key'] = $this->consumer->getKey(); - $_SESSION['resource_id'] = $this->resource_link->getId(); - $_SESSION['user_consumer_key'] = $this->user->getResourceLink()->getConsumer()->getKey(); - $_SESSION['user_id'] = $this->user->getId(); - $_SESSION['isStudent'] = $this->user->isLearner(); - $_SESSION['isContentItem'] = FALSE; - - /* do we have an admin API access token? */ - $haveToken = true; - if (empty($metadata['CANVAS_API_TOKEN'])) { - - /* ...if not, do we have a user API access token for this user? */ - $userToken = new UserAPIToken($_SESSION['user_consumer_key'], $_SESSION['user_id'], $sql); - if (empty($userToken->getToken())) { - - /* ...if this user has no token, let's start by getting one */ - $haveToken = false; - $this->redirectURL = "{$metadata['APP_URL']}/lti/token_request.php?oauth=request"; - } else { - - /* ...but if the user does have a token, rock on! */ - $_SESSION['isUserToken'] = true; - $_SESSION['apiToken'] = $userToken->getToken(); - //$_SESSION['apiUrl'] = $userToken->getAPIUrl(); - } - } else { - - /* ...if we have an admin API token, rock on! */ - $_SESSION['isUserToken'] = false; - $_SESSION['apiToken'] = $metadata['CANVAS_API_TOKEN']; - //$_SESSION['apiUrl'] = $metadata['CANVAS_API_URL']; - } - $_SESSION['apiUrl'] = 'https://' . $this->user->getResourceLink()->settings['custom_canvas_api_domain'] . '/api/v1'; - - /* pass control off to the app */ - if ($haveToken) { - $this->redirectURL = "{$metadata['APP_URL']}/app.php?lti-request=launch"; - } - - /* ...otherwise set an appropriate error message and fail */ - } else { - $this->reason = 'Invalid role'; - $this->isOK = false; - } - } - - /** - * Handle errors created while processing the LTI request - **/ - public function onError() { - global $metadata; // FIXME grown-ups don't program like this - - $this->redirectURL = "{$metadata['APP_URL']}/app.php?lti-request=error&reason={$this->reason}"; - } - - /** - * Handle dashboard requests (coming in LTI v2.0, I guess) - **/ - public function onDashboard() { - global $metadata; // FIXME grown-ups don't program like this - - $this->redirectURL = "{$metadata['APP_URL']}/app.php?lti-request=dashboard"; - } - - /** - * Handle configure requests (coming in LTI v2.0, I guess) - **/ - public function onConfigure() { - global $metadata; // FIXME grown-ups don't program like this - - $this->redirectURL = "{$metadata['APP_URL']}/app.php?lti-request=configure"; - } - - /** - * Handle content-item requests (that is we're a tool provider that adds a button in the content editor) - **/ - public function onContentItem() { - global $metadata; // FIXME grown-ups don't program like this - - $this->redirectURL = "{$metadata['APP_URL']}/app.php?lti-request=content-item"; - } -} - -/** - * Exceptions thrown by CanvasAPIviaLTI - * - * @author Seth Battis - **/ -class CanvasAPIviaLTI_Exception extends Exception { - const MISSING_SECRETS_FILE = 1; - const INVALID_SECRETS_FILE = 2; - const MYSQL_CONNECTION = 3; - const LAUNCH_REQUEST = 4; -} - -?> \ No newline at end of file diff --git a/classes/CanvasAPIviaLTI_Installer.php b/classes/CanvasAPIviaLTI_Installer.php deleted file mode 100644 index 946a1a1..0000000 --- a/classes/CanvasAPIviaLTI_Installer.php +++ /dev/null @@ -1,371 +0,0 @@ - - **/ -class CanvasAPIviaLTI_Installer { - const SECRETS_NEEDED_STEP = 1; - const SECRETS_ENTERED_STEP = 2; - const API_DECISION_NEEDED_STEP = 3; - const API_DECISION_ENTERED_STEP = 4; - const API_TOKEN_PROVIDED_STEP = 5; - - /** - * Generate a SECRETS_FILE from user input. - * - * @param scalar $step optional Where are we in the SECRETS_FILE creation workflow? (defaults to SECRETS_NEEDED_STEP -- the beginning) - * - * @throws CanvasAPIviaLTI_Installer_Exception If form submission does not contain all required MySQL credentals (host, username, password and database) - * @throws CanvasAPIviaLTI_Installer_Exception If SECRETS_FILE cannot be created - * @throws CanvasAPIviaLTI_Installer_Exception If $step is not a pre-defined *_STEP constant - **/ - public static function createSecretsFile($step = self::SECRETS_NEEDED_STEP) { - global $smarty; // FIXME grown-ups don't program like this - - switch ($step) { - case self::SECRETS_NEEDED_STEP: { - // FIXME passwords in clear text? oy. - $smarty->assign('content', ' -
-
-

Application Information

- - - - -
-
-

MySQL Connection

- - - - -
-
-

Canvas Developer Credentials

- - -
- - -
- '); - $smarty->display(); - exit; - } - - case self::SECRETS_ENTERED_STEP: { - if (isset($_REQUEST['name']) && isset($_REQUEST['id']) && isset($_REQUEST['admin_username']) && isset($_REQUEST['admin_password'])) { - if (isset($_REQUEST['host']) && isset($_REQUEST['username']) && isset($_REQUEST['password']) && isset($_REQUEST['database'])) { - $secrets = new SimpleXMLElement(''); - $app = $secrets->addChild('app'); - $app->addChild('name', $_REQUEST['name']); - $app->addChild('id', $_REQUEST['id']); - $admin = $app->addChild('admin'); - $admin->addChild('username', $_REQUEST['admin_username']); - $admin->addChild('password', $_REQUEST['admin_password']); - $mysql = $secrets->addChild('mysql'); - $mysql->addChild('host', $_REQUEST['host']); - $mysql->addChild('username', $_REQUEST['username']); - $mysql->addChild('password', $_REQUEST['password']); - $mysql->addChild('database', $_REQUEST['database']); - $oauth = $secrets->addChild('oauth'); - $oauth->addChild('id', $_REQUEST['oauth_id']); - $oauth->addChild('key', $_REQUEST['oauth_key']); - if ($secrets->asXML(SECRETS_FILE) == false) { - throw new CanvasAPIviaLTI_Exception( - 'Failed to create ' . SECRETS_FILE, - CanvasAPIviaLTI_Installer_Exception::SECRETS_FILE_CREATION - ); - } - - $htpasswdFile = __DIR__ . '/.htpasswd'; - shell_exec("htpasswd -bc $htpasswdFile {$_REQUEST['admin_username']} {$_REQUEST['admin_password']}"); - if (!file_exists($htpasswdFile)) { - throw new CanvasAPIviaLTI_Installer_Exception( - "Failed to create $htpasswdFile", - CanvasAPIviaLTI_Installer_Exception::HTPASSWD_FILE - ); - } - - $htaccessFile = __DIR__ . '/.htaccess'; - if(!file_put_contents($htaccessFile, "AuthType Basic\nAuthName \"{$secrets->app->name} Admin\"\nAuthUserFile $htpasswdFile\nRequire valid-user\n")) { - throw new CanvasAPIviaLTI_Installer_Exception( - "Failed to create $htaccessFile", - CanvasAPIviaLTI_Installer_Exception::HTACCESS_FILE - ); - } - } else { - throw new CanvasAPIviaLTI_Installer_Exception( - 'Missing a required mysql credential (host, username, password and database all required).', - CanvasAPIviaLTI_Installer_Exception::SECRETS_FILE_MYSQL - ); - } - $smarty->addMessage( - 'Secrets file created', - "secrets.xml contains your authentication credentials and - should be carefully protected. Be sure not to commit it to a public - repository!", - NotificationMessage::GOOD - ); - } else { - throw new CanvasAPIviaLTI_Installer_Exception( - 'Missing a required app identity (name, id, admin username and admin password all required).', - CanvasAPIviaLTI_Installer_Exception::SECRETS_FILE_APP - ); - } - - /* clear the processed step */ - unset($_REQUEST['step']); - - break; - } - - default: { - throw new CanvasAPIviaLTI_Installer_Exception( - "Unknown step ($step) in SECRETS_FILE creation.", - CanvasAPIviaLTI_Installer_Exception::SECRETS_NEEDED_STEP - ); - } - } - } - - /** - * Create database tables to back LTI_Tool_Provider - * - * @throws CanvasAPIviaLTI_Installer_Exception If database schema not found in vendors directory - * @throws CanvasAPIviaLTI_Installer_Exception If database tables are not created - **/ - public static function createLTIDatabaseTables() { - global $sql; // FIXME grown-ups don't program like this - global $smarty; // FIXME grown-ups don't program like this - - $ltiSchema = realpath(__DIR__ . '/../vendor/spvsoftwareproducts/LTI_Tool_Provider/lti-tables-mysql.sql'); - - if ($sql->query("SHOW TABLES LIKE 'lti_%'")->num_rows >= 5) { - $smarty->addMessage('LTI database tables exist', 'Database tables to support the LTI Tool Provider (TP) already exist and have not been re-created.'); - } elseif (file_exists($ltiSchema)) { - $queries = explode(";", file_get_contents($ltiSchema)); - $created = true; - foreach($queries as $query) { - if (!empty(trim($query))) { - if (!$sql->query($query)) { - throw new CanvasAPIviaLTI_Installer_Exception( - "Error creating LTI database tables: {$sql->error}", - CanvasAPIviaLTI_Installer_Exception::LTI_PREPARE_DATABASE - ); - } - } - } - - $smarty->addMessage( - 'LTI database tables created', - 'Database tables to support the LTI Tool Provider (TP) have been created in - your MySQL database.', - NotificationMessage::GOOD - ); - } else { - throw new CanvasAPIviaLTI_Exception("$ltiSchema not found."); - } - } - - /** - * Create database tables to back app - * - * @throws CanvasAPIviaLTI_Installer_Exception If database tables are not created - **/ - public static function createAppDatabaseTables() { - global $sql; // FIXME grown-ups don't program like this - global $smarty; // FIXME grown-ups don't program like this - - if (file_exists(SCHEMA_FILE)) { - $queries = explode(";", file_get_contents(SCHEMA_FILE)); - $created = true; - foreach ($queries as $query) { - if (!empty(trim($query))) { - if (preg_match('/CREATE\s+TABLE\s+(`([^`]+)`|\w+)/i', $query, $tableName)) { - $tableName = (empty($tableName[2]) ? $tableName[1] : $tableName[2]); - if ($sql->query("SHOW TABLES LIKE '$tableName'")->num_rows > 0) { - $created = false; - } else { - if (!$sql->query($query)) { - throw new CanvasAPIviaLTI_Installer_Exception( - "Error creating app database tables: {$sql->error}", - CanvasAPIviaLTI_Installer_Exception::APP_CREATE_TABLE - ); - } - } - } else { - if (!$sql->query($query)) { - throw new CanvasAPIviaLTI_Installer_Exception( - "Error creating app database tables: {$sql->error}", - CanvasAPIviaLTI_Installer_Exception::APP_PREPARE_DATABASE - ); - } - } - } - } - - if ($created) { - $smarty->addMessage( - 'App database tables created', - 'Database tables to support the application have been created in your - MySQL database.', - NotificationMessage::GOOD - ); - } else { - $smarty->addMessage( - 'App database tables exist', - 'Database tables to support the application already exist and have not - been re-created.' - ); - } - } - } - - /** - * Initialize the app metadata store, especially the APP_PATH and APP_URL - * - * @return AppMetadata - **/ - public static function createAppMetadata() { - global $secrets; // FIXME grown-ups don't program like this - global $sql; // FIXME grown-ups don't program like this - global $metadata; // FIXME grown-ups don't program like this - global $smarty; // FIXME grown-ups don't program like this - - $metadata = initAppMetadata(); - $metadata['APP_PATH'] = preg_replace('/\/classes$/', '', __DIR__); - $metadata['APP_URL'] = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on' ? 'http://' : 'https://') . $_SERVER['SERVER_NAME'] . preg_replace("|^{$_SERVER['DOCUMENT_ROOT']}(.*)$|", '$1', $metadata['APP_PATH']); - $metadata['APP_NAME'] = (string) $secrets->app->name; - $metadata['APP_ID'] = (string) $secrets->app->id; - $metadata['CANVAS_INSTANCE_URL_PLACEHOLDER'] = 'https://canvas.instructure.com'; - $smarty->assign('metadata', $metadata); - - $smarty->addMessage( - 'App metadata initialized', - 'Basic application metadata has been updated, including APP_PATH and APP_URL', - NotificationMessage::GOOD - ); - - return $metadata; - } - - /** - * Obtain a Canvas API token, if needed. - * - * @param scalar $step optional Where are we in the API token negotiation workflow? (defaults to API_DECISION_NEEDED_STEP -- the beginning) - * @param boolean $skip optional Skip this step (defaults to FALSE) - * - * @throws CanvasAPIviaLTI_Installer_Exception If $step is not a pre-defined *_STEP constant - **/ - public static function acquireAPIToken($step = self::API_DECISION_NEEDED_STEP, $skip = false) { - global $secrets; // FIXME grown-ups don't program like this - global $metadata; // FIXME grown-ups don't program like this - global $smarty; // FIXME grown-ups don't program like this - - if ($skip) { - if (isset($metadata['CANVAS_API_TOKEN']) || isset($metadata['CANVAS_API_USER'])) { - $api = new CanvasPest("{$metadata['CANVAS_INSTANCE_URL']}/login/oauth2", $metadata['CANVAS_API_TOKEN']); - $api->delete('token'); - unset($metadata['CANVAS_API_TOKEN']); - unset($metadata['CANVAS_API_USER']); - $smarty->addMessage( - 'Existing admin Canvas API token information expunged', - 'There was already an administrative access token stored in your - application metadata, and it has now been expunged.' - ); - } else { - $smarty->addMessage( - 'No admin Canvas API token acquired', - 'An administrative API token has not been acquired. Users will be asked to - acquire their own API tokens on their first use of the LTI.' - ); - } - } else { - switch ($step) { - case self::API_DECISION_NEEDED_STEP: { - $smarty->assign('content', ' -
- - - - - -
- or -
- - - -
- '); - $smarty->display(); - exit; - } - case self::API_DECISION_ENTERED_STEP: { - $oauth = new OAuthNegotiator(); - - if ($oauth->isAPIToken()) { - $metadata['CANVAS_API_TOKEN'] = $oauth->getToken(); - - $smarty->addMessage( - 'Admin Canvas API token acquired', - 'An administrative API access token has been acquired and stored in your application metadata.', - NotificationMessage::GOOD - ); - } - - /* clear the processed step */ - unset($_REQUEST['step']); - - break; - } - case self::API_TOKEN_PROVIDED_STEP: { - $smarty->addMessage( - 'Admin Canvas API token provided', - 'You provided an API access token and it has been stored in your application metadata.' - ); - break; - } - default: { - throw new CanvasAPIviaLTI_Installer_Exception( - "Unknown step ($step) in obtaining API token.", - CanvasAPIviaLTI_Installer_Exception::API_STEP_MISMATCH - ); - } - } - } - } -} - -/** - * Exceptions thrown by CanvasAPIviaLTI_Installer - * - * @author Seth Battis - **/ -class CanvasAPIviaLTI_Installer_Exception extends CanvasAPIviaLTI_Exception { - const SECRETS_FILE_CREATION = 1; - const SECRETS_FILE_APP = 2; - const SECRETS_FILE_MYSQL = 3; - const LTI_SCHEMA = 4; - const LTI_PREPARE_DATABASE = 5; - const LTI_CREATE_TABLE = 6; - const APP_SCHEMA = 7; - const APP_PREPARE_DATABASE = 8; - const APP_CREATE_TABLE = 9; - const API_STEP_MISMATCH = 10; - const API_URL = 14; - const API_TOKEN = 11; - const HTPASSWD_FILE = 12; - const HTACCESS_FILE = 13; -} - -?> \ No newline at end of file diff --git a/classes/README.md b/classes/README.md deleted file mode 100644 index d1d0a64..0000000 --- a/classes/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# classes/ - -PHP objects used by the app. ("Don't look at me -- I'm hideous!") \ No newline at end of file diff --git a/classes/UserAPIToken.php b/classes/UserAPIToken.php deleted file mode 100644 index efd9abb..0000000 --- a/classes/UserAPIToken.php +++ /dev/null @@ -1,191 +0,0 @@ - - **/ -class UserAPIToken { - - const USER_TOKENS_TABLE = 'user_tokens'; - - /** - * @var string $consumerKey The unique ID of the tool consumer from whence the user is making requests to the LTI - **/ - private $consumerKey; - - /** - * @var string $id The unique ID (within the context of a particular Tool Consumer) of a particular user - **/ - private $id; - - /** - * @var mysqli $sql A MySQL connection - **/ - private $sql; - - /** - * @var string|null $token This user's API access token (if acquired) or NULL if not yet acquired - **/ - private $token = null; - - /** - * @var string|null $apiUrl The URL of the API for which this user's token is valid, NULL if no token - **/ - private $apiUrl = null; - - /** - * Create a new UserAPIToken to either register a new user in the - * USER_TOKENS_TABLE or look up an existing user. - * - * @param string $consumerKey The unique ID of the Tool Consumer from whence the user is making requests to the LTI - * @param string $userId The unique ID of the user within that Tool Consumer - * @param mysqli $mysqli An active MySQL database connection to update USER_TOKEN_TABLE - * - * @throws UserAPIToken_Exception CONSUMER_KEY_REQUIRED If no consumer key is provided - * @throws UserAPIToken_Exception USER_ID_REQUIRED If no user ID is provided - * @throws UserAPIToken_Exception MYSQLI_REQUIRED If no MySQL database connection is provided - * @throws UserAPIToken_Excpetion MYSQLI_ERROR If the user cannot be found or inserted in USER_TOKEN_TABLE - **/ - public function __construct($consumerKey, $userId, $mysqli) { - - if (empty((string) $consumerKey)) { - throw new UserAPIToken_Exception( - 'A consumer key is required', - UserAPIToken_Exception::CONSUMER_KEY_REQUIRED - ); - } - - if (empty((string) $userId)) { - throw new UserAPIToken_Exception( - 'A user ID is required', - UserAPIToken_Exception::USER_ID_REQUIRED - ); - } - - if (!($mysqli instanceof mysqli)) { - throw new UserAPIToken_Exception( - 'A valid mysqli object is required', - UserAPIToken_Exception::MYSQLI_REQUIRED - ); - } - - $this->sql = $mysqli; - $this->consumerKey = $this->sql->real_escape_string($consumerKey); - $this->id = $this->sql->real_escape_string($userId); - - $result = $this->sql->query("SELECT * FROM `" . self::USER_TOKENS_TABLE . "` WHERE `consumer_key` = '{$this->consumerKey}' AND `id` = '{$this->id}'"); - $row = $result->fetch_assoc(); - if ($row) { - $this->token = $row['token']; - $this->apiUrl = $row['api_url']; - } else { - if (!$this->sql->query("INSERT INTO `" . self::USER_TOKENS_TABLE . "` (`consumer_key`, `id`) VALUES ('{$this->consumerKey}', '{$this->id}')")) { - throw new UserAPIToken_Exception( - "Error inserting a new user: {$this->sql->error}", - UserAPIToken_Exception::MYSQLI_ERROR - ); - } - } - } - - /** - * @return string|boolean The API access token for this user, or FALSE if no token has been acquired - **/ - public function getToken() { - if ($this->token) { - return $this->token; - } - return false; - } - - /** - * Stores a new API Token into USER_TOKEN_TABLE for this user - * - * @param string $token A new API access token for this user - * - * @return boolean Returns TRUE if the token is successfully stored in USER_TOKEN_TABLE, FALSE otherwise - * - * @throws UserAPIToken_Exception TOKEN_REQUIRED If no token is provided - **/ - public function setToken($token) { - if (empty($token)) { - throw new UserAPIToken_Exception( - 'A token is required', - UserAPIToken_Exception::TOKEN_REQUIRED - ); - } - if($this->consumerKey && $this->id && $this->sql) { - $_token = $this->sql->real_escape_string($token); - if (!$this->sql->query("UPDATE `" . self::USER_TOKENS_TABLE . "` set `token` = '$_token' WHERE `consumer_key` = '{$this->consumerKey}' AND `id` = '{$this->id}'")) { - throw new UserAPIToken_Exception( - "Error updating token: {$this->sql->error}", - UserAPIToken_Exception::MYSQLI_ERROR - ); - } - $this->token = $token; - - return true; - } - - return false; - } - - /** - * @return string|boolean The URL of the API for which the user's API token is valid, or FALSE if no token has been acquired - **/ - function getAPIUrl() { - if ($this->apiUrl) { - return $this->apiUrl; - } - return false; - } - - /** - * Stores a new URL for the API URL for which the user's API access token is valid in USER_TOKEN_TABLE - * - * @param string $apiUrl The URL of the API - * - * @return boolean TRUE if the URL of the API is stored in USER_TOKEN_TABLE, FALSE otherwise - * - * @throws UserAPITokenException API_URL_REQUIRED If no URL is provided - **/ - public function setAPIUrl($apiUrl) { - if (empty($apiUrl)) { - throw new UserAPIToken_Exception( - 'API URL is required', - UserAPIToken_Exception::API_URL_REQUIRED - ); - } - - if ($this->consumerKey && $this->id && $this->sql) { - $_apiUrl = $this->sql->real_escape_string($apiUrl); - if (!$this->sql->query("UPDATE `" . self::USER_TOKENS_TABLE . "` set `api_url` = '$_apiUrl' WHERE `consumer_key` = '{$this->consumerKey}' AND `id` = '{$this->id}'")) { - throw new UserAPIToken_Exception( - "Error updating API URL for user token: {$this->sql->error}", - UserAPIToken_Exception::MYSQLI_ERROR - ); - } - $this->apiUrl = $apiUrl; - return true; - } - return false; - } -} - -/** - * Exceptions thrown by the UserAPIToken - * - * @author Seth Battis - **/ -class UserAPIToken_Exception extends CanvasAPIviaLTI_Exception { - const CONSUMER_KEY_REQUIRED = 1; - const USER_ID_REQUIRED = 2; - const MYSQLI_REQUIRED = 3; - const MYSQLI_ERROR = 4; - const TOKEN_REQUIRED = 5; - const API_URL_REQUIRED = 6; -} - -?> \ No newline at end of file diff --git a/common-app.inc.php b/common-app.inc.php deleted file mode 100644 index 1705c9c..0000000 --- a/common-app.inc.php +++ /dev/null @@ -1,65 +0,0 @@ -getCache('accounts'); - if ($accounts === false) { - $accountsResponse = $api->get('accounts/1/sub_accounts', array('recursive' => 'true')); - $accounts = array(); - foreach ($accountsResponse as $account) { - $accounts[$account['id']] = $account; - } - $cache->setCache('accounts', $accounts, 7 * 24 * 60 * 60); - } - return $accounts; -} - -/** - * Get a listing of all terms organized for presentation in a select picker - * - * @return array - **/ -function getTermList() { - global $sql; // FIXME grown-ups don't code like this - global $api; // FIXME grown-ups don't code like this - - $cache = new \Battis\HierarchicalSimpleCache($sql, basename(__FILE__, '.php')); - - $terms = $cache->getCache('terms'); - if ($terms === false) { - $_terms = $api->get( - 'accounts/1/terms', - array( - 'workflow_state' => 'active' - ) - ); - $termsResponse = $_terms['enrollment_terms']; - $terms = array(); - foreach ($termsResponse as $term) { - $terms[$term['id']] = $term; - } - $cache->setCache('terms', $terms, 7 * 24 * 60 * 60); - } - return $terms; -} - -if (isset($_SESSION['toolProvider']->user)) { - $_SESSION['canvasInstanceUrl'] = 'https://' . $_SESSION['toolProvider']->user->getResourceLink()->settings['custom_canvas_api_domain']; -} - -if (isset($_SESSION['apiUrl']) && isset($_SESSION['apiToken'])) { - $api = new CanvasPest($_SESSION['apiUrl'], $_SESSION['apiToken']); -} - -$smarty->assign('category', \Battis\DataUtilities::titleCase(preg_replace('/[\-_]+/', ' ', basename(__DIR__)))); - -?> \ No newline at end of file diff --git a/common.inc.php b/common.inc.php index 6144ef1..d593e42 100644 --- a/common.inc.php +++ b/common.inc.php @@ -1,181 +1,30 @@ mysql->host, - (string) $secrets->mysql->username, - (string) $secrets->mysql->password, - (string) $secrets->mysql->database - ); - restore_error_handler(); - - if ($sql->connect_error) { - throw new CanvasAPIviaLTI_Exception( - $sql->connect_error, - CanvasAPIviaLTI_Exception::MYSQL_CONNECTION - ); - } - return $sql; -} +session_start(); -/** - * Initialize AppMetadata - * - * @return \Battis\AppMetadata - **/ -function initAppMetadata() { - global $secrets; // FIXME grown-ups don't program like this - global $sql; // FIXME grown-ups don't program like this - - $metadata = new AppMetadata($sql, (string) $secrets->app->id); - - return $metadata; +/* prepare the toolbox */ +if (empty($_SESSION[Toolbox::class])) { + $_SESSION[Toolbox::class] = Toolbox::fromConfiguration(CONFIG_FILE); } +$toolbox =& $_SESSION[Toolbox::class]; +$toolbox->getSmarty()->prependTemplateDir(__DIR__ . '/templates', basename(__DIR__)); +$toolbox->smarty_assign('category', DataUtilities::titleCase(preg_replace('/[\-_]+/', ' ', basename(__DIR__)))); -/** - * Preformat `var_dump()` - * - * @param mixed $var - * - * @return void - **/ -function html_var_dump($var) { - echo '
';
-	var_dump($var);
-	echo '
'; +/* set the Tool Consumer's instance URL, if present */ +if (empty($_SESSION[CANVAS_INSTANCE_URL]) && + !empty($_SESSION[ToolProvider::class]['canvas']['api_domain']) +) { + $_SESSION[CANVAS_INSTANCE_URL] = 'https://' . $_SESSION[ToolProvider::class]['canvas']['api_domain']; } - -/***************************************************************************** - * * - * The script begins here * - * * - *****************************************************************************/ - -/* assume everything's going to be fine... */ -$ready = true; - -/* preliminary interactive only initialization */ -if (php_sapi_name() != 'cli') { - session_start(); - - /* fire up the templating engine for interactive scripts */ - $smarty = StMarksSmarty::getSmarty(); - $smarty->addTemplateDir(__DIR__ . '/templates', 'starter-canvas-api-via-lti'); - $smarty->setFramed(true); -} - -/* initialization that needs to happen for interactive and CLI scripts */ -try { - /* initialize global variables */ - $secrets = initSecrets(); - $sql = initMySql(); - $metadata = initAppMetadata(); -} catch (CanvasAPIviaLTI_Exception $e) { - $smarty->addMessage( - 'Initialization Failure', - $e->getMessage(), - NotificationMessage::ERROR - ); - $smarty->display(); - exit; -} - -/* interactive initialization only */ -if ($ready && php_sapi_name() != 'cli') { - - /* allow web apps to use common.inc.php without LTI authentication */ - if (!defined('IGNORE_LTI')) { - - try { - if (midLaunch()) { - $ready = false; - } elseif (isset($_SESSION['toolProvider'])) { - $toolProvider = $_SESSION['toolProvider']; - } else { - throw new CanvasAPIviaLTI_Exception( - 'The LTI launch request is missing', - CanvasAPIviaLTI_Exception::LAUNCH_REQUEST - ); - } - - } catch (CanvasAPIviaLTI_Exception $e) { - $ready = false; - } - } - - if ($ready) { - $smarty->addStylesheet($metadata['APP_URL'] . '/css/canvas-api-via-lti.css', 'starter-canvas-api-via-lti'); - $smarty->addStylesheet($metadata['APP_URL'] . '/css/app.css'); - - if (!midLaunch() || !defined('IGNORE_LTI')) { - require_once(__DIR__ . '/common-app.inc.php'); - } - } -} - - -?> \ No newline at end of file diff --git a/composer.json b/composer.json index 1dc978c..170bef9 100644 --- a/composer.json +++ b/composer.json @@ -3,40 +3,41 @@ "description": "A dashboard for advisors in Canvas", "license": "LGPL-3.0", "config": { - "secure-http": false - }, - "repositories": [ - { - "type": "package", - "package": { - "name": "spvsoftwareproducts/LTI_Tool_Provider", - "version": "2.5.01", - "dist": { - "url": "http://projects.oscelot.org/gf/download/frsrelease/956/6025/LTI_Tool_Provider-2.5.01.zip", - "type": "zip" - }, - "license": "LGPL-3.0", - "authors": [ - { - "name": "Stephen Vickers" - } - ], - "homepage": "http://spvsoftwareproducts.com/php/lti_tool_provider/" - } - } - ], + "secure-http": false + }, + "repositories": [ + { + "type": "package", + "package": { + "name": "spvsoftwareproducts/LTI_Tool_Provider", + "version": "2.5.01", + "dist": { + "url": "http://projects.oscelot.org/gf/download/frsrelease/956/6025/LTI_Tool_Provider-2.5.01.zip", + "type": "zip" + }, + "license": "LGPL-3.0", + "authors": [ + { + "name": "Stephen Vickers" + } + ], + "homepage": "http://spvsoftwareproducts.com/php/lti_tool_provider/", + "autoload": { + "classmap": [""] + } + } + } + ], "require": { - "smtech/stmarkssmarty": "dev-master", - "smtech/canvaspest": "~1.0", - "smtech/oauth-negotiator": "~1.0", - "battis/appmetadata": "~1.0", - "spvsoftwareproducts/LTI_Tool_Provider": "~2.5", - "pear/log": "~1.12", - "battis/simplecache": "~1.0", + "smtech/stmarks-reflexive-canvas-lti": "~0.1", + "smtech/oauth2-canvaslms": "~1.0", + "battis/data-utilities": "~0.1", "roderik/pwgen-php": "~0.1" }, "autoload": { - "classmap": [""] + "psr-4": { + "smtech\\AdvisorDashboard\\": "src" + } }, "authors": [ { @@ -44,5 +45,5 @@ "email": "sethbattis@stmarksschool.org" } ], - "minimum-stability": "dev" + "minimum-stability": "stable" } diff --git a/composer.lock b/composer.lock index 5beb8e7..0241ae5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,27 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "4da6eb06c2ec02e61de543cbd8142dec", - "content-hash": "16879f2a53b7020c9aff06e8bef5a102", + "hash": "a27f893cdcf19f89091e781361f1bbbf", + "content-hash": "4f547057f1ef27e1ccebb76122a06d71", "packages": [ { "name": "battis/appmetadata", - "version": "v1.2.1", + "version": "v1.2.2", "source": { "type": "git", "url": "https://github.com/battis/appmetadata.git", - "reference": "a97bdc20627008ed165b638737b618a0d6f606c2" + "reference": "1d9f4c7e29576dbd4ae0b56bd97c29fc8ff4c3b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/battis/appmetadata/zipball/a97bdc20627008ed165b638737b618a0d6f606c2", - "reference": "a97bdc20627008ed165b638737b618a0d6f606c2", + "url": "https://api.github.com/repos/battis/appmetadata/zipball/1d9f4c7e29576dbd4ae0b56bd97c29fc8ff4c3b5", + "reference": "1d9f4c7e29576dbd4ae0b56bd97c29fc8ff4c3b5", "shasum": "" }, "type": "library", "autoload": { "psr-4": { - "Battis\\": "" + "Battis\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -38,29 +38,62 @@ } ], "description": "An object to store app metadata backed by a MySQL table", - "time": "2015-10-22 20:25:29" + "time": "2016-07-22 06:29:17" }, { "name": "battis/bootstrapsmarty", - "version": "v1.1.2", + "version": "v2.0", "source": { "type": "git", "url": "https://github.com/battis/bootstrapsmarty.git", - "reference": "097ff9d3931aedbc38677d207b5c4540b4de812c" + "reference": "c95297336ca37a48f3bcbc655275996ebbd0a2f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/battis/bootstrapsmarty/zipball/097ff9d3931aedbc38677d207b5c4540b4de812c", - "reference": "097ff9d3931aedbc38677d207b5c4540b4de812c", + "url": "https://api.github.com/repos/battis/bootstrapsmarty/zipball/c95297336ca37a48f3bcbc655275996ebbd0a2f9", + "reference": "c95297336ca37a48f3bcbc655275996ebbd0a2f9", "shasum": "" }, "require": { - "battis/data-utilities": "dev-master", - "bower-asset/bootstrap-colorpicker": "dev-master", - "bower-asset/bootstrap-datepicker": "dev-master", - "bower-asset/bootstrap-sortable": "dev-master", + "battis/data-utilities": "~0.1", + "bower-asset/bootstrap-colorpicker-2.x": "~2.3", + "bower-asset/bootstrap-datepicker-1.x": "~1.6", + "bower-asset/bootstrap-sortable-2.x": "~2.0", "fxp/composer-asset-plugin": "^1.1", - "smarty/smarty": "3.1.*" + "smarty/smarty": "~3.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Battis\\BootstrapSmarty\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Seth Battis", + "email": "seth@battis.net" + } + ], + "description": "A wrapper for Smarty to set (and maintain) defaults within a Bootstrap UI environment", + "time": "2016-08-01 13:10:10" + }, + { + "name": "battis/configxml", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/battis/configxml.git", + "reference": "b68daa917e052bd8f53c3be0378b23d33526f961" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/battis/configxml/zipball/b68daa917e052bd8f53c3be0378b23d33526f961", + "reference": "b68daa917e052bd8f53c3be0378b23d33526f961", + "shasum": "" }, "require-dev": { "phpunit/phpunit": "~5.0" @@ -68,7 +101,7 @@ "type": "library", "autoload": { "psr-4": { - "Battis\\BootstrapSmarty\\": "src/" + "Battis\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -81,12 +114,12 @@ "email": "seth@battis.net" } ], - "description": "A wrapper for Smarty to set (and maintain) defaults within a Bootstrap UI environment", - "time": "2016-04-14 02:09:19" + "description": "Load a configuration XML file for quick use", + "time": "2016-05-09 19:47:21" }, { "name": "battis/data-utilities", - "version": "dev-master", + "version": "v0.1", "source": { "type": "git", "url": "https://github.com/battis/data-utilities.git", @@ -122,7 +155,7 @@ }, { "name": "battis/educoder-pest-fork", - "version": "dev-master", + "version": "v1.0.1", "source": { "type": "git", "url": "https://github.com/battis/pest.git", @@ -197,20 +230,20 @@ }, { "name": "bower-asset/bootstrap", - "version": "dev-master", + "version": "v3.3.7", "source": { "type": "git", "url": "https://github.com/twbs/bootstrap.git", - "reference": "edefe0e77a39149f971c4215301f7f8a7f15f210" + "reference": "0b9c4a4007c44201dce9a6cc1a38407005c26c86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twbs/bootstrap/zipball/edefe0e77a39149f971c4215301f7f8a7f15f210", - "reference": "edefe0e77a39149f971c4215301f7f8a7f15f210", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/0b9c4a4007c44201dce9a6cc1a38407005c26c86", + "reference": "0b9c4a4007c44201dce9a6cc1a38407005c26c86", "shasum": "" }, "require": { - "bower-asset/jquery": ">=1.9.1,<=3" + "bower-asset/jquery": ">=1.9.1,<4.0" }, "type": "bower-asset-library", "extra": { @@ -245,17 +278,17 @@ ] }, { - "name": "bower-asset/bootstrap-colorpicker", - "version": "dev-master", + "name": "bower-asset/bootstrap-colorpicker-2.x", + "version": "2.3.3", "source": { "type": "git", "url": "https://github.com/mjolnic/bootstrap-colorpicker.git", - "reference": "7847a84ab7ec3b17474fb16b7e68fc02838efe52" + "reference": "59f06437c86b7c0e074aee8a699819fd1c07a173" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mjolnic/bootstrap-colorpicker/zipball/7847a84ab7ec3b17474fb16b7e68fc02838efe52", - "reference": "7847a84ab7ec3b17474fb16b7e68fc02838efe52", + "url": "https://api.github.com/repos/mjolnic/bootstrap-colorpicker/zipball/59f06437c86b7c0e074aee8a699819fd1c07a173", + "reference": "59f06437c86b7c0e074aee8a699819fd1c07a173", "shasum": "" }, "require": { @@ -281,27 +314,27 @@ } }, { - "name": "bower-asset/bootstrap-datepicker", - "version": "dev-master", + "name": "bower-asset/bootstrap-datepicker-1.x", + "version": "v1.6.1", "source": { "type": "git", "url": "https://github.com/eternicode/bootstrap-datepicker.git", - "reference": "3bde2d87a291fcc1808a86dc9317eba9d361b7a0" + "reference": "f4df9ac6679b15b12157324c556e0da1c628af6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/eternicode/bootstrap-datepicker/zipball/3bde2d87a291fcc1808a86dc9317eba9d361b7a0", - "reference": "3bde2d87a291fcc1808a86dc9317eba9d361b7a0", + "url": "https://api.github.com/repos/eternicode/bootstrap-datepicker/zipball/f4df9ac6679b15b12157324c556e0da1c628af6e", + "reference": "f4df9ac6679b15b12157324c556e0da1c628af6e", "shasum": "" }, "require": { - "bower-asset/jquery": ">=1.7.1,<3.0.0" + "bower-asset/jquery": ">=1.7.1" }, "type": "bower-asset-library", "extra": { "bower-asset-main": [ "dist/css/bootstrap-datepicker3.css", - "dist/js/bootstrap-datepicker.js" + "dist/js/bootstrap-datepicker.min.js" ], "bower-asset-ignore": [] }, @@ -310,8 +343,8 @@ ] }, { - "name": "bower-asset/bootstrap-sortable", - "version": "dev-master", + "name": "bower-asset/bootstrap-sortable-2.x", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/drvic10k/bootstrap-sortable.git", @@ -332,10 +365,7 @@ "bower-asset-main": "Scripts/bootstrap-sortable.js", "bower-asset-ignore": [ "Scripts/moment.min.js" - ], - "branch-alias": { - "dev-master": "2.0.0-dev" - } + ] }, "license": [ "MIT" @@ -351,16 +381,16 @@ }, { "name": "bower-asset/jquery", - "version": "2.2.4", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/jquery/jquery-dist.git", - "reference": "c0185ab7c75aab88762c5aae780b9d83b80eda72" + "reference": "6f02bc382c0529d3b4f68f6b2ad21876642dbbfe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/c0185ab7c75aab88762c5aae780b9d83b80eda72", - "reference": "c0185ab7c75aab88762c5aae780b9d83b80eda72", + "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/6f02bc382c0529d3b4f68f6b2ad21876642dbbfe", + "reference": "6f02bc382c0529d3b4f68f6b2ad21876642dbbfe", "shasum": "" }, "type": "bower-asset-library", @@ -382,7 +412,7 @@ }, { "name": "bower-asset/moment", - "version": "dev-master", + "version": "2.14.1", "source": { "type": "git", "url": "https://github.com/moment/moment.git", @@ -419,16 +449,16 @@ }, { "name": "fxp/composer-asset-plugin", - "version": "dev-master", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/fxpio/composer-asset-plugin.git", - "reference": "47c08367901b64d41d87f84e7f3fd2aa9417ea71" + "reference": "8d74bc7c714aadbda54622c216b2e7bf786595cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fxpio/composer-asset-plugin/zipball/47c08367901b64d41d87f84e7f3fd2aa9417ea71", - "reference": "47c08367901b64d41d87f84e7f3fd2aa9417ea71", + "url": "https://api.github.com/repos/fxpio/composer-asset-plugin/zipball/8d74bc7c714aadbda54622c216b2e7bf786595cb", + "reference": "8d74bc7c714aadbda54622c216b2e7bf786595cb", "shasum": "" }, "require": { @@ -471,7 +501,380 @@ "npm", "package" ], - "time": "2016-07-14 08:50:38" + "time": "2016-07-01 12:04:16" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.2.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "3f808fba627f2c5b69e2501217bf31af349c1427" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/3f808fba627f2c5b69e2501217bf31af349c1427", + "reference": "3f808fba627f2c5b69e2501217bf31af349c1427", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.3.1", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0", + "psr/log": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2016-07-15 17:22:37" + }, + { + "name": "guzzlehttp/promises", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "c10d860e2a9595f8883527fa0021c7da9e65f579" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/c10d860e2a9595f8883527fa0021c7da9e65f579", + "reference": "c10d860e2a9595f8883527fa0021c7da9e65f579", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-05-18 16:56:05" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "5c6447c9df362e8f8093bda8f5d8873fe5c7f65b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/5c6447c9df362e8f8093bda8f5d8873fe5c7f65b", + "reference": "5c6447c9df362e8f8093bda8f5d8873fe5c7f65b", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "PSR-7 message implementation", + "keywords": [ + "http", + "message", + "stream", + "uri" + ], + "time": "2016-06-24 23:00:38" + }, + { + "name": "ircmaxell/random-lib", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/RandomLib.git", + "reference": "13efa4368bb2ac88bb3b1459b487d907de4dbf7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ircmaxell/RandomLib/zipball/13efa4368bb2ac88bb3b1459b487d907de4dbf7c", + "reference": "13efa4368bb2ac88bb3b1459b487d907de4dbf7c", + "shasum": "" + }, + "require": { + "ircmaxell/security-lib": "1.0.*@dev", + "php": ">=5.3.2" + }, + "require-dev": { + "mikey179/vfsstream": "1.1.*", + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "RandomLib": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@ircmaxell.com", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A Library For Generating Secure Random Numbers", + "homepage": "https://github.com/ircmaxell/RandomLib", + "keywords": [ + "cryptography", + "random", + "random-numbers", + "random-strings" + ], + "time": "2015-01-15 16:31:45" + }, + { + "name": "ircmaxell/security-lib", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/SecurityLib.git", + "reference": "80934de3c482dcafb46b5756e59ebece082b6dc7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ircmaxell/SecurityLib/zipball/80934de3c482dcafb46b5756e59ebece082b6dc7", + "reference": "80934de3c482dcafb46b5756e59ebece082b6dc7", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "mikey179/vfsstream": "1.1.*" + }, + "type": "library", + "autoload": { + "psr-0": { + "SecurityLib": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@ircmaxell.com", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A Base Security Library", + "homepage": "https://github.com/ircmaxell/PHP-SecurityLib", + "time": "2013-04-30 18:00:34" + }, + { + "name": "league/oauth2-client", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "01f955b85040b41cf48885b078f7fd39a8be5411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/01f955b85040b41cf48885b078f7fd39a8be5411", + "reference": "01f955b85040b41cf48885b078f7fd39a8be5411", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "guzzlehttp/guzzle": "~6.0", + "ircmaxell/random-lib": "~1.1", + "php": ">=5.5.0" + }, + "require-dev": { + "jakub-onderka/php-parallel-lint": "0.8.*", + "mockery/mockery": "~0.9", + "phpunit/phpunit": "~4.0", + "satooshi/php-coveralls": "0.6.*", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "time": "2016-07-28 13:20:43" + }, + { + "name": "myclabs/php-enum", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "07da9d1a7469941ae05b046193fac4c83bdb0d7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/07da9d1a7469941ae05b046193fac4c83bdb0d7e", + "reference": "07da9d1a7469941ae05b046193fac4c83bdb0d7e", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "4.*", + "squizlabs/php_codesniffer": "1.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "time": "2015-07-22 16:14:03" }, { "name": "pear/log", @@ -581,6 +984,55 @@ ], "time": "2015-02-10 20:07:52" }, + { + "name": "psr/http-message", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2015-05-04 20:22:00" + }, { "name": "roderik/pwgen-php", "version": "0.1.5", @@ -627,16 +1079,16 @@ }, { "name": "smarty/smarty", - "version": "dev-master", + "version": "v3.1.29", "source": { "type": "git", "url": "https://github.com/smarty-php/smarty.git", - "reference": "6d7d36d9a675f69a18f5a8722dd22c0f2f70504d" + "reference": "35480f10e7ce9b0fdaf23d3799d7b79463919b1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/smarty-php/smarty/zipball/6d7d36d9a675f69a18f5a8722dd22c0f2f70504d", - "reference": "6d7d36d9a675f69a18f5a8722dd22c0f2f70504d", + "url": "https://api.github.com/repos/smarty-php/smarty/zipball/35480f10e7ce9b0fdaf23d3799d7b79463919b1e", + "reference": "35480f10e7ce9b0fdaf23d3799d7b79463919b1e", "shasum": "" }, "require": { @@ -678,7 +1130,7 @@ "keywords": [ "templating" ], - "time": "2016-07-19 18:22:10" + "time": "2015-12-21 01:57:06" }, { "name": "smtech/canvaspest", @@ -686,16 +1138,16 @@ "source": { "type": "git", "url": "https://github.com/smtech/canvaspest.git", - "reference": "d9b48fb1e368bbaccfd305c5fe49c07da1519f39" + "reference": "c1a5b7901e5d637afd05158541b1090d70027ed4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/smtech/canvaspest/zipball/d9b48fb1e368bbaccfd305c5fe49c07da1519f39", - "reference": "d9b48fb1e368bbaccfd305c5fe49c07da1519f39", + "url": "https://api.github.com/repos/smtech/canvaspest/zipball/c1a5b7901e5d637afd05158541b1090d70027ed4", + "reference": "c1a5b7901e5d637afd05158541b1090d70027ed4", "shasum": "" }, "require": { - "battis/educoder-pest-fork": "dev-master" + "battis/educoder-pest-fork": "~1.0" }, "type": "library", "autoload": { @@ -714,26 +1166,111 @@ } ], "description": "An extension of Pest to make RESTful queries to the Instructure Canvas API", - "time": "2016-06-10 13:35:42" + "time": "2016-07-25 02:29:48" + }, + { + "name": "smtech/lti-configuration-xml", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/smtech/lti-configuration-xml.git", + "reference": "ab9a963bb0f9d4a6689c20af20532d3af7f49a1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/smtech/lti-configuration-xml/zipball/ab9a963bb0f9d4a6689c20af20532d3af7f49a1f", + "reference": "ab9a963bb0f9d4a6689c20af20532d3af7f49a1f", + "shasum": "" + }, + "require": { + "myclabs/php-enum": "~1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "smtech\\LTI\\Configuration\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Seth Battis", + "email": "sethbattis@stmarksschool.org" + } + ], + "description": "Generate an LTI configuration XML file from parameters", + "time": "2016-07-26 18:38:05" + }, + { + "name": "smtech/oauth2-canvaslms", + "version": "v1.0", + "source": { + "type": "git", + "url": "https://github.com/smtech/oauth2-canvaslms.git", + "reference": "aa77bf14cb83c4666db8a110c3ecd74d46c2cece" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/smtech/oauth2-canvaslms/zipball/aa77bf14cb83c4666db8a110c3ecd74d46c2cece", + "reference": "aa77bf14cb83c4666db8a110c3ecd74d46c2cece", + "shasum": "" + }, + "require": { + "league/oauth2-client": "~1.0", + "php": ">=5.5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "smtech\\OAuth2\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seth Battis", + "email": "sethbattis@stmarksschool.org" + } + ], + "description": "Canvas LMS OAuth 2.0 support for the PHP League's OAuth 2.0 Client", + "time": "2016-07-14 18:35:06" }, { - "name": "smtech/oauth-negotiator", - "version": "v1.1.2", + "name": "smtech/reflexive-canvas-lti", + "version": "v1.0.2", "source": { "type": "git", - "url": "https://github.com/smtech/oauth-negotiator.git", - "reference": "79f1ec4e80f0eba390fb23ac23ad110197961dd7" + "url": "https://github.com/smtech/reflexive-canvas-lti.git", + "reference": "6282241595a0675bbeb4a5fcfc6e8871ec6853ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/smtech/oauth-negotiator/zipball/79f1ec4e80f0eba390fb23ac23ad110197961dd7", - "reference": "79f1ec4e80f0eba390fb23ac23ad110197961dd7", + "url": "https://api.github.com/repos/smtech/reflexive-canvas-lti/zipball/6282241595a0675bbeb4a5fcfc6e8871ec6853ad", + "reference": "6282241595a0675bbeb4a5fcfc6e8871ec6853ad", "shasum": "" }, "require": { - "battis/educoder-pest-fork": "dev-master" + "battis/appmetadata": "~1.0", + "battis/configxml": "~1.0", + "battis/data-utilities": "~0.1", + "myclabs/php-enum": "~1.0", + "pear/log": "~1.0", + "smtech/canvaspest": "~1.0", + "smtech/lti-configuration-xml": "~1.0", + "spvsoftwareproducts/lti_tool_provider": "~2.5" }, "type": "library", + "autoload": { + "psr-4": { + "smtech\\ReflexiveCanvasLTI\\": "src/" + } + }, "notification-url": "https://packagist.org/downloads/", "license": [ "LGPL-3.0" @@ -744,13 +1281,12 @@ "email": "sethbattis@stmarksschool.org" } ], - "description": "Negotiate an OAuth 2 authentication or API token", - "abandoned": "smtech/oauth2-canvaslms", - "time": "2016-02-15 19:30:24" + "description": "LTI tool provider class that includes the \"reflexive\" reach back into Canvas via the API", + "time": "2016-08-01 13:25:20" }, { "name": "smtech/stmarks-colors", - "version": "dev-master", + "version": "v1.1", "source": { "type": "git", "url": "https://github.com/smtech/stmarks-colors.git", @@ -782,25 +1318,62 @@ "time": "2016-03-17 15:26:18" }, { - "name": "smtech/stmarkssmarty", - "version": "dev-master", + "name": "smtech/stmarks-reflexive-canvas-lti", + "version": "v0.1", "source": { "type": "git", - "url": "https://github.com/smtech/stmarkssmarty.git", - "reference": "ddb27fba830765e3f663b693c7eb79746d97a8d4" + "url": "https://github.com/smtech/stmarks-reflexive-canvas-lti.git", + "reference": "06497222d29efbcdb89b6f053e92ce18ded8142d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/smtech/stmarkssmarty/zipball/ddb27fba830765e3f663b693c7eb79746d97a8d4", - "reference": "ddb27fba830765e3f663b693c7eb79746d97a8d4", + "url": "https://api.github.com/repos/smtech/stmarks-reflexive-canvas-lti/zipball/06497222d29efbcdb89b6f053e92ce18ded8142d", + "reference": "06497222d29efbcdb89b6f053e92ce18ded8142d", "shasum": "" }, "require": { - "battis/bootstrapsmarty": "^1.1", - "smtech/stmarks-colors": "dev-master" + "battis/data-utilities": "~0.1", + "battis/simplecache": "~1.0", + "smtech/reflexive-canvas-lti": "~1.0", + "smtech/stmarkssmarty": "~1.0" }, - "suggest": { - "battis/appmetadata": "MySQL-backed globals array for app metadata storage" + "type": "library", + "autoload": { + "psr-4": { + "smtech\\StMarksReflexiveCanvasLTI\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Seth Battis", + "email": "sethbattis@stmarksschool.org" + } + ], + "description": "A toolbox for managing LTI authentication and tool provider development (with St. Mark's templating)", + "time": "2016-08-01 15:10:04" + }, + { + "name": "smtech/stmarkssmarty", + "version": "v1.0", + "source": { + "type": "git", + "url": "https://github.com/smtech/stmarks-bootstrapsmarty.git", + "reference": "e481110eed523a1c6dc3e1a8d152785ab909cfc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/smtech/stmarks-bootstrapsmarty/zipball/e481110eed523a1c6dc3e1a8d152785ab909cfc9", + "reference": "e481110eed523a1c6dc3e1a8d152785ab909cfc9", + "shasum": "" + }, + "require": { + "battis/bootstrapsmarty": "~2.0", + "battis/data-utilities": "~0.1", + "smtech/stmarks-colors": "~1.0" }, "type": "library", "autoload": { @@ -819,7 +1392,7 @@ } ], "description": "A standard Smarty install for St. Mark's projects", - "time": "2016-03-17 20:07:04" + "time": "2016-08-01 13:20:55" }, { "name": "spvsoftwareproducts/LTI_Tool_Provider", @@ -831,6 +1404,11 @@ "shasum": null }, "type": "library", + "autoload": { + "classmap": [ + "" + ] + }, "license": [ "LGPL-3.0" ], @@ -844,10 +1422,8 @@ ], "packages-dev": [], "aliases": [], - "minimum-stability": "dev", - "stability-flags": { - "smtech/stmarkssmarty": 20 - }, + "minimum-stability": "stable", + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": [], diff --git a/config.example.xml b/config.example.xml new file mode 100644 index 0000000..029f4b1 --- /dev/null +++ b/config.example.xml @@ -0,0 +1,34 @@ + + + + + + Advisor Dashboard + A dashboard for advisors in Canvas + smtech-advisor-dashboard + images/icon.png + index.php + + + + index.php + + + + logs/advisor-dashboard.log + + + + + localhost + advisordashboard + GlD5x}eq69%Wni2 + advisordashboard + + + + + https://canvas.instructure.com + 1106~3AmvhPCaGcuFhGpKgAr0Fe3mvfRJYfp6jtil5rQol8qfqQo2oUlVW35TUOSWN5Ul + + diff --git a/course/common.inc.php b/course/common.inc.php index 0da4be9..1e91a62 100644 --- a/course/common.inc.php +++ b/course/common.inc.php @@ -1,27 +1,27 @@ addTemplateDir(__DIR__ . '/templates', basename(__DIR__)); +require_once __DIR__ . '/../common.inc.php'; -$cache = new \Battis\HierarchicalSimpleCache($sql, basename(__DIR__)); -$cache->pushKey($_SESSION['courseId']); -$cache->pushKey(basename(__FILE__, '.php')); +use smtech\ReflexiveCanvasLTI\LTI\ToolProvider; -$firstStudent = $cache->getCache('first-student'); -if ($firstStudent === false) { - $enrollments = $api->get( - "courses/{$_SESSION['courseId']}/enrollments", - array( +$toolbox->getSmarty()->prependTemplateDir(__DIR__ . '/templates', basename(__DIR__)); + +$toolbox->cache_pushKey($_SESSION[ToolProvider::class]['canvas']['course_id']); + +/* get and cache ID of first student in the advisory group */ +$firstStudent = $toolbox->cache_get('first-student'); +if (empty($firstStudent)) { + $enrollments = $toolbox->api_get( + 'courses/' . $_SESSION[ToolProvider::class]['canvas']['course_id'] . '/enrollments', + [ 'role[]' => 'StudentEnrollment' - ) + ] ); $firstStudent = $enrollments[0]['user']['id']; - $cache->setCache('first-student', $firstStudent, 7*24*60*60); + $toolbox->cache_set('first-student', $firstStudent); } -$smarty->assign('facultyJournal', "{$_SESSION['canvasInstanceUrl']}/users/$firstStudent/user_notes?course_id={$_SESSION['courseId']}&course_name=Advisory%20Group"); - -$cache->popKey(); +/* generate faculty journal URL for use by `smtech/canvashack-plugin-faculty-journal` */ +$toolbox->smarty_assign('facultyJournal', $_SESSION[CANVAS_INSTANCE_URL] . "/users/$firstStudent/user_notes?course_id=" . $_SESSION[ToolProvider::class]['canvas']['course_id'] . '&course_name=Advisory%20Group'); -?> \ No newline at end of file +$toolbox->cache_popKey(); diff --git a/course/index.php b/course/index.php index 3b33053..ed30967 100644 --- a/course/index.php +++ b/course/index.php @@ -1,7 +1,4 @@ \ No newline at end of file diff --git a/course/observers.php b/course/observers.php index 88a3fb6..390dc81 100644 --- a/course/observers.php +++ b/course/observers.php @@ -1,48 +1,45 @@ setLifetime(60*60); /* 1 hour */ - -$cache->pushKey(basename(__FILE__, '.php')); +$toolbox->cache_pushKey(basename(__FILE__, '.php')); { + $observers = $toolbox->cache_get('observers'); + if ($observers === false) { + $observers = []; + $enrollments = $toolbox->api_get( + 'courses/' . $_SESSION[COURSE_ID] . '/enrollments', [ + 'role[]' => 'ObserverEnrollment' // FIXME this shouldn't requrie the faux-array + ] + ); + foreach ($enrollments as $enrollment) { + $observers[] = $toolbox->api_get("users/{$enrollment['user']['id']}/profile"); + } + $toolbox->cache_set('observers', $observers); + } -$observers = $cache->getCache('observers'); -if ($observers === false) { - $observers = array(); - $enrollments = $api->get( - "courses/{$_SESSION['courseId']}/enrollments", - array( - 'role[]' => 'ObserverEnrollment' // FIXME this shouldn't requrie the faux-array - ) - ); - foreach ($enrollments as $enrollment) { - $observers[] = $api->get("users/{$enrollment['user']['id']}/profile"); + $observees = $toolbox->cache_get('observees'); + if ($observees === false) { + $observees = []; + foreach ($observers as $observer) { + $response = $toolbox->api_get("users/{$observer['id']}/observees"); + $observees[$observer['id']] = $response[0]; + } + $toolbox->cache_set('observees', $observees); } - $cache->setCache('observers', $observers); -} -$observees = $cache->getCache('observees'); -if ($observees === false) { - $observees = array(); + $passwords = []; foreach ($observers as $observer) { - $response = $api->get("users/{$observer['id']}/observees"); - $observees[$observer['id']] = $response[0]; + $response = $toolbox->mysql_query(" + SELECT * FROM `observers` WHERE `id` = '{$observer['id']}' LIMIT 1 + "); + $password = $response->fetch_assoc(); + $passwords[$observer['id']] = $password['password']; } - $cache->setCache('observees', $observees); -} - -$passwords = array(); -foreach ($observers as $observer) { - $response = $sql->query(" - SELECT * FROM `observers` WHERE `id` = '{$observer['id']}' LIMIT 1 - "); - $password = $response->fetch_assoc(); - $passwords[$observer['id']] = $password['password']; -} +} $toolbox->cache_popKey(); -$smarty->assign('observers', $observers); -$smarty->assign('passwords', $passwords); -$smarty->assign('observees', $observees); -$smarty->display('observers.tpl'); - -?> \ No newline at end of file +$toolbox->smarty_assign([ + 'observers' => $observers, + 'passwords' => $passwords, + 'observees' => $observees +]); +$toolbox->smarty_display('observers.tpl'); diff --git a/course/relative-grades.php b/course/relative-grades.php index bd8d010..42a74fe 100644 --- a/course/relative-grades.php +++ b/course/relative-grades.php @@ -1,77 +1,83 @@ getAccountList(); +function isAcademic($account) { + global $accounts; if ($account == 132) { // FIXME really, hard-coded values? Really? return true; } elseif ($account == 1 || !is_integer($account)) { return false; } else { - return isAcademic($accounts[$account]['parent_account_id'], $accounts); + return isAcademic($accounts[$account]['parent_account_id']); } } -$cache->pushKey(basename(__FILE__, '.php')); { - $terms = getTermList(); - - $advisees = $cache->getCache('advisees'); - if ($advisees === false) { - $advisees = $api->get( - "courses/{$_SESSION['courseId']}/enrollments", - array( - 'role[]' => 'StudentEnrollment' // FIXME this shouldn't require the faux-array - ) - ); - $cache->setCache('advisees', $advisees); - } - - $advisee = (isset($_REQUEST['advisee']) ? $_REQUEST['advisee'] : $advisees[0]['user']['id']); - $cache->pushKey($advisee); { - $courses = $cache->getCache('courses'); - if ($courses === false) { - $allCourses = $api->get( - "courses", - array( - 'as_user_id' => $advisee - ) - ); - - $courses = array(); - $today = time(); - foreach ($allCourses as $course) { - if ( - isAcademic($course['account_id']) && - strtotime($terms[$course['enrollment_term_id']]['start_at']) < $today && - strtotime($terms[$course['enrollment_term_id']]['end_at']) > $today - ) { - - $courses[$course['id']] = $course; - } - } - $cache->setCache('courses', $courses); - } - - $analytics = $cache->getCache('analytics'); - if ($analytics === false) { - $analytics = array(); - foreach ($courses as $course) { - $analytics[$course['id']] = $api->get("courses/{$course['id']}/analytics/users/$advisee/assignments"); - } - $cache->setCache('analytics', $analytics); +$toolbox->cache_pushKey(basename(__FILE__, '.php')); + +$terms = $toolbox->getTermList(); + +$advisees = $toolbox->cache_get('advisees'); +if ($advisees === false) { + $advisees = $toolbox->api_get( + 'courses/' . $_SESSION[COURSE_ID] . '/enrollments', [ + 'role[]' => 'StudentEnrollment' // FIXME this shouldn't require the faux-array + ] + ); + $toolbox->cache_set('advisees', $advisees); +} + +$advisee = (isset($_REQUEST['advisee']) ? $_REQUEST['advisee'] : $advisees[0]['user']['id']); + +$toolbox->cache_pushKey($advisee); + +$courses = $toolbox->cache_get('courses'); +if ($courses === false) { + $allCourses = $toolbox->api_get("users/$advisee/courses"); + + $courses = []; + foreach ($allCourses as $course) { + if ( + !empty($course['account_id']) && + isAcademic($course['account_id']) + ) { + + $courses[$course['id']] = $course; } - } $cache->popKey(); -} $cache->popKey(); + } + $toolbox->cache_set('courses', $courses); +} + +$analytics = $toolbox->cache_get('analytics'); +if ($analytics === false) { + $analytics = []; + foreach ($courses as $course) { + $analytics[$course['id']] = $toolbox->api_get("courses/{$course['id']}/analytics/users/$advisee/assignments"); + } + $toolbox->cache_set('analytics', $analytics); +} + +$toolbox->cache_popKey(); +$toolbox->cache_popKey(); -$smarty->assign('advisee', $advisee); -$smarty->assign('advisees', $advisees); -$smarty->assign('terms', $terms); -$smarty->assign('courses', $courses); -$smarty->assign('analytics', $analytics); +$toolbox->smarty_assign([ + 'advisee' => $advisee, + 'advisees' => $advisees, + 'terms' => $terms, + 'courses' => $courses, + 'analytics' => $analytics, + 'canvasInstanceUrl' => $_SESSION[CANVAS_INSTANCE_URL] +]); -$smarty->display('relative-grades.tpl'); +/* + * FIXME unclear why the post-bootstrap-scripts block isn't working in the + * relative-grades.tpl file + */ +$toolbox->getSmarty()->addScript(DataUtilities::URLfromPath(__DIR__ . '/../js/Chart.min.js')); +$toolbox->getSmarty()->addScript(DataUtilities::URLfromPath(__DIR__ . '/../js/relative-grades.js.php') . "?advisee={$advisee}"); -?> \ No newline at end of file +$toolbox->smarty_display('relative-grades.tpl'); diff --git a/course/templates/navigation-menu.tpl b/course/templates/navigation-menu.tpl index 7c53e39..55f5837 100644 --- a/course/templates/navigation-menu.tpl +++ b/course/templates/navigation-menu.tpl @@ -4,4 +4,4 @@
  • Observer Logins
  • Faculty Journal
  • -{/block} \ No newline at end of file +{/block} diff --git a/course/templates/relative-grades.tpl b/course/templates/relative-grades.tpl index d64d564..fc16f54 100644 --- a/course/templates/relative-grades.tpl +++ b/course/templates/relative-grades.tpl @@ -3,7 +3,7 @@ {block name="subcontent"} {include file="select-advisee.tpl"} - +

    Below are visualizations of your advisee's relative performance in their classes, as compared to the other students' performance on each assignment.

      @@ -13,18 +13,21 @@
    • The red line represents the lowest score on each assignment.
    • The blue line is the highest score on each assignment.
    - +

    Nota bene: zero-point assignments and ungraded assignments (assignments where the maximum grade was zero) have been filtered out of this view. If your advisee has "bottomed out" at zero on an assignment, it may mean that their submission is not yet graded, while other students' submissions have been graded.

    - +
    + {foreach $courses as $course} {/foreach} {/block} -{block name="scripts"} +{block name="post-bootstrap-scripts" append} -{/block} \ No newline at end of file +{/block} diff --git a/css/app.css b/css/app.css deleted file mode 100644 index be64f8e..0000000 --- a/css/app.css +++ /dev/null @@ -1 +0,0 @@ -/* app-specific styles could go in here */ \ No newline at end of file diff --git a/css/canvas-api-via-lti.css b/css/canvas-api-via-lti.css deleted file mode 100644 index e69de29..0000000 diff --git a/generate-csv.php b/generate-csv.php index f9bd376..7524140 100644 --- a/generate-csv.php +++ b/generate-csv.php @@ -1,28 +1,24 @@ getCache($_REQUEST['data']); + $data = $toolbox->cache_get($_REQUEST['data']); if (is_array($data)) { - + $filename = (empty($_REQUEST['filename']) ? date('Y-m-d_H-i-s') : $_REQUEST['filename']); if (!preg_match('/.*\.csv$/i', $filename)) { $filename .= '.csv'; } - + /* http://code.stephenmorley.org/php/creating-downloadable-csv-files/ */ /* output headers so that the file is downloaded rather than displayed */ header('Content-Type: text/csv; charset=utf-8'); header("Content-Disposition: attachment; filename=$filename"); - + /* create a file pointer connected to the output stream */ $output = fopen('php://output', 'w'); @@ -30,7 +26,6 @@ fputcsv($output, $row); } fclose($output); + exit; } } - -?> \ No newline at end of file diff --git a/docs/account-level-dashboard.png b/images/account-level-dashboard.png similarity index 100% rename from docs/account-level-dashboard.png rename to images/account-level-dashboard.png diff --git a/docs/course-level-dashboard.png b/images/course-level-dashboard.png similarity index 100% rename from docs/course-level-dashboard.png rename to images/course-level-dashboard.png diff --git a/lti/icon.png b/images/icon.png similarity index 100% rename from lti/icon.png rename to images/icon.png diff --git a/docs/relative-grades.png b/images/relative-grades.png similarity index 100% rename from docs/relative-grades.png rename to images/relative-grades.png diff --git a/index.php b/index.php new file mode 100644 index 0000000..92bc20a --- /dev/null +++ b/index.php @@ -0,0 +1,93 @@ +lti_isLaunching()) { + $_SESSION = []; /* clear all session data */ + $toolbox->lti_authenticate(); + exit; +} + +/* if authenticated LTI launch, redirect to appropriate placement view */ +if (!empty($_SESSION[ToolProvider::class]['canvas']['account_id'])) { + $_SESSION[ACCOUNT_ID] = $_SESSION[ToolProvider::class]['canvas']['account_id']; + header("Location: account/"); + exit; +} elseif (!empty($_SESSION[ToolProvider::class]['canvas']['course_id'])) { + $_SESSION[COURSE_ID] = $_SESSION[ToolProvider::class]['canvas']['course_id']; + header('Location: course/'); + exit; + +/* if not authenticated, default to showing credentials */ +} else { + $action = (empty($action) ? + ACTION_CONFIG : + $action + ); +} + +/* process any actions */ +switch ($action) { + + /* reset cached install data from config file */ + case ACTION_INSTALL: { + $_SESSION['toolbox'] = Toolbox::fromConfiguration(CONFIG_FILE, true); + $toolbox =& $_SESSION['toolbox']; + + /* patch in newly-acquired token via oauth, if present */ + if (!empty($_SESSION['TOOL_CANVAS_API'])) { + $toolbox->config('TOOL_CANVAS_API', $_SESSION['TOOL_CANVAS_API']); + unset($_SESSION['TOOL_CANVAS_API']); + } + + /* test to see if we can connect to the API */ + try { + $toolbox->getAPI(); + } catch (ConfigurationException $e) { + + /* if there isn't an API token in config.xml, are there OAuth credentials? */ + $canvas = $toolbox->config('TOOL_CANVAS_API'); + if ($e->getCode() === ConfigurationException::CANVAS_API_INCORRECT && + !empty($canvas['key']) && + !empty($canvas['secret']) + ) { + /* if so, request an API access token interactively */ + header('Location: ' . $toolbox->config('APP_URL') . "/admin/oauth.php?oauth-return={$_SERVER['REQUEST_URI']}"); + exit; + } else { /* no (understandable) API credentials available -- doh! */ + throw $e; + } + } + + /* finish by opening consumers control panel */ + header("Location: admin/"); + exit; + } + + /* show LTI configuration XML file */ + case ACTION_CONFIG: { + header('Content-type: application/xml'); + echo $toolbox->saveConfigurationXML(); + exit; + } +} diff --git a/js/common.inc.php b/js/common.inc.php index cfb246e..3d51d9a 100644 --- a/js/common.inc.php +++ b/js/common.inc.php @@ -1,6 +1,4 @@ \ No newline at end of file +define('IGNORE_LTI', true); +require_once __DIR__ . '/../common.inc.php'; diff --git a/js/relative-grades.js.php b/js/relative-grades.js.php index 429df38..0d039a4 100644 --- a/js/relative-grades.js.php +++ b/js/relative-grades.js.php @@ -1,7 +1,7 @@ pushKey(basename(__FILE__, '.js.php')); -$cache->pushKey($_REQUEST['advisee']); +$toolbox->cache_pushKey(basename(__FILE__, '.js.php')); +$toolbox->cache_pushKey($_REQUEST['advisee']); -$analytics = $cache->getCache('analytics'); +$analytics = $toolbox->cache_get('analytics'); if ($analytics === false) { exit; } @@ -46,7 +46,7 @@ function normalize($numerator, $denominator = false) { $analytic): ?> @@ -108,7 +112,7 @@ function normalize($numerator, $denominator = false) { } ] }; - + var options = { pointDot: false, scaleShowGridLines: false @@ -117,7 +121,9 @@ function normalize($numerator, $denominator = false) { // Get context with jQuery - using jQuery's .get() method. var ctx = $("#course_").get(0).getContext("2d"); + // TODO detect empty datasets and remove canvas and replace with message + // This will get the first returned node in the jQuery collection. var chart = new Chart(ctx).Line(data, options); - \ No newline at end of file + diff --git a/logs/.gitignore b/logs/.gitignore new file mode 100644 index 0000000..397b4a7 --- /dev/null +++ b/logs/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/lti/.htaccess b/lti/.htaccess deleted file mode 100644 index 50f2bfa..0000000 --- a/lti/.htaccess +++ /dev/null @@ -1,6 +0,0 @@ -# force processing of config.xml as PHP - - ForceType application/x-httpd-php - SetHandler application/x-httpd-php - - diff --git a/lti/README.md b/lti/README.md deleted file mode 100644 index 5635ae1..0000000 --- a/lti/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# lti/ - -Files supporting behavior as an LTI Tool Provider (TP). \ No newline at end of file diff --git a/lti/common.inc.php b/lti/common.inc.php deleted file mode 100644 index 070955b..0000000 --- a/lti/common.inc.php +++ /dev/null @@ -1,5 +0,0 @@ - \ No newline at end of file diff --git a/lti/config.xml b/lti/config.xml deleted file mode 100644 index 8f8326f..0000000 --- a/lti/config.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - getArrayCopy())) as $field) { - echo '' . $metadata[$field] . ''; - } - ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/lti/launch.php b/lti/launch.php deleted file mode 100644 index 20b8efc..0000000 --- a/lti/launch.php +++ /dev/null @@ -1,25 +0,0 @@ -setParameterConstraint('oauth_consumer_key', TRUE, 50); -$toolProvider->setParameterConstraint('resource_link_id', TRUE, 50, array('basic-lti-launch-request')); -$toolProvider->setParameterConstraint('user_id', TRUE, 50, array('basic-lti-launch-request')); -$toolProvider->setParameterConstraint('roles', TRUE, NULL, array('basic-lti-launch-request')); - -$_SESSION['toolProvider'] = $toolProvider; - -/* process the LTI request from the Tool Consumer (TC) */ -$toolProvider->handle_request(); - -?> \ No newline at end of file diff --git a/lti/token_request.php b/lti/token_request.php deleted file mode 100644 index f77add0..0000000 --- a/lti/token_request.php +++ /dev/null @@ -1,47 +0,0 @@ -assign('content', '

    Token Request

    -

    This application requires access to the Canvas APIs. Canvas is about to ask you to give permission for this.

    -

    Click to continue

    '); - $smarty->display(); - exit; - } - case 'process': { - $oauth = new OAuthNegotiator( - 'https://' . $toolProvider->user->getResourceLink()->settings['custom_canvas_api_domain'] . '/login/oauth2', - (string) $secrets->oauth->id, - (string) $secrets->oauth->key, - "{$_SERVER['PHP_SELF']}?oauth=complete", - (string) $secrets->app->name - ); - break; - } - case 'complete': { - $user = new UserAPIToken($_SESSION['user_consumer_key'], $_SESSION['user_id'], $sql); - $user->setToken($oauth->getToken()); - $user->setAPIUrl("{$metadata['CANVAS_INSTANCE_URL']}/api/v1"); - - $_SESSION['apiToken'] = $user->getToken(); - $_SESSION['apiUrl'] = $user->getAPIUrl(); - $_SESSION['isUserToken'] = true; - - header("Location: {$metadata['APP_URL']}/app.php"); - exit; - } - } -} - -?> \ No newline at end of file diff --git a/secrets-example.xml b/secrets-example.xml deleted file mode 100644 index c2c1318..0000000 --- a/secrets-example.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - Canvas API via LTI Starter - canvas-api-via-lti-starter - - admin - s00pers3kr3t - - - - localhost - hax0r - s00pers3kr3t - cavl - - - 000000000000001 - 6987c1e292a98deff97c97f2cbc49985 - - diff --git a/src/Toolbox.php b/src/Toolbox.php new file mode 100644 index 0000000..e51bfda --- /dev/null +++ b/src/Toolbox.php @@ -0,0 +1,98 @@ + + * @version v1.2 + */ +class Toolbox extends \smtech\StMarksReflexiveCanvasLTI\Toolbox +{ + + /** + * Configure course and account navigation placements + * + * @return Generator + */ + public function getGenerator() + { + parent::getGenerator(); + + $this->generator->setOptionProperty( + Option::COURSE_NAVIGATION(), + 'visibility', + 'admins' + ); + $this->generator->setOptionProperty( + Option::ACCOUNT_NAVIGATION(), + 'visibility', + 'admins' + ); + + return $this->generator; + } + + /** + * Get a listing of all accounts organized for presentation in a select picker + * + * @return array + **/ + function getAccountList() + { + $cache = new HierarchicalSimpleCache($this->getMySQL(), __CLASS__); + + $accounts = $cache->getCache('accounts'); + if ($accounts === false) { + $accountsResponse = $this->api_get( + 'accounts/1/sub_accounts', + [ + 'recursive' => 'true' + ] + ); + $accounts = []; + foreach ($accountsResponse as $account) { + $accounts[$account['id']] = $account; + } + $cache->setCache('accounts', $accounts); + } + + return $accounts; + } + + /** + * Get a listing of all terms organized for presentation in a select picker + * + * @return array + **/ + function getTermList() + { + $cache = new HierarchicalSimpleCache($this->getMySQL(), __CLASS__); + + $terms = $cache->getCache('terms'); + if ($terms === false) { + $_terms = $this->api_get( + 'accounts/1/terms', + [ + 'workflow_state' => 'active' + ] + ); + $termsResponse = $_terms['enrollment_terms']; + $terms = []; + foreach ($termsResponse as $term) { + $terms[$term['id']] = $term; + } + $cache->setCache('terms', $terms); + } + + return $terms; + } +} diff --git a/templates/consumers-control-panel.tpl b/templates/consumers-control-panel.tpl new file mode 100644 index 0000000..42eb33d --- /dev/null +++ b/templates/consumers-control-panel.tpl @@ -0,0 +1,105 @@ +{assign var="formLabelWidth" value=$formLabelWidth|Default: 4} +{extends file="subpage.tpl"} + +{block name="subcontent"} + +
    + + + + + + + + + + + {if count($consumers) > 0} + {foreach $consumers as $_consumer} + {if empty($key) || (!empty($key) && $key != $_consumer->getKey())} + {$selected = false} + {else} + {$selected = true} + {/if} + + + + + + {if !$selected} + + {/if} + + {/foreach} + {else} + + + + {/if} + +
    NameConsumer KeyShared SecretEnabled
    {$_consumer->name}{$_consumer->getKey()}{$_consumer->secret} +
    + enabled}checked="checked"{/if} readonly /> +
    +
    +
    +
    + + + +
    +
    +
    No consumers registered yet.
    +
    +
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + + {if !empty($key)} + + {/if} + +
    +
    +
    +
    + +
    +

    To install this LTI, users should choose configuration type By URL and provide their consumer key and secret above. They should point their installer at:

    +

    {$appUrl}?action=config

    +
    + +{/block} diff --git a/templates/lti-consumers.tpl b/templates/lti-consumers.tpl deleted file mode 100644 index 3a21403..0000000 --- a/templates/lti-consumers.tpl +++ /dev/null @@ -1,94 +0,0 @@ -{extends file="page.tpl"} -{block name="content"} - -
    - {if count($consumers) > 0} - - {foreach $consumers as $consumer} - {if empty($requestKey) || (!empty($requestKey) && $requestKey != $consumer['consumer_key'])} - {assign var="closed" value=true} - {else} - {assign var="closed" value=false} - {/if} - - - {if $closed} - - {/if} - - {/foreach} -
    -
    - {foreach $fields as $field} - {if !empty($consumer[$field])} -
    {$field}
    -
    {$consumer[$field]}
    - {/if} - {/foreach} -
    -
    -
    -
    - - - -
    -
    -
    - {else} -

    No consumers

    - {/if} -
    -
    -
    -
    - -
    - -
    -
    - -
    - -
    - - {if !empty($name)}{/if} -
    -
    - -
    - -
    - -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    - - {if !empty($name)} - - {/if} - -
    -
    -
    -
    - -
    -

    To install this LTI, users should choose configuration type By URL and provide their consumer key and secret above. They should point their installer at:

    -

    {$metadata['APP_CONFIG_URL']}

    -
    - -{/block} \ No newline at end of file diff --git a/templates/oauth-form.tpl b/templates/oauth-form.tpl new file mode 100644 index 0000000..6ef41a8 --- /dev/null +++ b/templates/oauth-form.tpl @@ -0,0 +1,14 @@ +{extends file="form.tpl"} + +{block name="form-content"} + +
    + + + + +
    + + {assign var="formButton" value="Log In"} + +{/block} diff --git a/templates/oauth.tpl b/templates/oauth.tpl new file mode 100644 index 0000000..5d3ce9d --- /dev/null +++ b/templates/oauth.tpl @@ -0,0 +1,15 @@ +{extends file="page.tpl"} + +{block name="content"} + + + +
    +

    This tool requires access to the Canvas APIs. You can provide this access either by editing your config.xml file to include the URL of your Canvas API and a valid API access token, or by interactively authenticating to Canvas to issue an API token directly right now.

    +
    + + {include file="oauth-form.tpl"} + +{/block}