From 702747ba0b750eb5df149ebdb3fe9b831ec53e80 Mon Sep 17 00:00:00 2001 From: iturgeon Date: Sun, 13 Nov 2022 21:01:28 +0100 Subject: [PATCH 1/5] LTI overhaul to support Sakai 22.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change var to const for js constants being injected into the page from the server * Allows the before_play_start event to determine if the lti content item picker should be displayed * Move picker display function into a common controller * Add an api method that’ll validate and sign a content item selection lti request * injects LTI_MESSAGE_TYPE and LTI_KEY js constants into the picker page template * LTILaunch gains a method to load the lti config that is associated with a lti key * Updated a few strings that specifically mention canvas * Moves all classes, tests, migrations, and configs from fuel/app/modules/lti into fuel/app * overhaul lti test provider to simplify * simplify lti launch picker code when sending content selection messages to lti/assignment * fixed issue with starting plays with referrer urls longer than 255 characters fixes #1418 * updates oauth validation in fuel by ensuring the request uri is not masked by FuelPHP * Overhaul lit test provider a bit - mostly to simplify and use lessons * fix code sniff errors * add deps to docker image * attempt to fix yarn cache issue * Changes docker build process to ensure the built image has admin:make_paths_writable run to avoid having to run it every startup --- docker/run_build_assets.sh | 2 +- docker/run_tests.sh | 2 +- docker/run_tests_lint.sh | 2 +- fuel/app/classes/basetest.php | 45 +- .../lti => }/classes/controller/lti.php | 25 +- .../controller/lti}/error.php | 3 +- fuel/app/classes/controller/lti/test.php | 389 ++++++++++++++++++ fuel/app/classes/controller/qsets.php | 2 +- fuel/app/classes/controller/questions.php | 6 +- .../{modules/lti => }/classes/ltievents.php | 14 +- .../{modules/lti => }/classes/ltilaunch.php | 24 +- .../lti => }/classes/ltiusermanager.php | 11 +- fuel/app/classes/materia/api/v1.php | 54 ++- fuel/app/classes/materia/fuel/core/cookie.php | 2 +- fuel/app/classes/materia/session/play.php | 2 +- fuel/app/classes/materia/widget/instance.php | 2 +- .../{modules/lti => }/classes/model/lti.php | 2 - fuel/app/{modules/lti => }/classes/oauth.php | 11 +- fuel/app/config/config.php | 2 +- fuel/app/config/event.php | 20 +- fuel/app/config/lti.php | 110 ++++- fuel/app/config/routes.php | 9 +- .../052_create_lti.php} | 0 .../modules/lti/classes/controller/test.php | 297 ------------- fuel/app/modules/lti/config/lti.php | 110 ----- fuel/app/tests/api/v1.php | 85 ++++ fuel/app/{modules/lti => }/tests/basetest.php | 14 +- .../app/{modules/lti => }/tests/ltievents.php | 141 ++++--- .../app/{modules/lti => }/tests/ltilaunch.php | 8 +- .../lti => }/tests/ltiusermanager.php | 80 ++-- fuel/app/{modules/lti => }/tests/oauth.php | 32 +- fuel/app/tests/service/user.php | 4 +- .../default/layouts/lti_sign_and_launch.php | 31 ++ .../themes/default/layouts/test_provider.php | 99 +++++ .../default/lti/layouts/test_learner.php | 26 -- .../default/lti/layouts/test_provider.php | 311 -------------- .../default/{lti => }/partials/config_xml.php | 0 .../partials/error_autoplay_misconfigured.php | 2 +- .../{lti => }/partials/error_general.php | 0 .../partials/error_lti_guest_mode.php | 0 .../partials/error_unknown_assignment.php | 2 +- .../{lti => }/partials/error_unknown_user.php | 2 +- .../{lti => }/partials/open_preview.php | 4 +- .../{lti => }/partials/outcomes_xml.php | 0 .../default/{lti => }/partials/post_login.php | 0 .../{lti => }/partials/select_item.php | 0 .../{lti => }/partials/select_item_js.php | 0 materia-app.Dockerfile | 16 +- package.json | 2 +- yarn.lock | 6 +- 50 files changed, 1035 insertions(+), 976 deletions(-) rename fuel/app/{modules/lti => }/classes/controller/lti.php (86%) rename fuel/app/{modules/lti/classes/controller => classes/controller/lti}/error.php (97%) create mode 100644 fuel/app/classes/controller/lti/test.php rename fuel/app/{modules/lti => }/classes/ltievents.php (96%) rename fuel/app/{modules/lti => }/classes/ltilaunch.php (86%) rename fuel/app/{modules/lti => }/classes/ltiusermanager.php (94%) rename fuel/app/{modules/lti => }/classes/model/lti.php (97%) rename fuel/app/{modules/lti => }/classes/oauth.php (93%) rename fuel/app/{modules/lti/migrations/001_create_lti.php => migrations/052_create_lti.php} (100%) delete mode 100644 fuel/app/modules/lti/classes/controller/test.php delete mode 100644 fuel/app/modules/lti/config/lti.php rename fuel/app/{modules/lti => }/tests/basetest.php (92%) rename fuel/app/{modules/lti => }/tests/ltievents.php (74%) rename fuel/app/{modules/lti => }/tests/ltilaunch.php (87%) rename fuel/app/{modules/lti => }/tests/ltiusermanager.php (84%) rename fuel/app/{modules/lti => }/tests/oauth.php (73%) create mode 100644 fuel/app/themes/default/layouts/lti_sign_and_launch.php create mode 100644 fuel/app/themes/default/layouts/test_provider.php delete mode 100644 fuel/app/themes/default/lti/layouts/test_learner.php delete mode 100644 fuel/app/themes/default/lti/layouts/test_provider.php rename fuel/app/themes/default/{lti => }/partials/config_xml.php (100%) rename fuel/app/themes/default/{lti => }/partials/error_autoplay_misconfigured.php (73%) rename fuel/app/themes/default/{lti => }/partials/error_general.php (100%) rename fuel/app/themes/default/{lti => }/partials/error_lti_guest_mode.php (100%) rename fuel/app/themes/default/{lti => }/partials/error_unknown_assignment.php (72%) rename fuel/app/themes/default/{lti => }/partials/error_unknown_user.php (91%) rename fuel/app/themes/default/{lti => }/partials/open_preview.php (91%) rename fuel/app/themes/default/{lti => }/partials/outcomes_xml.php (100%) rename fuel/app/themes/default/{lti => }/partials/post_login.php (100%) rename fuel/app/themes/default/{lti => }/partials/select_item.php (100%) rename fuel/app/themes/default/{lti => }/partials/select_item_js.php (100%) diff --git a/docker/run_build_assets.sh b/docker/run_build_assets.sh index cc438cc64..31cf0338a 100755 --- a/docker/run_build_assets.sh +++ b/docker/run_build_assets.sh @@ -22,4 +22,4 @@ docker run \ --mount type=bind,source="$(pwd)"/../,target=/build \ --mount source=materia-asset-build-vol,target=/build/node_modules \ node:12.11.1-alpine \ - /bin/ash -c "apk add --no-cache git && cd build && yarn install --frozen-lockfile --non-interactive --production --silent --pure-lockfile --force" + /bin/ash -c "apk add --no-cache git && cd build && yarn install --frozen-lockfile --non-interactive --production --silent --pure-lockfile --force --check-files --cache-folder .ycache && rm -rf .ycache" diff --git a/docker/run_tests.sh b/docker/run_tests.sh index 63b5a5087..3e6660317 100755 --- a/docker/run_tests.sh +++ b/docker/run_tests.sh @@ -15,4 +15,4 @@ DCTEST="docker-compose -f docker-compose.yml -f docker-compose.override.test.yml set -e set -o xtrace -$DCTEST run --rm app /wait-for-it.sh mysql:3306 -t 20 -- composer run testci -- "$@" +$DCTEST run -T --rm app /wait-for-it.sh mysql:3306 -t 20 -- composer run testci -- "$@" diff --git a/docker/run_tests_lint.sh b/docker/run_tests_lint.sh index 12117fd47..945ec6bb0 100755 --- a/docker/run_tests_lint.sh +++ b/docker/run_tests_lint.sh @@ -10,4 +10,4 @@ DCTEST="docker-compose -f docker-compose.yml -f docker-compose.override.test.yml set -e set -o xtrace -$DCTEST run --rm --no-deps app composer sniff-ci +$DCTEST run -T --rm --no-deps app composer sniff-ci diff --git a/fuel/app/classes/basetest.php b/fuel/app/classes/basetest.php index 25ab46453..2bfab2a3d 100644 --- a/fuel/app/classes/basetest.php +++ b/fuel/app/classes/basetest.php @@ -54,6 +54,13 @@ protected function tearDown(): void } } + protected static function remove_all_roles_for_user($user_id) + { + \DB::delete('perm_role_to_user') + ->where('user_id', $user_id) + ->execute(); + } + protected static function clear_fuel_input() { // reset fuelphp's input class @@ -196,6 +203,10 @@ protected function _as_student() \Fuel\Tasks\Admin::new_user($uname, 'test', 'd', 'student', 'testStudent@ucf.edu', $pword); $user = \Model_User::find_by_username($uname); } + else + { + static::remove_all_roles_for_user($user->id); + } $login = \Service_User::login($uname, $pword); $this->assertTrue($login); @@ -215,13 +226,16 @@ protected function _as_author() { require_once(APPPATH.'/tasks/admin.php'); \Fuel\Tasks\Admin::new_user($uname, 'Prof', 'd', 'Author', 'testAuthor@ucf.edu', $pword); - \Fuel\Tasks\Admin::give_user_role($uname, 'basic_author'); $user = \Model_User::find_by_username($uname); } + else + { + static::remove_all_roles_for_user($user->id); + } + \Materia\Perm_Manager::add_users_to_roles_system_only([$user->id], [\Materia\Perm_Role::AUTHOR]); $login = \Service_User::login($uname, $pword); $this->assertTrue($login); - $this->users_to_clean[] = $user; return $user; } @@ -238,10 +252,14 @@ protected function _as_author_2() { require_once(APPPATH.'/tasks/admin.php'); \Fuel\Tasks\Admin::new_user($uname, 'test', 'd', 'author', 'testAuthor2@ucf.edu', $pword); - \Fuel\Tasks\Admin::give_user_role($uname, 'basic_author'); $user = \Model_User::find_by_username($uname); } + else + { + static::remove_all_roles_for_user($user->id); + } + \Materia\Perm_Manager::add_users_to_roles_system_only([$user->id], [\Materia\Perm_Role::AUTHOR]); $login = \Service_User::login($uname, $pword); $this->assertTrue($login); $this->users_to_clean[] = $user; @@ -260,10 +278,14 @@ protected function _as_author_3() { require_once(APPPATH.'/tasks/admin.php'); \Fuel\Tasks\Admin::new_user($uname, 'test', 'd', 'author', 'testAuthor3@ucf.edu', $pword); - \Fuel\Tasks\Admin::give_user_role($uname, 'basic_author'); $user = \Model_User::find_by_username($uname); } + else + { + static::remove_all_roles_for_user($user->id); + } + \Materia\Perm_Manager::add_users_to_roles_system_only([$user->id], [\Materia\Perm_Role::AUTHOR]); $login = \Service_User::login($uname, $pword); $this->assertTrue($login); $this->users_to_clean[] = $user; @@ -282,12 +304,14 @@ protected function _as_super_user() { require_once(APPPATH.'/tasks/admin.php'); \Fuel\Tasks\Admin::new_user($uname, 'test', 'd', 'su', 'testSu@ucf.edu', $pword); - // TODO: super_user should get all these rights inherently right??????!!!! - \Fuel\Tasks\Admin::give_user_role($uname, 'super_user'); - \Fuel\Tasks\Admin::give_user_role($uname, 'basic_author'); $user = \Model_User::find_by_username($uname); } + else + { + static::remove_all_roles_for_user($user->id); + } + \Materia\Perm_Manager::add_users_to_roles_system_only([$user->id], [\Materia\Perm_Role::AUTHOR, \Materia\Perm_Role::SU]); $login = \Service_User::login($uname, $pword); $this->assertTrue($login); $this->users_to_clean[] = $user; @@ -305,11 +329,14 @@ protected function _as_noauth() { require_once(APPPATH.'/tasks/admin.php'); \Fuel\Tasks\Admin::new_user($uname, 'test', 'd', 'noauth', 'testNoAuth@ucf.edu', $pword); - // TODO: super_user should get all these rights inherently right??????!!!! - \Fuel\Tasks\Admin::give_user_role($uname, 'no_author'); $user = \Model_User::find_by_username($uname); } + else + { + static::remove_all_roles_for_user($user->id); + } + \Materia\Perm_Manager::add_users_to_roles_system_only([$user->id], [\Materia\Perm_Role::NOAUTH]); $login = \Service_User::login($uname, $pword); $this->assertTrue($login); $this->users_to_clean[] = $user; diff --git a/fuel/app/modules/lti/classes/controller/lti.php b/fuel/app/classes/controller/lti.php similarity index 86% rename from fuel/app/modules/lti/classes/controller/lti.php rename to fuel/app/classes/controller/lti.php index 177884c83..f8a8dd914 100644 --- a/fuel/app/modules/lti/classes/controller/lti.php +++ b/fuel/app/classes/controller/lti.php @@ -4,8 +4,6 @@ * License outlined in licenses folder */ -namespace Lti; - class Controller_Lti extends \Controller { use \Trait_Analytics; @@ -20,7 +18,7 @@ public function before() */ public function action_index() { - $cfg = \Config::get('lti::lti.consumers.default'); + $cfg = \Config::get('lti.consumers.default'); // TODO: this is hard coded for Canvas, figure out if the request carries any info we can use to figure this out $this->theme->set_template('partials/config_xml'); $this->theme->get_template() @@ -58,8 +56,8 @@ public function action_login() $this->theme->set_partial('content', 'partials/post_login'); $this->insert_analytics(); - \Js::push_inline('var BASE_URL = "'.\Uri::base().'";'); - \Js::push_inline('var STATIC_CROSSDOMAIN = "'.\Config::get('materia.urls.static').'";'); + \Js::push_inline('const BASE_URL = "'.\Uri::base().'";'); + \Js::push_inline('const STATIC_CROSSDOMAIN = "'.\Config::get('materia.urls.static').'";'); \Css::push_group('core'); @@ -77,8 +75,10 @@ public function action_picker(bool $authenticate = true) $launch = LtiLaunch::from_request(); if ($authenticate && ! LtiUserManager::authenticate($launch)) return \Response::redirect('/lti/error/unknown_user'); - $system = ucfirst(\Input::post('tool_consumer_info_product_family_code', 'this system')); - $is_selector_mode = \Input::post('selection_directive') === 'select_link' || \Input::post('lti_message_type') === 'ContentItemSelectionRequest'; + $system = \Input::post('tool_consumer_info_product_family_code', 'this system'); + $lti_message_type = \Input::post('lti_message_type', 'none'); + $lti_key = \Input::post('oauth_consumer_key', ''); + $is_selector_mode = \Input::post('selection_directive') === 'select_link' || $lti_message_type === 'ContentItemSelectionRequest'; $return_url = \Input::post('launch_presentation_return_url') ?? \Input::post('content_item_return_url'); \Materia\Log::profile(['action_picker', \Input::post('selection_directive'), $system, $is_selector_mode ? 'yes' : 'no', $return_url], 'lti'); @@ -89,17 +89,18 @@ public function action_picker(bool $authenticate = true) \Js::push_inline('var BASE_URL = "'.\Uri::base().'";'); \Js::push_inline('var WIDGET_URL = "'.\Config::get('materia.urls.engines').'";'); \Js::push_inline('var STATIC_CROSSDOMAIN = "'.\Config::get('materia.urls.static').'";'); - \Js::push_inline($this->theme->view('partials/select_item_js') - ->set('system', $system)); - \Css::push_group(['core', 'lti']); - + \Js::push_inline('var LTI_MESSAGE_TYPE = "'.$lti_message_type.'"'); + \Js::push_inline('var system = "'.htmlentities($system).'"'); + \Js::push_inline('const LTI_KEY = "'.$lti_key.'"'); if ($is_selector_mode && ! empty($return_url)) { \Js::push_inline('var RETURN_URL = "'.$return_url.'"'); } + \Css::push_group(['core', 'lti']); + $this->theme->get_template() - ->set('title', 'Select a Widget for Use in '.$system) + ->set('title', 'Select a Widget for Use in '.ucfirst($system)) ->set('page_type', 'lti-select'); $this->theme->set_partial('content', 'partials/select_item'); diff --git a/fuel/app/modules/lti/classes/controller/error.php b/fuel/app/classes/controller/lti/error.php similarity index 97% rename from fuel/app/modules/lti/classes/controller/error.php rename to fuel/app/classes/controller/lti/error.php index 5511dc85e..06becd2cf 100644 --- a/fuel/app/modules/lti/classes/controller/error.php +++ b/fuel/app/classes/controller/lti/error.php @@ -4,9 +4,8 @@ * License outlined in licenses folder */ -namespace Lti; -class Controller_Error extends \Controller +class Controller_Lti_Error extends \Controller { use \Trait_Analytics; protected $_content_partial = 'partials/error_general'; diff --git a/fuel/app/classes/controller/lti/test.php b/fuel/app/classes/controller/lti/test.php new file mode 100644 index 000000000..a8ac211bf --- /dev/null +++ b/fuel/app/classes/controller/lti/test.php @@ -0,0 +1,389 @@ +'; + print_r(\Input::get()); + echo ''; + } + + public function get_sign_and_launch() + { + $params = \Input::get(); + + $use_bad_signature = isset($params['use_bad_signature']); + unset($params['use_bad_signature']); + + if (isset($params['use_random_user'])) + { + unset($params['use_random_user']); + $role = 'LEARNER'; + if (isset($params['roles']) && strpos($params['roles'], 'Instructor') !== false) + { + $role = 'INSTRUCTOR'; + } + $random_number = rand(0, 100000); + $name = "_LTI_{$role}_{$random_number}"; + $params['user_id'] = $random_number; + $params['custom_canvas_user_id'] = $random_number; + $params['lis_person_sourcedid'] = $name; + $params['lis_person_contact_email_primary'] = "{$name}@mailinator.com"; + $params['lis_person_name_given'] = $name; + $params['lis_person_name_family'] = $name; + } + + if (isset($params['use_no_email'])) + { + unset($params['lis_person_contact_email_primary']); + unset($params['use_no_email']); + } + + $endpoint = $params['endpoint']; + $secret = \Config::get('lti.consumers.default.secret'); + $hmcsha1 = new \Eher\OAuth\HmacSha1(); + $consumer = new \Eher\OAuth\Consumer('', $secret); + $request = \Eher\OAuth\Request::from_consumer_and_token($consumer, null, 'POST', $endpoint, $params); + $request->sign_request($hmcsha1, $consumer, ''); + $signed_params = $request->get_parameters(); + if ($use_bad_signature) + { + $signed_params['oauth_signature'] = 'THIS_IS_A_BAD_SIGNATURE'; + } + + $this->theme = \Theme::instance(); + $this->theme->set_template('layouts/lti_sign_and_launch') + ->set_safe(['post' => json_encode($signed_params)]); + + return \Response::forge(\Theme::instance()->render()); + } + + public function get_embed() + { + $embed_type = \Input::get('embed_type', false); + $url = \Input::get('url'); + + if ( $embed_type != 'basic_lti' ) return; + + $widget = str_replace(\Uri::base(false).'embed/', '', $url); + $parts = explode('/', $widget); + + //check to see if we have an LTI association for this widget already + // normally we would only check for 'resource_link', since an assignment can't have more than one widget associated with it + // but for the purpose of the LTI test provider, we'll relax that requirement + $check = Model_Lti::query() + ->where('resource_link', 'test-resource') + ->where('item_id', $parts[0]) + ->get_one(); + + if (empty($check)) + { + $user = \Model_User::find_current(); + + $assoc = Model_Lti::forge(); + $assoc->resource_link = 'test-resource'; + $assoc->consumer_guid = 'test'; + $assoc->item_id = $parts[0]; + $assoc->user_id = $user->id; + $assoc->consumer = 'default'; + $assoc->name = $user->first.' '.$user->last; + $assoc->context_id = 'test_context'; + $assoc->context_title = 'test_context'; + $assoc->save(); + } + + return \Response::redirect("/lti/success/{$parts[0]}?embed_type={$embed_type}&url={$url}"); + } + + // Generate a bunch of sample lti launch examples to test + public function get_provider() + { + $assignment_url = \Uri::create('lti/assignment'); + $picker_url = \Uri::create('lti/picker'); + $validate_url = \Uri::create('lti/test/validate'); + $login_url = \Uri::create('lti/login'); + $play_launch_url_modern_embed = \Uri::create('embed').'/HASH_ID/HUMAN-FRIENDLY-NAME'; + $play_launch_url_modern_play = \Uri::create('play').'/HASH_ID/HUMAN-FRIENDLY-NAME'; + $play_launch_url_legacy = \Uri::create('lti/assignment').'?widget=HASH_ID'; + + $base_params = [ + 'resource_link_id' => 'test-resource', + 'context_id' => 'test-context', + 'lis_result_sourcedid' => 'test-source-id', + 'roles' => 'Instructor', + 'oauth_consumer_key' => \Config::get('lti.consumers.default.key'), + 'lti_message_type' => 'basic-lti-launch-request', + 'tool_consumer_instance_guid' => 999999, + 'tool_consumer_info_product_family_code' => 'materia_test', + 'tool_consumer_instance_contact_email' => 'SYSTEM@mailinator.com', + 'launch_presentation_document_target' => 'iframe', + 'user_id' => 999999, + 'custom_canvas_user_id' => 999999, + 'lis_person_sourcedid' => '_LTI_INSTRUCTOR_', + 'lis_person_contact_email_primary' => '_LTI_INSTRUCTOR_@mailinator.com', + 'lis_person_name_given' => '_LTI_INSTRUCTOR_', + 'lis_person_name_family' => '_LTI_INSTRUCTOR_', + 'custom-inst-id' => '', + 'selection_directive' => '', + ]; + + $launch_args = [ + 'LTI Assignment Launch' => [ + [ + 'label' => 'As Learner (bad signature)', + 'params' => array_merge($base_params, [ + 'lti_url' => 'lti_url', + 'resource_link' => 'use_bad_signature', + 'roles' => 'Learner', + 'user_id' => 1111111, + 'lis_person_sourcedid' => '_LTI_LEARNER_', + 'lis_person_contact_email_primary' => '_LTI_LEARNER_@mailinator.com', + 'lis_person_name_given' => '_LTI_LEARNER_', + 'lis_person_name_family' => '_LTI_LEARNER_', + 'use_bad_signature' => true + ]), + 'endpoint' => $assignment_url, + ], + [ + 'label' => 'As Learner', + 'params' => array_merge($base_params, [ + 'lti_url' => 'lti_url', + 'custom_widget_instance_id' => 'custom_widget_instance_id', + 'resource_link' => 'use_bad_signature', + 'roles' => 'Learner', + 'user_id' => 1111111, + 'lis_person_sourcedid' => '_LTI_LEARNER_', + 'lis_person_contact_email_primary' => '_LTI_LEARNER_@mailinator.com', + 'lis_person_name_given' => '_LTI_LEARNER_', + 'lis_person_name_family' => '_LTI_LEARNER_', + ]), + 'endpoint' => $assignment_url, + ], + [ + 'label' => 'As Random New Learner', + 'params' => array_merge($base_params, [ + 'lti_url' => 'lti_url', + 'resource_link' => 'lti_test_launch1', + 'roles' => 'Learner', + 'user_id' => 1111111, + 'lis_person_sourcedid' => '_LTI_LEARNER_', + 'lis_person_contact_email_primary' => '_LTI_LEARNER_@mailinator.com', + 'lis_person_name_given' => '_LTI_LEARNER_', + 'lis_person_name_family' => '_LTI_LEARNER_', + 'use_random_user' => true + ]), + 'endpoint' => $assignment_url, + ], + [ + 'label' => 'As Random New Learner without an email address', + 'params' => array_merge($base_params, [ + 'lti_url' => 'lti_url', + 'resource_link' => 'lti_test_launch2', + 'roles' => 'Learner', + 'user_id' => 1111111, + 'lis_person_sourcedid' => '_LTI_LEARNER_', + 'lis_person_contact_email_primary' => '_LTI_LEARNER_@mailinator.com', + 'lis_person_name_given' => '_LTI_LEARNER_', + 'lis_person_name_family' => '_LTI_LEARNER_', + 'use_random_user' => true, + 'use_no_email' => true, + ]), + 'endpoint' => $assignment_url, + ], + [ + 'label' => 'As the guest/test student', + 'params' => array_merge($base_params, [ + 'lti_url' => 'lti_url', + 'custom_widget_instance_id' => 'custom_widget_instance_id', + 'resource_link' => 'lti_test_launch3', + 'roles' => 'Learner', + 'user_id' => 1111111, + 'lis_person_sourcedid' => '_LTI_LEARNER_', + 'lis_person_contact_email_primary' => '_LTI_LEARNER_@mailinator.com', + 'lis_person_name_given' => '_LTI_LEARNER_', + 'lis_person_name_family' => '_LTI_LEARNER_', + 'lis_person_sourcedid' => '', + 'lis_person_contact_email_primary' => 'notifications@instructure.com', + 'lis_person_name_given' => 'Test', + 'lis_person_name_family' => 'Student', + + ]), + 'endpoint' => $assignment_url, + ], + [ + 'label' => 'As An Instructor', + 'params' => array_merge($base_params, [ + 'lti_url' => 'lti_url', + 'custom_widget_instance_id' => 'custom_widget_instance_id', + 'resource_link' => 'lti_test_launch4', + ]), + 'endpoint' => $assignment_url, + ], + [ + 'label' => 'As A Random New Instructor', + 'params' => array_merge($base_params, [ + 'lti_url' => 'lti_url', + 'custom_widget_instance_id' => 'custom_widget_instance_id', + 'resource_link' => 'lti_test_launch5', + 'use_random_user' => true, + ]), + 'endpoint' => $assignment_url, + ], + ], + 'LTI Navigation Launch' => [ + [ + 'label' => 'Launch as instructor', + 'params' => array_merge($base_params, [ + 'selection_directive' => 'select_link', + ]), + 'endpoint' => $login_url, + ], + [ + 'label' => 'As Random New Learner', + 'params' => array_merge($base_params, [ + 'selection_directive' => 'select_link', + 'lti_url' => 'lti_url', + 'resource_link' => 'lti_test_launch1', + 'roles' => 'Learner', + 'user_id' => 1111111, + 'lis_person_sourcedid' => '_LTI_LEARNER_', + 'lis_person_contact_email_primary' => '_LTI_LEARNER_@mailinator.com', + 'lis_person_name_given' => '_LTI_LEARNER_', + 'lis_person_name_family' => '_LTI_LEARNER_', + 'use_random_user' => true + ]), + 'endpoint' => $login_url, + ], + [ + 'label' => 'Launch as instructor (bad signature)', + 'params' => array_merge($base_params, [ + 'selection_directive' => 'select_link', + 'use_bad_signature' => true, + ]), + 'endpoint' => $login_url, + ], + ], + 'LTI Picker Launch' => [ + [ + 'label' => 'Launch as instructor', + 'params' => array_merge($base_params, [ + 'launch_presentation_return_url' => \Uri::create('lti/test/embed'), + 'selection_directive' => 'select_link', + ]), + 'endpoint' => $picker_url, + ], + [ + 'label' => 'Launch as Random New Instructor', + 'params' => array_merge($base_params, [ + 'launch_presentation_return_url' => \Uri::create('lti/test/embed'), + 'selection_directive' => 'select_link', + 'use_random_user' => true + ]), + 'endpoint' => $picker_url, + ], + [ + 'label' => 'Launch as instructor (bad signature)', + 'params' => array_merge($base_params, [ + 'launch_presentation_return_url' => \Uri::create('lti/test/embed'), + 'selection_directive' => 'select_link', + 'use_bad_signature' => true, + ]), + 'endpoint' => $picker_url, + ], + + [ + 'label' => 'Launch as instructor - assignment url for sakai', + 'params' => array_merge($base_params, [ + 'launch_presentation_return_url' => \Uri::create('lti/test/embed'), + 'selection_directive' => 'ContentItemSelectionRequest', + ]), + 'endpoint' => $assignment_url, + ], + [ + 'label' => 'Launch as Random New Instructor - assignment url for sakai', + 'params' => array_merge($base_params, [ + 'launch_presentation_return_url' => \Uri::create('lti/test/embed'), + 'selection_directive' => 'ContentItemSelectionRequest', + 'use_random_user' => true + ]), + 'endpoint' => $assignment_url, + ], + [ + 'label' => 'Launch as instructor (bad signature) - assignment url for sakai', + 'params' => array_merge($base_params, [ + 'launch_presentation_return_url' => \Uri::create('lti/test/embed'), + 'selection_directive' => 'ContentItemSelectionRequest', + 'use_bad_signature' => true, + ]), + 'endpoint' => $assignment_url, + ], + ], + 'Other' => [ + [ + 'label' => 'Test Validation', + 'params' => $base_params, + 'endpoint' => $validate_url, + ], + [ + 'label' => 'Test Validation (bad signature)', + 'params' => array_merge($base_params, [ + 'use_bad_signature' => true, + ]), + 'endpoint' => $validate_url, + ], + [ + 'label' => 'Unknown Assignment Error', + 'params' => array_merge($base_params, ['resource_link_id' => 'this-will-not-work']), + 'endpoint' => $assignment_url, + ], + ] + ]; + + $this->theme = \Theme::instance(); + $this->theme->set_template('layouts/test_provider') + ->set([ + 'launch_args' => $launch_args, + 'base_params' => $base_params, + 'endpoints' => [ + 'assignment_url' => $assignment_url, + 'picker_url' => $picker_url, + 'validate_url' => $validate_url, + 'login_url' => $login_url, + 'modern_play_embed' => $play_launch_url_modern_embed, + 'modern_play' => $play_launch_url_modern_play, + 'legacy_play' => $play_launch_url_legacy, + ] + ]); + + return \Response::forge(\Theme::instance()->render()); + } + + // Reports if OAuth is able to validate the signature + public function post_validate() + { + echo 'LTI OAuth Validation '.(\Oauth::validate_post() ? 'PASSED!' : 'FAILED'); + echo '
';
+		print_r(\Input::post());
+		echo '
'; + } + +} diff --git a/fuel/app/classes/controller/qsets.php b/fuel/app/classes/controller/qsets.php index 53500b7cf..db1656c44 100644 --- a/fuel/app/classes/controller/qsets.php +++ b/fuel/app/classes/controller/qsets.php @@ -35,7 +35,7 @@ public function action_import() public function action_confirm() { if (\Service_User::verify_session() !== true ) throw new HttpNotFoundException; - + Css::push_group(['core', 'rollback_dialog']); Js::push_group(['angular', 'jquery', 'materia', 'author']); diff --git a/fuel/app/classes/controller/questions.php b/fuel/app/classes/controller/questions.php index 7944e343b..eccfa75e3 100644 --- a/fuel/app/classes/controller/questions.php +++ b/fuel/app/classes/controller/questions.php @@ -16,9 +16,9 @@ public function action_import() Css::push_group(['core', 'question_import']); Js::push_group(['angular', 'jquery', 'materia', 'author', 'dataTables']); - Js::push_inline('var BASE_URL = "'.Uri::base().'";'); - Js::push_inline('var WIDGET_URL = "'.Config::get('materia.urls.engines').'";'); - Js::push_inline('var STATIC_CROSSDOMAIN = "'.Config::get('materia.urls.static').'";'); + Js::push_inline('const BASE_URL = "'.Uri::base().'";'); + Js::push_inline('const WIDGET_URL = "'.Config::get('materia.urls.engines').'";'); + Js::push_inline('const STATIC_CROSSDOMAIN = "'.Config::get('materia.urls.static').'";'); $theme = Theme::instance(); $theme->set_template('layouts/main'); diff --git a/fuel/app/modules/lti/classes/ltievents.php b/fuel/app/classes/ltievents.php similarity index 96% rename from fuel/app/modules/lti/classes/ltievents.php rename to fuel/app/classes/ltievents.php index 7c9ca6ff0..db6601fd6 100644 --- a/fuel/app/modules/lti/classes/ltievents.php +++ b/fuel/app/classes/ltievents.php @@ -1,10 +1,8 @@ guest_access) $redirect = '/lti/error/guest_mode'; @@ -160,12 +160,12 @@ public static function on_score_updated_event($event_args) $body = \Theme::instance()->view('lti/partials/outcomes_xml', $view_data)->render(); - if (\Config::get('lti::lti.log_for_debug', false)) + if (\Config::get('lti.log_for_debug', false)) { \Materia\Log::profile(['score-outcome-sent', $body], 'lti-launch'); } - $success = Oauth::send_body_hashed_post($launch->service_url, $body, $secret, $key); + $success = \Oauth::send_body_hashed_post($launch->service_url, $body, $secret, $key); static::log($play_id, 'outcome-'.($success ? 'success' : 'failure'), $max_score); diff --git a/fuel/app/modules/lti/classes/ltilaunch.php b/fuel/app/classes/ltilaunch.php similarity index 86% rename from fuel/app/modules/lti/classes/ltilaunch.php rename to fuel/app/classes/ltilaunch.php index 6a11d9484..1de8fff42 100644 --- a/fuel/app/modules/lti/classes/ltilaunch.php +++ b/fuel/app/classes/ltilaunch.php @@ -1,6 +1,5 @@ 'ContentItemSelection', + 'lti_version' => 'LTI-1p0', + 'content_items' => $content_items, + 'data' => '{"sentBy": "Materia"}', + 'oauth_nonce' => sodium_bin2hex(random_bytes(SODIUM_CRYPTO_STREAM_KEYBYTES)), + 'oauth_timestamp' => time(), + 'oauth_callback' => 'about:blank', + 'oauth_consumer_key' => $lti_key, + 'oauth_signature_method' => 'HMAC-SHA1', + 'oauth_version' => '1.0', + ]; + + $secret = $lti_config['secret'] ?? false; + $hmc_sha1 = new \Eher\OAuth\HmacSha1(); + $consumer = new \Eher\OAuth\Consumer('', $secret); + + // assumes the results will be sent via POST + $request = \Eher\OAuth\Request::from_consumer_and_token($consumer, null, 'post', $url, $params); + $request->sign_request($hmc_sha1, $consumer, ''); + $results = $request->get_parameters(); + + // Remove GET params in $url from $results as they may mess up validation. + // if duplicated here. (ex: Sakai will fail validation) + $query_str = parse_url($url, PHP_URL_QUERY); + parse_str($query_str, $query_params); + if (is_array($query_params)) + { + $keys = array_keys($query_params); + foreach ($keys as $key) + { + if (isset($results[$key])) + { + unset($results[$key]); + } + } + } + + return $results; + } + + /** * @return bool, true if successfully deleted widget instance, false otherwise. */ @@ -201,7 +253,7 @@ static public function widget_instance_new($widget_id=null, $name=null, $qset=nu * @param int $open_at * @param int $close_at * @param int $attempts - * @param bool $guest_access + * @param bool $guest_accesss * @param bool $is_student_made * * @return array An associative array with details about the save diff --git a/fuel/app/classes/materia/fuel/core/cookie.php b/fuel/app/classes/materia/fuel/core/cookie.php index 6bdb13d46..0dfd18e00 100644 --- a/fuel/app/classes/materia/fuel/core/cookie.php +++ b/fuel/app/classes/materia/fuel/core/cookie.php @@ -41,7 +41,7 @@ public static function set($name, $value, $expiration = null, $path = null, $dom // is_null($secure) and $secure = static::$config['secure']; is_null($http_only) and $http_only = static::$config['http_only']; //static::$config is protected - can't get it in an extended class, hack workaround here - is_null($same_site) and $same_site = 'None'; + empty($same_site) and $same_site = 'None'; // add the current time so we have an offset $expiration = $expiration > 0 ? $expiration + time() : 0; diff --git a/fuel/app/classes/materia/session/play.php b/fuel/app/classes/materia/session/play.php index 8c559990f..5d6443a12 100644 --- a/fuel/app/classes/materia/session/play.php +++ b/fuel/app/classes/materia/session/play.php @@ -65,7 +65,7 @@ public function start($user_id=0, $inst_id=0, $context_id=false, $is_preview=fal // Essentially true but fragile. $is_lti = array_key_exists('lti_message_type', $this->environment_data['input']) || array_key_exists('token', $this->environment_data['input']); $this->auth = $is_lti ? 'lti' : ''; - $this->referrer_url = \Input::referrer(); + $this->referrer_url = mb_strimwidth(\Input::referrer(), 0, 255); // Preview Plays dont log anything if ($is_preview) return static::start_preview($inst_id); diff --git a/fuel/app/classes/materia/widget/instance.php b/fuel/app/classes/materia/widget/instance.php index f5a69c6b2..f91ebe317 100644 --- a/fuel/app/classes/materia/widget/instance.php +++ b/fuel/app/classes/materia/widget/instance.php @@ -592,7 +592,7 @@ public function allows_guest_players() */ public function lti_associations() { - return \Lti\Model_Lti::query() + return \Model_Lti::query() ->where('item_id', $this->id) ->get(); } diff --git a/fuel/app/modules/lti/classes/model/lti.php b/fuel/app/classes/model/lti.php similarity index 97% rename from fuel/app/modules/lti/classes/model/lti.php rename to fuel/app/classes/model/lti.php index 96acdba05..bac423d2f 100644 --- a/fuel/app/modules/lti/classes/model/lti.php +++ b/fuel/app/classes/model/lti.php @@ -1,7 +1,5 @@ build_signature($hasher, $consumer, false); if ($new_sig !== $signature) throw new \Exception('Authorization signature failure.'); @@ -27,7 +26,7 @@ public static function validate_post() } catch (\Exception $e) { - \Materia\Log::profile(['invalid-oauth-received', $e->getMessage(), \Uri::current(), print_r(\Input::post(), 1)], 'lti-error-dump'); + \Materia\Log::profile(['invalid-oauth-received', $e->getMessage(), \Uri::main(), print_r(\Input::post(), 1)], 'lti-error-dump'); } return false; @@ -39,8 +38,8 @@ public static function build_post_args(\Model_User $user, $endpoint, $params, $k $oauth_params = [ 'oauth_consumer_key' => $key, 'lti_message_type' => 'basic-lti-launch-request', - 'tool_consumer_instance_guid' => \Config::get('lti::lti.tool_consumer_instance_guid'), - 'tool_consumer_info_product_family_code' => \Config::get('lti::lti.tool_consumer_info_product_family_code'), + 'tool_consumer_instance_guid' => \Config::get('lti.tool_consumer_instance_guid'), + 'tool_consumer_info_product_family_code' => \Config::get('lti.tool_consumer_info_product_family_code'), 'tool_consumer_instance_contact_email' => \Config::get('materia.system_email'), 'tool_consumer_info_version' => \Config::get('materia.system_version'), 'user_id' => $user->id, diff --git a/fuel/app/config/config.php b/fuel/app/config/config.php index 63d0ceb18..903784bca 100644 --- a/fuel/app/config/config.php +++ b/fuel/app/config/config.php @@ -308,7 +308,7 @@ * If you don't want the config in a group use null as groupname. */ 'config' => array( - 'lti::lti', + 'lti', ), /** diff --git a/fuel/app/config/event.php b/fuel/app/config/event.php index eb74a1ed4..6d0623780 100644 --- a/fuel/app/config/event.php +++ b/fuel/app/config/event.php @@ -1,23 +1,17 @@ [ // LTI Events - 'score_updated' => '\Lti\LtiEvents::on_score_updated_event', - 'widget_instance_delete' => '\Lti\LtiEvents::on_widget_instance_delete_event', - 'play_completed' => '\Lti\LtiEvents::on_play_completed_event', - 'before_play_start' => '\Lti\LtiEvents::on_before_play_start_event', - 'play_start' => '\Lti\LtiEvents::on_play_start_event', - 'before_score_display' => '\Lti\LtiEvents::on_before_score_display_event', - 'before_single_score_review' => '\Lti\LtiEvents::on_before_single_score_review' + 'score_updated' => '\LtiEvents::on_score_updated_event', + 'widget_instance_delete' => '\LtiEvents::on_widget_instance_delete_event', + 'play_completed' => '\LtiEvents::on_play_completed_event', + 'before_play_start' => '\LtiEvents::on_before_play_start_event', + 'play_start' => '\LtiEvents::on_play_start_event', + 'before_score_display' => '\LtiEvents::on_before_score_display_event', + 'before_single_score_review' => '\LtiEvents::on_before_single_score_review' /* 'app_created' => function() diff --git a/fuel/app/config/lti.php b/fuel/app/config/lti.php index ad38b1e5d..171687d56 100644 --- a/fuel/app/config/lti.php +++ b/fuel/app/config/lti.php @@ -1,6 +1,110 @@ '1', + 'launch_presentation_return_url' => \Uri::create('lti/return'), + 'tool_consumer_info_product_family_code' => 'materia', + 'tool_consumer_instance_guid' => $_ENV['LTI_GUID'] ?? 'ucfopen.github.io', + 'graceful_fallback_to_default' => $_ENV['BOOL_LTI_GRACEFUL_CONFIG_FALLBACK'] ?? true, + 'log_for_debug' => $_ENV['BOOL_LTI_LOG_FOR_DEBUGGING'] ?? false, + + 'consumers' => [ + // The array key here is matched to 'tool_consumer_info_product_family_code' in lti launches + // if there is no specific match, 'default' is used as a fallback + // NOTE that custom consumers do not merge with default + // you need to re-define every key for each consumer + 'default' => [ + // these display in the consumer's dialogs + 'title' => 'Materia Widget Assignment', + 'description' => 'Add a Materia Widget as an assignment', + + // the platform that this lti consumer is intended to match up with + 'platform' => 'canvas.instructure.com', + + // Choose the key value of an LTI paramater to use as our username + // In this case the value of lis_person_sourceid may be 'dave'. We will try to match username = 'dave' + 'remote_username' => $_ENV['LTI_REMOTE_USERNAME'] ?? 'lis_person_sourcedid', + + // When looking or creating local users based on the external system, what fields do we use as an identifier? + // remote_identifier is the name of the lti data sent + // local_identifier is the name of the user object property that we will match the remote identifier against + // ex: incoming lis_person_sourceid = 'dave', we'll look for Model_User::query()->where($local_identifier, Input::post($remote_identifier)) + // another option is to use email instead of sourcedid, remote = 'lis_person_contact_email_primary' and local = 'email' + 'remote_identifier' => $_ENV['LTI_REMOTE_IDENTIFIER'] ?? 'lis_person_sourcedid', + 'local_identifier' => 'username', + + // When true, materia will accept user data from the external system. + // This means it will create users we don't have and update their user + // data if it changes. It will NOT update any external roles + // (see 'use_launch_roles') + 'creates_users' => $_ENV['BOOL_LTI_CREATE_USERS'] ?? true, + + // allow an external system to define user roles in Materia + 'use_launch_roles' => $_ENV['BOOL_LTI_USE_LAUNCH_ROLES'] ?? true, + + // which auth driver will do the final work authenticating this user + 'auth_driver' => 'LtiAuth', + + // Should we bother saving the assocation of the chosen widget to the resource + // most LTI consumers do not actually know which widget they are requesting + // however materia allows the LTI consumer to send and optional message that can request a specific widget + 'save_assoc' => true, + + // How many seconds should the oauth token be valid since created + 'timeout' => 3600, + + // Define the privacy level this integration to the consumer + // public + 'privacy' => 'public', + + // Define aspects of the course navigation link + // such as whether it is available at all, who can see it, and what text it displays + 'course_nav_default' => ($_ENV['BOOL_LTI_COURSE_NAV_DEFAULT'] ?? false) ? 'enabled' : 'disabled', + 'course_nav_enabled' => 'true', + 'course_nav_text' => 'Materia', + 'course_nav_visibility' => 'members', + + 'tool_id' => $_ENV['LTI_TOOL_ID'] ?? 'io.github.ucfopen', + + // Canvas launches with `ext_outcome_data_values_accepted=text,url` + // this flag upgrades `url` support to `ltiLaunchUrl` + // I believe this is a custom Canvas convention. + // Disabled if the LTI consumer doesn't handle upgrading url to ltiLaunchUrl + 'upgrade_to_launch_url' => true, + + // Security Settings CHANGE SECRET AT LEAST!!! + 'secret' => $_ENV['LTI_SECRET'], + 'key' => $_ENV['LTI_KEY'], + + ], + + // Example Obojobo assignment integration + /* + 'obojobo' => [ + 'title' => 'Materia Widget Assignment', + 'description' => 'Add a Materia Widget to your Learning Module', + 'platform' => 'obojobo.ucf.edu', + 'remote_username' => 'lis_person_sourcedid', + 'remote_identifier' => 'lis_person_sourcedid', + 'local_identifier' => 'username', + 'creates_users' => true, + 'use_launch_roles' => true, + 'auth_driver' => 'Materiaauth', + 'save_assoc' => false, + 'timeout' => 3600, + 'privacy' => 'public', + 'course_nav_default' => 'enabled' + 'course_nav_enabled' => 'true', + 'course_nav_text' => 'Materia', + 'course_nav_visibility' => 'members', + 'tool_id' => 'io.github.ucfopen', + 'upgrade_to_launch_url' => true, + 'secret' => 'secret', + 'key' => 'key', + ], + */ + ] +]; diff --git a/fuel/app/config/routes.php b/fuel/app/config/routes.php index 0c66e8a3b..49c02ea77 100644 --- a/fuel/app/config/routes.php +++ b/fuel/app/config/routes.php @@ -34,7 +34,14 @@ 'preview/(:alnum)(/.*)?' => 'widgets/preview_widget/$1', 'preview-embed/(:alnum)(/.*)?' => 'widgets/play_embedded_preview/$1', 'embed/(:alnum)(/.*)?' => 'widgets/play_embedded/$1', - 'lti/assignment?' => 'widgets/play_embedded/$1', // legacy LTI url + 'lti/assignment?' => function () { + if(\Input::param('lti_message_type') === 'ContentItemSelectionRequest') + { + return \Request::forge('lti/picker', false)->execute(); + } + + return \Request::forge('widgets/play_embedded', false)->execute(); + }, 'data/export/(:alnum)' => 'data/export/$1', "scores/single/{$play_id}/(:alnum)(/.*)?" => 'scores/single/$1/$2', diff --git a/fuel/app/modules/lti/migrations/001_create_lti.php b/fuel/app/migrations/052_create_lti.php similarity index 100% rename from fuel/app/modules/lti/migrations/001_create_lti.php rename to fuel/app/migrations/052_create_lti.php diff --git a/fuel/app/modules/lti/classes/controller/test.php b/fuel/app/modules/lti/classes/controller/test.php deleted file mode 100644 index 24e977e30..000000000 --- a/fuel/app/modules/lti/classes/controller/test.php +++ /dev/null @@ -1,297 +0,0 @@ -where('resource_link', 'test-resource') - ->where('item_id', $parts[0]) - ->get_one(); - - if (empty($check)) - { - $user = \Model_User::find_current(); - - $assoc = Model_Lti::forge(); - $assoc->resource_link = 'test-resource'; - $assoc->consumer_guid = 'test'; - $assoc->item_id = $parts[0]; - $assoc->user_id = $user->id; - $assoc->consumer = 'default'; - $assoc->name = $user->first.' '.$user->last; - $assoc->context_id = 'test_context'; - $assoc->context_title = 'test_context'; - $assoc->save(); - } - - return \Response::redirect("/lti/success/{$parts[0]}?embed_type={$embed_type}&url={$url}"); - } - - public function get_provider() - { - $assignment_url = \Uri::create('lti/assignment'); - $picker_url = \Uri::create('lti/picker'); - $validate_url = \Uri::create('lti/test/validate'); - $login_url = \Uri::create('lti/login'); - - $validation_params = $this->create_test_case([], $validate_url); - - $instructor_params = $this->create_test_case([ - 'launch_presentation_return_url' => \Uri::create('lti/test/embed'), - 'selection_directive' => 'select_link', - 'roles' => 'Instructor' - ], $picker_url); - - $login_params = $this->create_test_case([ - 'launch_presentation_return_url' => \Uri::create('lti/test/redirect'), - 'selection_directive' => 'select_link', - 'roles' => 'Instructor' - ], $login_url); - - $new_instructor_params = $this->create_test_case([ - 'launch_presentation_return_url' => \Uri::create('lti/test/embed'), - 'selection_directive' => 'select_link', - 'roles' => 'Instructor' - ], $picker_url, $this->create_new_random_user()); - - $unknown_role_params = $this->create_test_case(['roles' => 'Chocobo'], $assignment_url); - - $unknown_assignment_params = $this->create_test_case(['resource_link_id' => 'this-will-not-work'], $assignment_url); - - $view_args = [ - 'validation_params' => $validation_params[0], - 'validation_endpoint' => $validation_params[1], - - 'instructor_params' => $instructor_params[0], - 'instructor_endpoint' => $instructor_params[1], - - 'login_params' => $login_params[0], - 'login_endpoint' => $login_params[1], - - 'new_instructor_params' => $new_instructor_params[0], - 'new_instructor_endpoint' => $new_instructor_params[1], - - 'unknown_assignment_params' => $unknown_assignment_params[0], - 'unknown_assignment_endpoint' => $unknown_assignment_params[1], - - 'learner_endpoint' => \Uri::create('lti/test/learner') - ]; - - $this->theme = \Theme::instance(); - $this->theme->set_template('layouts/test_provider') - ->set($view_args); - - return \Response::forge(\Theme::instance()->render()); - } - - public function post_validate() - { - var_dump(\Input::post()); - echo \Lti\Oauth::validate_post() ? 'PASSED!' : 'FAILED'; - } - - protected static function get_and_unset_post($name) - { - $value = \Input::post($name); - unset($_POST[$name]); - return $value; - } - - public function post_learner() - { - $lti_url = static::get_and_unset_post('lti_url'); - if (empty($lti_url)) return \Response::forge('LTI Assignment URL can not be blank!', 500); - - $context_id = static::get_and_unset_post('context_id'); - $resource_link_id = static::get_and_unset_post('resource_link'); - $custom_inst_id = static::get_and_unset_post('custom_widget_instance_id'); - - $use_bad_signature = static::get_and_unset_post('use_bad_signature') ?: false; - $as_instructor = static::get_and_unset_post('as_instructor') ?: false; - $as_instructor2 = static::get_and_unset_post('as_instructor2') ?: false; - $as_test_student = static::get_and_unset_post('test_student') ?: false; - $as_new_learner_email = static::get_and_unset_post('new_learner_email') ?: false; - $as_new_learner_no_email = static::get_and_unset_post('new_learner_no_email') ?: false; - - switch (true) - { - case $as_instructor: - $learner_params = $this->create_test_case([ - 'roles' => 'Instructor', - 'context_id' => $context_id, - 'resource_link_id' => $resource_link_id, - 'custom_widget_instance_id' => $custom_inst_id - ], $lti_url); - break; - - case $as_instructor2: - $learner_params = $this->create_test_case([ - 'roles' => 'Instructor', - 'context_id' => $context_id, - 'resource_link_id' => $resource_link_id, - 'custom_widget_instance_id' => $custom_inst_id - ], $lti_url, false, false, 2); - break; - - case $as_test_student: - $test_student = new \Model_User([ - 'username' => '', - 'email' => 'notifications@instructure.com', - 'first' => 'Test', - 'last' => 'Student' - ]); - $learner_params = $this->create_test_case([ - 'context_id' => $context_id, - 'resource_link_id' => $resource_link_id, - 'custom_widget_instance_id' => $custom_inst_id - ], $lti_url, $test_student); - $learner_params[0]['user_id'] = ''; - break; - - case $as_new_learner_email: - $learner_params = $this->create_test_case([ - 'context_id' => $context_id, - 'resource_link_id' => $resource_link_id, - 'custom_widget_instance_id' => $custom_inst_id - ], $lti_url, $this->create_new_random_user()); - $learner_params[0]['user_id'] = ''; - break; - - case $as_new_learner_no_email: - $learner_params = $this->create_test_case([ - 'context_id' => $context_id, - 'resource_link_id' => $resource_link_id, - 'custom_widget_instance_id' => $custom_inst_id - ], $lti_url, $this->create_new_random_user(false)); - $learner_params[0]['user_id'] = ''; - break; - - default: - $learner_params = $this->create_test_case([ - 'context_id' => $context_id, - 'resource_link_id' => $resource_link_id, - 'custom_widget_instance_id' => $custom_inst_id, - ], $lti_url); - break; - } - - if ($use_bad_signature) - { - $learner_params[0]['oauth_signature'] = 'this will fail'; - } - - $this->theme = \Theme::instance(); - $this->theme->set_template('layouts/test_learner') - ->set_safe(['post' => json_encode($learner_params[0])]) - ->set(['assignment_url' => $lti_url]); - - return \Response::forge(\Theme::instance()->render()); - } - - protected function create_test_case($custom_params, $endpoint, $user = false, $passback_url = false, $new_faculty_user_override_number = false) - { - $key = \Config::get('lti::lti.consumers.default.key'); - $secret = \Config::get('lti::lti.consumers.default.secret'); - - $base_params = [ - 'resource_link_id' => 'test-resource', - 'context_id' => 'test-context', - 'lis_result_sourcedid' => 'test-source-id', - 'roles' => 'Learner' - ]; - - $params = array_merge($base_params, $custom_params); - - if ($user === false || $new_faculty_user_override_number) - { - // grab our test instructor - $base_username = '_LTI_INSTRUCTOR_'; - $username = $new_faculty_user_override_number ? $base_username.$new_faculty_user_override_number : $base_username; - $user = \Model_User::find_by_username($username); - - if ( ! $user) - { - // none - make one - $user_id = \Auth::instance()->create_user($username, uniqid(), $username.'@materia.com', 1, []); - - //manually add first/last name to make up for simpleauth not doing it - $user = \Model_User::find($user_id); - $user->first = $username; - $user->last = $username; - $user->profile_fields = ['notify' => true, 'avatar' => 'gravatar']; - $user->save(); - - // add basic_author permissions - \Materia\Perm_Manager::add_users_to_roles_system_only([$user_id], ['basic_author']); - } - } - - $params = \Lti\Oauth::build_post_args($user, $endpoint, $params, $key, $secret, $passback_url); - - return [$params, $endpoint]; - } - - protected function create_new_random_user($with_email = true) - { - $rand = substr(md5(microtime()), 0, 10); - - $user = new \Model_User([ - 'username' => 'test_lti_user'.$rand, - 'email' => $with_email ? 'test.lti.user'.$rand.'@materia.com' : '', - 'first' => 'Unofficial Test', - 'last' => "User $rand" - ]); - - return $user; - } - - protected function get_rand_active_widget_id() - { - $ids = \DB::select('id') - ->from('widget_instance') - ->where('is_draft', '0') - ->where('is_deleted', '0') - ->execute() - ->as_array(); - - $item = array_rand($ids); - - return $ids[$item]['id']; - } - -} diff --git a/fuel/app/modules/lti/config/lti.php b/fuel/app/modules/lti/config/lti.php deleted file mode 100644 index 171687d56..000000000 --- a/fuel/app/modules/lti/config/lti.php +++ /dev/null @@ -1,110 +0,0 @@ - '1', - 'launch_presentation_return_url' => \Uri::create('lti/return'), - 'tool_consumer_info_product_family_code' => 'materia', - 'tool_consumer_instance_guid' => $_ENV['LTI_GUID'] ?? 'ucfopen.github.io', - 'graceful_fallback_to_default' => $_ENV['BOOL_LTI_GRACEFUL_CONFIG_FALLBACK'] ?? true, - 'log_for_debug' => $_ENV['BOOL_LTI_LOG_FOR_DEBUGGING'] ?? false, - - 'consumers' => [ - // The array key here is matched to 'tool_consumer_info_product_family_code' in lti launches - // if there is no specific match, 'default' is used as a fallback - // NOTE that custom consumers do not merge with default - // you need to re-define every key for each consumer - 'default' => [ - // these display in the consumer's dialogs - 'title' => 'Materia Widget Assignment', - 'description' => 'Add a Materia Widget as an assignment', - - // the platform that this lti consumer is intended to match up with - 'platform' => 'canvas.instructure.com', - - // Choose the key value of an LTI paramater to use as our username - // In this case the value of lis_person_sourceid may be 'dave'. We will try to match username = 'dave' - 'remote_username' => $_ENV['LTI_REMOTE_USERNAME'] ?? 'lis_person_sourcedid', - - // When looking or creating local users based on the external system, what fields do we use as an identifier? - // remote_identifier is the name of the lti data sent - // local_identifier is the name of the user object property that we will match the remote identifier against - // ex: incoming lis_person_sourceid = 'dave', we'll look for Model_User::query()->where($local_identifier, Input::post($remote_identifier)) - // another option is to use email instead of sourcedid, remote = 'lis_person_contact_email_primary' and local = 'email' - 'remote_identifier' => $_ENV['LTI_REMOTE_IDENTIFIER'] ?? 'lis_person_sourcedid', - 'local_identifier' => 'username', - - // When true, materia will accept user data from the external system. - // This means it will create users we don't have and update their user - // data if it changes. It will NOT update any external roles - // (see 'use_launch_roles') - 'creates_users' => $_ENV['BOOL_LTI_CREATE_USERS'] ?? true, - - // allow an external system to define user roles in Materia - 'use_launch_roles' => $_ENV['BOOL_LTI_USE_LAUNCH_ROLES'] ?? true, - - // which auth driver will do the final work authenticating this user - 'auth_driver' => 'LtiAuth', - - // Should we bother saving the assocation of the chosen widget to the resource - // most LTI consumers do not actually know which widget they are requesting - // however materia allows the LTI consumer to send and optional message that can request a specific widget - 'save_assoc' => true, - - // How many seconds should the oauth token be valid since created - 'timeout' => 3600, - - // Define the privacy level this integration to the consumer - // public - 'privacy' => 'public', - - // Define aspects of the course navigation link - // such as whether it is available at all, who can see it, and what text it displays - 'course_nav_default' => ($_ENV['BOOL_LTI_COURSE_NAV_DEFAULT'] ?? false) ? 'enabled' : 'disabled', - 'course_nav_enabled' => 'true', - 'course_nav_text' => 'Materia', - 'course_nav_visibility' => 'members', - - 'tool_id' => $_ENV['LTI_TOOL_ID'] ?? 'io.github.ucfopen', - - // Canvas launches with `ext_outcome_data_values_accepted=text,url` - // this flag upgrades `url` support to `ltiLaunchUrl` - // I believe this is a custom Canvas convention. - // Disabled if the LTI consumer doesn't handle upgrading url to ltiLaunchUrl - 'upgrade_to_launch_url' => true, - - // Security Settings CHANGE SECRET AT LEAST!!! - 'secret' => $_ENV['LTI_SECRET'], - 'key' => $_ENV['LTI_KEY'], - - ], - - // Example Obojobo assignment integration - /* - 'obojobo' => [ - 'title' => 'Materia Widget Assignment', - 'description' => 'Add a Materia Widget to your Learning Module', - 'platform' => 'obojobo.ucf.edu', - 'remote_username' => 'lis_person_sourcedid', - 'remote_identifier' => 'lis_person_sourcedid', - 'local_identifier' => 'username', - 'creates_users' => true, - 'use_launch_roles' => true, - 'auth_driver' => 'Materiaauth', - 'save_assoc' => false, - 'timeout' => 3600, - 'privacy' => 'public', - 'course_nav_default' => 'enabled' - 'course_nav_enabled' => 'true', - 'course_nav_text' => 'Materia', - 'course_nav_visibility' => 'members', - 'tool_id' => 'io.github.ucfopen', - 'upgrade_to_launch_url' => true, - 'secret' => 'secret', - 'key' => 'key', - ], - */ - ] -]; diff --git a/fuel/app/tests/api/v1.php b/fuel/app/tests/api/v1.php index 577b97529..0ca540e94 100644 --- a/fuel/app/tests/api/v1.php +++ b/fuel/app/tests/api/v1.php @@ -19,6 +19,91 @@ public function test_allPublicAPIMethodsHaveTests() } } + public function test_lti_sign_content_item_selection() + { + $url = 'https://someurl.com/something?some_var=10&another_var=20'; + $content_items = '{ + "@context" : "http://purl.imsglobal.org/ctx/lti/v1/ContentItem", + "@graph" : [ + { + "@type": "LtiLinkItem", + "mediaType": "application/vnd.ims.lti.v1.ltilink", + "@id": "https://materia.edu/play/3c@jd!", + "url": "https://materia.edu/play/3c@jd!", + "title": "The widgets name", + "text": "A Materia Crossword Activity", + "placementAdvice": { + "presentationDocumentTarget": "frame", + }, + } + ] + }'; + + $invalid_lti_key = "key_that_doesnt_exist"; + $valid_lti_key = "materia-lti-key"; + + // ======= AS NO ONE ======== + $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $invalid_lti_key); + $this->assertInstanceOf('\Materia\Msg', $output); + $this->assertEquals('Invalid Login', $output->title); + + $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $valid_lti_key); + $this->assertInstanceOf('\Materia\Msg', $output); + $this->assertEquals('Invalid Login', $output->title); + + // ======= STUDENT ======== + $this->_as_student(); + $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $invalid_lti_key); + $this->assertInstanceOf('\Materia\Msg', $output); + $this->assertEquals('Permission Denied', $output->title); + + $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $valid_lti_key); + $this->assertInstanceOf('\Materia\Msg', $output); + $this->assertEquals('Permission Denied', $output->title); + + // ======= AUTHOR ======== + $this->_as_author(); + $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $invalid_lti_key); + $this->assertInstanceOf('\Materia\Msg', $output); + $this->assertEquals('Validation Error', $output->title); + + $this->_as_author(); + $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $valid_lti_key); + $this->assertNotInstanceOf('\Materia\Msg', $output); + $this->assertIsArray($output); + $this->assertArrayHasKey('oauth_version', $output); + $this->assertArrayHasKey('oauth_nonce', $output); + $this->assertArrayHasKey('oauth_timestamp', $output); + $this->assertArrayHasKey('oauth_consumer_key', $output); + $this->assertArrayHasKey('lti_message_type', $output); + $this->assertArrayHasKey('lti_version', $output); + $this->assertArrayHasKey('data', $output); + $this->assertArrayHasKey('oauth_callback', $output); + $this->assertArrayHasKey('oauth_signature_method', $output); + $this->assertArrayHasKey('oauth_signature', $output); + + // ======= SU ======== + $this->_as_super_user(); + $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $invalid_lti_key); + $this->assertInstanceOf('\Materia\Msg', $output); + $this->assertEquals('Validation Error', $output->title); + + $this->_as_super_user(); + $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $valid_lti_key); + $this->assertNotInstanceOf('\Materia\Msg', $output); + $this->assertIsArray($output); + $this->assertArrayHasKey('oauth_version', $output); + $this->assertArrayHasKey('oauth_nonce', $output); + $this->assertArrayHasKey('oauth_timestamp', $output); + $this->assertArrayHasKey('oauth_consumer_key', $output); + $this->assertArrayHasKey('lti_message_type', $output); + $this->assertArrayHasKey('lti_version', $output); + $this->assertArrayHasKey('data', $output); + $this->assertArrayHasKey('oauth_callback', $output); + $this->assertArrayHasKey('oauth_signature_method', $output); + $this->assertArrayHasKey('oauth_signature', $output); + } + public function test_widgets_get() { $this->make_disposable_widget(); diff --git a/fuel/app/modules/lti/tests/basetest.php b/fuel/app/tests/basetest.php similarity index 92% rename from fuel/app/modules/lti/tests/basetest.php rename to fuel/app/tests/basetest.php index 2c0286644..bafae25cd 100644 --- a/fuel/app/modules/lti/tests/basetest.php +++ b/fuel/app/tests/basetest.php @@ -23,7 +23,7 @@ protected function reset_input() static::clear_fuel_input(); $_POST = []; $_GET = []; - $class = new ReflectionClass("\Lti\LtiLaunch"); + $class = new ReflectionClass("\LtiLaunch"); foreach (['launch'] as $value) { $property = $class->getProperty($value); @@ -32,7 +32,7 @@ protected function reset_input() } - $class = new ReflectionClass("\Lti\LtiEvents"); + $class = new ReflectionClass("\LtiEvents"); foreach (['inst_id'] as $value) { $property = $class->getProperty($value); @@ -105,7 +105,7 @@ protected function create_test_lti_association($launch = false, $user_id = 1) { if ( ! $launch) $launch = $this->create_testing_launch_vars(); - $assoc = \Lti\Model_Lti::forge(); + $assoc = \Model_Lti::forge(); $assoc->resource_link = $launch->resource_id; $assoc->consumer_guid = $launch->consumer_id; @@ -122,10 +122,10 @@ protected function create_test_lti_association($launch = false, $user_id = 1) protected function create_test_oauth_launch($custom_params, $endpoint, $user = false, $passback_url = false) { $this->reset_input(); - \Config::load('lti::lti', true, true); + \Config::load('lti', true, true); - $key = \Config::get('lti::lti.consumers.default.key'); - $secret = \Config::get('lti::lti.consumers.default.secret'); + $key = \Config::get('lti.consumers.default.key'); + $secret = \Config::get('lti.consumers.default.secret'); $base_params = [ 'resource_link_id' => 'test-resource', @@ -158,7 +158,7 @@ protected function create_test_oauth_launch($custom_params, $endpoint, $user = f } } - $post_args = \Lti\Oauth::build_post_args($user, $endpoint, $params, $key, $secret, $passback_url); + $post_args = \Oauth::build_post_args($user, $endpoint, $params, $key, $secret, $passback_url); \Input::_set('post', $post_args); return $post_args; diff --git a/fuel/app/modules/lti/tests/ltievents.php b/fuel/app/tests/ltievents.php similarity index 74% rename from fuel/app/modules/lti/tests/ltievents.php rename to fuel/app/tests/ltievents.php index d7fe8218d..755ad331e 100644 --- a/fuel/app/modules/lti/tests/ltievents.php +++ b/fuel/app/tests/ltievents.php @@ -1,9 +1,6 @@ $inst_id, 'is_embedded' => true]; // Not an LTI launch, so nothing should happen (and no events thrown) - $result = \Lti\LtiEvents::on_before_play_start_event($event_args); + $result = \LtiEvents::on_before_play_start_event($event_args); $this->assertCount(0, $result); } @@ -25,7 +22,7 @@ public function test_on_before_play_start_event_shows_error_for_bad_oauth_reques // Test a first launch, OAuth should fail \Input::_set('post', ['resource_link_id' => 'test-resource', 'lti_message_type' => 'ContentItemSelectionRequest']); - $result = \Lti\LtiEvents::on_before_play_start_event($event_args); + $result = \LtiEvents::on_before_play_start_event($event_args); $this->assertArrayHasKey('redirect', $result); $this->assertCount(1, $result); @@ -34,14 +31,14 @@ public function test_on_before_play_start_event_shows_error_for_bad_oauth_reques public function test_on_before_play_start_event_throws_unknown_user_exception_for_bad_user() { - \Config::set("lti::lti.consumers.default.creates_users", false); + \Config::set("lti.consumers.default.creates_users", false); $user = $this->make_random_student(); $user->username = 'non-existant-user'; list($author, $widget_instance, $inst_id) = $this->create_instance(); $event_args = ['inst_id' => $inst_id, 'is_embedded' => true]; - $this->create_test_oauth_launch([], \Uri::current(), $user); + $this->create_test_oauth_launch([], \Uri::main(), $user); - $result = \Lti\LtiEvents::on_before_play_start_event($event_args); + $result = \LtiEvents::on_before_play_start_event($event_args); $this->assertCount(1, $result); $this->assertArrayHasKey('redirect', $result); $this->assertEquals('/lti/error/unknown_user', $result['redirect']); @@ -49,13 +46,13 @@ public function test_on_before_play_start_event_throws_unknown_user_exception_fo public function test_on_before_play_start_event_throws_unknown_assignment_exception_for_bad_assignment() { - \Config::set("lti::lti.consumers.default.creates_users", true); + \Config::set("lti.consumers.default.creates_users", true); $user = $this->make_random_student(); list($author, $widget_instance, $inst_id) = $this->create_instance(); $event_args = ['inst_id' => false, 'is_embedded' => true]; - $this->create_test_oauth_launch([], \Uri::current(), $user); + $this->create_test_oauth_launch([], \Uri::main(), $user); - $result = \Lti\LtiEvents::on_before_play_start_event($event_args); + $result = \LtiEvents::on_before_play_start_event($event_args); $this->assertCount(1, $result); $this->assertArrayHasKey('redirect', $result); $this->assertEquals('/lti/error/unknown_assignment', $result['redirect']); @@ -63,15 +60,15 @@ public function test_on_before_play_start_event_throws_unknown_assignment_except public function test_on_before_play_start_event_throws_guest_mode_exception() { - \Config::set("lti::lti.consumers.default.creates_users", true); + \Config::set("lti.consumers.default.creates_users", true); $user = $this->make_random_student(); list($author, $widget_instance, $inst_id) = $this->create_instance(); $widget_instance->guest_access = true; $widget_instance->db_store(); $event_args = ['inst_id' => $inst_id, 'is_embedded' => true]; - $this->create_test_oauth_launch([], \Uri::current(), $user); + $this->create_test_oauth_launch([], \Uri::main(), $user); - $result = \Lti\LtiEvents::on_before_play_start_event($event_args); + $result = \LtiEvents::on_before_play_start_event($event_args); $this->assertCount(1, $result); $this->assertArrayHasKey('redirect', $result); $this->assertEquals('/lti/error/guest_mode', $result['redirect']); @@ -79,35 +76,35 @@ public function test_on_before_play_start_event_throws_guest_mode_exception() public function test_on_before_play_start_event_saves_lti_association_for_first_launch() { - \Config::set("lti::lti.consumers.default.creates_users", true); + \Config::set("lti.consumers.default.creates_users", true); $resource_id = $this->get_uniq_string(); $user = $this->make_random_student(); list($author, $widget_instance, $inst_id) = $this->create_instance(); $event_args = ['inst_id' => $inst_id, 'is_embedded' => true]; - $this->create_test_oauth_launch(['resource_link_id' => $resource_id], \Uri::current(), $user); + $this->create_test_oauth_launch(['resource_link_id' => $resource_id], \Uri::main(), $user); // No association found - $this->assertEquals(0, \Lti\Model_Lti::query()->where('resource_link', $resource_id)->count()); + $this->assertEquals(0, \Model_Lti::query()->where('resource_link', $resource_id)->count()); // This should store an association - \Lti\LtiEvents::on_before_play_start_event($event_args); + \LtiEvents::on_before_play_start_event($event_args); // One association found - $this->assertEquals(1, \Lti\Model_Lti::query()->where('resource_link', $resource_id)->count()); + $this->assertEquals(1, \Model_Lti::query()->where('resource_link', $resource_id)->count()); } public function test_on_play_start_event_does_nothing_when_not_lti() { // First play - \Config::set("lti::lti.consumers.default.creates_users", true); + \Config::set("lti.consumers.default.creates_users", true); $resource_id = $this->get_uniq_string(); $user = $this->make_random_student(); list($author, $widget_instance, $inst_id) = $this->create_instance(); - \Lti\LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); + \LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); $play_id = \Materia\Api_V1::session_play_create($inst_id); - \Lti\LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); + \LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); // Nothing should be in the session $session_data = \Session::get("lti-{$play_id}", false); @@ -119,16 +116,16 @@ public function test_on_play_start_event_does_nothing_when_not_lti() public function test_on_play_start_event_stores_request_into_session_for_first_launch() { // First play - \Config::set("lti::lti.consumers.default.creates_users", true); + \Config::set("lti.consumers.default.creates_users", true); $resource_id = $this->get_uniq_string(); $user = $this->make_random_student(); list($author, $widget_instance, $inst_id) = $this->create_instance(); - $this->create_test_oauth_launch(['resource_link_id' => $resource_id], \Uri::current(), $user); + $this->create_test_oauth_launch(['resource_link_id' => $resource_id], \Uri::main(), $user); - \Lti\LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); + \LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); $play_id = \Materia\Api_V1::session_play_create($inst_id); - \Lti\LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); + \LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); $session_data = \Session::get("lti-{$play_id}", false); $this->assertEquals($inst_id, $session_data->inst_id); @@ -138,24 +135,24 @@ public function test_on_play_start_event_stores_request_into_session_for_first_l public function test_on_play_start_event_links_session_data_to_play_id_for_replay() { // First play - \Config::set("lti::lti.consumers.default.creates_users", true); + \Config::set("lti.consumers.default.creates_users", true); $resource_id = $this->get_uniq_string(); $user = $this->make_random_student(); list($author, $widget_instance, $inst_id) = $this->create_instance(); - $this->create_test_oauth_launch(['resource_link_id' => $resource_id], \Uri::current(), $user); + $this->create_test_oauth_launch(['resource_link_id' => $resource_id], \Uri::main(), $user); - \Lti\LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); + \LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); $play_id = \Materia\Api_V1::session_play_create($inst_id); - \Lti\LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); + \LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); // Replay $this->reset_input(); $_GET['token'] = $token = $play_id; - \Lti\LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); + \LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); $play_id = \Materia\Api_V1::session_play_create($inst_id); - \Lti\LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); + \LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); $session_data = \Session::get("lti-link-{$play_id}", false); $this->assertEquals($token, $session_data); @@ -165,60 +162,60 @@ public function test_on_play_start_event_links_session_data_to_play_id_for_repla public function test_on_score_updated_event_returns_false_when_not_lti() { // First play - \Config::set("lti::lti.consumers.default.creates_users", true); + \Config::set("lti.consumers.default.creates_users", true); $resource_id = $this->get_uniq_string(); $user = $this->make_random_student(); list($author, $widget_instance, $inst_id) = $this->create_instance(); - \Lti\LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); + \LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); $play_id = \Materia\Api_V1::session_play_create($inst_id); - \Lti\LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); + \LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); - $result = \Lti\LtiEvents::on_score_updated_event([$play_id, $inst_id, $author->id, 66, 99]); + $result = \LtiEvents::on_score_updated_event([$play_id, $inst_id, $author->id, 66, 99]); $this->assertFalse($result); } public function test_on_play_completed_event_returns_empty_array_when_not_lti() { // First play - \Config::set("lti::lti.consumers.default.creates_users", true); + \Config::set("lti.consumers.default.creates_users", true); $resource_id = $this->get_uniq_string(); $user = $this->make_random_student(); list($author, $widget_instance, $inst_id) = $this->create_instance(); - \Lti\LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); + \LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); $play_id = \Materia\Api_V1::session_play_create($inst_id); - \Lti\LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); + \LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); - \Lti\LtiEvents::on_score_updated_event([$play_id, $inst_id, $author->id, 66, 99]); + \LtiEvents::on_score_updated_event([$play_id, $inst_id, $author->id, 66, 99]); $play = new \Materia\Session_Play(); $play->get_by_id($play_id); - $result = \Lti\LtiEvents::on_play_completed_event($play); + $result = \LtiEvents::on_play_completed_event($play); $this->assertSame([], $result); } public function test_on_play_completed_event_returns_score_url_for_lti() { // First play - \Config::set("lti::lti.consumers.default.creates_users", true); + \Config::set("lti.consumers.default.creates_users", true); $resource_id = $this->get_uniq_string(); $user = $this->make_random_student(); list($author, $widget_instance, $inst_id) = $this->create_instance(); - $this->create_test_oauth_launch(['resource_link_id' => $resource_id], \Uri::current(), $user); + $this->create_test_oauth_launch(['resource_link_id' => $resource_id], \Uri::main(), $user); - \Lti\LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); + \LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); $play_id = \Materia\Api_V1::session_play_create($inst_id); - \Lti\LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); + \LtiEvents::on_play_start_event(['inst_id' => $inst_id, 'play_id' => $play_id]); - \Lti\LtiEvents::on_score_updated_event([$play_id, $inst_id, $author->id, 66, 99]); + \LtiEvents::on_score_updated_event([$play_id, $inst_id, $author->id, 66, 99]); $play = new \Materia\Session_Play(); $play->get_by_id($play_id); - $result = \Lti\LtiEvents::on_play_completed_event($play); + $result = \LtiEvents::on_play_completed_event($play); $this->assertTrue(array_key_exists('score_url', $result)); } @@ -227,15 +224,15 @@ public function test_on_widget_instance_delete_event() list($author, $widget_instance, $inst_id) = $this->create_instance(); $play_id = \Materia\Api_V1::session_play_create($inst_id); $resource_id = $this->get_uniq_string(); - $this->create_test_oauth_launch(['resource_link_id' => $resource_id], \Uri::current()); - \Lti\LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); + $this->create_test_oauth_launch(['resource_link_id' => $resource_id], \Uri::main()); + \LtiEvents::on_before_play_start_event(['inst_id' => $inst_id, 'is_embedded' => true]); // There should be a lti association $lti_data = \DB::select()->from('lti')->where('item_id', $inst_id)->execute(); $this->assertEquals(1, count($lti_data)); // Trigger the event - \Lti\LtiEvents::on_widget_instance_delete_event(['inst_id' => $inst_id]); + \LtiEvents::on_widget_instance_delete_event(['inst_id' => $inst_id]); // The lti association should be gone $lti_data = \DB::select()->from('lti')->where('item_id', $inst_id)->execute(); @@ -251,7 +248,7 @@ public function test_save_widget_association() $assocs_before = $this->get_all_associations(); - \Config::set("lti::lti.consumers.default-test.save_assoc", true); + \Config::set("lti.consumers.default-test.save_assoc", true); // create an instance $qset = $this->create_new_qset('question', 'answer'); @@ -262,7 +259,7 @@ public function test_save_widget_association() $launch = $this->create_testing_launch_vars($resource_link, $widget->id, '~materia_system_only', ['Learner']); $launch->inst_id = $widget_instance->id; - $save_lti_resource_id_to_widget_association = static::get_protected_method('\Lti\LtiEvents', 'save_lti_association_if_needed'); + $save_lti_resource_id_to_widget_association = static::get_protected_method('\LtiEvents', 'save_lti_association_if_needed'); $assoc_result = $save_lti_resource_id_to_widget_association->invoke(null, $launch); $this->assertTrue($assoc_result); $this->validate_new_assocation_saved($assocs_before); @@ -288,10 +285,10 @@ public function test_save_widget_association() */ public function test_create_lti_association_if_needed_creates_new_association() { - $create_lti_association_if_needed = static::get_protected_method('\Lti\LtiEvents', 'save_lti_association_if_needed'); - $find_assoc_from_resource_id = static::get_protected_method('\Lti\LtiEvents', 'find_assoc_from_resource_id'); + $create_lti_association_if_needed = static::get_protected_method('\LtiEvents', 'save_lti_association_if_needed'); + $find_assoc_from_resource_id = static::get_protected_method('\LtiEvents', 'find_assoc_from_resource_id'); - \Config::set("lti::lti.consumers.default-test.save_assoc", true); + \Config::set("lti.consumers.default-test.save_assoc", true); $resource_id = 'test-resource-A'; list($author, $widget_instance, $inst_id) = $this->create_instance(); @@ -303,17 +300,17 @@ public function test_create_lti_association_if_needed_creates_new_association() $this->assertTrue($result); $assoc1 = $find_assoc_from_resource_id->invoke(null, $resource_id); - $this->assertInstanceOf('\Lti\Model_Lti', $assoc1); + $this->assertInstanceOf('\Model_Lti', $assoc1); $this->assertEquals($assoc1->resource_link, $resource_id); $this->assertEquals($assoc1->item_id, $inst_id); } public function test_create_lti_association_if_needed_shouldnt_modify_association_for_same_widget() { - $create_lti_association_if_needed = static::get_protected_method('\Lti\LtiEvents', 'save_lti_association_if_needed'); - $find_assoc_from_resource_id = static::get_protected_method('\Lti\LtiEvents', 'find_assoc_from_resource_id'); + $create_lti_association_if_needed = static::get_protected_method('\LtiEvents', 'save_lti_association_if_needed'); + $find_assoc_from_resource_id = static::get_protected_method('\LtiEvents', 'find_assoc_from_resource_id'); - \Config::set("lti::lti.consumers.default-test.save_assoc", true); + \Config::set("lti.consumers.default-test.save_assoc", true); $resource_id = 'test-resource-A'; list($author, $widget_instance, $inst_id) = $this->create_instance(); @@ -324,7 +321,7 @@ public function test_create_lti_association_if_needed_shouldnt_modify_associatio $result = $create_lti_association_if_needed->invoke(null, $launch); $this->assertTrue($result); $assoc1 = $find_assoc_from_resource_id->invoke(null, $resource_id); - $this->assertInstanceOf('\Lti\Model_Lti', $assoc1); + $this->assertInstanceOf('\Model_Lti', $assoc1); $this->assertEquals($assoc1->resource_link, $resource_id); $this->assertEquals($assoc1->item_id, $inst_id); @@ -332,7 +329,7 @@ public function test_create_lti_association_if_needed_shouldnt_modify_associatio $result = $create_lti_association_if_needed->invoke(null, $launch); $this->assertTrue($result); $assoc2 = $find_assoc_from_resource_id->invoke(null, $resource_id); - $this->assertInstanceOf('\Lti\Model_Lti', $assoc2); + $this->assertInstanceOf('\Model_Lti', $assoc2); $this->assertEquals($assoc2->resource_link, $resource_id); $this->assertEquals($assoc2->item_id, $inst_id); $this->assertEquals($assoc2->id, $assoc1->id); // same row! @@ -346,10 +343,10 @@ public function test_create_lti_association_if_needed_shouldnt_modify_associatio public function test_create_lti_association_if_needed_creates_new_association_for_different_resource_link() { - $create_lti_association_if_needed = static::get_protected_method('\Lti\LtiEvents', 'save_lti_association_if_needed'); - $find_assoc_from_resource_id = static::get_protected_method('\Lti\LtiEvents', 'find_assoc_from_resource_id'); + $create_lti_association_if_needed = static::get_protected_method('\LtiEvents', 'save_lti_association_if_needed'); + $find_assoc_from_resource_id = static::get_protected_method('\LtiEvents', 'find_assoc_from_resource_id'); - \Config::set("lti::lti.consumers.default-test.save_assoc", true); + \Config::set("lti.consumers.default-test.save_assoc", true); $resource_id = 'test-resource-A'; list($author, $widget_instance, $inst_id) = $this->create_instance(); @@ -360,7 +357,7 @@ public function test_create_lti_association_if_needed_creates_new_association_fo $result = $create_lti_association_if_needed->invoke(null, $launch); $this->assertTrue($result); $assoc1 = $find_assoc_from_resource_id->invoke(null, $resource_id); - $this->assertInstanceOf('\Lti\Model_Lti', $assoc1); + $this->assertInstanceOf('\Model_Lti', $assoc1); $this->assertEquals($assoc1->resource_link, $resource_id); $this->assertEquals($assoc1->item_id, $inst_id); @@ -369,7 +366,7 @@ public function test_create_lti_association_if_needed_creates_new_association_fo $result = $create_lti_association_if_needed->invoke(null, $launch); $this->assertTrue($result); $assoc3 = $find_assoc_from_resource_id->invoke(null, $launch->resource_id); - $this->assertInstanceOf('\Lti\Model_Lti', $assoc3); + $this->assertInstanceOf('\Model_Lti', $assoc3); $this->assertEquals($assoc3->resource_link, $launch->resource_id); $this->assertEquals($assoc3->item_id, $inst_id); $this->assertNotEquals($assoc3->id, $assoc1->id); @@ -377,10 +374,10 @@ public function test_create_lti_association_if_needed_creates_new_association_fo public function test_create_lti_association_if_needed_updates_existing_association_for_new_widget() { - $create_lti_association_if_needed = static::get_protected_method('\Lti\LtiEvents', 'save_lti_association_if_needed'); - $find_assoc_from_resource_id = static::get_protected_method('\Lti\LtiEvents', 'find_assoc_from_resource_id'); + $create_lti_association_if_needed = static::get_protected_method('\LtiEvents', 'save_lti_association_if_needed'); + $find_assoc_from_resource_id = static::get_protected_method('\LtiEvents', 'find_assoc_from_resource_id'); - \Config::set("lti::lti.consumers.default-test.save_assoc", true); + \Config::set("lti.consumers.default-test.save_assoc", true); $resource_id = 'test-resource-A'; list($author, $widget_instance, $inst_id) = $this->create_instance(); @@ -391,7 +388,7 @@ public function test_create_lti_association_if_needed_updates_existing_associati $result = $create_lti_association_if_needed->invoke(null, $launch); $this->assertTrue($result); $assoc1 = $find_assoc_from_resource_id->invoke(null, $resource_id); - $this->assertInstanceOf('\Lti\Model_Lti', $assoc1); + $this->assertInstanceOf('\Model_Lti', $assoc1); $this->assertEquals($assoc1->resource_link, $resource_id); $this->assertEquals($assoc1->item_id, $inst_id); @@ -402,7 +399,7 @@ public function test_create_lti_association_if_needed_updates_existing_associati $result = $create_lti_association_if_needed->invoke(null, $launch); $this->assertTrue($result); $assoc4 = $find_assoc_from_resource_id->invoke(null, $launch->resource_id); - $this->assertInstanceOf('\Lti\Model_Lti', $assoc4); + $this->assertInstanceOf('\Model_Lti', $assoc4); $this->assertEquals($assoc4->resource_link, $launch->resource_id); $this->assertNotEquals($assoc4->item_id, $inst_id); $this->assertEquals($assoc4->item_id, $new_inst_id); @@ -423,7 +420,7 @@ protected function create_instance() protected function get_all_associations() { - $assocs = \Lti\Model_Lti::find('all'); + $assocs = \Model_Lti::find('all'); $assocs_clone = []; diff --git a/fuel/app/modules/lti/tests/ltilaunch.php b/fuel/app/tests/ltilaunch.php similarity index 87% rename from fuel/app/modules/lti/tests/ltilaunch.php rename to fuel/app/tests/ltilaunch.php index 9fb605f54..97621ca54 100644 --- a/fuel/app/modules/lti/tests/ltilaunch.php +++ b/fuel/app/tests/ltilaunch.php @@ -1,14 +1,12 @@ get_uniq_string(); @@ -34,7 +32,7 @@ public function test_get_widget_from_request() public function test_from_request() { $this->create_testing_post('test-resource', 'user', ['Student']); - $launch = \Lti\LtiLaunch::from_request(); + $launch = \LtiLaunch::from_request(); // $this->assertEquals($launch->remote_id, 'user'); $this->assertEquals('test-resource', $launch->resource_id); @@ -43,7 +41,7 @@ public function test_from_request() public function test_find_assoc_from_resource_id() { - $find_assoc_from_resource_id = static::get_protected_method('\Lti\LtiEvents', 'find_assoc_from_resource_id'); + $find_assoc_from_resource_id = static::get_protected_method('\LtiEvents', 'find_assoc_from_resource_id'); $resource_id = $this->get_uniq_string(); $launch = $this->create_testing_launch_vars($resource_id); diff --git a/fuel/app/modules/lti/tests/ltiusermanager.php b/fuel/app/tests/ltiusermanager.php similarity index 84% rename from fuel/app/modules/lti/tests/ltiusermanager.php rename to fuel/app/tests/ltiusermanager.php index 579be83e1..41ba99b99 100644 --- a/fuel/app/modules/lti/tests/ltiusermanager.php +++ b/fuel/app/tests/ltiusermanager.php @@ -1,8 +1,6 @@ 'LtiTestAuthDriver']); - \Config::load("lti::lti", true, true); - \Config::set("lti::lti.consumers.default.auth_driver", 'LtiTestAuthDriver'); + \Config::load("lti", true, true); + \Config::set("lti.consumers.default.auth_driver", 'LtiTestAuthDriver'); parent::setUp(); } public function test_authenticate_raises_exception_for_non_existant_auth_driver() { - \Config::set("lti::lti.consumers.default.auth_driver", 'PotatoAuthDriver'); + \Config::set("lti.consumers.default.auth_driver", 'PotatoAuthDriver'); // Exception thrown for auth driver that can't be found try { $launch = $this->create_testing_launch_vars('resource-link', 1, '~materia_system_only', ['Learner']); - \Lti\LtiUserManager::authenticate($launch); + \LtiUserManager::authenticate($launch); $this->fail('Exception expected'); } catch(\Exception $e) @@ -43,7 +41,7 @@ public function test_authenticate_finds_existing_user() $launch = $this->create_testing_launch_vars('resource-link-gocu1', $user->username, $user->username, ['Learner']); $_POST['roles'] = 'Learner'; $launch->email = 'gocu1@test.test'; - \Lti\LtiUserManager::authenticate($launch); + \LtiUserManager::authenticate($launch); $this->assertEquals($user->id, Auth_Login_LtiTestAuthDriver::$last_force_login_user->id); }; @@ -59,7 +57,7 @@ public function test_authenticate_does_not_promote_student() $user = $this->make_random_student(); $launch = $this->create_testing_launch_vars('resource-link-gocu1', $user->username, $user->username, ['Instructor']); $launch->email = 'gocu1@test.test'; - self::assertTrue(\Lti\LtiUserManager::authenticate($launch)); + self::assertTrue(\LtiUserManager::authenticate($launch)); $this->assertNotInstructor(Auth_Login_LtiTestAuthDriver::$last_force_login_user); } @@ -69,7 +67,7 @@ public function test_authenticate_does_promote_student() $user = $this->make_random_student(); $launch = $this->create_testing_launch_vars('resource-link-gocu1', $user->username, $user->username, ['Instructor']); $launch->email = 'gocu1@test.test'; - self::assertTrue(\Lti\LtiUserManager::authenticate($launch)); + self::assertTrue(\LtiUserManager::authenticate($launch)); $this->assertIsInstructor(Auth_Login_LtiTestAuthDriver::$last_force_login_user); } @@ -83,7 +81,7 @@ public function test_authenticate_does_not_demote_instructor() $launch = $this->create_testing_launch_vars('resource-link-gocu1', $user->username, $user->username, ['Learner']); $launch->email = 'gocu1@test.test'; - \Lti\LtiUserManager::authenticate($launch); + \LtiUserManager::authenticate($launch); $this->assertIsInstructor(Auth_Login_LtiTestAuthDriver::$last_force_login_user); }; @@ -100,7 +98,7 @@ public function test_authenticate_does_demote_instructor() $launch = $this->create_testing_launch_vars('resource-link-gocu1', $user->username, $user->username, ['Learner']); $launch->email = 'gocu1@test.test'; - \Lti\LtiUserManager::authenticate($launch); + \LtiUserManager::authenticate($launch); $this->assertNotInstructor(Auth_Login_LtiTestAuthDriver::$last_force_login_user); }; @@ -118,7 +116,7 @@ public function test_authenticate_not_creating_students() $launch = $this->create_testing_launch_vars('resource-link-gocu1', $expected_username, $expected_username, ['Learner']); $launch->email = 'gocu1@test.test'; - $result = \Lti\LtiUserManager::authenticate($launch); + $result = \LtiUserManager::authenticate($launch); $this->assertFalse($result); $user = \Model_User::find_by_username($expected_username); @@ -139,7 +137,7 @@ public function test_authenticate_creating_students() $launch = $this->create_testing_launch_vars('resource-link-gocu1', $expected_username, $expected_username, ['Learner']); $launch->email = 'gocu1@test.test'; - $result = \Lti\LtiUserManager::authenticate($launch); + $result = \LtiUserManager::authenticate($launch); $this->assertTrue($result); $user = Auth_Login_LtiTestAuthDriver::$last_force_login_user; @@ -161,7 +159,7 @@ public function test_authenticate_not_creating_instructors() $launch = $this->create_testing_launch_vars('resource-link-gocu1', $expected_username, $expected_username, ['Instructor']); $launch->email = 'gocu1@test.test'; - $result = \Lti\LtiUserManager::authenticate($launch); + $result = \LtiUserManager::authenticate($launch); $this->assertFalse($result); $user = \Model_User::find_by_username($expected_username); @@ -177,7 +175,7 @@ public function test_authenticate_creating_instructors() $this->create_users_and_use_launch_roles(true, false); $expected_username = $this->get_uniq_string(); $launch = $this->create_testing_launch_vars('resource-link-gocu1', $expected_username, $expected_username, ['Instructor']); - $this->assertTrue(\Lti\LtiUserManager::authenticate($launch)); + $this->assertTrue(\LtiUserManager::authenticate($launch)); $user = Auth_Login_LtiTestAuthDriver::$last_force_login_user; $this->assertEquals($expected_username, $user->username); $this->assertNotInstructor($user); @@ -189,7 +187,7 @@ public function test_authenticate_creating_not_instructors() $expected_username = $this->get_uniq_string(); $launch = $this->create_testing_launch_vars('resource-link-gocu1', $expected_username, $expected_username, ['Instructor']); $launch->email = 'gocu1@test.test'; - $this->assertTrue(\Lti\LtiUserManager::authenticate($launch)); + $this->assertTrue(\LtiUserManager::authenticate($launch)); $user = Auth_Login_LtiTestAuthDriver::$last_force_login_user; $this->assertEquals($expected_username, $user->username); $this->assertIsInstructor($user); @@ -207,7 +205,7 @@ public function test_authenticate_not_updating_user_info() $launch->email = 'gocu3@test.test'; $launch->first = 'First2'; - \Lti\LtiUserManager::authenticate($launch); + \LtiUserManager::authenticate($launch); $user = Auth_Login_LtiTestAuthDriver::$last_force_login_user; $this->assertSame($expected_first, $user->first); }; @@ -231,7 +229,7 @@ public function test_authenticate_updating_user_info() $launch->email = 'gocu3@test.test'; $launch->first = 'First2'; - \Lti\LtiUserManager::authenticate($launch); + \LtiUserManager::authenticate($launch); $user = Auth_Login_LtiTestAuthDriver::$last_force_login_user; $this->assertSame('First2', $user->first); }; @@ -244,86 +242,86 @@ public function test_is_lti_admin_is_content_creator() { $this->create_testing_post(); \Input::_set('post', ['roles' => 'Administrator']); - $launch = \Lti\LtiLaunch::from_request(); - $this->assertTrue(\Lti\LtiUserManager::is_lti_user_a_content_creator($launch)); + $launch = \LtiLaunch::from_request(); + $this->assertTrue(\LtiUserManager::is_lti_user_a_content_creator($launch)); } public function test_is_lti_instructor_is_content_creator() { $this->create_testing_post(); \Input::_set('post', ['roles' => 'Instructor']); - $launch = \Lti\LtiLaunch::from_request(); - $this->assertTrue(\Lti\LtiUserManager::is_lti_user_a_content_creator($launch)); + $launch = \LtiLaunch::from_request(); + $this->assertTrue(\LtiUserManager::is_lti_user_a_content_creator($launch)); } public function test_is_lti_learner_is_content_creator() { $this->create_testing_post(); \Input::_set('post', ['roles' => 'Learner']); - $launch = \Lti\LtiLaunch::from_request(); - $this->assertFalse(\Lti\LtiUserManager::is_lti_user_a_content_creator($launch)); + $launch = \LtiLaunch::from_request(); + $this->assertFalse(\LtiUserManager::is_lti_user_a_content_creator($launch)); } public function test_is_lti_student_not_content_creator() { $this->create_testing_post(); \Input::_set('post', ['roles' => 'Student']); - $launch = \Lti\LtiLaunch::from_request(); - $this->assertFalse(\Lti\LtiUserManager::is_lti_user_a_content_creator($launch)); + $launch = \LtiLaunch::from_request(); + $this->assertFalse(\LtiUserManager::is_lti_user_a_content_creator($launch)); } public function test_is_lti_mixed_instructor_is_content_creator() { $this->create_testing_post(); \Input::_set('post', ['roles' => 'Instructor,Instructor']); - $launch = \Lti\LtiLaunch::from_request(); - $this->assertTrue(\Lti\LtiUserManager::is_lti_user_a_content_creator($launch)); + $launch = \LtiLaunch::from_request(); + $this->assertTrue(\LtiUserManager::is_lti_user_a_content_creator($launch)); } public function test_is_lti_mixed_student_not_content_creator() { $this->create_testing_post(); \Input::_set('post', ['roles' => 'Student,Student']); - $launch = \Lti\LtiLaunch::from_request(); - $this->assertFalse(\Lti\LtiUserManager::is_lti_user_a_content_creator($launch)); + $launch = \LtiLaunch::from_request(); + $this->assertFalse(\LtiUserManager::is_lti_user_a_content_creator($launch)); } public function test_is_lti_unkown_not_content_creator() { $this->create_testing_post(); \Input::_set('post', ['roles' => '']); - $launch = \Lti\LtiLaunch::from_request(); - $this->assertFalse(\Lti\LtiUserManager::is_lti_user_a_content_creator($launch)); + $launch = \LtiLaunch::from_request(); + $this->assertFalse(\LtiUserManager::is_lti_user_a_content_creator($launch)); } public function test_is_lti_student_admin_is_content_creator() { $this->create_testing_post(); \Input::_set('post', ['roles' => 'Student,Learner,Administrator']); - $launch = \Lti\LtiLaunch::from_request(); - $this->assertTrue(\Lti\LtiUserManager::is_lti_user_a_content_creator($launch)); + $launch = \LtiLaunch::from_request(); + $this->assertTrue(\LtiUserManager::is_lti_user_a_content_creator($launch)); } public function test_is_lti_instructor_student_is_content_creator() { $this->create_testing_post(); \Input::_set('post', ['roles' => 'Instructor,Student,Dogs']); - $launch = \Lti\LtiLaunch::from_request(); - $this->assertTrue(\Lti\LtiUserManager::is_lti_user_a_content_creator($launch)); + $launch = \LtiLaunch::from_request(); + $this->assertTrue(\LtiUserManager::is_lti_user_a_content_creator($launch)); } public function test_is_lti_student_daft_punk_not_content_creator() { $this->create_testing_post(); \Input::_set('post', ['roles' => 'DaftPunk,student,Shaq']); - $launch = \Lti\LtiLaunch::from_request(); - $this->assertFalse(\Lti\LtiUserManager::is_lti_user_a_content_creator($launch)); + $launch = \LtiLaunch::from_request(); + $this->assertFalse(\LtiUserManager::is_lti_user_a_content_creator($launch)); } protected function create_users_and_use_launch_roles($creates_users, $use_launch_roles) { - \Config::set("lti::lti.consumers.default.creates_users", $creates_users); - \Config::set("lti::lti.consumers.default.use_launch_roles", $use_launch_roles); + \Config::set("lti.consumers.default.creates_users", $creates_users); + \Config::set("lti.consumers.default.use_launch_roles", $use_launch_roles); } protected function assertIsInstructor($user) diff --git a/fuel/app/modules/lti/tests/oauth.php b/fuel/app/tests/oauth.php similarity index 73% rename from fuel/app/modules/lti/tests/oauth.php rename to fuel/app/tests/oauth.php index 2f03f7881..abbdd0288 100644 --- a/fuel/app/modules/lti/tests/oauth.php +++ b/fuel/app/tests/oauth.php @@ -1,8 +1,6 @@ create_test_oauth_launch([], \Uri::current(), $user); + $this->create_test_oauth_launch([], \Uri::main(), $user); - $valid = \Lti\Oauth::validate_post(); + $valid = \Oauth::validate_post(); $this->assertEquals(true, $valid); } @@ -23,9 +21,9 @@ public function test_oauth_validate_fails_with_no_signature() require_once(APPPATH.'/tasks/admin.php'); $user_id = \Fuel\Tasks\Admin::instant_user(); $user = \Model_User::find($user_id); - $this->create_test_oauth_launch([], \Uri::current(), $user); + $this->create_test_oauth_launch([], \Uri::main(), $user); $this->unset_post_prop('oauth_signature'); - $valid = \Lti\Oauth::validate_post(); + $valid = \Oauth::validate_post(); $this->assertEquals(false, $valid); } @@ -34,9 +32,9 @@ public function test_oauth_validate_fails_with_no_oauth_timestamp() require_once(APPPATH.'/tasks/admin.php'); $user_id = \Fuel\Tasks\Admin::instant_user(); $user = \Model_User::find($user_id); - $this->create_test_oauth_launch([], \Uri::current(), $user); + $this->create_test_oauth_launch([], \Uri::main(), $user); $this->unset_post_prop('oauth_timestamp'); - $valid = \Lti\Oauth::validate_post(); + $valid = \Oauth::validate_post(); $this->assertEquals(false, $valid); } @@ -45,9 +43,9 @@ public function test_oauth_validate_fails_with_no_oauth_nonce() require_once(APPPATH.'/tasks/admin.php'); $user_id = \Fuel\Tasks\Admin::instant_user(); $user = \Model_User::find($user_id); - $this->create_test_oauth_launch([], \Uri::current(), $user); + $this->create_test_oauth_launch([], \Uri::main(), $user); $this->unset_post_prop('oauth_nonce'); - $valid = \Lti\Oauth::validate_post(); + $valid = \Oauth::validate_post(); $this->assertEquals(false, $valid); } @@ -56,9 +54,9 @@ public function test_oauth_validate_fails_with_no_oauth_consumer_key() require_once(APPPATH.'/tasks/admin.php'); $user_id = \Fuel\Tasks\Admin::instant_user(); $user = \Model_User::find($user_id); - $this->create_test_oauth_launch([], \Uri::current(), $user); + $this->create_test_oauth_launch([], \Uri::main(), $user); $this->unset_post_prop('oauth_consumer_key'); - $valid = \Lti\Oauth::validate_post(); + $valid = \Oauth::validate_post(); $this->assertEquals(false, $valid); } @@ -67,9 +65,9 @@ public function test_oauth_validate_fails_with_an_old_signature() require_once(APPPATH.'/tasks/admin.php'); $user_id = \Fuel\Tasks\Admin::instant_user(); $user = \Model_User::find($user_id); - $this->create_test_oauth_launch([], \Uri::current(), $user); + $this->create_test_oauth_launch([], \Uri::main(), $user); \Input::_set('post', ['oauth_consumer_key' => time() - 3601]); - $valid = \Lti\Oauth::validate_post(); + $valid = \Oauth::validate_post(); $this->assertEquals(false, $valid); } @@ -79,15 +77,15 @@ public function test_oauth_validate_fails_with_an_incorrect_signature() $user_id = \Fuel\Tasks\Admin::instant_user(); $user = \Model_User::find($user_id); - $this->create_test_oauth_launch([], \Uri::current(), $user); + $this->create_test_oauth_launch([], \Uri::main(), $user); \Input::_set('post', ['oauth_signature' => 'nope']); - $valid = \Lti\Oauth::validate_post(); + $valid = \Oauth::validate_post(); $this->assertEquals(false, $valid); } public function test_oauth_validate_fails_with_no_arguments() { - $valid = \Lti\Oauth::validate_post(); + $valid = \Oauth::validate_post(); $this->assertEquals(false, $valid); } diff --git a/fuel/app/tests/service/user.php b/fuel/app/tests/service/user.php index 51ca40652..4644edf05 100644 --- a/fuel/app/tests/service/user.php +++ b/fuel/app/tests/service/user.php @@ -23,7 +23,9 @@ public function test_admin_update_changes_user() $data->useGravatar = false; // update him - \Service_User::update_user($user->id, $data); + $changes = \Service_User::update_user($user->id, $data); + + $this->assertEquals($changes, ['is_student' => 1, 'email' => 1, 'notify' => 1, 'useGravatar' => 1]); // make sure the variables were set $this->assertEquals($data->email, $user->email); diff --git a/fuel/app/themes/default/layouts/lti_sign_and_launch.php b/fuel/app/themes/default/layouts/lti_sign_and_launch.php new file mode 100644 index 000000000..49d8af93f --- /dev/null +++ b/fuel/app/themes/default/layouts/lti_sign_and_launch.php @@ -0,0 +1,31 @@ + + + + Materia Test as Provider + + + + + + +

Signed request created, sending

+
+
+ + + diff --git a/fuel/app/themes/default/layouts/test_provider.php b/fuel/app/themes/default/layouts/test_provider.php new file mode 100644 index 000000000..e0b2365c2 --- /dev/null +++ b/fuel/app/themes/default/layouts/test_provider.php @@ -0,0 +1,99 @@ + + + + Materia Test as Provider + + + + + + + + +
+

Test Materia LTI Launches

+
+
+

This page will act as an LMS sending an LTI request to Materia. The Iframe below will show Materia's responses. Grab the corner to test resizing the iframe.

+ + + + +
+ $section) + { + echo "

{$section_name}

"; + foreach($section as $launch) + { + ?> + $value) + { + echo("$name=$value&"); + } + ?>"> ()
+ +
+

resource_link_id is used to determine which widget is linked to the current LMS's assignment/module/whatever. The association is inserted into the lti table as soon as it can be. Sometimes Canvas doesn't send the id when the instructor is choosing a widget. As a result, we try to give the LMS a launch url that contains the inst id when possible. Materia will attempt to choose the correct widget based on the lti table, the url, and the launch param custom-inst-id

+

lti_message_type can change the behavior of the assignment launch url and picker. Sending ContentItemSelectionRequest will tell both to display the picker and tell them how to return their params.

+
+
+

Customized LTI Launch

+
URL TO SEND LTI POST REQUEST TO:
+ $value) + { + echo '
'; + } + ?> + +
+ + + $value) + { + echo $key . ':
'; + } + ?> + +
+ + + +
+ + diff --git a/fuel/app/themes/default/lti/layouts/test_learner.php b/fuel/app/themes/default/lti/layouts/test_learner.php deleted file mode 100644 index c0b9012d6..000000000 --- a/fuel/app/themes/default/lti/layouts/test_learner.php +++ /dev/null @@ -1,26 +0,0 @@ - - - - Materia Test as Provider - - - - - - -

Loading...

-
-
- - - diff --git a/fuel/app/themes/default/lti/layouts/test_provider.php b/fuel/app/themes/default/lti/layouts/test_provider.php deleted file mode 100644 index a0022a7b1..000000000 --- a/fuel/app/themes/default/lti/layouts/test_provider.php +++ /dev/null @@ -1,311 +0,0 @@ - - - - Materia Test as Provider - - - - - - - - -
-

Use materia as an LTI Provider (inserted into another system)

-
-
-

This page will act as an LMS sending an LTI request to Materia.

-
- -
- - - -
- -

LTI Navigation Launch

- -
- $value) : ?> - - - - - - - -
- - -
- $value) : ?> - - - -
- -
-

LTI Picker Launch

- -
- $value) : ?> - - - - - - - -
- -
- $value) : ?> - - - -
- - -
- $value) : ?> - - - -
- - -
- -
-

LTI Assignment Launch

- - LTI Assignment URL: - - - - -
- -
- - Context ID: - - -
- -
- - Resource Link ID: - - -
- -
- - [Custom Inst ID (POST)]: - - -
- - -
- - - - - - -
- -
- - - - - -
- -
- - - - - - -
- -
- - - - - - -
- -
- - - - - - -
- -
- - - - - - -
- -
- - - - - - -
- -
-

Other

- - -
- $value) : ?> - - - -
- -
- $value) : ?> - - - - - - - -
- -
- $value) : ?> - - - -
- -
- - diff --git a/fuel/app/themes/default/lti/partials/config_xml.php b/fuel/app/themes/default/partials/config_xml.php similarity index 100% rename from fuel/app/themes/default/lti/partials/config_xml.php rename to fuel/app/themes/default/partials/config_xml.php diff --git a/fuel/app/themes/default/lti/partials/error_autoplay_misconfigured.php b/fuel/app/themes/default/partials/error_autoplay_misconfigured.php similarity index 73% rename from fuel/app/themes/default/lti/partials/error_autoplay_misconfigured.php rename to fuel/app/themes/default/partials/error_autoplay_misconfigured.php index 514c6c407..3b71f29b1 100644 --- a/fuel/app/themes/default/lti/partials/error_autoplay_misconfigured.php +++ b/fuel/app/themes/default/partials/error_autoplay_misconfigured.php @@ -4,7 +4,7 @@
-

This Materia assignment hasn't been setup correctly in .

+

This Materia assignment hasn't been setup correctly in .

Non-autoplaying widgets can not be used as graded assignments.

view('partials/help/support_info') ?> diff --git a/fuel/app/themes/default/lti/partials/error_general.php b/fuel/app/themes/default/partials/error_general.php similarity index 100% rename from fuel/app/themes/default/lti/partials/error_general.php rename to fuel/app/themes/default/partials/error_general.php diff --git a/fuel/app/themes/default/lti/partials/error_lti_guest_mode.php b/fuel/app/themes/default/partials/error_lti_guest_mode.php similarity index 100% rename from fuel/app/themes/default/lti/partials/error_lti_guest_mode.php rename to fuel/app/themes/default/partials/error_lti_guest_mode.php diff --git a/fuel/app/themes/default/lti/partials/error_unknown_assignment.php b/fuel/app/themes/default/partials/error_unknown_assignment.php similarity index 72% rename from fuel/app/themes/default/lti/partials/error_unknown_assignment.php rename to fuel/app/themes/default/partials/error_unknown_assignment.php index 042b0e43e..d7cb158be 100644 --- a/fuel/app/themes/default/lti/partials/error_unknown_assignment.php +++ b/fuel/app/themes/default/partials/error_unknown_assignment.php @@ -4,7 +4,7 @@
-

This Materia assignment hasn't been setup correctly in .

+

This Materia assignment hasn't been setup correctly in .

Your instructor will need to complete the setup process.

view('partials/help/support_info') ?> diff --git a/fuel/app/themes/default/lti/partials/error_unknown_user.php b/fuel/app/themes/default/partials/error_unknown_user.php similarity index 91% rename from fuel/app/themes/default/lti/partials/error_unknown_user.php rename to fuel/app/themes/default/partials/error_unknown_user.php index 19cfca3fd..04928a62b 100644 --- a/fuel/app/themes/default/lti/partials/error_unknown_user.php +++ b/fuel/app/themes/default/partials/error_unknown_user.php @@ -4,7 +4,7 @@
-

Materia can not determine who you are using the information provided by .

+

Materia can not determine who you are using the information provided by .

This may occur if you are using a non-standard account or if your information is missing from Materia due to recent changes to your account.

If you need help accessing this tool, contact support.

diff --git a/fuel/app/themes/default/lti/partials/open_preview.php b/fuel/app/themes/default/partials/open_preview.php similarity index 91% rename from fuel/app/themes/default/lti/partials/open_preview.php rename to fuel/app/themes/default/partials/open_preview.php index 35b6ee63a..ee899999b 100644 --- a/fuel/app/themes/default/lti/partials/open_preview.php +++ b/fuel/app/themes/default/partials/open_preview.php @@ -16,7 +16,7 @@ req.open('POST', '/api/instance/request_access') req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded') req.send(`inst_id=${inst_id}&owner_id=${owner_id}`) - + let el = document.getElementById(`owner-${owner_id}`) el.setAttribute('disabled', 'disabled') el.className = 'button disabled' @@ -57,7 +57,7 @@

Preview restricted by widget permissions in Materia.

-

To view the widget in Canvas as a student, view the assignment while in Student View.

+

If you're using Canvas, you can use Student View to simulate a student's experience.

diff --git a/fuel/app/themes/default/lti/partials/outcomes_xml.php b/fuel/app/themes/default/partials/outcomes_xml.php similarity index 100% rename from fuel/app/themes/default/lti/partials/outcomes_xml.php rename to fuel/app/themes/default/partials/outcomes_xml.php diff --git a/fuel/app/themes/default/lti/partials/post_login.php b/fuel/app/themes/default/partials/post_login.php similarity index 100% rename from fuel/app/themes/default/lti/partials/post_login.php rename to fuel/app/themes/default/partials/post_login.php diff --git a/fuel/app/themes/default/lti/partials/select_item.php b/fuel/app/themes/default/partials/select_item.php similarity index 100% rename from fuel/app/themes/default/lti/partials/select_item.php rename to fuel/app/themes/default/partials/select_item.php diff --git a/fuel/app/themes/default/lti/partials/select_item_js.php b/fuel/app/themes/default/partials/select_item_js.php similarity index 100% rename from fuel/app/themes/default/lti/partials/select_item_js.php rename to fuel/app/themes/default/partials/select_item_js.php diff --git a/materia-app.Dockerfile b/materia-app.Dockerfile index 5e7dd5374..2e9ef218a 100644 --- a/materia-app.Dockerfile +++ b/materia-app.Dockerfile @@ -65,7 +65,7 @@ RUN composer install --no-cache --no-dev --no-progress --no-scripts --prefer-dis # ===================================================================================================== FROM node:12.11.1-alpine AS yarn_stage -RUN apk add --no-cache git +RUN apk add --no-cache git python make g++ COPY ./public /build/public COPY ./package.json /build/package.json @@ -73,7 +73,7 @@ COPY ./process_assets.js /build/process_assets.js COPY ./yarn.lock /build/yarn.lock # make sure the directory where asset_hash.json is generated exists RUN mkdir -p /build/fuel/app/config/ -RUN cd build && yarn install --frozen-lockfile --non-interactive --production --silent --pure-lockfile --force +RUN cd build && yarn install --frozen-lockfile --non-interactive --production --pure-lockfile --force --check-files --cache-folder .ycache && rm -rf .ycache # ===================================================================================================== @@ -89,3 +89,15 @@ USER www-data COPY --from=composer_stage --chown=www-data:www-data /var/www/html /var/www/html COPY --from=yarn_stage --chown=www-data:www-data /build/public /var/www/html/public COPY --from=yarn_stage --chown=www-data:www-data /build/fuel/app/config/asset_hash.json /var/www/html/fuel/app/config/asset_hash.json + +# set bogus values so admin task can run +ENV AUTH_SIMPLEAUTH_SALT=TEMPINVALIDVALUE +ENV SYSTEM_EMAIL=TEMPINVALIDVALUE +ENV LTI_SECRET=TEMPINVALIDVALUE + +RUN php oil r admin:make_paths_writable + +# unset bogus values +ENV AUTH_SIMPLEAUTH_SALT= +ENV SYSTEM_EMAIL= +ENV LTI_SECRET= diff --git a/package.json b/package.json index f09351754..8ce61f3ee 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "fs-extra": "^8.0.1", - "materia-server-client-assets": "2.4.2", + "materia-server-client-assets": "https://github.com/iturgeon/Materia-Server-Client-Assets.git#842eaadcf38811bbaa37d804049cc36a4f643a9b", "napa": "^3.0.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 7fd237246..6fdacc25b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -957,10 +957,9 @@ make-dir@^1.0.0: dependencies: pify "^3.0.0" -materia-server-client-assets@2.4.2: +"materia-server-client-assets@https://github.com/iturgeon/Materia-Server-Client-Assets.git#842eaadcf38811bbaa37d804049cc36a4f643a9b": version "2.4.2" - resolved "https://registry.yarnpkg.com/materia-server-client-assets/-/materia-server-client-assets-2.4.2.tgz#c4111c2671dafef2926eb8bd385099828da5c913" - integrity sha512-L6wMYvlMISabmh7naQ8kNGLBwqgMVqof7zC1UnS9SkEd61Syv4zpa1I/SYSd/M2MVQmHNaX5l0pFC89tTWeOVg== + resolved "https://github.com/iturgeon/Materia-Server-Client-Assets.git#842eaadcf38811bbaa37d804049cc36a4f643a9b" dependencies: angular "1.8.0" js-snakecase "^1.2.0" @@ -1028,7 +1027,6 @@ napa@^3.0.0: ngmodal@ucfcdl/ngModal#v1.2.2: version "1.2.2" - uid "6abad982bdb8f258ffcdc20316a907c2292399d2" resolved "https://codeload.github.com/ucfcdl/ngModal/tar.gz/6abad982bdb8f258ffcdc20316a907c2292399d2" nodemon@^2.0.2: From c705e9116dda2f28648ad2d9137c76d8d8179fbd Mon Sep 17 00:00:00 2001 From: iturgeon Date: Tue, 24 Jan 2023 23:52:38 +0100 Subject: [PATCH 2/5] fix for improving content item lti post --- fuel/app/classes/materia/api/v1.php | 34 +++++++++++++++++++++++------ fuel/app/config/lti.php | 3 +++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/fuel/app/classes/materia/api/v1.php b/fuel/app/classes/materia/api/v1.php index 7b72da261..0c7a87fbb 100644 --- a/fuel/app/classes/materia/api/v1.php +++ b/fuel/app/classes/materia/api/v1.php @@ -85,21 +85,28 @@ static public function lti_sign_content_item_selection(string $url, string $cont // assumes the results will be sent via POST $request = \Eher\OAuth\Request::from_consumer_and_token($consumer, null, 'post', $url, $params); + $base_string = $request->get_signature_base_string(); $request->sign_request($hmc_sha1, $consumer, ''); $results = $request->get_parameters(); + \Materia\Log::profile(['lti-content-item-select', $url, print_r($params, 1), print_r($results, 1), $base_string,], 'lti-launch'); + // Remove GET params in $url from $results as they may mess up validation. // if duplicated here. (ex: Sakai will fail validation) - $query_str = parse_url($url, PHP_URL_QUERY); - parse_str($query_str, $query_params); - if (is_array($query_params)) + if ($lti_config['tmp_enable_lti_signature_duplicate_cleanup'] === true) { - $keys = array_keys($query_params); - foreach ($keys as $key) + $query_str = parse_url($url, PHP_URL_QUERY); + $query_params = self::_safer_parse_str($query_str); + if (is_array($query_params)) { - if (isset($results[$key])) + $keys = array_keys($query_params); + foreach ($keys as $key) { - unset($results[$key]); + \Fuel\Core\Log::debug($key); + if (isset($results[$key])) + { + unset($results[$key]); + } } } } @@ -1219,4 +1226,17 @@ static private function _get_instance_for_play_id($play_id) if ( ! ($inst = Widget_Instance_Manager::get($inst_id))) throw new \HttpNotFoundException; return $inst; } + + + // this function is needed to protect variable names in the query string, like dots, from becoming underscores + static private function _safer_parse_str($data) + { + $data = preg_replace_callback('/(?:^|(?<=&))[^=[]+/', function($match) { + return bin2hex(urldecode($match[0])); + }, $data); + + parse_str($data, $values); + + return array_combine(array_map('hex2bin', array_keys($values)), $values); + } } diff --git a/fuel/app/config/lti.php b/fuel/app/config/lti.php index 171687d56..17e37eea9 100644 --- a/fuel/app/config/lti.php +++ b/fuel/app/config/lti.php @@ -79,6 +79,9 @@ 'secret' => $_ENV['LTI_SECRET'], 'key' => $_ENV['LTI_KEY'], + // temporary + 'tmp_enable_lti_signature_duplicate_cleanup' => $_ENV['TMP_ENABLE_LTI_SIGNATURE_DUPLICATE_CLEANUP'] ?? true, + ], // Example Obojobo assignment integration From 3ed60729c65e037318f47aeba9f92c96199cbeb8 Mon Sep 17 00:00:00 2001 From: iturgeon Date: Thu, 2 Feb 2023 22:41:49 +0100 Subject: [PATCH 3/5] fix score passback bug after simplifying lti module --- fuel/app/classes/ltievents.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuel/app/classes/ltievents.php b/fuel/app/classes/ltievents.php index db6601fd6..7d4613114 100644 --- a/fuel/app/classes/ltievents.php +++ b/fuel/app/classes/ltievents.php @@ -158,7 +158,7 @@ public static function on_score_updated_event($event_args) 'extension_value' => $extension_value ]; - $body = \Theme::instance()->view('lti/partials/outcomes_xml', $view_data)->render(); + $body = \Theme::instance()->view('partials/outcomes_xml', $view_data)->render(); if (\Config::get('lti.log_for_debug', false)) { From acbfbd728b32811ac8fb812d2aac8de10a25983d Mon Sep 17 00:00:00 2001 From: iturgeon Date: Sat, 4 Feb 2023 12:20:03 +0100 Subject: [PATCH 4/5] code cleanup for lti_sign_content_item_selection --- fuel/app/classes/materia/api/v1.php | 66 ++-------------------------- fuel/app/classes/oauth.php | 67 +++++++++++++++++++++++++++++ fuel/app/config/lti.php | 6 ++- fuel/app/tests/api/v1.php | 4 +- 4 files changed, 77 insertions(+), 66 deletions(-) diff --git a/fuel/app/classes/materia/api/v1.php b/fuel/app/classes/materia/api/v1.php index 6f20e0928..fb6e446a6 100644 --- a/fuel/app/classes/materia/api/v1.php +++ b/fuel/app/classes/materia/api/v1.php @@ -62,59 +62,13 @@ static public function lti_sign_content_item_selection(string $url, string $cont if (\Materia\Perm_Manager::does_user_have_role([\Materia\Perm_Role::AUTHOR, \Materia\Perm_Role::SU]) !== true) return Msg::no_perm(); if (\Service_User::verify_session('no_author')) return Msg::no_perm(); - $lti_config = \LtiLaunch::config_from_key($lti_key); - - if (is_null($lti_config)) return Msg::invalid_input('Lti key not found.'); - - $params = [ - 'lti_message_type' => 'ContentItemSelection', - 'lti_version' => 'LTI-1p0', - 'content_items' => $content_items, - 'data' => '{"sentBy": "Materia"}', - 'oauth_nonce' => sodium_bin2hex(random_bytes(SODIUM_CRYPTO_STREAM_KEYBYTES)), - 'oauth_timestamp' => time(), - 'oauth_callback' => 'about:blank', - 'oauth_consumer_key' => $lti_key, - 'oauth_signature_method' => 'HMAC-SHA1', - 'oauth_version' => '1.0', - ]; - - $secret = $lti_config['secret'] ?? false; - $hmc_sha1 = new \Eher\OAuth\HmacSha1(); - $consumer = new \Eher\OAuth\Consumer('', $secret); - - // assumes the results will be sent via POST - $request = \Eher\OAuth\Request::from_consumer_and_token($consumer, null, 'post', $url, $params); - $base_string = $request->get_signature_base_string(); - $request->sign_request($hmc_sha1, $consumer, ''); - $results = $request->get_parameters(); - - \Materia\Log::profile(['lti-content-item-select', $url, print_r($params, 1), print_r($results, 1), $base_string,], 'lti-launch'); - - // Remove GET params in $url from $results as they may mess up validation. - // if duplicated here. (ex: Sakai will fail validation) - if ($lti_config['tmp_enable_lti_signature_duplicate_cleanup'] === true) - { - $query_str = parse_url($url, PHP_URL_QUERY); - $query_params = self::_safer_parse_str($query_str); - if (is_array($query_params)) - { - $keys = array_keys($query_params); - foreach ($keys as $key) - { - \Fuel\Core\Log::debug($key); - if (isset($results[$key])) - { - unset($results[$key]); - } - } - } + try { + return \Oauth::sign_content_item_selection($url, $content_items, $lti_key); + } catch (\Exception $e) { + return new Msg(Msg::ERROR, $e->getMessage()); } - - return $results; } - /** * @return bool, true if successfully deleted widget instance, false otherwise. */ @@ -1227,16 +1181,4 @@ static private function _get_instance_for_play_id($play_id) return $inst; } - - // this function is needed to protect variable names in the query string, like dots, from becoming underscores - static private function _safer_parse_str($data) - { - $data = preg_replace_callback('/(?:^|(?<=&))[^=[]+/', function($match) { - return bin2hex(urldecode($match[0])); - }, $data); - - parse_str($data, $values); - - return array_combine(array_map('hex2bin', array_keys($values)), $values); - } } diff --git a/fuel/app/classes/oauth.php b/fuel/app/classes/oauth.php index 67549702c..0c5f4e3b4 100644 --- a/fuel/app/classes/oauth.php +++ b/fuel/app/classes/oauth.php @@ -60,6 +60,73 @@ public static function build_post_args(\Model_User $user, $endpoint, $params, $k return $request->get_parameters(); } + // a custom parse_str that protects dots in variable names in the query string + static private function _safer_parse_str($data) + { + $data = preg_replace_callback('/(?:^|(?<=&))[^=[]+/', function($match) { + return bin2hex(urldecode($match[0])); + }, $data); + + parse_str($data, $values); + + return array_combine(array_map('hex2bin', array_keys($values)), $values); + } + + + public static function sign_content_item_selection(string $url, string $content_items, string $lti_key) + { + $lti_config = \LtiLaunch::config_from_key($lti_key); + + if (is_null($lti_config)) + { + throw new \Exception('Lti key not found.'); + } + + $params = [ + 'lti_message_type' => 'ContentItemSelection', + 'lti_version' => 'LTI-1p0', + 'content_items' => $content_items, + 'data' => '{"sent_by": "Materia"}', + 'oauth_nonce' => sodium_bin2hex(random_bytes(SODIUM_CRYPTO_STREAM_KEYBYTES)), + 'oauth_timestamp' => time(), + 'oauth_callback' => 'about:blank', + 'oauth_consumer_key' => $lti_key, + 'oauth_signature_method' => 'HMAC-SHA1', + 'oauth_version' => '1.0', + ]; + + $secret = $lti_config['secret'] ?? false; + $hmc_sha1 = new \Eher\OAuth\HmacSha1(); + $consumer = new \Eher\OAuth\Consumer('', $secret); + + $request = \Eher\OAuth\Request::from_consumer_and_token($consumer, null, 'post', $url, $params); + $base_string = $request->get_signature_base_string(); + $request->sign_request($hmc_sha1, $consumer, ''); + $results = $request->get_parameters(); + + \Materia\Log::profile(['lti-content-item-select', $url, print_r($params, 1), print_r($results, 1), $base_string,], 'lti-launch'); + + if ($lti_config['enforce_unique_params'] === true) + { + // if a param in the url, remove it from the results + $query_str = parse_url($url, PHP_URL_QUERY); + $query_params = self::_safer_parse_str($query_str); + if (is_array($query_params)) + { + $keys = array_keys($query_params); + foreach ($keys as $key) + { + if (isset($results[$key])) + { + unset($results[$key]); + } + } + } + } + + return $results; + } + public static function send_body_hashed_post($endpoint, $body, $secret, $key = null) { // ================ BUILD OAUTH REQUEST ========================= diff --git a/fuel/app/config/lti.php b/fuel/app/config/lti.php index 17e37eea9..f3daaa168 100644 --- a/fuel/app/config/lti.php +++ b/fuel/app/config/lti.php @@ -79,8 +79,10 @@ 'secret' => $_ENV['LTI_SECRET'], 'key' => $_ENV['LTI_KEY'], - // temporary - 'tmp_enable_lti_signature_duplicate_cleanup' => $_ENV['TMP_ENABLE_LTI_SIGNATURE_DUPLICATE_CLEANUP'] ?? true, + // When sending LTI ContentItem Selection requests, Sakai updated and required + // that GET params in the url aren't duplicated in the body of params + // as they caused oauth signature validation to fail in Sakai. + 'enforce_unique_params' => $_ENV['LTI_ENFORCE_UNIQUE_PARAMS'] ?? true, ], diff --git a/fuel/app/tests/api/v1.php b/fuel/app/tests/api/v1.php index 0ca540e94..dd8244230 100644 --- a/fuel/app/tests/api/v1.php +++ b/fuel/app/tests/api/v1.php @@ -65,7 +65,7 @@ public function test_lti_sign_content_item_selection() $this->_as_author(); $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $invalid_lti_key); $this->assertInstanceOf('\Materia\Msg', $output); - $this->assertEquals('Validation Error', $output->title); + $this->assertEquals('Lti key not found.', $output->title); $this->_as_author(); $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $valid_lti_key); @@ -86,7 +86,7 @@ public function test_lti_sign_content_item_selection() $this->_as_super_user(); $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $invalid_lti_key); $this->assertInstanceOf('\Materia\Msg', $output); - $this->assertEquals('Validation Error', $output->title); + $this->assertEquals('Lti key not found.', $output->title); $this->_as_super_user(); $output = Api_V1::lti_sign_content_item_selection($url, $content_items, $valid_lti_key); From 63384fe36d2faff56b941af9a63f3526fa65bb8b Mon Sep 17 00:00:00 2001 From: iturgeon Date: Sat, 4 Feb 2023 12:58:31 +0100 Subject: [PATCH 5/5] renames LtiLaunch::config for clarity --- fuel/app/classes/ltievents.php | 10 ++++++---- fuel/app/classes/ltilaunch.php | 4 ++-- fuel/app/classes/ltiusermanager.php | 4 ++-- fuel/app/classes/oauth.php | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/fuel/app/classes/ltievents.php b/fuel/app/classes/ltievents.php index 7d4613114..978b3c744 100644 --- a/fuel/app/classes/ltievents.php +++ b/fuel/app/classes/ltievents.php @@ -127,8 +127,10 @@ public static function on_score_updated_event($event_args) $launch = static::session_get_launch($play_id); - $secret = LtiLaunch::config()['secret'] ?? false; - $key = LtiLaunch::config()['key'] ?? false; + $cfg = LtiLaunch::config_from_request(); + + $secret = $cfg['secret'] ?? false; + $key = $cfg['key'] ?? false; if ( ! ($max_score >= 0) || empty($launch->inst_id) || empty($launch->source_id) || empty($launch->service_url) || empty($secret)) { @@ -145,7 +147,7 @@ public static function on_score_updated_event($event_args) if (strpos($launch->outcome_ext, 'url') !== false) { // url supported, does the config say we should upgrade it to ltiLaunchUrl? - $extension_type = LtiLaunch::config()['upgrade_to_launch_url'] ? 'ltiLaunchUrl' : 'url'; + $extension_type = $cfg['upgrade_to_launch_url'] ? 'ltiLaunchUrl' : 'url'; $extension_value = \Uri::create("/scores/single/{$play_id}/{$inst_id}"); } @@ -287,7 +289,7 @@ protected static function save_lti_association_if_needed($launch) { // if the configuration says we don't save associations, just return now - if ( ! (LtiLaunch::config()['save_assoc'] ?? true)) return true; + if ( ! (LtiLaunch::config_from_request()['save_assoc'] ?? true)) return true; // Search for any associations with this item id and resource link $assoc = static::find_assoc_from_resource_id($launch->resource_id); diff --git a/fuel/app/classes/ltilaunch.php b/fuel/app/classes/ltilaunch.php index 1de8fff42..522e2b9b1 100644 --- a/fuel/app/classes/ltilaunch.php +++ b/fuel/app/classes/ltilaunch.php @@ -12,7 +12,7 @@ public static function from_request() if (isset(static::$launch)) return static::$launch; if ( ! \Input::param('lti_message_type')) return null; - $config = static::config(); + $config = static::config_from_request(); // these are configurable to let username and user_id come from custom launch variables $remote_id_field = $config['remote_identifier'] ?? 'username'; @@ -52,7 +52,7 @@ public static function from_request() return static::$launch; } - public static function config() + public static function config_from_request() { if ( ! empty(static::$config)) { diff --git a/fuel/app/classes/ltiusermanager.php b/fuel/app/classes/ltiusermanager.php index 2e5f00e90..21ee53206 100644 --- a/fuel/app/classes/ltiusermanager.php +++ b/fuel/app/classes/ltiusermanager.php @@ -11,7 +11,7 @@ class LtiUserManager public static function authenticate($launch) { // =================== LOAD CONFIGURATION ============================ - $cfg = LtiLaunch::config(); + $cfg = LtiLaunch::config_from_request(); $local_id_field = $cfg['local_identifier'] ?? 'username'; $auth_driver = $cfg['auth_driver'] ?? ''; $creates_users = $cfg['creates_users'] ?? true; @@ -159,7 +159,7 @@ protected static function get_or_create_user($launch, $search_field, $auth_drive */ protected static function update_user_roles(\Model_User $user, $launch, $auth) { - $cfg = LtiLaunch::config(); + $cfg = LtiLaunch::config_from_request(); if ($cfg['use_launch_roles'] && method_exists($auth, 'update_role')) { $auth->update_role($user->id, static::is_lti_user_a_content_creator($launch)); diff --git a/fuel/app/classes/oauth.php b/fuel/app/classes/oauth.php index 0c5f4e3b4..98c1d5000 100644 --- a/fuel/app/classes/oauth.php +++ b/fuel/app/classes/oauth.php @@ -9,7 +9,7 @@ public static function validate_post() $signature = \Input::post('oauth_signature', ''); $timestamp = (int) \Input::post('oauth_timestamp', 0); $nonce = \Input::post('oauth_nonce', false); - $lti_config = LtiLaunch::config(); + $lti_config = LtiLaunch::config_from_request(); if (empty($signature)) throw new \Exception('Authorization signature is missing.'); if (empty($nonce)) throw new \Exception('Authorization fingerprint is missing.');