diff --git a/composer.json b/composer.json index 7e1286e2e5..d61d48fc3d 100755 --- a/composer.json +++ b/composer.json @@ -51,7 +51,6 @@ "clue/graph": "~0.9.0", "culqi/culqi-php": "1.3.4", "ddeboer/data-import": "@stable", - "gedmo/doctrine-extensions": "~2.3", "doctrine/data-fixtures": "~1.0@dev", "doctrine/dbal": "~2.5", "doctrine/migrations": "~1.0@dev", @@ -62,6 +61,7 @@ "ezyang/htmlpurifier": "~4.9", "facebook/php-sdk-v4": "~5.0", "firebase/php-jwt": "~5.0", + "gedmo/doctrine-extensions": "~2.3", "graphp/algorithms": "~0.8.0", "graphp/graphviz": "~0.2.0", "guzzlehttp/guzzle": "~6.0", @@ -81,9 +81,15 @@ "mpdf/mpdf": "6.1.*", "ocramius/proxy-manager": "~1.0|2.0.*", "onelogin/php-saml": "^3.0", + "packbackbooks/lti-1p3-tool": "^1.1", "paragonie/random-lib": "2.0.0", "patchwork/utf8": "~1.2", "php-ffmpeg/php-ffmpeg": "0.5.1", + "php-http/guzzle6-adapter": "^2.0", + "php-xapi/client": "0.7.x-dev", + "php-xapi/repository-api": "dev-master as 0.3.1", + "php-xapi/repository-doctrine": "dev-master", + "php-xapi/symfony-serializer": "2.1.0 as 2.0", "phpmailer/phpmailer": "~6.1", "phpoffice/phpexcel": "~1.8", "phpoffice/phpword": "~0.14", @@ -102,8 +108,8 @@ "symfony/dom-crawler": "~3.4|~4.0", "symfony/filesystem": "~3.0|~4.0", "symfony/http-foundation": "~2.8|~3.0", - "symfony/serializer": "~3.0|~4.0", "symfony/security": "~3.0|~4.0", + "symfony/serializer": "~3.0|~4.0", "symfony/validator": "~3.0|~4.0", "symfony/yaml": "~3.0|~4.0", "szymach/c-pchart": "~3.0", @@ -111,16 +117,11 @@ "twig/extensions": "~1.0", "twig/twig": "1.*", "webit/eval-math": "1.0.1", - "yuloh/bccomp-polyfill": "dev-master", + "yuloh/bccomp-polyfill": "dev-master", "zendframework/zend-config": "~3.0", "zendframework/zend-feed": "~2.6|^3.0", "zendframework/zend-http": "~2.6|^3.0", - "zendframework/zend-soap": "~2.6|^3.0", - "php-http/guzzle6-adapter": "^2.0", - "php-xapi/client": "0.7.x-dev", - "php-xapi/repository-api": "dev-master as 0.3.1", - "php-xapi/repository-doctrine": "dev-master", - "php-xapi/symfony-serializer": "2.1.0 as 2.0" + "zendframework/zend-soap": "~2.6|^3.0" }, "require-dev": { "behat/behat": "~3.5", diff --git a/plugin/lti_provider/Entity/Platform.php b/plugin/lti_provider/Entity/Platform.php new file mode 100755 index 0000000000..1dcbacc1f9 --- /dev/null +++ b/plugin/lti_provider/Entity/Platform.php @@ -0,0 +1,247 @@ +id; + } + + /** + * Set id. + * + * @param int $id + * + * @return Platform + */ + public function setId(int $id): Platform + { + $this->id = $id; + + return $this; + } + + /** + * + * Get kid. + * + * @return string + */ + public function getKid(): string + { + return $this->kid; + } + + /** + * Set kid. + * + * @param string $kid + */ + public function setKid(string $kid) + { + $this->kid = $kid; + + return $this; + } + + /** + * Get Issuer + * + * @return string + */ + public function getIssuer(): string + { + return $this->issuer; + } + + /** + * Set issuer + * + * @param string $issuer + */ + public function setIssuer(string $issuer) + { + $this->issuer = $issuer; + + return $this; + } + + /** + * Get client ID + * + * @return string + */ + public function getClientId(): string + { + return $this->clientId; + } + + /** + * Set client ID + * + * @param string $clientId + */ + public function setClientId(string $clientId) + { + $this->clientId = $clientId; + + return $this; + } + + /** + * Get auth login URL + * + * @return string + */ + public function getAuthLoginUrl(): string + { + return $this->authLoginUrl; + } + + /** + * Set auth login URL + * + * @param string $authLoginUrl + */ + public function setAuthLoginUrl(string $authLoginUrl) + { + $this->authLoginUrl = $authLoginUrl; + + return $this; + } + + /** + * Get auth token URL + * + * @return string + */ + public function getAuthTokenUrl(): string + { + return $this->authTokenUrl; + } + + /** + * Set auth token URL + * + * @param string $authTokenUrl + */ + public function setAuthTokenUrl(string $authTokenUrl) + { + $this->authTokenUrl = $authTokenUrl; + + return $this; + } + + /** + * Get key set URL + * + * @return string + */ + public function getKeySetUrl(): string + { + return $this->keySetUrl; + } + + /** + * Set key set URL + * + * @param string $keySetUrl + */ + public function setKeySetUrl(string $keySetUrl) + { + $this->keySetUrl = $keySetUrl; + + return $this; + } + + /** + * Get Deployment ID + * + * @return string + */ + public function getDeploymentId(): string + { + return $this->deploymentId; + } + + /** + * Set Deployment ID + * + * @param string $deploymentId + */ + public function setDeploymentId(string $deploymentId) + { + $this->deploymentId = $deploymentId; + + return $this; + } +} diff --git a/plugin/lti_provider/Entity/PlatformKey.php b/plugin/lti_provider/Entity/PlatformKey.php new file mode 100755 index 0000000000..8bcfb0db35 --- /dev/null +++ b/plugin/lti_provider/Entity/PlatformKey.php @@ -0,0 +1,129 @@ +id; + } + + /** + * Set id. + */ + public function setId(int $id): PlatformKey + { + $this->id = $id; + + return $this; + } + + /** + * + * Get kid. + * + * @return string + */ + public function getKid(): string + { + return $this->kid; + } + + /** + * Set kid. + * + * @param string $kid + */ + public function setKid(string $kid) + { + $this->kid = $kid; + + return $this; + } + + /** + * Get privateKey. + * + * @return string + */ + public function getPrivateKey(): string + { + return $this->privateKey; + } + + /** + * Set privateKey. + * + * @param string $privateKey + * + * @return PlatformKey + */ + public function setPrivateKey(string $privateKey): PlatformKey + { + $this->privateKey = $privateKey; + + return $this; + } + + /** + * @return string + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + /** + * @param string $publicKey + */ + public function setPublicKey(string $publicKey) + { + $this->publicKey = $publicKey; + + return $this; + } +} diff --git a/plugin/lti_provider/LtiProviderPlugin.php b/plugin/lti_provider/LtiProviderPlugin.php new file mode 100644 index 0000000000..b3a5b5b3cd --- /dev/null +++ b/plugin/lti_provider/LtiProviderPlugin.php @@ -0,0 +1,312 @@ + + */ +class LtiProviderPlugin extends Plugin +{ + + const TABLE_PLATFORM = 'plugin_lti_provider_platform'; + + public $isAdminPlugin = true; + + protected function __construct() + { + $version = '1.0'; + $author = 'Christian Beeznest'; + + $message = Display::return_message($this->get_lang('Description')); + + if ($this->areTablesCreated()) { + $publicKey = $this->getPublicKey(); + + $pkHtml = '
+ +
+
'.$publicKey.'
+
+
+
'; + } else { + $pkHtml = $this->get_lang('GenerateKeyPairInfo'); + } + + $settings = [ + $message => 'html', + 'name' => 'text', + 'launch_url' => 'text', + 'login_url' => 'text', + 'redirect_url' => 'text', + $pkHtml => 'html', + 'enabled' => 'boolean', + ]; + parent::__construct($version, $author, $settings); + + } + + /** + * Get the public key + * + * @return string + */ + public function getPublicKey(): string + { + $publicKey = ''; + $platformKey = Database::getManager() + ->getRepository('ChamiloPluginBundle:LtiProvider\PlatformKey') + ->findOneBy([]); + + if ($platformKey) { + $publicKey = $platformKey->getPublicKey(); + } + + return $publicKey; + } + + /** + * Get the class instance + * + * @staticvar LtiProviderPlugin $result + * @return LtiProviderPlugin + */ + public static function create(): LtiProviderPlugin + { + static $result = null; + + return $result ?: $result = new self(); + } + + public static function isInstructor() + { + api_is_allowed_to_edit(false, true); + } + + /** + * Get the plugin directory name + */ + public function get_name(): string + { + return 'lti_provider'; + } + + /** + * Install the plugin. Setup the database + */ + public function install() + { + $pluginEntityPath = $this->getEntityPath(); + + if (!is_dir($pluginEntityPath)) { + if (!is_writable(dirname($pluginEntityPath))) { + $message = get_lang('ErrorCreatingDir').': '.$pluginEntityPath; + Display::addFlash(Display::return_message($message, 'error')); + + return; + } + + mkdir($pluginEntityPath, api_get_permissions_for_new_directories()); + } + + $fs = new Filesystem(); + $fs->mirror(__DIR__.'/Entity/', $pluginEntityPath, null, ['override']); + + $this->createPluginTables(); + } + + /** + * @return string + */ + public function getEntityPath(): string + { + return api_get_path(SYS_PATH).'src/Chamilo/PluginBundle/Entity/'.$this->getCamelCaseName(); + } + + /** + * Creates the plugin tables on database + * + * @return void + */ + private function createPluginTables(): void + { + if ($this->areTablesCreated()) { + return; + } + + $queries = [ + "CREATE TABLE plugin_lti_provider_platform ( + id int NOT NULL AUTO_INCREMENT, + issuer varchar(255) NOT NULL, + client_id varchar(255) NOT NULL, + kid varchar(255) NOT NULL, + auth_login_url varchar(255) NOT NULL, + auth_token_url varchar(255) NOT NULL, + key_set_url varchar(255) NOT NULL, + deployment_id varchar(255) NOT NULL, + PRIMARY KEY(id) + ) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB", + "CREATE TABLE plugin_lti_provider_platform_key ( + id INT AUTO_INCREMENT NOT NULL, + kid VARCHAR(255) NOT NULL, + public_key LONGTEXT NOT NULL, + private_key LONGTEXT NOT NULL, + PRIMARY KEY(id) + ) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB", + ]; + + foreach ($queries as $query) { + Database::query($query); + } + + } + + private function areTablesCreated(): bool + { + $entityManager = Database::getManager(); + $connection = $entityManager->getConnection(); + + return $connection->getSchemaManager()->tablesExist(self::TABLE_PLATFORM); + } + + /** + * Save configuration for plugin. + * + * Generate a new key pair for platform when enabling plugin. + * + * @throws OptimisticLockException + * @throws \Doctrine\ORM\ORMException + * + * @return $this|Plugin + */ + public function performActionsAfterConfigure() + { + $em = Database::getManager(); + + /** @var PlatformKey $platformKey */ + $platformKey = $em + ->getRepository('ChamiloPluginBundle:LtiProvider\PlatformKey') + ->findOneBy([]); + + if ($this->get('enabled') === 'true') { + if (!$platformKey) { + $platformKey = new PlatformKey(); + } + + $keyPair = self::generatePlatformKeys(); + + $platformKey->setKid($keyPair['kid']); + $platformKey->publicKey = $keyPair['public']; + $platformKey->setPrivateKey($keyPair['private']); + + $em->persist($platformKey); + } else { + if ($platformKey) { + $em->remove($platformKey); + } + } + + $em->flush(); + + return $this; + } + + /** + * Generate a key pair and key id for the platform. + * + * Rerturn a associative array like ['kid' => '...', 'private' => '...', 'public' => '...']. + * + * @return array + */ + private static function generatePlatformKeys(): array + { + // Create the private and public key + $res = openssl_pkey_new( + [ + 'digest_alg' => 'sha256', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ] + ); + + // Extract the private key from $res to $privateKey + $privateKey = ''; + openssl_pkey_export($res, $privateKey); + + // Extract the public key from $res to $publicKey + $publicKey = openssl_pkey_get_details($res); + + return [ + 'kid' => bin2hex(openssl_random_pseudo_bytes(10)), + 'private' => $privateKey, + 'public' => $publicKey["key"], + ]; + } + + /** + * Unistall plugin. Clear the database + */ + public function uninstall() + { + $pluginEntityPath = $this->getEntityPath(); + $fs = new Filesystem(); + + if ($fs->exists($pluginEntityPath)) { + $fs->remove($pluginEntityPath); + } + + try { + $this->dropPluginTables(); + } catch (DBALException $e) { + error_log('Error while uninstalling IMS/LTI plugin: '.$e->getMessage()); + } + } + + /** + * Drops the plugin tables on database + * + * @return boolean + */ + private function dropPluginTables(): bool + { + Database::query("DROP TABLE IF EXISTS plugin_lti_provider_platform"); + Database::query("DROP TABLE IF EXISTS plugin_lti_provider_platform_key"); + + return true; + } + + /** + * @param array $params + */ + public function trimParams(array &$params) + { + foreach ($params as $key => $value) { + $newValue = preg_replace('/\s+/', ' ', $value); + $params[$key] = trim($newValue); + } + } + + /** + * @throws Exception + * + * @return null|SimpleXMLElement + */ + private function getRequestXmlElement(): ?SimpleXMLElement + { + $request = file_get_contents("php://input"); + + if (empty($request)) { + return null; + } + + return new SimpleXMLElement($request); + } + +} diff --git a/plugin/lti_provider/README.md b/plugin/lti_provider/README.md new file mode 100644 index 0000000000..a206eb5960 --- /dev/null +++ b/plugin/lti_provider/README.md @@ -0,0 +1,43 @@ +Lti/Provider plugin +=== + +Version 1.0 + +> This plugin is meant to be later integrated into Chamilo (in a major version +release). + +Lti provider is compatible only for Lti 1.3 Advance, defines the possibility to integrate tools or content into Platforms LMS. +In this case Chamilo is used as provider , this plugin allows a student inside a course to play in a breakout game with with certain difficulty options (Deep Linkings) which is scored (Assigment and Grade Services) and comparing with the members of the course (NRP Services). + +# Installation + +1. Install the plugin from the Plugins page +2. Enable the plugin from the Lti Provider Plugin Settings page +3. Assign to the Administrator region (in the regions management page) +4. Add the LTI connection details to try out this app (Configure page) +5. To configure the Platforms LMS for registration and deployment + +# DB tables + +## v1.0 +```sql +CREATE TABLE plugin_lti_provider_platform ( + id int NOT NULL AUTO_INCREMENT, + issuer varchar(255) NOT NULL, + client_id varchar(255) NOT NULL, + kid int(255) NOT NULL, + auth_login_url varchar(255) NOT NULL, + auth_token_url varchar(255) NOT NULL, + key_set_url varchar(255) NOT NULL, + deployment_id varchar(255) NOT NULL, + PRIMARY KEY(id) +) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; + +CREATE TABLE plugin_lti_provider_platform_key ( + id INT AUTO_INCREMENT NOT NULL, + kid VARCHAR(255) NOT NULL, + public_key LONGTEXT NOT NULL, + private_key LONGTEXT NOT NULL, + PRIMARY KEY(id) +) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB; +``` diff --git a/plugin/lti_provider/admin.php b/plugin/lti_provider/admin.php new file mode 100644 index 0000000000..086c681bbf --- /dev/null +++ b/plugin/lti_provider/admin.php @@ -0,0 +1,34 @@ +get('enabled') !== 'true') { + api_not_allowed(true); +} + +$em = Database::getManager(); + +$platforms = $em->getRepository('ChamiloPluginBundle:LtiProvider\Platform')->findAll(); + +$interbreadcrumb[] = ['url' => api_get_path(WEB_CODE_PATH).'admin/index.php', 'name' => get_lang('PlatformAdmin')]; + +$htmlHeadXtra[] = api_get_css( + api_get_path(WEB_PLUGIN_PATH).'lti_provider/assets/style.css' +); + +$template = new Template($plugin->get_title()); +$template->assign('platforms', $platforms); + +$content = $template->fetch('lti_provider/view/admin.tpl'); + +$template->assign('header', $plugin->get_title()); +$template->assign('content', $content); +$template->display_one_col_template(); diff --git a/plugin/lti_provider/create.php b/plugin/lti_provider/create.php new file mode 100644 index 0000000000..c1a13eda33 --- /dev/null +++ b/plugin/lti_provider/create.php @@ -0,0 +1,57 @@ +build(); + +if ($form->validate()) { + $formValues = $form->exportValues(); + $platform = new Platform(); + $platform->setIssuer($formValues['issuer']); + $platform->setClientId($formValues['client_id']); + $platform->setAuthLoginUrl($formValues['auth_login_url']); + $platform->setAuthTokenUrl($formValues['auth_token_url']); + $platform->setKeySetUrl($formValues['key_set_url']); + $platform->setDeploymentId($formValues['deployment_id']); + $platform->setKid($formValues['kid']); + + $em->persist($platform); + $em->flush(); + + Display::addFlash( + Display::return_message($plugin->get_lang('PlatformConnectionAdded'), 'success') + ); + + header('Location: '.api_get_path(WEB_PLUGIN_PATH).'lti_provider/admin.php'); + exit; +} + +$form->setDefaultValues(); + +$interbreadcrumb[] = ['url' => api_get_path(WEB_CODE_PATH).'admin/index.php', 'name' => get_lang('PlatformAdmin')]; +$interbreadcrumb[] = ['url' => api_get_path(WEB_PLUGIN_PATH).'lti_provider/admin.php', 'name' => $plugin->get_title()]; + +$pageTitle = $plugin->get_lang('AddPlatform'); + +$template = new Template($pageTitle); +$template->assign('form', $form->returnForm()); + +$content = $template->fetch('lti_provider/view/add.tpl'); + +$template->assign('header', $pageTitle); +$template->assign('content', $content); +$template->display_one_col_template(); diff --git a/plugin/lti_provider/db/lti13_cache.php b/plugin/lti_provider/db/lti13_cache.php new file mode 100644 index 0000000000..8630b9a4d6 --- /dev/null +++ b/plugin/lti_provider/db/lti13_cache.php @@ -0,0 +1,59 @@ +loadCache(); + + return $this->cache[$key]; + } + + private function loadCache() + { + $cache = file_get_contents(sys_get_temp_dir().'/lti_cache.txt'); + if (empty($cache)) { + file_put_contents(sys_get_temp_dir().'/lti_cache.txt', '{}'); + $this->cache = []; + } + $this->cache = json_decode($cache, true); + } + + public function cacheLaunchData($key, $jwtBody): Lti13Cache + { + $this->cache[$key] = $jwtBody; + $this->saveCache(); + + return $this; + } + + private function saveCache() + { + file_put_contents(sys_get_temp_dir().'/lti_cache.txt', json_encode($this->cache)); + } + + public function cacheNonce($nonce): Lti13Cache + { + $this->cache['nonce'][$nonce] = true; + $this->saveCache(); + + return $this; + } + + public function checkNonce($nonce): bool + { + $this->loadCache(); + if (!isset($this->cache['nonce'][$nonce])) { + return false; + } + + return true; + } +} diff --git a/plugin/lti_provider/db/lti13_cookie.php b/plugin/lti_provider/db/lti13_cookie.php new file mode 100644 index 0000000000..c1811547fb --- /dev/null +++ b/plugin/lti_provider/db/lti13_cookie.php @@ -0,0 +1,41 @@ + time() + $exp, + ]; + + // SameSite none and secure will be required for tools to work inside iframes + $sameSiteOptions = [ + 'samesite' => 'None', + 'secure' => false, + 'httponly' => true, + ]; + + setcookie($name, $value, array_merge($cookieOptions, $sameSiteOptions, $options)); + + // Set a second fallback cookie in the event that "SameSite" is not supported + setcookie("LEGACY_".$name, $value, array_merge($cookieOptions, $options)); + + return $this; + } +} diff --git a/plugin/lti_provider/db/lti13_database.php b/plugin/lti_provider/db/lti13_database.php new file mode 100644 index 0000000000..f746343382 --- /dev/null +++ b/plugin/lti_provider/db/lti13_database.php @@ -0,0 +1,76 @@ +getLtiConnection(); + if (empty($ltiCustomers[$iss])) { + return false; + } + + if (!isset($clientId)) { + $clientId = $ltiCustomers[$iss]['client_id']; + } + + return LtiRegistration::new() + ->setAuthLoginUrl($ltiCustomers[$iss]['auth_login_url']) + ->setAuthTokenUrl($ltiCustomers[$iss]['auth_token_url']) + ->setClientId($clientId) + ->setKeySetUrl($ltiCustomers[$iss]['key_set_url']) + ->setKid($ltiCustomers[$iss]['kid']) + ->setIssuer($iss) + ->setToolPrivateKey($this->getPrivateKey()); + } + + private function getLtiConnection(): array + { + $em = Database::getManager(); + $platforms = $em->getRepository('ChamiloPluginBundle:LtiProvider\Platform')->findAll(); + + $ltiCustomers = []; + foreach ($platforms as $platform) { + $issuer = $platform->getIssuer(); + $ltiCustomers[$issuer] = [ + 'client_id' => $platform->getClientId(), + 'auth_login_url' => $platform->getAuthLoginUrl(), + 'auth_token_url' => $platform->getAuthTokenUrl(), + 'key_set_url' => $platform->getKeySetUrl(), + 'kid' => $platform->getKid(), + 'deployment' => [$platform->getDeploymentId()], + ]; + } + Session::write('iss', $ltiCustomers); + + return $ltiCustomers; + } + + private function getPrivateKey() + { + $privateKey = ''; + $platformKey = Database::getManager() + ->getRepository('ChamiloPluginBundle:LtiProvider\PlatformKey') + ->findOneBy([]); + if ($platformKey) { + $privateKey = $platformKey->getPrivateKey(); + } + + return $privateKey; + } + + public function findDeployment($iss, $deploymentId, $clientId = null) + { + if (!in_array($deploymentId, $_SESSION['iss'][$iss]['deployment'])) { + return false; + } + + return LtiDeployment::new()->setDeploymentId($deploymentId); + } +} diff --git a/plugin/lti_provider/delete.php b/plugin/lti_provider/delete.php new file mode 100644 index 0000000000..a8cfff1b8e --- /dev/null +++ b/plugin/lti_provider/delete.php @@ -0,0 +1,32 @@ +find('ChamiloPluginBundle:LtiProvider\Platform', $platformId); + +$em->remove($platform); +$em->flush(); + +Display::addFlash( + Display::return_message($plugin->get_lang('PlatformDeleted'), 'success') +); + +header('Location: '.api_get_path(WEB_PLUGIN_PATH).'lti_provider/admin.php'); +exit; diff --git a/plugin/lti_provider/edit.php b/plugin/lti_provider/edit.php new file mode 100644 index 0000000000..0d151eb58b --- /dev/null +++ b/plugin/lti_provider/edit.php @@ -0,0 +1,71 @@ +find('ChamiloPluginBundle:LtiProvider\Platform', $platformId); + +if (!$platform) { + Display::addFlash( + Display::return_message($plugin->get_lang('NoPlatform'), 'error') + ); + header('Location: '.api_get_path(WEB_PLUGIN_PATH).'lti_provider/admin.php'); + exit; +} + +$form = new FrmEdit('lti_provider_edit_platform', [], $platform); +$form->build(); + +if ($form->validate()) { + $formValues = $form->exportValues(); + + $platform->setIssuer($formValues['issuer']); + $platform->setClientId($formValues['client_id']); + $platform->setAuthLoginUrl($formValues['auth_login_url']); + $platform->setAuthTokenUrl($formValues['auth_token_url']); + $platform->setKeySetUrl($formValues['key_set_url']); + $platform->setDeploymentId($formValues['deployment_id']); + $platform->setKid($formValues['kid']); + + $em->persist($platform); + $em->flush(); + + Display::addFlash( + Display::return_message($plugin->get_lang('PlatformEdited'), 'success') + ); + + header('Location: '.api_get_path(WEB_PLUGIN_PATH).'lti_provider/admin.php'); + exit; +} else { + $form->setDefaultValues(); +} + +$interbreadcrumb[] = ['url' => api_get_path(WEB_CODE_PATH).'admin/index.php', 'name' => get_lang('PlatformAdmin')]; +$interbreadcrumb[] = ['url' => api_get_path(WEB_PLUGIN_PATH).'lti_provider/admin.php', 'name' => $plugin->get_title()]; + +$template = new Template($plugin->get_lang('EditPlatform')); +$template->assign('form', $form->returnForm()); + +$content = $template->fetch('lti_provider/view/add.tpl'); + +$template->assign('header', $plugin->get_title()); +$template->assign('content', $content); +$template->display_one_col_template(); diff --git a/plugin/lti_provider/install.php b/plugin/lti_provider/install.php new file mode 100644 index 0000000000..c37fcaf320 --- /dev/null +++ b/plugin/lti_provider/install.php @@ -0,0 +1,16 @@ +install(); diff --git a/plugin/lti_provider/lang/english.php b/plugin/lti_provider/lang/english.php new file mode 100644 index 0000000000..cdaf848376 --- /dev/null +++ b/plugin/lti_provider/lang/english.php @@ -0,0 +1,31 @@ +get_info(); diff --git a/plugin/lti_provider/provider_settings.php b/plugin/lti_provider/provider_settings.php new file mode 100644 index 0000000000..d2ea628212 --- /dev/null +++ b/plugin/lti_provider/provider_settings.php @@ -0,0 +1,63 @@ +getPublicKey(); + +try { + if ($enabled !== 'true') { + throw new Exception(get_lang('Forbidden')); + } + + + $html = '
' + .'
'.$plugin->get_lang('Name').'
' + .'
'.$name.'
' + .'
' + .'
' + .'
'.$plugin->get_lang('LaunchUrl').'
' + .'
'.$launchUrl.'
' + .'
' + .'
' + .'
'.$plugin->get_lang('LoginUrl').'
' + .'
'.$loginUrl.'
' + .'
' + .'
' + .'
'.$plugin->get_lang('RedirectUrl').'
' + .'
'.$redirectUrl.'
' + .'
' + .'
' + .'
'.$plugin->get_lang('PublicKey').'
' + .'
'.$publicKey.'
' + .'
'; + + $response->setContent($html); +} catch (Exception $exception) { + $response->setContent( + Display::return_message($exception->getMessage(), 'error') + ); +} + +$response->send(); diff --git a/plugin/lti_provider/src/Form/FrmAdd.php b/plugin/lti_provider/src/Form/FrmAdd.php new file mode 100644 index 0000000000..92d8320697 --- /dev/null +++ b/plugin/lti_provider/src/Form/FrmAdd.php @@ -0,0 +1,81 @@ +platform = $platform; + } + + /** + * Build the form + */ + public function build() + { + $plugin = LtiProviderPlugin::create(); + + $this->addHeader($plugin->get_lang('ConnectionDetails')); + + $this->addText('issuer', $plugin->get_lang('PlatformName')); + $this->addUrl('auth_login_url', $plugin->get_lang('AuthLoginUrl')); + $this->addUrl('auth_token_url', $plugin->get_lang('AuthTokenUrl')); + $this->addUrl('key_set_url', $plugin->get_lang('KeySetUrl')); + $this->addText('client_id', $plugin->get_lang('ClientId')); + $this->addText('deployment_id', $plugin->get_lang('DeploymentId')); + $this->addText('kid', $plugin->get_lang('KeyId')); + + $this->addButtonCreate($plugin->get_lang('AddPlatform')); + $this->applyFilter('__ALL__', 'trim'); + + } + + public function setDefaultValues() + { + $defaults = []; + + if (!$this->platform) { + $this->platform = new Platform(); + } + + $defaults['issuer'] = $this->platform->getIssuer(); + $defaults['auth_login_url'] = $this->platform->getAuthLoginUrl(); + $defaults['auth_token_url'] = $this->platform->getAuthTokenUrl(); + $defaults['key_set_url'] = $this->platform->getKeySetUrl(); + $defaults['client_id'] = $this->platform->getClientId(); + $defaults['deployment_id'] = $this->platform->getDeploymentId(); + $defaults['kid'] = $this->platform->getKid(); + + $this->setDefaults($defaults); + } + + public function returnForm(): string + { + $js = ""; + + return $js.parent::returnForm(); // TODO: Change the autogenerated stub + } +} diff --git a/plugin/lti_provider/src/Form/FrmEdit.php b/plugin/lti_provider/src/Form/FrmEdit.php new file mode 100644 index 0000000000..5e8116cc62 --- /dev/null +++ b/plugin/lti_provider/src/Form/FrmEdit.php @@ -0,0 +1,75 @@ +platform = $platform; + } + + /** + * Build the form. + * + * @param bool $globalMode + * + * @throws Exception + */ + public function build(bool $globalMode = true) + { + $plugin = LtiProviderPlugin::create(); + $this->addHeader($plugin->get_lang('ConnectionDetails')); + + $this->addText('issuer', $plugin->get_lang('PlatformName')); + $this->addUrl('auth_login_url', $plugin->get_lang('AuthLoginUrl')); + $this->addUrl('auth_token_url', $plugin->get_lang('AuthTokenUrl')); + $this->addUrl('key_set_url', $plugin->get_lang('KeySetUrl')); + $this->addText('client_id', $plugin->get_lang('ClientId')); + $this->addText('deployment_id', $plugin->get_lang('DeploymentId')); + $this->addText('kid', $plugin->get_lang('KeyId')); + + $this->addButtonCreate($plugin->get_lang('EditPlatform')); + $this->addHidden('id', $this->platform->getId()); + $this->addHidden('action', 'edit'); + $this->applyFilter('__ALL__', 'trim'); + } + + /** + * @throws Exception + */ + public function setDefaultValues() + { + $defaults = []; + $defaults['issuer'] = $this->platform->getIssuer(); + $defaults['auth_login_url'] = $this->platform->getAuthLoginUrl(); + $defaults['auth_token_url'] = $this->platform->getAuthTokenUrl(); + $defaults['key_set_url'] = $this->platform->getKeySetUrl(); + $defaults['client_id'] = $this->platform->getClientId(); + $defaults['deployment_id'] = $this->platform->getDeploymentId(); + $defaults['kid'] = $this->platform->getKid(); + + $this->setDefaults($defaults); + } +} diff --git a/plugin/lti_provider/src/LtiProvider.php b/plugin/lti_provider/src/LtiProvider.php new file mode 100644 index 0000000000..e9981902b3 --- /dev/null +++ b/plugin/lti_provider/src/LtiProvider.php @@ -0,0 +1,65 @@ +doOidcLoginRedirect(api_get_path(WEB_PLUGIN_PATH)."lti_provider/web/game.php", $request) + ->doRedirect(); + } + + /** + * Lti Message Launch + * + * @param bool $fromCache + * @param null $launchId + * + * @throws Lti1p3\LtiException + * + * @return LtiMessageLaunch + */ + public function launch(bool $fromCache = false, $launchId = null): LtiMessageLaunch + { + if ($fromCache) { + $launch = LtiMessageLaunch::fromCache($launchId, new Lti13Database(), new Lti13Cache()); + } else { + $launch = LtiMessageLaunch::new(new Lti13Database(), new Lti13Cache(), new Lti13Cookie)->validate(); + } + + return $launch; + } + +} diff --git a/plugin/lti_provider/uninstall.php b/plugin/lti_provider/uninstall.php new file mode 100644 index 0000000000..db390eada4 --- /dev/null +++ b/plugin/lti_provider/uninstall.php @@ -0,0 +1,16 @@ +uninstall(); diff --git a/plugin/lti_provider/view/add.tpl b/plugin/lti_provider/view/add.tpl new file mode 100644 index 0000000000..361c736d0f --- /dev/null +++ b/plugin/lti_provider/view/add.tpl @@ -0,0 +1,5 @@ +
+
+ {{ form }} +
+
diff --git a/plugin/lti_provider/view/admin.tpl b/plugin/lti_provider/view/admin.tpl new file mode 100644 index 0000000000..80da0feed7 --- /dev/null +++ b/plugin/lti_provider/view/admin.tpl @@ -0,0 +1,52 @@ + +{{ 'LtiProviderDescription'|get_plugin_lang('LtiProviderPlugin') }} + + +
+ + {{ 'ConnectionDetails'|get_plugin_lang('LtiProviderPlugin') }} + + + {{ 'AddPlatform'|get_plugin_lang('LtiProviderPlugin') }} + +
+
+ + + + + + + + + + + + + {% for platform in platforms %} + {% set url_params = {'id': platform.getId}|url_encode() %} + + + + + + + + + {% endfor %} + + +
{{ 'PlatformName'|get_plugin_lang('LtiProviderPlugin') }}{{ 'ClientId'|get_plugin_lang('LtiProviderPlugin') }}{{ 'DeploymentId'|get_plugin_lang('LtiProviderPlugin') }}{{ 'KeyId'|get_plugin_lang('LtiProviderPlugin') }}{{ 'URLs'|get_plugin_lang('LtiProviderPlugin') }}{{ 'Actions'|get_lang }}
{{ platform.getIssuer }}{{ platform.getClientId }}{{ platform.getDeploymentId }}{{ platform.getKid }} +

{{ 'AuthLoginUrl'|get_plugin_lang('LtiProviderPlugin') }}:
{{ platform.getAuthLoginUrl }}

+

{{ 'AuthTokenUrl'|get_plugin_lang('LtiProviderPlugin') }}:
{{ platform.getAuthTokenUrl }}

+

{{ 'KeySetUrl'|get_plugin_lang('LtiProviderPlugin') }}:
{{ platform.getKeySetUrl }}

+
+ + {{ 'edit.png'|img(22, 'Edit'|get_lang) }} + + + {{ 'delete.png'|img(22, 'Delete'|get_lang) }} + +
+
+ diff --git a/plugin/lti_provider/web/api/score.php b/plugin/lti_provider/web/api/score.php new file mode 100644 index 0000000000..2738b0776e --- /dev/null +++ b/plugin/lti_provider/web/api/score.php @@ -0,0 +1,49 @@ +launch(true, $_REQUEST['launch_id']); + +if (!$launch->hasAgs()) { + throw new Exception("Don't have grades!"); +} + +$grades = $launch->getAgs(); + +$score = Packback\Lti1p3\LtiGrade::new() + ->setScoreGiven($_REQUEST['score']) + ->setScoreMaximum(100) + ->setTimestamp(date(DateTime::ISO8601)) + ->setActivityProgress('Completed') + ->setGradingProgress('FullyGraded') + ->setUserId($launch->getLaunchData()['sub']); + + +$scoreLineitem = Packback\Lti1p3\LtiLineitem::new() + ->setTag('score') + ->setScoreMaximum(100) + ->setLabel('Score') + ->setResourceId($launch->getLaunchData()['https://purl.imsglobal.org/spec/lti/claim/resource_link']['id']); + +$grades->putGrade($score, $scoreLineitem); + + +$time = Packback\Lti1p3\LtiGrade::new() + ->setScoreGiven($_REQUEST['time']) + ->setScoreMaximum(999) + ->setTimestamp(DateTime::ISO8601) + ->setActivityProgress('Completed') + ->setGradingProgress('FullyGraded') + ->setUserId($launch->getLaunchData()['sub']); + +$timeLineitem = Packback\Lti1p3\LtiLineitem::new() + ->setTag('time') + ->setScoreMaximum(999) + ->setLabel('Time Taken') + ->setResourceId('time'.$launch->getLaunchData()['https://purl.imsglobal.org/spec/lti/claim/resource_link']['id']); + +$grades->putGrade($time, $timeLineitem); + +echo '{"success" : true}'; \ No newline at end of file diff --git a/plugin/lti_provider/web/api/scoreboard.php b/plugin/lti_provider/web/api/scoreboard.php new file mode 100644 index 0000000000..f575279c08 --- /dev/null +++ b/plugin/lti_provider/web/api/scoreboard.php @@ -0,0 +1,51 @@ +launch(true, $_REQUEST['launch_id']); + +if (!$launch->hasNrps()) { + throw new Exception("Don't have names and roles!"); +} +if (!$launch->hasAgs()) { + throw new Exception("Don't have grades!"); +} + +$ags = $launch->getAgs(); + +$scoreLineitem = Packback\Lti1p3\LtiLineitem::new() + ->setTag('score') + ->setScoreMaximum(100) + ->setLabel('Score') + ->setResourceId($launch->getLaunchData()['https://purl.imsglobal.org/spec/lti/claim/resource_link']['id']); +$scores = $ags->getGrades($scoreLineitem); + +$timeLineitem = Packback\Lti1p3\LtiLineitem::new() + ->setTag('time') + ->setScoreMaximum(999) + ->setLabel('Time Taken') + ->setResourceId('time'.$launch->getLaunchData()['https://purl.imsglobal.org/spec/lti/claim/resource_link']['id']); +$times = $ags->getGrades($timeLineitem); + +$members = $launch->getNrps()->getMembers(); + +$scoreboard = []; +foreach ($scores as $score) { + $result = ['score' => $score['resultScore']]; + foreach ($times as $time) { + if ($time['userId'] === $score['userId']) { + $result['time'] = $time['resultScore']; + break; +} + } + foreach ($members as $member) { + if ($member['user_id'] === $score['userId']) { + $result['name'] = $member['name']; + break; + } + } + $scoreboard[] = $result; +} +echo json_encode($scoreboard); \ No newline at end of file diff --git a/plugin/lti_provider/web/configure.php b/plugin/lti_provider/web/configure.php new file mode 100644 index 0000000000..b43ef1b54d --- /dev/null +++ b/plugin/lti_provider/web/configure.php @@ -0,0 +1,20 @@ +launch(true, $_REQUEST['launch_id']); + +if (!$launch->isDeepLinkLaunch()) { + throw new Exception("Must be a deep link!"); +} + +$resource = LtiDeepLinkResource::new() + ->setUrl(api_get_path(WEB_PLUGIN_PATH)."lti_provider/web/game.php") + ->setCustomParams(['difficulty' => $_REQUEST['diff']]) + ->setTitle('Breakout ' . $_REQUEST['diff'] . ' mode!'); + +$launch->getDeepLink() + ->outputResponseForm([$resource]); diff --git a/plugin/lti_provider/web/game.php b/plugin/lti_provider/web/game.php new file mode 100644 index 0000000000..345bf28f23 --- /dev/null +++ b/plugin/lti_provider/web/game.php @@ -0,0 +1,32 @@ +launch(); + +$htmlHeadXtra[] = api_get_css( + api_get_path(WEB_PLUGIN_PATH).'lti_provider/web/static/breakout.css' +); + +$template = new Template('Game demo'); + +$courseCode = $launch->getLaunchData()['https://purl.imsglobal.org/spec/lti/claim/context']['label']; +$diff = 'normal'; +if ($launch->isDeepLinkLaunch()) { + $diff = $launch->getLaunchData()['https://purl.imsglobal.org/spec/lti/claim/custom']['difficulty']; +} +$username = $launch->getLaunchData()['given_name']; +$template->assign('launch', $launch); +$template->assign('courseCode', $courseCode); +$template->assign('diff', $diff); +$template->assign('username', $username); + +$content = $template->fetch('lti_provider/web/view/game.tpl'); +$template->assign('content', $content); +$template->display_no_layout_template(); + + +?> diff --git a/plugin/lti_provider/web/login.php b/plugin/lti_provider/web/login.php new file mode 100644 index 0000000000..8af2fbdd0d --- /dev/null +++ b/plugin/lti_provider/web/login.php @@ -0,0 +1,8 @@ +login($_REQUEST); diff --git a/plugin/lti_provider/web/static/breakout.css b/plugin/lti_provider/web/static/breakout.css new file mode 100644 index 0000000000..db6d8f2c76 --- /dev/null +++ b/plugin/lti_provider/web/static/breakout.css @@ -0,0 +1,56 @@ +body { + font-family: 'Gugi', cursive; +} +#scoreboard { + border: solid 1px #000; + border-left: none; + border-top-right-radius: 20px; + border-bottom-right-radius: 20px; + padding-bottom: 12px; + background: linear-gradient(to bottom, rgb(0, 0, 0), rgb(0, 0, 50) 500px); + color: white; +} +th, td { + color: white; +} +.dl-config { + font-family: Verdana, Geneva, Tahoma, sans-serif; + display: block; + width: 400px; + position: absolute; + left:50%; + margin-left: -200px; +} +.dl-config h1 { + text-align: center; +} +.dl-config ul { + list-style: none; + padding: 0; +} +.dl-config ul li a { + display: block; + width: 200px; + height:50px; + text-align: center; + border: black solid 1px; + border-radius: 15px; + margin:auto; + text-decoration: none; + color: black; + margin-top:8px; + font-size: 30px; + line-height: 46px; +} +.dl-config ul li a:hover { + background-color: #dddddd; +} +#game-screen { + position: relative; + height: 550px; + width: 100%; +} +#btn-again { + right: 0px; + position: absolute; +} \ No newline at end of file diff --git a/plugin/lti_provider/web/static/breakout.js b/plugin/lti_provider/web/static/breakout.js new file mode 100644 index 0000000000..e2d7cbb3d3 --- /dev/null +++ b/plugin/lti_provider/web/static/breakout.js @@ -0,0 +1,464 @@ +var c = document.getElementById("breakout"); +var ctx = window.c.getContext("2d"); + +var bgctx = document.getElementById("breakoutbg").getContext("2d"); +bgctx.beginPath(); +var bggrad = ctx.createLinearGradient(0,0,0,c.height); +bggrad.addColorStop(0,"rgb(0, 0, 0)"); +bggrad.addColorStop(1,"rgb(0, 0, 50)"); +bgctx.fillStyle = bggrad; +bgctx.rect(0, 0, c.width, c.height); +bgctx.fill(); +bgctx.font = "10px Gugi"; +bgctx.fillStyle = '#FFFFFF'; +bgctx.textAlign = "left"; +bgctx.fillText("Powered By Turnitin", 6, c.height-6); + +var score = 0; + +var difficulty = { + hard: { + speed_multiplier:1.5 + }, + normal: { + speed_multiplier:1 + }, + easy: { + speed_multiplier:0.7 + }, +} + +var ball = { + pos : { + x: window.c.width/2-200, + y : window.c.height/2-2, + }, + vel : { + x : 6 * difficulty[curr_diff]['speed_multiplier'], + y : 6 * difficulty[curr_diff]['speed_multiplier'], + }, + r: 10, + rot: 0, + velr: 0, + render: function() { + this.pos.x += this.vel.x; + this.pos.y += this.vel.y; + if (this.oob(window.c.width, this.pos.x, this.r)) { + this.vel.x = -this.vel.x; + this.pos.x += this.vel.x; + this.pos.y -= this.vel.y; + } + if (this.oob(window.c.height, this.pos.y, this.r)) { + if (this.pos.y > window.c.height - this.r) { + window.endGame(); + } + this.vel.y = -this.vel.y; + this.pos.y += this.vel.y; + this.pos.x -= this.vel.x; + } + + window.ctx.save(); + window.ctx.beginPath(); + var gradient = ctx.createRadialGradient(this.pos.x, this.pos.y, 2, this.pos.x, this.pos.y, 10); + window.ctx.fillStyle = "rgb(255, 232, 102)"; + window.ctx.strokeStyle = "rgb(255, 232, 102)"; + window.ctx.setLineDash([5,5]); + window.ctx.lineWidth = 4; + window.ctx.translate(this.pos.x, this.pos.y); + window.ctx.rotate(this.rot * Math.PI); + window.ctx.arc(0, 0, this.r, 0, 2 * Math.PI); + if (this.vel.x > 0) { + this.velr = 0.01; + } else if (this.vel.x < 0) { + this.velr = -0.01; + } else { + this.velr = 0; + } + this.rot += this.velr; + window.ctx.fill(); + window.ctx.stroke(); + window.ctx.restore(); + + }, + oob : function(max, curr, offset) { + if (curr < offset || curr > (max-offset)) { + return true; + } + }, + left : function() { + return this.pos.x - this.r; + }, + right : function() { + return this.pos.x + this.r; + }, + top : function() { + return this.pos.y - this.r; + }, + bottom : function() { + return this.pos.y + this.r; + }, +}; + +var paddle = { + pos : { + x: window.c.width/2+2, + y : window.c.height-40, + }, + width : 80, + height : 20, + render: function() { + window.ctx.beginPath(); + var gradient = ctx.createLinearGradient(this.pos.x,this.pos.y,this.pos.x,this.pos.y + this.height); + gradient.addColorStop(0,"#999999"); + gradient.addColorStop(0.7,"#eeeeee"); + gradient.addColorStop(1,"#999999"); + ctx.fillStyle = gradient; + var hh = this.height/2; + window.ctx.arc(this.pos.x + hh, this.pos.y + hh, hh, 0.5 * Math.PI, 1.5 * Math.PI); + window.ctx.rect(this.pos.x + hh, this.pos.y, this.width - this.height, this.height); + window.ctx.arc(this.pos.x + this.width - hh, this.pos.y + hh, hh, 1.5 * Math.PI, 0.5 * Math.PI); + window.ctx.fill(); + window.ctx.stroke(); + }, + left : function() { + return this.pos.x; + }, + right : function() { + return this.pos.x + this.width; + }, + top : function() { + return this.pos.y; + }, + bottom : function() { + return this.pos.y + this.height; + }, + test_hit : function() { + var hitx = this.test_hit_x(); + var hity = this.test_hit_y(); + if (!hitx || !hity) { + return 0; + } + if (hity) { + window.ball.vel.y = -Math.abs(window.ball.vel.y); + window.ball.pos.y += window.ball.vel.y; + window.ball.pos.x -= window.ball.vel.x; + } + if (hitx) { + var xdiff = window.ball.pos.x - (this.pos.x + (this.width/2)); + window.ball.vel.x = (xdiff > 0 ? Math.ceil(xdiff / 5) : Math.floor(xdiff / 5)) * difficulty[curr_diff]['speed_multiplier']; + window.ball.pos.x += window.ball.vel.x; + } + return 1; + }, + test_hit_x : function() { + if (this.left() > window.ball.right()) { + return 0; + } + if (this.right() < window.ball.left()) { + return 0; + } + return 1; + }, + test_hit_y : function() { + if (this.top() > window.ball.bottom()) { + return 0; + } + if (this.bottom() < window.ball.top()) { + return 0; + } + return 1; + }, + move : function() { + if (window.press_left) { + if (this.pos.x > 0) { + this.pos.x -= 8; + } + } + if (window.press_right) { + if (this.pos.x < window.c.width - this.width) { + this.pos.x += 8; + } + } + } +}; + +function fire() { + this.r = 0, + this.a = 0, + this.render = function() { + if (this.a < 0.2) { + this.reset(); + } + + this.pos.x += this.vel.x + (Math.random() * 2) - 1; + this.pos.y += this.vel.y + (Math.random() * 2) - 1; + + this.r *= 0.95; + this.a *= 0.95; + + window.ctx.beginPath(); + window.ctx.fillStyle = 'rgba(' + (239 - this.green) +', ' + this.green + ', 66,'+ this.a +')'; + window.ctx.arc(this.pos.x, this.pos.y, this.r, 0, 2 * Math.PI); + window.ctx.fill(); + + if (this.green < 232) { + this.green += 8; + } + }, + this.reset = function() { + this.pos = { + x: window.ball.pos.x, + y : window.ball.pos.y, + }; + this.vel = { + x : (Math.random() * 4) - 2, + y : (Math.random() * 4) - 2, + }; + this.r = (Math.random() * 5) + 1; + this.a = 0.9; + this.green = 62; + } +} + +function brick() { + this.id = 0, + this.pos = { + x: 40, + y: 40, + }, + this.vely = 0, + this.rot = 0, + this.velr = 0, + this.hit = false, + this.last_hitx = false, + this.last_hity = false, + this.width = 40, + this.height = 20, + this.render = function() { + if (this.hit) { + if (this.pos.y > window.c.height + 60) { + return; + } + this.vely++; + this.pos.y += this.vely; + ctx.save() + window.ctx.beginPath(); + this.rot += this.velr; + ctx.translate(this.pos.x + (this.width/2), this.pos.y + (this.height/2)); + ctx.rotate(this.rot * Math.PI); + var gradient = ctx.createRadialGradient(-(this.width/2) +10, -(this.height/2) +5, 0, -(this.width/2) + 40, -(this.height/2) + 15, 40); + gradient.addColorStop(0, 'rgba(137, 211, 234, 0.2)'); + gradient.addColorStop(1, 'rgba(137, 211, 234, 1)'); + ctx.strokeStyle = 'rgba(254, 254, 254, 0.8)'; + ctx.fillStyle = gradient; + window.ctx.rect(-(this.width/2), -(this.height/2), this.width, this.height); + window.ctx.fill(); + window.ctx.stroke(); + ctx.restore(); + return; + } + window.ctx.beginPath(); + var gradient = ctx.createRadialGradient(this.pos.x + 10, this.pos.y + 5, 0, this.pos.x + 40, this.pos.y + 15, 40); + gradient.addColorStop(0, 'rgba(137, 211, 234, 0.2)'); + gradient.addColorStop(1, 'rgba(137, 211, 234, 1)'); + ctx.strokeStyle = 'rgba(254, 254, 254, 0.8)'; + ctx.fillStyle = gradient; + window.ctx.rect(this.pos.x, this.pos.y, this.width, this.height); + window.ctx.fill(); + window.ctx.stroke(); + }, + this.test_hit = function() { + if (this.hit) { + return 0; + } + var hitx = this.test_hit_x(); + var hity = this.test_hit_y(); + if (!hitx || !hity) { + this.last_hitx = hitx; + this.last_hity = hity; + return 0; + } + if (this.last_hity) { + window.ball.vel.y = -window.ball.vel.y; + window.ball.pos.y += window.ball.vel.y; + window.ball.pos.x -= window.ball.vel.x; + } + if (this.last_hitx) { + window.ball.vel.x = -window.ball.vel.x; + window.ball.pos.x += window.ball.vel.x; + window.ball.pos.y -= window.ball.vel.y; + } + if (!this.last_hity && this.last_hitx) { + window.ball.vel.x = -window.ball.vel.x; + window.ball.pos.x += window.ball.vel.x; + window.ball.vel.y = -window.ball.vel.y; + window.ball.pos.y += window.ball.vel.y; + } + this.last_hitx = hitx; + this.last_hity = hity; + this.hit = true; + this.velr = (Math.random() * 0.04) - 0.02; + window.score++; + return 1; + }, + this.test_hit_x = function() { + if (this.left() > window.ball.right()) { + return 0; + } + if (this.right() < window.ball.left()) { + return 0; + } + return 1; + }, + this.test_hit_y = function() { + if (this.top() > window.ball.bottom()) { + return 0; + } + if (this.bottom() < window.ball.top()) { + return 0; + } + return 1; + }, + + + this.left = function() { + return this.pos.x; + }, + this.right = function() { + return this.pos.x + this.width; + }, + this.top = function() { + return this.pos.y; + }, + this.bottom = function() { + return this.pos.y + this.height; + } +}; + +press_left = false; +press_right = false; + +document.addEventListener('keydown', (event) => { + const keyName = event.key; + if (keyName == "ArrowLeft") { + press_left = true; + } + if (keyName == "ArrowRight") { + press_right = true; + } + }); + +document.addEventListener('keyup', (event) => { + const keyName = event.key; + if (keyName == "ArrowLeft") { + press_left = false; + } + if (keyName == "ArrowRight") { + press_right = false; + } + if (keyName == " ") { + if (pause && !gameover) { + if (!start_time) { + start_time = Math.floor(Date.now() / 1000); + } + pause = false; + frame(); + } else { + pause = true; + } + } +}); + +var bricks = []; + +for (var h = 0; h < 6; h++) { + for (var w = 0; w < 18; w++) { + var brickid = (18*h)+w; + bricks[brickid] = new brick(); + bricks[brickid].pos.x = 40+(w*40); + bricks[brickid].pos.y = 40+(h*20); + bricks[brickid].id = brickid; + } +} +var fires = []; + +for (var i = 0; i < 80; i++) { + fires[i] = new fire(); +} +startFireCount = 1; + +pause = true; +gameover = false; + +var frame = function() { + if (window.score >= window.bricks.length) { + endGame(); + } + window.ctx.clearRect(0, 0, window.c.width, window.c.height); + for (var i = 0; i < window.bricks.length; i++) { + window.bricks[i].render(); + } + for (var i = 0; i < window.bricks.length; i++) { + if (window.bricks[i].test_hit()) { + break; + } + } + for (var i = 0; i < window.fires.length && i < startFireCount; i++) { + window.fires[i].render(); + } + if (startFireCount <= window.fires.length) { + startFireCount++; + } + window.paddle.move(); + window.paddle.render(); + window.paddle.test_hit(); + window.ball.render(); + + if (pause) { + if (!gameover) { + ctx.font = "50px Gugi"; + ctx.fillStyle = '#FFFFFF'; + ctx.textAlign = "center"; + ctx.fillText("Ready " + curr_user_name, c.width/2, c.height/2); + ctx.fillText("Press Space to Start", c.width/2, c.height/2 + 60); + } + } else { + requestAnimationFrame(frame); + } +} + +start_time = false; +document.fonts.load('50px Gugi').then(frame); + +var endGame = function() { + window.pause = true; + window.gameover = true; + window.submitScore(); +} + +var refreshScoreBoard = function() { + var scores = JSON.parse(this.responseText); + console.log(scores); + var output = 'ScoreTimeName'; + for (var i = 0; i < scores.length; i++) { + output += '' + scores[i].score + '' + scores[i].time + 's' + scores[i].name + ''; + } + document.getElementById("leadertable").innerHTML = output; +} + +var submitScore = function() { + var time_taken = Math.floor(Date.now() / 1000) - start_time; + var xhttp = new XMLHttpRequest(); + xhttp.addEventListener("load", getScoreBoard); + xhttp.open("GET", "api/score.php?launch_id=" + launch_id + "&score=" + window.score + "&time=" + time_taken, false); + xhttp.send(); +} + +var getScoreBoard = function() { + var xhttp = new XMLHttpRequest(); + xhttp.addEventListener("load", refreshScoreBoard); + xhttp.open("GET", "api/scoreboard.php?launch_id=" + launch_id, true); + xhttp.send(); +} + +getScoreBoard(); diff --git a/plugin/lti_provider/web/view/game.tpl b/plugin/lti_provider/web/view/game.tpl new file mode 100644 index 0000000000..0cf54a7b67 --- /dev/null +++ b/plugin/lti_provider/web/view/game.tpl @@ -0,0 +1,35 @@ + +{% if launch.isDeepLinkLaunch %} +
+

Pick a Difficulty

+ +
+{% else %} + +
+
+
+

Scoreboard - {{ courseCode }}

+ +
+
+ + + + +
+
+
+ + + + +{% endif %} \ No newline at end of file