Merge branch 'MDL-62599-master' of https://github.com/snake/moodle
authorJake Dallimore <jake@moodle.com>
Wed, 8 May 2019 03:48:51 +0000 (11:48 +0800)
committerJake Dallimore <jake@moodle.com>
Wed, 8 May 2019 03:48:51 +0000 (11:48 +0800)
72 files changed:
.eslintignore
.stylelintignore
lib/classes/plugin_manager.php
lib/php-jwt/LICENSE [new file with mode: 0644]
lib/php-jwt/README.md [new file with mode: 0644]
lib/php-jwt/composer.json [new file with mode: 0644]
lib/php-jwt/readme_moodle.txt [new file with mode: 0644]
lib/php-jwt/src/BeforeValidException.php [new file with mode: 0644]
lib/php-jwt/src/ExpiredException.php [new file with mode: 0644]
lib/php-jwt/src/JWT.php [new file with mode: 0644]
lib/php-jwt/src/SignatureInvalidException.php [new file with mode: 0644]
lib/thirdpartylibs.xml
mod/lti/OAuthBody.php
mod/lti/amd/build/tool_card_controller.min.js
mod/lti/amd/src/tool_card_controller.js
mod/lti/auth.php [new file with mode: 0644]
mod/lti/backup/moodle2/backup_lti_stepslib.php
mod/lti/certs.php [new file with mode: 0644]
mod/lti/classes/external.php
mod/lti/classes/local/ltiservice/resource_base.php
mod/lti/classes/local/ltiservice/response.php
mod/lti/classes/local/ltiservice/service_base.php
mod/lti/classes/output/tool_configure_page.php
mod/lti/classes/task/clean_access_tokens.php [new file with mode: 0644]
mod/lti/contentitem.php
mod/lti/contentitem_return.php
mod/lti/db/install.php [new file with mode: 0644]
mod/lti/db/install.xml
mod/lti/db/services.php
mod/lti/db/tasks.php [new file with mode: 0644]
mod/lti/db/upgrade.php
mod/lti/edit_form.php
mod/lti/instructor_edit_tool_type.php
mod/lti/lang/en/lti.php
mod/lti/launch.php
mod/lti/locallib.php
mod/lti/mod_form.php
mod/lti/service.php
mod/lti/service/basicoutcomes/classes/local/resources/basicoutcomes.php [new file with mode: 0644]
mod/lti/service/basicoutcomes/classes/local/service/basicoutcomes.php [new file with mode: 0644]
mod/lti/service/basicoutcomes/classes/privacy/provider.php [new file with mode: 0644]
mod/lti/service/basicoutcomes/lang/en/ltiservice_basicoutcomes.php [new file with mode: 0644]
mod/lti/service/basicoutcomes/version.php [new file with mode: 0644]
mod/lti/service/gradebookservices/classes/local/resources/lineitem.php
mod/lti/service/gradebookservices/classes/local/resources/lineitems.php
mod/lti/service/gradebookservices/classes/local/resources/results.php
mod/lti/service/gradebookservices/classes/local/resources/scores.php
mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php
mod/lti/service/gradebookservices/lang/en/ltiservice_gradebookservices.php
mod/lti/service/memberships/classes/local/resources/contextmemberships.php
mod/lti/service/memberships/classes/local/resources/linkmemberships.php
mod/lti/service/memberships/classes/local/service/memberships.php
mod/lti/service/memberships/lang/en/ltiservice_memberships.php
mod/lti/service/profile/classes/local/resources/profile.php
mod/lti/service/toolproxy/classes/local/resources/toolproxy.php
mod/lti/service/toolsettings/classes/local/resources/contextsettings.php
mod/lti/service/toolsettings/classes/local/resources/linksettings.php
mod/lti/service/toolsettings/classes/local/resources/systemsettings.php
mod/lti/service/toolsettings/classes/local/service/toolsettings.php
mod/lti/service/toolsettings/lang/en/ltiservice_toolsettings.php
mod/lti/servicelib.php
mod/lti/services.php
mod/lti/templates/tool_card.mustache
mod/lti/templates/tool_config_modal_body.mustache [new file with mode: 0644]
mod/lti/templates/tool_config_modal_footer.mustache [new file with mode: 0644]
mod/lti/tests/locallib_test.php
mod/lti/tests/task_clean_access_tokens_test.php [new file with mode: 0644]
mod/lti/token.php [new file with mode: 0644]
mod/lti/typessettings.php
mod/lti/upgrade.txt
mod/lti/version.php
mod/lti/view.php

index 14e635f..a262404 100644 (file)
@@ -64,6 +64,7 @@ lib/amd/src/adapter.js
 lib/validateurlsyntax.php
 lib/amd/src/popper.js
 lib/geopattern-php/
+lib/php-jwt/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index 128c038..6bcaefe 100644 (file)
@@ -65,6 +65,7 @@ lib/amd/src/adapter.js
 lib/validateurlsyntax.php
 lib/amd/src/popper.js
 lib/geopattern-php/
+lib/php-jwt/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index 96c4537..4eaa742 100644 (file)
@@ -1810,7 +1810,7 @@ class core_plugin_manager {
             ),
 
             'ltiservice' => array(
-                'gradebookservices', 'memberships', 'profile', 'toolproxy', 'toolsettings'
+                'gradebookservices', 'memberships', 'profile', 'toolproxy', 'toolsettings', 'basicoutcomes'
             ),
 
             'mlbackend' => array(
diff --git a/lib/php-jwt/LICENSE b/lib/php-jwt/LICENSE
new file mode 100644 (file)
index 0000000..cb0c49b
--- /dev/null
@@ -0,0 +1,30 @@
+Copyright (c) 2011, Neuman Vong
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Neuman Vong nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/php-jwt/README.md b/lib/php-jwt/README.md
new file mode 100644 (file)
index 0000000..b1a7a3a
--- /dev/null
@@ -0,0 +1,200 @@
+[![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt)
+[![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt)
+[![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt)
+[![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt)
+
+PHP-JWT
+=======
+A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519).
+
+Installation
+------------
+
+Use composer to manage your dependencies and download PHP-JWT:
+
+```bash
+composer require firebase/php-jwt
+```
+
+Example
+-------
+```php
+<?php
+use \Firebase\JWT\JWT;
+
+$key = "example_key";
+$token = array(
+    "iss" => "http://example.org",
+    "aud" => "http://example.com",
+    "iat" => 1356999524,
+    "nbf" => 1357000000
+);
+
+/**
+ * IMPORTANT:
+ * You must specify supported algorithms for your application. See
+ * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
+ * for a list of spec-compliant algorithms.
+ */
+$jwt = JWT::encode($token, $key);
+$decoded = JWT::decode($jwt, $key, array('HS256'));
+
+print_r($decoded);
+
+/*
+ NOTE: This will now be an object instead of an associative array. To get
+ an associative array, you will need to cast it as such:
+*/
+
+$decoded_array = (array) $decoded;
+
+/**
+ * You can add a leeway to account for when there is a clock skew times between
+ * the signing and verifying servers. It is recommended that this leeway should
+ * not be bigger than a few minutes.
+ *
+ * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef
+ */
+JWT::$leeway = 60; // $leeway in seconds
+$decoded = JWT::decode($jwt, $key, array('HS256'));
+
+?>
+```
+Example with RS256 (openssl)
+----------------------------
+```php
+<?php
+use \Firebase\JWT\JWT;
+
+$privateKey = <<<EOD
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQC8kGa1pSjbSYZVebtTRBLxBz5H4i2p/llLCrEeQhta5kaQu/Rn
+vuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t0tyazyZ8JXw+KgXTxldMPEL9
+5+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4ehde/zUxo6UvS7UrBQIDAQAB
+AoGAb/MXV46XxCFRxNuB8LyAtmLDgi/xRnTAlMHjSACddwkyKem8//8eZtw9fzxz
+bWZ/1/doQOuHBGYZU8aDzzj59FZ78dyzNFoF91hbvZKkg+6wGyd/LrGVEB+Xre0J
+Nil0GReM2AHDNZUYRv+HYJPIOrB0CRczLQsgFJ8K6aAD6F0CQQDzbpjYdx10qgK1
+cP59UHiHjPZYC0loEsk7s+hUmT3QHerAQJMZWC11Qrn2N+ybwwNblDKv+s5qgMQ5
+5tNoQ9IfAkEAxkyffU6ythpg/H0Ixe1I2rd0GbF05biIzO/i77Det3n4YsJVlDck
+ZkcvY3SK2iRIL4c9yY6hlIhs+K9wXTtGWwJBAO9Dskl48mO7woPR9uD22jDpNSwe
+k90OMepTjzSvlhjbfuPN1IdhqvSJTDychRwn1kIJ7LQZgQ8fVz9OCFZ/6qMCQGOb
+qaGwHmUK6xzpUbbacnYrIM6nLSkXgOAwv7XXCojvY614ILTK3iXiLBOxPu5Eu13k
+eUz9sHyD6vkgZzjtxXECQAkp4Xerf5TGfQXGXhxIX52yH+N2LtujCdkQZjXAsGdm
+B2zNzvrlgRmgBrklMTrMYgm1NPcW+bRLGcwgW2PTvNM=
+-----END RSA PRIVATE KEY-----
+EOD;
+
+$publicKey = <<<EOD
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8kGa1pSjbSYZVebtTRBLxBz5H
+4i2p/llLCrEeQhta5kaQu/RnvuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t
+0tyazyZ8JXw+KgXTxldMPEL95+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4
+ehde/zUxo6UvS7UrBQIDAQAB
+-----END PUBLIC KEY-----
+EOD;
+
+$token = array(
+    "iss" => "example.org",
+    "aud" => "example.com",
+    "iat" => 1356999524,
+    "nbf" => 1357000000
+);
+
+$jwt = JWT::encode($token, $privateKey, 'RS256');
+echo "Encode:\n" . print_r($jwt, true) . "\n";
+
+$decoded = JWT::decode($jwt, $publicKey, array('RS256'));
+
+/*
+ NOTE: This will now be an object instead of an associative array. To get
+ an associative array, you will need to cast it as such:
+*/
+
+$decoded_array = (array) $decoded;
+echo "Decode:\n" . print_r($decoded_array, true) . "\n";
+?>
+```
+
+Changelog
+---------
+
+#### 5.0.0 / 2017-06-26
+- Support RS384 and RS512.
+  See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)!
+- Add an example for RS256 openssl.
+  See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)!
+- Detect invalid Base64 encoding in signature.
+  See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)!
+- Update `JWT::verify` to handle OpenSSL errors.
+  See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)!
+- Add `array` type hinting to `decode` method
+  See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)!
+- Add all JSON error types.
+  See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)!
+- Bugfix 'kid' not in given key list.
+  See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)!
+- Miscellaneous cleanup, documentation and test fixes.
+  See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115),
+  [#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and
+  [#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman),
+  [@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)!
+
+#### 4.0.0 / 2016-07-17
+- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)!
+- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)!
+- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)!
+- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)!
+
+#### 3.0.0 / 2015-07-22
+- Minimum PHP version updated from `5.2.0` to `5.3.0`.
+- Add `\Firebase\JWT` namespace. See
+[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to
+[@Dashron](https://github.com/Dashron)!
+- Require a non-empty key to decode and verify a JWT. See
+[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to
+[@sjones608](https://github.com/sjones608)!
+- Cleaner documentation blocks in the code. See
+[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to
+[@johanderuijter](https://github.com/johanderuijter)!
+
+#### 2.2.0 / 2015-06-22
+- Add support for adding custom, optional JWT headers to `JWT::encode()`. See
+[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to
+[@mcocaro](https://github.com/mcocaro)!
+
+#### 2.1.0 / 2015-05-20
+- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew
+between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)!
+- Add support for passing an object implementing the `ArrayAccess` interface for
+`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)!
+
+#### 2.0.0 / 2015-04-01
+- **Note**: It is strongly recommended that you update to > v2.0.0 to address
+  known security vulnerabilities in prior versions when both symmetric and
+  asymmetric keys are used together.
+- Update signature for `JWT::decode(...)` to require an array of supported
+  algorithms to use when verifying token signatures.
+
+
+Tests
+-----
+Run the tests using phpunit:
+
+```bash
+$ pear install PHPUnit
+$ phpunit --configuration phpunit.xml.dist
+PHPUnit 3.7.10 by Sebastian Bergmann.
+.....
+Time: 0 seconds, Memory: 2.50Mb
+OK (5 tests, 5 assertions)
+```
+
+New Lines in private keys
+-----
+
+If your private key contains `\n` characters, be sure to wrap it in double quotes `""`
+and not single quotes `''` in order to properly interpret the escaped characters.
+
+License
+-------
+[3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause).
diff --git a/lib/php-jwt/composer.json b/lib/php-jwt/composer.json
new file mode 100644 (file)
index 0000000..b76ffd1
--- /dev/null
@@ -0,0 +1,29 @@
+{
+    "name": "firebase/php-jwt",
+    "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+    "homepage": "https://github.com/firebase/php-jwt",
+    "authors": [
+        {
+            "name": "Neuman Vong",
+            "email": "neuman+pear@twilio.com",
+            "role": "Developer"
+        },
+        {
+            "name": "Anant Narayanan",
+            "email": "anant@php.net",
+            "role": "Developer"
+        }
+    ],
+    "license": "BSD-3-Clause",
+    "require": {
+        "php": ">=5.3.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Firebase\\JWT\\": "src"
+        }
+    },
+    "require-dev": {
+        "phpunit/phpunit": " 4.8.35"
+    }
+}
diff --git a/lib/php-jwt/readme_moodle.txt b/lib/php-jwt/readme_moodle.txt
new file mode 100644 (file)
index 0000000..6366cf8
--- /dev/null
@@ -0,0 +1,10 @@
+Description of php-jwt library import into Moodle
+
+Instructions
+------------
+1.  Visit [https://github.com/firebase/php-jwt].
+2.  Click on 'X releases'.
+3.  Download the latest release.
+4.  Unzip it in lib as php-jwt.
+5.  Update entry for this library in lib/thirdpartylibs.xml.
+
diff --git a/lib/php-jwt/src/BeforeValidException.php b/lib/php-jwt/src/BeforeValidException.php
new file mode 100644 (file)
index 0000000..a6ee2f7
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+namespace Firebase\JWT;
+
+class BeforeValidException extends \UnexpectedValueException
+{
+
+}
diff --git a/lib/php-jwt/src/ExpiredException.php b/lib/php-jwt/src/ExpiredException.php
new file mode 100644 (file)
index 0000000..3597370
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+namespace Firebase\JWT;
+
+class ExpiredException extends \UnexpectedValueException
+{
+
+}
diff --git a/lib/php-jwt/src/JWT.php b/lib/php-jwt/src/JWT.php
new file mode 100644 (file)
index 0000000..33e2ed4
--- /dev/null
@@ -0,0 +1,379 @@
+<?php
+
+namespace Firebase\JWT;
+use \DomainException;
+use \InvalidArgumentException;
+use \UnexpectedValueException;
+use \DateTime;
+
+/**
+ * JSON Web Token implementation, based on this spec:
+ * https://tools.ietf.org/html/rfc7519
+ *
+ * PHP version 5
+ *
+ * @category Authentication
+ * @package  Authentication_JWT
+ * @author   Neuman Vong <neuman@twilio.com>
+ * @author   Anant Narayanan <anant@php.net>
+ * @license  http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
+ * @link     https://github.com/firebase/php-jwt
+ */
+class JWT
+{
+
+    /**
+     * When checking nbf, iat or expiration times,
+     * we want to provide some extra leeway time to
+     * account for clock skew.
+     */
+    public static $leeway = 180;
+
+    /**
+     * Allow the current timestamp to be specified.
+     * Useful for fixing a value within unit testing.
+     *
+     * Will default to PHP time() value if null.
+     */
+    public static $timestamp = null;
+
+    public static $supported_algs = array(
+        'HS256' => array('hash_hmac', 'SHA256'),
+        'HS512' => array('hash_hmac', 'SHA512'),
+        'HS384' => array('hash_hmac', 'SHA384'),
+        'RS256' => array('openssl', 'SHA256'),
+        'RS384' => array('openssl', 'SHA384'),
+        'RS512' => array('openssl', 'SHA512'),
+    );
+
+    /**
+     * Decodes a JWT string into a PHP object.
+     *
+     * @param string        $jwt            The JWT
+     * @param string|array  $key            The key, or map of keys.
+     *                                      If the algorithm used is asymmetric, this is the public key
+     * @param array         $allowed_algs   List of supported verification algorithms
+     *                                      Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
+     *
+     * @return object The JWT's payload as a PHP object
+     *
+     * @throws UnexpectedValueException     Provided JWT was invalid
+     * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
+     * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
+     * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
+     * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
+     *
+     * @uses jsonDecode
+     * @uses urlsafeB64Decode
+     */
+    public static function decode($jwt, $key, array $allowed_algs = array())
+    {
+        $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp;
+
+        if (empty($key)) {
+            throw new InvalidArgumentException('Key may not be empty');
+        }
+        $tks = explode('.', $jwt);
+        if (count($tks) != 3) {
+            throw new UnexpectedValueException('Wrong number of segments');
+        }
+        list($headb64, $bodyb64, $cryptob64) = $tks;
+        if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) {
+            throw new UnexpectedValueException('Invalid header encoding');
+        }
+        if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) {
+            throw new UnexpectedValueException('Invalid claims encoding');
+        }
+        if (false === ($sig = static::urlsafeB64Decode($cryptob64))) {
+            throw new UnexpectedValueException('Invalid signature encoding');
+        }
+        if (empty($header->alg)) {
+            throw new UnexpectedValueException('Empty algorithm');
+        }
+        if (empty(static::$supported_algs[$header->alg])) {
+            throw new UnexpectedValueException('Algorithm not supported');
+        }
+        if (!in_array($header->alg, $allowed_algs)) {
+            throw new UnexpectedValueException('Algorithm not allowed');
+        }
+        if (is_array($key) || $key instanceof \ArrayAccess) {
+            if (isset($header->kid)) {
+                if (!isset($key[$header->kid])) {
+                    throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
+                }
+                $key = $key[$header->kid];
+            } else {
+                throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
+            }
+        }
+
+        // Check the signature
+        if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
+            throw new SignatureInvalidException('Signature verification failed');
+        }
+
+        // Check if the nbf if it is defined. This is the time that the
+        // token can actually be used. If it's not yet that time, abort.
+        if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
+            throw new BeforeValidException(
+                'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf)
+            );
+        }
+
+        // Check that this token has been created before 'now'. This prevents
+        // using tokens that have been created for later use (and haven't
+        // correctly used the nbf claim).
+        if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
+            throw new BeforeValidException(
+                'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat)
+            );
+        }
+
+        // Check if this token has expired.
+        if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
+            throw new ExpiredException('Expired token');
+        }
+
+        return $payload;
+    }
+
+    /**
+     * Converts and signs a PHP object or array into a JWT string.
+     *
+     * @param object|array  $payload    PHP object or array
+     * @param string        $key        The secret key.
+     *                                  If the algorithm used is asymmetric, this is the private key
+     * @param string        $alg        The signing algorithm.
+     *                                  Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
+     * @param mixed         $keyId
+     * @param array         $head       An array with header elements to attach
+     *
+     * @return string A signed JWT
+     *
+     * @uses jsonEncode
+     * @uses urlsafeB64Encode
+     */
+    public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null)
+    {
+        $header = array('typ' => 'JWT', 'alg' => $alg);
+        if ($keyId !== null) {
+            $header['kid'] = $keyId;
+        }
+        if ( isset($head) && is_array($head) ) {
+            $header = array_merge($head, $header);
+        }
+        $segments = array();
+        $segments[] = static::urlsafeB64Encode(static::jsonEncode($header));
+        $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload));
+        $signing_input = implode('.', $segments);
+
+        $signature = static::sign($signing_input, $key, $alg);
+        $segments[] = static::urlsafeB64Encode($signature);
+
+        return implode('.', $segments);
+    }
+
+    /**
+     * Sign a string with a given key and algorithm.
+     *
+     * @param string            $msg    The message to sign
+     * @param string|resource   $key    The secret key
+     * @param string            $alg    The signing algorithm.
+     *                                  Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
+     *
+     * @return string An encrypted message
+     *
+     * @throws DomainException Unsupported algorithm was specified
+     */
+    public static function sign($msg, $key, $alg = 'HS256')
+    {
+        if (empty(static::$supported_algs[$alg])) {
+            throw new DomainException('Algorithm not supported');
+        }
+        list($function, $algorithm) = static::$supported_algs[$alg];
+        switch($function) {
+            case 'hash_hmac':
+                return hash_hmac($algorithm, $msg, $key, true);
+            case 'openssl':
+                $signature = '';
+                $success = openssl_sign($msg, $signature, $key, $algorithm);
+                if (!$success) {
+                    throw new DomainException("OpenSSL unable to sign data");
+                } else {
+                    return $signature;
+                }
+        }
+    }
+
+    /**
+     * Verify a signature with the message, key and method. Not all methods
+     * are symmetric, so we must have a separate verify and sign method.
+     *
+     * @param string            $msg        The original message (header and body)
+     * @param string            $signature  The original signature
+     * @param string|resource   $key        For HS*, a string key works. for RS*, must be a resource of an openssl public key
+     * @param string            $alg        The algorithm
+     *
+     * @return bool
+     *
+     * @throws DomainException Invalid Algorithm or OpenSSL failure
+     */
+    private static function verify($msg, $signature, $key, $alg)
+    {
+        if (empty(static::$supported_algs[$alg])) {
+            throw new DomainException('Algorithm not supported');
+        }
+
+        list($function, $algorithm) = static::$supported_algs[$alg];
+        switch($function) {
+            case 'openssl':
+                $success = openssl_verify($msg, $signature, $key, $algorithm);
+                if ($success === 1) {
+                    return true;
+                } elseif ($success === 0) {
+                    return false;
+                }
+                // returns 1 on success, 0 on failure, -1 on error.
+                throw new DomainException(
+                    'OpenSSL error: ' . openssl_error_string()
+                );
+            case 'hash_hmac':
+            default:
+                $hash = hash_hmac($algorithm, $msg, $key, true);
+                if (function_exists('hash_equals')) {
+                    return hash_equals($signature, $hash);
+                }
+                $len = min(static::safeStrlen($signature), static::safeStrlen($hash));
+
+                $status = 0;
+                for ($i = 0; $i < $len; $i++) {
+                    $status |= (ord($signature[$i]) ^ ord($hash[$i]));
+                }
+                $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash));
+
+                return ($status === 0);
+        }
+    }
+
+    /**
+     * Decode a JSON string into a PHP object.
+     *
+     * @param string $input JSON string
+     *
+     * @return object Object representation of JSON string
+     *
+     * @throws DomainException Provided string was invalid JSON
+     */
+    public static function jsonDecode($input)
+    {
+        if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
+            /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you
+             * to specify that large ints (like Steam Transaction IDs) should be treated as
+             * strings, rather than the PHP default behaviour of converting them to floats.
+             */
+            $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
+        } else {
+            /** Not all servers will support that, however, so for older versions we must
+             * manually detect large ints in the JSON string and quote them (thus converting
+             *them to strings) before decoding, hence the preg_replace() call.
+             */
+            $max_int_length = strlen((string) PHP_INT_MAX) - 1;
+            $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input);
+            $obj = json_decode($json_without_bigints);
+        }
+
+        if (function_exists('json_last_error') && $errno = json_last_error()) {
+            static::handleJsonError($errno);
+        } elseif ($obj === null && $input !== 'null') {
+            throw new DomainException('Null result with non-null input');
+        }
+        return $obj;
+    }
+
+    /**
+     * Encode a PHP object into a JSON string.
+     *
+     * @param object|array $input A PHP object or array
+     *
+     * @return string JSON representation of the PHP object or array
+     *
+     * @throws DomainException Provided object could not be encoded to valid JSON
+     */
+    public static function jsonEncode($input)
+    {
+        $json = json_encode($input);
+        if (function_exists('json_last_error') && $errno = json_last_error()) {
+            static::handleJsonError($errno);
+        } elseif ($json === 'null' && $input !== null) {
+            throw new DomainException('Null result with non-null input');
+        }
+        return $json;
+    }
+
+    /**
+     * Decode a string with URL-safe Base64.
+     *
+     * @param string $input A Base64 encoded string
+     *
+     * @return string A decoded string
+     */
+    public static function urlsafeB64Decode($input)
+    {
+        $remainder = strlen($input) % 4;
+        if ($remainder) {
+            $padlen = 4 - $remainder;
+            $input .= str_repeat('=', $padlen);
+        }
+        return base64_decode(strtr($input, '-_', '+/'));
+    }
+
+    /**
+     * Encode a string with URL-safe Base64.
+     *
+     * @param string $input The string you want encoded
+     *
+     * @return string The base64 encode of what you passed in
+     */
+    public static function urlsafeB64Encode($input)
+    {
+        return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
+    }
+
+    /**
+     * Helper method to create a JSON error.
+     *
+     * @param int $errno An error number from json_last_error()
+     *
+     * @return void
+     */
+    private static function handleJsonError($errno)
+    {
+        $messages = array(
+            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
+            JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
+            JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
+            JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
+            JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
+        );
+        throw new DomainException(
+            isset($messages[$errno])
+            ? $messages[$errno]
+            : 'Unknown JSON error: ' . $errno
+        );
+    }
+
+    /**
+     * Get the number of bytes in cryptographic strings.
+     *
+     * @param string
+     *
+     * @return int
+     */
+    private static function safeStrlen($str)
+    {
+        if (function_exists('mb_strlen')) {
+            return mb_strlen($str, '8bit');
+        }
+        return strlen($str);
+    }
+}
diff --git a/lib/php-jwt/src/SignatureInvalidException.php b/lib/php-jwt/src/SignatureInvalidException.php
new file mode 100644 (file)
index 0000000..27332b2
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+namespace Firebase\JWT;
+
+class SignatureInvalidException extends \UnexpectedValueException
+{
+
+}
index 2c1a9ea..85d046b 100644 (file)
     <license>MIT</license>
     <version>1.1.1</version>
   </library>
+  <library>
+    <location>php-jwt</location>
+    <name>A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to RFC 7519</name>
+    <license>BSD</license>
+    <version>5.0.0</version>
+    <licenseversion>3-Clause</licenseversion>
+  </library>
 </libraries>
index 5b8033d..147b695 100644 (file)
@@ -39,13 +39,44 @@ defined('MOODLE_INTERNAL') || die;
 require_once($CFG->dirroot . '/mod/lti/OAuth.php');
 require_once($CFG->dirroot . '/mod/lti/TrivialStore.php');
 
-function get_oauth_key_from_headers() {
-    $requestheaders = OAuthUtil::get_headers();
+/**
+ *
+ * @param int $typeid LTI type ID.
+ * @param string[] $scopes  Array of scopes which give permission for the current request.
+ *
+ * @return string|int|boolean  The OAuth consumer key, the LTI type ID for the validated bearer token,
+                               true for requests not requiring a scope, otherwise false.
+ */
+function get_oauth_key_from_headers($typeid = null, $scopes = null) {
+    global $DB;
+
+    $now = time();
 
-    if (@substr($requestheaders['Authorization'], 0, 6) == "OAuth ") {
-        $headerparameters = OAuthUtil::split_header($requestheaders['Authorization']);
+    $requestheaders = OAuthUtil::get_headers();
 
-        return format_string($headerparameters['oauth_consumer_key']);
+    if (isset($requestheaders['Authorization'])) {
+        if (substr($requestheaders['Authorization'], 0, 6) == "OAuth ") {
+            $headerparameters = OAuthUtil::split_header($requestheaders['Authorization']);
+
+            return format_string($headerparameters['oauth_consumer_key']);
+        } else if (empty($scopes)) {
+            return true;
+        } else if (substr($requestheaders['Authorization'], 0, 7) == 'Bearer ') {
+            $tokenvalue = trim(substr($requestheaders['Authorization'], 7));
+            $conditions = array('token' => $tokenvalue);
+            if (!empty($typeid)) {
+                $conditions['typeid'] = intval($typeid);
+            }
+            $token = $DB->get_record('lti_access_tokens', $conditions);
+            if ($token) {
+                // Log token access.
+                $DB->set_field('lti_access_tokens', 'lastaccess', $now, array('id' => $token->id));
+                $permittedscopes = json_decode($token->scope);
+                if ((intval($token->validuntil) > $now) && !empty(array_intersect($scopes, $permittedscopes))) {
+                    return intval($token->typeid);
+                }
+            }
+        }
     }
     return false;
 }
@@ -63,7 +94,7 @@ function handle_oauth_body_post($oauthconsumerkey, $oauthconsumersecret, $body,
         }
     }
 
-    if (@substr($requestheaders['Authorization'], 0, 6) == "OAuth ") {
+    if (isset($requestheaders['Authorization']) && (substr($requestheaders['Authorization'], 0, 6) == "OAuth ")) {
         $headerparameters = OAuthUtil::split_header($requestheaders['Authorization']);
         $oauthbodyhash = $headerparameters['oauth_body_hash'];
     }
index 177c7db..cd98833 100644 (file)
Binary files a/mod/lti/amd/build/tool_card_controller.min.js and b/mod/lti/amd/build/tool_card_controller.min.js differ
index 4050f72..763037f 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  * @since      3.1
  */
-define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/tool_type', 'mod_lti/events', 'mod_lti/keys',
+ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'core/modal_factory',
+        'mod_lti/tool_type', 'mod_lti/events', 'mod_lti/keys',
         'core/str'],
-        function($, ajax, notification, templates, toolType, ltiEvents, KEYS, str) {
+        function($, ajax, notification, templates, modalFactory, toolType, ltiEvents, KEYS, str) {
 
     var SELECTORS = {
         DELETE_BUTTON: '.delete',
@@ -631,6 +632,52 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/t
         }
     };
 
+    /**
+     * Sets up the templates for the tool configuration modal on this tool type card.
+     *
+     * @method registerModal
+     * @private
+     * @param {JQuery} element jQuery object representing the tool card.
+     */
+    var registerModal = function(element) {
+        var trigger = $('#' + element.data('uniqid') + '-' + element.data('deploymentid'));
+        var context = {
+            'uniqid': element.data('uniqid'),
+            'platformid': element.data('platformid'),
+            'clientid': element.data('clientid'),
+            'deploymentid': element.data('deploymentid'),
+            'urls': {
+                'publickeyset': element.data('publickeyseturl'),
+                'accesstoken': element.data('accesstokenurl'),
+                'authrequest': element.data('authrequesturl')
+            }
+        };
+        var bodyPromise = templates.render('mod_lti/tool_config_modal_body', context);
+        var mailTo = 'mailto:?subject=' + encodeURIComponent(element.data('mailtosubject')) +
+            '&body=' + encodeURIComponent(element.data('platformidstr')) + ':%20' +
+            encodeURIComponent(element.data('platformid')) + '%0D%0A' +
+            encodeURIComponent(element.data('clientidstr')) + ':%20' +
+            encodeURIComponent(element.data('clientid')) + '%0D%0A' +
+            encodeURIComponent(element.data('deploymentidstr')) + ':%20' +
+            encodeURIComponent(element.data('deploymentid')) + '%0D%0A' +
+            encodeURIComponent(element.data('publickeyseturlstr')) + ':%20' +
+            encodeURIComponent(element.data('publickeyseturl')) + '%0D%0A' +
+            encodeURIComponent(element.data('accesstokenurlstr')) + ':%20' +
+            encodeURIComponent(element.data('accesstokenurl')) + '%0D%0A' +
+            encodeURIComponent(element.data('authrequesturlstr')) + ':%20' +
+            encodeURIComponent(element.data('authrequesturl')) + '%0D%0A';
+        context = {
+            'mailto': mailTo
+        };
+        var footerPromise = templates.render('mod_lti/tool_config_modal_footer', context);
+        modalFactory.create({
+          large: true,
+          title: element.data('modaltitle'),
+          body: bodyPromise,
+          footer: footerPromise,
+        }, trigger);
+    };
+
     return /** @alias module:mod_lti/tool_card_controller */ {
 
         /**
@@ -640,6 +687,7 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/templates', 'mod_lti/t
          */
         init: function(element) {
             registerEventListeners(element);
+            registerModal(element);
         }
     };
 });
diff --git a/mod/lti/auth.php b/mod/lti/auth.php
new file mode 100644 (file)
index 0000000..68c5ccd
--- /dev/null
@@ -0,0 +1,152 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file responds to a login authentication request
+ *
+ * @package    mod_lti
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../config.php');
+require_once($CFG->dirroot . '/mod/lti/locallib.php');
+
+$scope = optional_param('scope', '', PARAM_TEXT);
+$responsetype = optional_param('response_type', '', PARAM_TEXT);
+$clientid = optional_param('client_id', '', PARAM_TEXT);
+$redirecturi = optional_param('redirect_uri', '', PARAM_TEXT);
+$loginhint = optional_param('login_hint', '', PARAM_TEXT);
+$ltimessagehint = optional_param('lti_message_hint', 0, PARAM_INT);
+$state = optional_param('state', '', PARAM_TEXT);
+$responsemode = optional_param('response_mode', '', PARAM_TEXT);
+$nonce = optional_param('nonce', '', PARAM_TEXT);
+$prompt = optional_param('prompt', '', PARAM_TEXT);
+
+$ok = !empty($scope) && !empty($responsetype) && !empty($clientid) &&
+      !empty($redirecturi) && !empty($loginhint) &&
+      !empty($nonce) && !empty($SESSION->lti_message_hint);
+
+if (!$ok) {
+    $error = 'invalid_request';
+}
+if ($ok && ($scope !== 'openid')) {
+    $ok = false;
+    $error = 'invalid_scope';
+}
+if ($ok && ($responsetype !== 'id_token')) {
+    $ok = false;
+    $error = 'unsupported_response_type';
+}
+if ($ok) {
+    list($courseid, $typeid, $id, $titleb64, $textb64) = explode(',', $SESSION->lti_message_hint, 5);
+    $ok = ($id !== $ltimessagehint);
+    if (!$ok) {
+        $error = 'invalid_request';
+    } else {
+        $config = lti_get_type_type_config($typeid);
+        $ok = ($clientid === $config->lti_clientid);
+        if (!$ok) {
+            $error = 'unauthorized_client';
+        }
+    }
+}
+if ($ok && ($loginhint !== $USER->id)) {
+    $ok = false;
+    $error = 'access_denied';
+}
+if ($ok) {
+    $uris = array_map("trim", explode("\n", $config->lti_redirectionuris));
+    $ok = in_array($redirecturi, $uris);
+    if (!$ok) {
+        $error = 'invalid_request';
+        $desc = 'Unregistered redirect_uri ' . $redirecturi;
+    }
+}
+if ($ok) {
+    if (isset($responsemode)) {
+        $ok = ($responsemode === 'form_post');
+        if (!$ok) {
+            $error = 'invalid_request';
+            $desc = 'Invalid response_mode';
+        }
+    } else {
+        $ok = false;
+        $error = 'invalid_request';
+        $desc = 'Missing response_mode';
+    }
+}
+if ($ok && !empty($prompt) && ($prompt !== 'none')) {
+    $ok = false;
+    $error = 'invalid_request';
+    $desc = 'Invalid prompt';
+}
+
+if ($ok) {
+    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+    if ($id) {
+        $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST);
+        $context = context_module::instance($cm->id);
+        require_login($course, true, $cm);
+        require_capability('mod/lti:view', $context);
+        $lti = $DB->get_record('lti', array('id' => $cm->instance), '*', MUST_EXIST);
+        list($endpoint, $params) = lti_get_launch_data($lti, $nonce);
+    } else {
+        require_login($course);
+        $context = context_course::instance($courseid);
+        require_capability('moodle/course:manageactivities', $context);
+        require_capability('mod/lti:addcoursetool', $context);
+        // Set the return URL. We send the launch container along to help us avoid frames-within-frames when the user returns.
+        $returnurlparams = [
+            'course' => $courseid,
+            'id' => $typeid,
+            'sesskey' => sesskey()
+        ];
+        $returnurl = new \moodle_url('/mod/lti/contentitem_return.php', $returnurlparams);
+        // Prepare the request.
+        $title = base64_decode($titleb64);
+        $text = base64_decode($textb64);
+        $request = lti_build_content_item_selection_request($typeid, $course, $returnurl, $title, $text,
+                                                            [], [], false, false, false, false, false, $nonce);
+        $endpoint = $request->url;
+        $params = $request->params;
+    }
+} else {
+    $params['error'] = $error;
+    if (!empty($desc)) {
+        $params['error_description'] = $desc;
+    }
+}
+if (isset($state)) {
+    $params['state'] = $state;
+}
+unset($SESSION->lti_message_hint);
+$r = '<form action="' . $redirecturi . "\" name=\"ltiAuthForm\" id=\"ltiAuthForm\" " .
+     "method=\"post\" enctype=\"application/x-www-form-urlencoded\">\n";
+if (!empty($params)) {
+    foreach ($params as $key => $value) {
+        $key = htmlspecialchars($key);
+        $value = htmlspecialchars($value);
+        $r .= "  <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
+    }
+}
+$r .= "</form>\n";
+$r .= "<script type=\"text/javascript\">\n" .
+    "//<![CDATA[\n" .
+    "document.ltiAuthForm.submit();\n" .
+    "//]]>\n" .
+    "</script>\n";
+echo $r;
index 7b4b457..eae88a7 100644 (file)
@@ -99,6 +99,8 @@ class backup_lti_activity_structure_step extends backup_activity_structure_step
             'state',
             'course',
             'coursevisible',
+            'ltiversion',
+            'clientid',
             'toolproxyid',
             'enabledcapability',
             'parameter',
diff --git a/mod/lti/certs.php b/mod/lti/certs.php
new file mode 100644 (file)
index 0000000..4618aa1
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file returns an array of available public keys
+ *
+ * @package    mod_lti
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_DEBUG_DISPLAY', true);
+define('NO_MOODLE_COOKIES', true);
+
+require_once(__DIR__ . '/../../config.php');
+
+$jwks = array('keys' => array());
+
+$privatekey = get_config('mod_lti', 'privatekey');
+$res = openssl_pkey_get_private($privatekey);
+$details = openssl_pkey_get_details($res);
+
+$jwk = array();
+$jwk['kty'] = 'RSA';
+$jwk['alg'] = 'RS256';
+$jwk['kid'] = get_config('mod_lti', 'kid');
+$jwk['e'] = strtr(base64_encode($details['rsa']['e']), '+/', '-_');
+$jwk['n'] = strtr(base64_encode($details['rsa']['n']), '+/', '-_');
+$jwk['use'] = 'sig';
+
+$jwks['keys'][] = $jwk;
+
+@header('Content-Type: application/json; charset=utf-8');
+
+echo json_encode($jwks, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
index e9ca83e..b6e081e 100644 (file)
@@ -53,11 +53,17 @@ class mod_lti_external extends external_api {
                 'id' => new external_value(PARAM_INT, 'Tool type id'),
                 'name' => new external_value(PARAM_NOTAGS, 'Tool type name'),
                 'description' => new external_value(PARAM_NOTAGS, 'Tool type description'),
+                'platformid' => new external_value(PARAM_TEXT, 'Platform ID'),
+                'clientid' => new external_value(PARAM_TEXT, 'Client ID'),
+                'deploymentid' => new external_value(PARAM_INT, 'Deployment ID'),
                 'urls' => new external_single_structure(
                     array(
                         'icon' => new external_value(PARAM_URL, 'Tool type icon URL'),
                         'edit' => new external_value(PARAM_URL, 'Tool type edit URL'),
                         'course' => new external_value(PARAM_URL, 'Tool type edit URL', VALUE_OPTIONAL),
+                        'publickeyset' => new external_value(PARAM_URL, 'Public Keyset URL'),
+                        'accesstoken' => new external_value(PARAM_URL, 'Access Token URL'),
+                        'authrequest' => new external_value(PARAM_URL, 'Authorisation Request URL'),
                     )
                 ),
                 'state' => new external_single_structure(
index 32a02f8..19fefa5 100644 (file)
@@ -184,13 +184,18 @@ abstract class resource_base {
     public function get_endpoint() {
 
         $this->parse_template();
-        $url = $this->get_service()->get_service_path() . $this->get_template();
+        $template = preg_replace('/[\(\)]/', '', $this->get_template());
+        $url = $this->get_service()->get_service_path() . $template;
         foreach ($this->params as $key => $value) {
             $url = str_replace('{' . $key . '}', $value, $url);
         }
         $toolproxy = $this->get_service()->get_tool_proxy();
         if (!empty($toolproxy)) {
+            $url = str_replace('{config_type}', 'toolproxy', $url);
             $url = str_replace('{tool_proxy_id}', $toolproxy->guid, $url);
+        } else {
+            $url = str_replace('{config_type}', 'tool', $url);
+            $url = str_replace('{tool_proxy_id}', $this->get_service()->get_type()->id, $url);
         }
 
         return $url;
@@ -204,6 +209,52 @@ abstract class resource_base {
      */
     public abstract function execute($response);
 
+    /**
+     * Check to make sure the request is valid.
+     *
+     * @param int $typeid                   The typeid we want to use
+     * @param string $body                  Body of HTTP request message
+     * @param string[] $scopes              Array of scope(s) required for incoming request
+     *
+     * @return boolean
+     */
+    public function check_tool($typeid, $body = null, $scopes = null) {
+
+        $ok = $this->get_service()->check_tool($typeid, $body, $scopes);
+        if ($ok) {
+            if ($this->get_service()->get_tool_proxy()) {
+                $toolproxyjson = $this->get_service()->get_tool_proxy()->toolproxy;
+            }
+            if (!empty($toolproxyjson)) {
+                // Check tool proxy to ensure service being requested is included.
+                $toolproxy = json_decode($toolproxyjson);
+                if (!empty($toolproxy) && isset($toolproxy->security_contract->tool_service)) {
+                    $contexts = lti_get_contexts($toolproxy);
+                    $tpservices = $toolproxy->security_contract->tool_service;
+                    foreach ($tpservices as $service) {
+                        $fqid = lti_get_fqid($contexts, $service->service);
+                        $id = explode('#', $fqid, 2);
+                        if ($this->get_id() === $id[1]) {
+                            $ok = true;
+                            break;
+                        }
+                    }
+                }
+                if (!$ok) {
+                    debugging('Requested service not permitted: ' . $this->get_id(), DEBUG_DEVELOPER);
+                }
+            } else {
+                // Check that the scope required for the service request is included in those granted for the
+                // access token being used.
+                $permittedscopes = $this->get_service()->get_permitted_scopes();
+                $ok = is_null($permittedscopes) || empty($scopes) || !empty(array_intersect($permittedscopes, $scopes));
+            }
+        }
+
+        return $ok;
+
+    }
+
     /**
      * Check to make sure the request is valid.
      *
@@ -211,9 +262,13 @@ abstract class resource_base {
      * @param string $body          Body of HTTP request message
      *
      * @return boolean
+     * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
+     * @see resource_base::check_tool()
      */
     public function check_tool_proxy($toolproxyguid, $body = null) {
 
+        debugging('check_tool_proxy() is deprecated to allow LTI 1 connections to support services. ' .
+                  'Please use resource_base::check_tool() instead.', DEBUG_DEVELOPER);
         $ok = false;
         if ($this->get_service()->check_tool_proxy($toolproxyguid, $body)) {
             $toolproxyjson = $this->get_service()->get_tool_proxy()->toolproxy;
@@ -234,7 +289,7 @@ abstract class resource_base {
                     }
                 }
                 if (!$ok) {
-                    debugging('Requested service not included in tool proxy: ' . $this->get_id(), DEBUG_DEVELOPER);
+                    debugging('Requested service not included in tool proxy: ' . $this->get_id());
                 }
             }
         }
@@ -252,8 +307,12 @@ abstract class resource_base {
      * @param string $body                  Body of HTTP request message
      *
      * @return boolean
+     * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
+     * @see resource_base::check_tool()
      */
     public function check_type($typeid, $contextid, $permissionrequested, $body = null) {
+        debugging('check_type() is deprecated to allow LTI 1 connections to support services. ' .
+                  'Please use resource_base::check_tool() instead.', DEBUG_DEVELOPER);
         $ok = false;
         if ($this->get_service()->check_type($typeid, $contextid, $body)) {
             $neededpermissions = $this->get_permissions($typeid);
@@ -269,7 +328,6 @@ abstract class resource_base {
             }
         }
         return $ok;
-
     }
 
     /**
@@ -277,8 +335,12 @@ abstract class resource_base {
      *
      * @param int $ltitype Type of LTI
      * @return array with the permissions related to this resource by the $ltitype or empty if none.
+     * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
+     * @see resource_base::check_tool()
      */
     public function get_permissions($ltitype) {
+        debugging('get_permissions() is deprecated to allow LTI 1 connections to support services. ' .
+                  'Please use resource_base::check_tool() instead.', DEBUG_DEVELOPER);
         return array();
     }
 
@@ -304,9 +366,10 @@ abstract class resource_base {
 
         if (empty($this->params)) {
             $this->params = array();
-            if (isset($_SERVER['PATH_INFO']) && !empty($_SERVER['PATH_INFO'])) {
+            if (!empty($_SERVER['PATH_INFO'])) {
                 $path = explode('/', $_SERVER['PATH_INFO']);
-                $parts = explode('/', $this->get_template());
+                $template = preg_replace('/\([0-9a-zA-Z_\-,\/]+\)/', '', $this->get_template());
+                $parts = explode('/', $template);
                 for ($i = 0; $i < count($parts); $i++) {
                     if ((substr($parts[$i], 0, 1) == '{') && (substr($parts[$i], -1) == '}')) {
                         $value = '';
index 8f4bcab..a36417e 100644 (file)
@@ -114,12 +114,16 @@ class response {
      * @return string
      */
     public function get_reason() {
-        if (empty($this->reason)) {
-            $this->reason = $this->responsecodes[$this->code];
+        $code = $this->code;
+        if (($code < 200) || ($code >= 600)) {
+            $code = 500;  // Status code must be between 200 and 599.
+        }
+        if (empty($this->reason) && array_key_exists($code, $this->responsecodes)) {
+            $this->reason = $this->responsecodes[$code];
         }
         // Use generic reason for this category (based on first digit) if a specific reason is not defined.
         if (empty($this->reason)) {
-            $this->reason = $this->responsecodes[intval($this->code / 100) * 100];
+            $this->reason = $this->responsecodes[intval($code / 100) * 100];
         }
         return $this->reason;
     }
@@ -222,13 +226,28 @@ class response {
         foreach ($this->additionalheaders as $header) {
             header($header);
         }
-        if (($this->code >= 200) && ($this->code < 300)) {
+        if ((($this->code >= 200) && ($this->code < 300)) || !empty($this->body)) {
             if (!empty($this->contenttype)) {
-                header("Content-Type: {$this->contenttype};charset=UTF-8");
+                header("Content-Type: {$this->contenttype}; charset=utf-8");
             }
             if (!empty($this->body)) {
                 echo $this->body;
             }
+        } else if ($this->code >= 400) {
+            header("Content-Type: application/json; charset=utf-8");
+            $body = new \stdClass();
+            $body->status = $this->code;
+            $body->reason = $this->get_reason();
+            $body->request = new \stdClass();
+            $body->request->method = $_SERVER['REQUEST_METHOD'];
+            $body->request->url = $_SERVER['REQUEST_URI'];
+            if (isset($_SERVER['HTTP_ACCEPT'])) {
+                $body->request->accept = $_SERVER['HTTP_ACCEPT'];
+            }
+            if (isset($_SERVER['CONTENT_TYPE'])) {
+                $body->request->contentType = explode(';', $_SERVER['CONTENT_TYPE'], 2)[0];
+            }
+            echo json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
         }
     }
 
index 26a89c1..5ad7a57 100644 (file)
@@ -49,6 +49,8 @@ abstract class service_base {
 
     /** Label representing an LTI 2 message type */
     const LTI_VERSION2P0 = 'LTI-2p0';
+    /** Service enabled */
+    const SERVICE_ENABLED = 1;
 
     /** @var string ID for the service. */
     protected $id;
@@ -58,6 +60,10 @@ abstract class service_base {
     protected $unsigned;
     /** @var stdClass Tool proxy object for the current service request. */
     private $toolproxy;
+    /** @var stdClass LTI type object for the current service request. */
+    private $type;
+    /** @var array LTI type config array for the current service request. */
+    private $typeconfig;
     /** @var array Instances of the resources associated with this service. */
     protected $resources;
 
@@ -71,6 +77,8 @@ abstract class service_base {
         $this->name = null;
         $this->unsigned = false;
         $this->toolproxy = null;
+        $this->type = null;
+        $this->typeconfig = null;
         $this->resources = null;
 
     }
@@ -86,6 +94,17 @@ abstract class service_base {
 
     }
 
+    /**
+     * Get the service compoent ID.
+     *
+     * @return string
+     */
+    public function get_component_id() {
+
+        return 'ltiservice_' . $this->id;
+
+    }
+
     /**
      * Get the service name.
      *
@@ -132,6 +151,54 @@ abstract class service_base {
 
     }
 
+    /**
+     * Get the type object.
+     *
+     * @return stdClass
+     */
+    public function get_type() {
+
+        return $this->type;
+
+    }
+
+    /**
+     * Set the LTI type object.
+     *
+     * @param object $type The LTI type for this service request
+     *
+     * @var stdClass
+     */
+    public function set_type($type) {
+
+        $this->type = $type;
+
+    }
+
+    /**
+     * Get the type config array.
+     *
+     * @return array|null
+     */
+    public function get_typeconfig() {
+
+        return $this->typeconfig;
+
+    }
+
+    /**
+     * Set the LTI type config object.
+     *
+     * @param array $typeconfig The LTI type config for this service request
+     *
+     * @var array
+     */
+    public function set_typeconfig($typeconfig) {
+
+        $this->typeconfig = $typeconfig;
+
+    }
+
     /**
      * Get the resources for this service.
      *
@@ -139,6 +206,17 @@ abstract class service_base {
      */
     abstract public function get_resources();
 
+    /**
+     * Get the scope(s) permitted for this service.
+     *
+     * A null value indicates that no scopes are required to access the service.
+     *
+     * @return array|null
+     */
+    public function get_permitted_scopes() {
+        return null;
+    }
+
     /**
      * Returns the configuration options for this service.
      *
@@ -152,8 +230,10 @@ abstract class service_base {
      * Return an array with the names of the parameters that the service will be saving in the configuration
      *
      * @return array  Names list of the parameters that the service will be saving in the configuration
+     * @deprecated since Moodle 3.7 - please do not use this function any more.
      */
     public function get_configuration_parameter_names() {
+        debugging('get_configuration_parameter_names() has been deprecated.', DEBUG_DEVELOPER);
         return array();
     }
 
@@ -244,6 +324,59 @@ abstract class service_base {
 
     }
 
+    /**
+     * Check that the request has been properly signed and is permitted.
+     *
+     * @param string $typeid    LTI type ID
+     * @param string $body      Request body (null if none)
+     * @param string[] $scopes  Array of required scope(s) for incoming request
+     *
+     * @return boolean
+     */
+    public function check_tool($typeid, $body = null, $scopes = null) {
+
+        $ok = true;
+        $toolproxy = null;
+        $consumerkey = lti\get_oauth_key_from_headers($typeid, $scopes);
+        if ($consumerkey === false) {
+            $ok = $this->is_unsigned();
+        } else {
+            if (empty($typeid) && is_int($consumerkey)) {
+                $typeid = $consumerkey;
+            }
+            if (!empty($typeid)) {
+                $this->type = lti_get_type($typeid);
+                $this->typeconfig = lti_get_type_config($typeid);
+                $ok = !empty($this->type->id);
+                if ($ok && !empty($this->type->toolproxyid)) {
+                    $this->toolproxy = lti_get_tool_proxy($this->type->toolproxyid);
+                }
+            } else {
+                $toolproxy = lti_get_tool_proxy_from_guid($consumerkey);
+                if ($toolproxy !== false) {
+                    $this->toolproxy = $toolproxy;
+                }
+            }
+        }
+        if ($ok && is_string($consumerkey)) {
+            if (!empty($this->toolproxy)) {
+                $key = $this->toolproxy->guid;
+                $secret = $this->toolproxy->secret;
+            } else {
+                $key = $this->typeconfig['resourcekey'];
+                $secret = $this->typeconfig['password'];
+            }
+            if (!$this->is_unsigned() && ($key == $consumerkey)) {
+                $ok = $this->check_signature($key, $secret, $body);
+            } else {
+                $ok = $this->is_unsigned();
+            }
+        }
+
+        return $ok;
+
+    }
+
     /**
      * Check that the request has been properly signed.
      *
@@ -251,9 +384,13 @@ abstract class service_base {
      * @param string $body           Request body (null if none)
      *
      * @return boolean
+     * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
+     * @see service_base::check_tool()
      */
     public function check_tool_proxy($toolproxyguid, $body = null) {
 
+        debugging('check_tool_proxy() is deprecated to allow LTI 1 connections to support services. ' .
+                  'Please use service_base::check_tool() instead.', DEBUG_DEVELOPER);
         $ok = false;
         $toolproxy = null;
         $consumerkey = lti\get_oauth_key_from_headers();
@@ -274,7 +411,9 @@ abstract class service_base {
         if ($ok) {
             $this->toolproxy = $toolproxy;
         }
+
         return $ok;
+
     }
 
     /**
@@ -285,8 +424,12 @@ abstract class service_base {
      * @param string $body Request body (null if none)
      *
      * @return bool
+     * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
+     * @see service_base::check_tool()
      */
     public function check_type($typeid, $courseid, $body = null) {
+        debugging('check_type() is deprecated to allow LTI 1 connections to support services. ' .
+                  'Please use service_base::check_tool() instead.', DEBUG_DEVELOPER);
         $ok = false;
         $tool = null;
         $consumerkey = lti\get_oauth_key_from_headers();
index abd852f..65cb4c7 100644 (file)
@@ -23,6 +23,8 @@
  */
 namespace mod_lti\output;
 
+defined('MOODLE_INTERNAL') || die;
+
 require_once($CFG->dirroot.'/mod/lti/locallib.php');
 
 use moodle_url;
diff --git a/mod/lti/classes/task/clean_access_tokens.php b/mod/lti/classes/task/clean_access_tokens.php
new file mode 100644 (file)
index 0000000..4f27413
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A scheduled task for lti module.
+ *
+ * @package    mod_lti
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_lti\task;
+
+use core\task\scheduled_task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class containing the scheduled task for lti module.
+ *
+ * @package    mod_lti
+ * @copyright  2018 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class clean_access_tokens extends scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('cleanaccesstokens', 'mod_lti');
+    }
+
+    /**
+     * Run lti cron.
+     */
+    public function execute() {
+        global $DB;
+
+        $DB->delete_records_select('lti_access_tokens', 'validuntil < ?', [time()]);
+    }
+}
index 0b6b872..2528cba 100644 (file)
@@ -32,6 +32,16 @@ $courseid = required_param('course', PARAM_INT);
 $title = optional_param('title', '', PARAM_TEXT);
 $text = optional_param('text', '', PARAM_RAW);
 
+$config = lti_get_type_type_config($id);
+if ($config->lti_ltiversion === LTI_VERSION_1P3) {
+    if (!isset($SESSION->lti_initiatelogin_status)) {
+        echo lti_initiate_login($courseid, 0, null, $config, 'ContentItemSelectionRequest', $title, $text);
+        exit;
+    } else {
+        unset($SESSION->lti_initiatelogin_status);
+    }
+}
+
 // Check access and capabilities.
 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
 require_login($course);
index 53cc5de..173e26e 100644 (file)
@@ -28,12 +28,26 @@ require_once($CFG->dirroot . '/mod/lti/locallib.php');
 
 $id = required_param('id', PARAM_INT);
 $courseid = required_param('course', PARAM_INT);
-$messagetype = required_param('lti_message_type', PARAM_TEXT);
-$version = required_param('lti_version', PARAM_TEXT);
-$consumerkey = required_param('oauth_consumer_key', PARAM_RAW);
-$items = optional_param('content_items', '', PARAM_RAW);
-$errormsg = optional_param('lti_errormsg', '', PARAM_TEXT);
-$msg = optional_param('lti_msg', '', PARAM_TEXT);
+
+$jwt = optional_param('JWT', '', PARAM_RAW);
+
+if (!empty($jwt)) {
+    $params = lti_convert_from_jwt($id, $jwt);
+    $consumerkey = $params['oauth_consumer_key'] ?? '';
+    $messagetype = $params['lti_message_type'] ?? '';
+    $version = $params['lti_version'] ?? '';
+    $items = $params['content_items'] ?? '';
+    $errormsg = $params['lti_errormsg'] ?? '';
+    $msg = $params['lti_msg'] ?? '';
+} else {
+    $consumerkey = required_param('oauth_consumer_key', PARAM_RAW);
+    $messagetype = required_param('lti_message_type', PARAM_TEXT);
+    $version = required_param('lti_version', PARAM_TEXT);
+    $items = optional_param('content_items', '', PARAM_RAW);
+    $errormsg = optional_param('lti_errormsg', '', PARAM_TEXT);
+    $msg = optional_param('lti_msg', '', PARAM_TEXT);
+    lti_verify_oauth_signature($id, $consumerkey);
+}
 
 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
 require_login($course);
diff --git a/mod/lti/db/install.php b/mod/lti/db/install.php
new file mode 100644 (file)
index 0000000..9adf9db
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Post installation and migration code.
+ *
+ * @package    mod_lti
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Stub for database installation.
+ */
+function xmldb_lti_install() {
+    global $CFG;
+
+    // Add platform ID configuration setting.
+    set_config('platformid', $CFG->wwwroot, 'mod_lti');
+
+    // Create the private key.
+    $kid = bin2hex(openssl_random_pseudo_bytes(10));
+    set_config('kid', $kid, 'mod_lti');
+    $config = array(
+        "digest_alg" => "sha256",
+        "private_key_bits" => 2048,
+        "private_key_type" => OPENSSL_KEYTYPE_RSA,
+    );
+    $res = openssl_pkey_new($config);
+    openssl_pkey_export($res, $privatekey);
+    set_config('privatekey', $privatekey, 'mod_lti');
+}
index 040f32e..2c5af4c 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/lti/db" VERSION="20151204" COMMENT="XMLDB file for Moodle mod/lti"
+<XMLDB PATH="mod/lti/db" VERSION="20190303" COMMENT="XMLDB file for Moodle mod/lti"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
 >
@@ -72,6 +72,8 @@
         <FIELD NAME="state" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="2" SEQUENCE="false" COMMENT="Active = 1, Pending = 2, Rejected = 3"/>
         <FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="coursevisible" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="ltiversion" TYPE="char" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="clientid" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="toolproxyid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Primary key of related tool proxy (null for LTI 1 tools)"/>
         <FIELD NAME="enabledcapability" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Enabled capabilities, one per line (null for LTI 1 tools)"/>
         <FIELD NAME="parameter" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Launch parameters, one per line (null for LTI 1 tools)"/>
@@ -88,6 +90,7 @@
       <INDEXES>
         <INDEX NAME="course" UNIQUE="false" FIELDS="course"/>
         <INDEX NAME="tooldomain" UNIQUE="false" FIELDS="tooldomain"/>
+        <INDEX NAME="clientid" UNIQUE="true" FIELDS="clientid"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="lti_types_config" COMMENT="Basic LTI types configuration">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="toolproxyid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Primary key of related tool proxy"/>
+        <FIELD NAME="typeid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Primary key of course (null for system-wide settings)"/>
         <FIELD NAME="coursemoduleid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Primary key of course module - tool link added to course (null for system-wide and context-wide settings)"/>
         <FIELD NAME="settings" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Setting values as JSON"/>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <KEY NAME="toolproxy" TYPE="foreign" FIELDS="toolproxyid" REFTABLE="lti_tool_proxies" REFFIELDS="id" COMMENT="The tool proxy to which the setting relates"/>
+        <KEY NAME="typeid" TYPE="foreign" FIELDS="typeid" REFTABLE="lti_types" REFFIELDS="id"/>
         <KEY NAME="course" TYPE="foreign" FIELDS="course" REFTABLE="course" REFFIELDS="id" COMMENT="The course to which the setting relates"/>
         <KEY NAME="coursemodule" TYPE="foreign" FIELDS="coursemoduleid" REFTABLE="lti" REFFIELDS="id" COMMENT="The module instance to which the setting relates"/>
       </KEYS>
         <INDEX NAME="ltiid" UNIQUE="false" FIELDS="ltiid"/>
       </INDEXES>
     </TABLE>
+    <TABLE NAME="lti_access_tokens" COMMENT="Security tokens for accessing of LTI services">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="typeid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Basic LTI type id"/>
+        <FIELD NAME="scope" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Scope values as JSON array"/>
+        <FIELD NAME="token" TYPE="char" LENGTH="128" NOTNULL="true" SEQUENCE="false" COMMENT="security token, aka private access key"/>
+        <FIELD NAME="validuntil" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="timestamp - valid until data"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="created timestamp"/>
+        <FIELD NAME="lastaccess" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="last access timestamp"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="typeid" TYPE="foreign" FIELDS="typeid" REFTABLE="lti_types" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="token" UNIQUE="true" FIELDS="token"/>
+      </INDEXES>
+    </TABLE>
   </TABLES>
 </XMLDB>
\ No newline at end of file
index c90397a..32c41e8 100644 (file)
@@ -24,6 +24,8 @@
  * @since      Moodle 3.0
  */
 
+defined('MOODLE_INTERNAL') || die;
+
 $functions = array(
 
     'mod_lti_get_tool_launch_data' => array(
diff --git a/mod/lti/db/tasks.php b/mod/lti/db/tasks.php
new file mode 100644 (file)
index 0000000..6a41203
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file defines tasks performed by the plugin.
+ *
+ * @package    mod_lti
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// List of tasks.
+$tasks = array(
+    array(
+        'classname' => 'mod_lti\task\clean_access_tokens',
+        'blocking' => 0,
+        'minute' => 'R',
+        'hour' => 'R',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
+    )
+);
index f77a661..135bc29 100644 (file)
@@ -61,6 +61,8 @@
 function xmldb_lti_upgrade($oldversion) {
     global $CFG, $DB;
 
+    $dbman = $DB->get_manager();
+
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
@@ -125,6 +127,104 @@ function xmldb_lti_upgrade($oldversion) {
     // Automatically generated Moodle v3.6.0 release upgrade line.
     // Put any upgrade step following this.
 
-    return true;
+    if ($oldversion < 2019031300) {
+        // Define table lti_access_tokens to be updated.
+        $table = new xmldb_table('lti_types');
+
+        // Define field ltiversion to be added to lti_types.
+        $field = new xmldb_field('ltiversion', XMLDB_TYPE_CHAR, 10, null, XMLDB_NOTNULL, null, null, 'coursevisible');
+
+        // Conditionally launch add field ltiversion.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+            $DB->set_field_select('lti_types', 'ltiversion', 'LTI-1p0', 'toolproxyid IS NULL');
+            $DB->set_field_select('lti_types', 'ltiversion', 'LTI-2p0', 'toolproxyid IS NOT NULL');
+        }
+
+        // Define field clientid to be added to lti_types.
+        $field = new xmldb_field('clientid', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'ltiversion');
+
+        // Conditionally launch add field clientid.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Define index clientid (unique) to be added to lti_types.
+        $index = new xmldb_index('clientid', XMLDB_INDEX_UNIQUE, array('clientid'));
+
+        // Conditionally launch add index clientid.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Add platform ID configuration setting.
+        set_config('platformid', $CFG->wwwroot, 'mod_lti');
+
+        // Create the private key.
+        $kid = bin2hex(openssl_random_pseudo_bytes(10));
+        set_config('kid', $kid, 'mod_lti');
+        $config = array(
+            "digest_alg" => "sha256",
+            "private_key_bits" => 2048,
+            "private_key_type" => OPENSSL_KEYTYPE_RSA,
+        );
+        $res = openssl_pkey_new($config);
+        openssl_pkey_export($res, $privatekey);
+        set_config('privatekey', $privatekey, 'mod_lti');
+
+        // Lti savepoint reached.
+        upgrade_mod_savepoint(true, 2019031300, 'lti');
+    }
 
+    if ($oldversion < 2019031301) {
+        // Define table lti_access_tokens to be created.
+        $table = new xmldb_table('lti_access_tokens');
+
+        // Adding fields to table lti_access_tokens.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('typeid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('scope', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
+        $table->add_field('token', XMLDB_TYPE_CHAR, '128', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('validuntil', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('lastaccess', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+
+        // Adding keys to table lti_access_tokens.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('typeid', XMLDB_KEY_FOREIGN, array('typeid'), 'lti_types', array('id'));
+
+        // Add an index.
+        $table->add_index('token', XMLDB_INDEX_UNIQUE, array('token'));
+
+        // Conditionally launch create table for lti_access_tokens.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Lti savepoint reached.
+        upgrade_mod_savepoint(true, 2019031301, 'lti');
+    }
+
+    if ($oldversion < 2019031302) {
+        // Define field typeid to be added to lti_tool_settings.
+        $table = new xmldb_table('lti_tool_settings');
+        $field = new xmldb_field('typeid', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'toolproxyid');
+
+        // Conditionally launch add field typeid.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Define key typeid (foreign) to be added to lti_tool_settings.
+        $table = new xmldb_table('lti_tool_settings');
+        $key = new xmldb_key('typeid', XMLDB_KEY_FOREIGN, ['typeid'], 'lti_types', ['id']);
+
+        // Launch add key typeid.
+        $dbman->add_key($table, $key);
+
+        // Lti savepoint reached.
+        upgrade_mod_savepoint(true, 2019031302, 'lti');
+    }
+
+    return true;
 }
index 5e618de..8a3fe0b 100644 (file)
@@ -68,11 +68,13 @@ class mod_lti_edit_types_form extends moodleform {
      * Define this form.
      */
     public function definition() {
-        global $CFG;
+        global $CFG, $PAGE;
 
         $mform    =& $this->_form;
 
-        $istool = $this->_customdata && $this->_customdata->istool;
+        $istool = $this->_customdata && isset($this->_customdata->istool) && $this->_customdata->istool;
+        $typeid = $this->_customdata->id ?? '';
+        $clientid = $this->_customdata->clientid ?? '';
 
         // Add basiclti elements.
         $mform->addElement('header', 'setup', get_string('tool_settings', 'lti'));
@@ -96,14 +98,54 @@ class mod_lti_edit_types_form extends moodleform {
         }
 
         if (!$istool) {
+            $options = array(
+                LTI_VERSION_1 => get_string('oauthsecurity', 'lti'),
+                LTI_VERSION_1P3 => get_string('jwtsecurity', 'lti'),
+            );
+            $mform->addElement('select', 'lti_ltiversion', get_string('ltiversion', 'lti'), $options);
+            $mform->setType('lti_ltiversion', PARAM_TEXT);
+            $mform->addHelpButton('lti_ltiversion', 'ltiversion', 'lti');
+            $mform->setDefault('lti_ltiversion', LTI_VERSION_1);
+
             $mform->addElement('text', 'lti_resourcekey', get_string('resourcekey_admin', 'lti'));
             $mform->setType('lti_resourcekey', PARAM_TEXT);
             $mform->addHelpButton('lti_resourcekey', 'resourcekey_admin', 'lti');
+            $mform->hideIf('lti_resourcekey', 'lti_ltiversion', 'eq', LTI_VERSION_1P3);
             $mform->setForceLtr('lti_resourcekey');
 
             $mform->addElement('passwordunmask', 'lti_password', get_string('password_admin', 'lti'));
             $mform->setType('lti_password', PARAM_TEXT);
             $mform->addHelpButton('lti_password', 'password_admin', 'lti');
+            $mform->hideIf('lti_password', 'lti_ltiversion', 'eq', LTI_VERSION_1P3);
+
+            if (!empty($typeid)) {
+                $mform->addElement('text', 'lti_clientid_disabled', get_string('clientidadmin', 'lti'));
+                $mform->setType('lti_clientid_disabled', PARAM_TEXT);
+                $mform->addHelpButton('lti_clientid_disabled', 'clientidadmin', 'lti');
+                $mform->hideIf('lti_clientid_disabled', 'lti_ltiversion', 'neq', LTI_VERSION_1P3);
+                $mform->disabledIf('lti_clientid_disabled', null);
+                $mform->setForceLtr('lti_clientid_disabled');
+                $mform->addElement('hidden', 'lti_clientid');
+                $mform->setType('lti_clientid', PARAM_TEXT);
+            }
+
+            $mform->addElement('textarea', 'lti_publickey', get_string('publickey', 'lti'), array('rows' => 8, 'cols' => 60));
+            $mform->setType('lti_publickey', PARAM_TEXT);
+            $mform->addHelpButton('lti_publickey', 'publickey', 'lti');
+            $mform->hideIf('lti_publickey', 'lti_ltiversion', 'neq', LTI_VERSION_1P3);
+            $mform->setForceLtr('lti_publickey');
+
+            $mform->addElement('text', 'lti_initiatelogin', get_string('initiatelogin', 'lti'), array('size' => '64'));
+            $mform->setType('lti_initiatelogin', PARAM_URL);
+            $mform->addHelpButton('lti_initiatelogin', 'initiatelogin', 'lti');
+            $mform->hideIf('lti_initiatelogin', 'lti_ltiversion', 'neq', LTI_VERSION_1P3);
+
+            $mform->addElement('textarea', 'lti_redirectionuris', get_string('redirectionuris', 'lti'),
+                array('rows' => 3, 'cols' => 60));
+            $mform->setType('lti_redirectionuris', PARAM_TEXT);
+            $mform->addHelpButton('lti_redirectionuris', 'redirectionuris', 'lti');
+            $mform->hideIf('lti_redirectionuris', 'lti_ltiversion', 'neq', LTI_VERSION_1P3);
+            $mform->setForceLtr('lti_redirectionuris');
         }
 
         if ($istool) {
index 2ea72e6..cca3fb3 100644 (file)
@@ -64,12 +64,18 @@ if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) {
 
 echo $OUTPUT->header();
 
-$form = new mod_lti_edit_types_form();
+if ($action == 'edit') {
+    $type = lti_get_type_type_config($typeid);
+} else {
+    $type = new stdClass();
+    $type->lti_clientid = null;
+}
+
+$form = new mod_lti_edit_types_form($url, (object)array('id' => $typeid, 'clientid' => $type->lti_clientid));
 
 // If the user just opened an add or edit form.
 if ($action == 'add' || $action == 'edit') {
     if ($action == 'edit') {
-        $type = lti_get_type_type_config($typeid);
         $form->set_data($type);
     }
     echo $OUTPUT->heading(get_string('toolsetup', 'lti'));
index 17ac2ad..9749e60 100644 (file)
@@ -93,7 +93,12 @@ $string['cannot_edit'] = 'You may not edit this tool configuration.';
 $string['capabilities'] = 'Capabilities';
 $string['capabilitiesrequired'] = 'This tool requires access to the following data in order to activate:';
 $string['capabilities_help'] = 'Select those capabilities which you wish to offer to the tool provider.  More than one capability can be selected.';
+$string['cleanaccesstokens'] = 'External tool removal of expired access tokens';
 $string['click_to_continue'] = '<a href="{$a->link}" target="_top">Click to continue</a>';
+$string['clientidadmin'] = 'Client ID';
+$string['clientidadmin_help'] = 'The client ID can be thought of as a unique value used to identify a tool.
+It is created automatically for each tool which uses the JWT security profile introduced in LTI 1.3 and should
+be part of the details passed to the provider of the tool so that they can configure the connection at their end.';
 $string['comment'] = 'Comment';
 $string['configpassword'] = 'Default remote tool password';
 $string['configpreferheight'] = 'Default preferred height';
@@ -217,7 +222,10 @@ $string['indicator:cognitivedepth'] = 'LTI cognitive';
 $string['indicator:cognitivedepth_help'] = 'This indicator is based on the cognitive depth reached by the student in an LTI activity.';
 $string['indicator:socialbreadth'] = 'LTI social';
 $string['indicator:socialbreadth_help'] = 'This indicator is based on the social breadth reached by the student in an LTI activity.';
+$string['initiatelogin'] = 'Initiate Login URL';
+$string['initiatelogin_help'] = 'The tool URL to which requests for initiating a login are to be sent.  This URL is required before a message can be successfully sent to the tool.';
 $string['invalidid'] = 'LTI ID was incorrect';
+$string['jwtsecurity'] = 'LTI 1.3';
 $string['launch_in_moodle'] = 'Launch tool in Moodle';
 $string['launch_in_popup'] = 'Launch tool in a pop-up';
 $string['launch_url'] = 'Tool URL';
@@ -262,6 +270,8 @@ $string['lti_launch_error_unsigned_help'] = '<p>This error may be a result of a
 $string['lti_tool_request_added'] = 'Tool configuration request successfully submitted. You may need to contact an administrator to complete the tool configuration.';
 $string['lti_tool_request_existing'] = 'A tool configuration for the tool domain has already been submitted.';
 $string['ltiunknownserviceapicall'] = 'LTI unknown service API call.';
+$string['ltiversion'] = 'LTI version';
+$string['ltiversion_help'] = 'The version of LTI being used for signing messages and service requests: LTI 1.0/1.1 and LTI 2.0 use the OAuth 1.0A security profile; LTI 1.3.0 uses JWTs.';
 $string['main_admin'] = 'General help';
 $string['main_admin_help'] = 'External tools allow Moodle users to seamlessly interact with learning resources hosted remotely. Through a special
 launch protocol, the remote tool will have access to some general information about the launching user. For example,
@@ -316,6 +326,7 @@ $string['noprofileservice'] = 'Profile service not found';
 $string['noservers'] = 'No servers found';
 $string['notypes'] = 'There are currently no LTI tools set up in Moodle. Click the Install link above to add some.';
 $string['noviewusers'] = 'No users were found with permissions to use this tool';
+$string['oauthsecurity'] = 'LTI 1.0/1.1';
 $string['optionalsettings'] = 'Optional settings';
 $string['organization'] = 'Organization details';
 $string['organizationdescr'] = 'Organization description';
@@ -382,9 +393,13 @@ $string['privacy:metadata:timemodified'] = 'The time when the record was modifie
 $string['privacy:metadata:userid'] = 'The ID of the user accessing the LTI Consumer';
 $string['privacy:metadata:useridnumber'] = 'The ID number of the user accessing the LTI Consumer';
 $string['privacy:metadata:username'] = 'The username of the user accessing the LTI Consumer';
+$string['publickey'] = 'Public key';
+$string['publickey_help'] = 'The public key (in PEM format) provided by the tool to allow signatures of incoming messages and service requests to be verified.';
 $string['quickgrade'] = 'Allow quick grading';
 $string['quickgrade_help'] = 'If enabled, multiple tools can be graded on one page. Add grades and comments then click the "Save all my feedback" button to save all changes for that page.';
 $string['redirect'] = 'You will be redirected in few seconds. If you are not, press the button.';
+$string['redirectionuris'] = 'Redirection URI(s)';
+$string['redirectionuris_help'] = 'A list of URIs (one per line) which the tool uses when making authorisation requests.  At least one must be registered before a message can be successfully sent to the tool.';
 $string['register'] = 'Register';
 $string['register_warning'] = 'The registration page seems to be taking a while to open. If it does not appear, check that you entered the correct URL in the configuration settings. If Moodle is using https, ensure the tool you are configuring supports https and you are using https in the URL.';
 $string['registertype'] = 'Configure a new external tool registration';
@@ -486,6 +501,16 @@ $string['tooldescription'] = 'Tool description';
 $string['tooldescription_help'] = 'The description of the tool that will be displayed to teachers in the activity list.
 
 This should describe what the tool is for and what it does and any additional information the teacher may need to know.';
+$string['tooldetailsaccesstokenurl'] = 'Access Token URL';
+$string['tooldetailsauthrequesturl'] = 'Authentication request URL';
+$string['tooldetailsclientid'] = 'Client ID';
+$string['tooldetailsdeploymentid'] = 'Deployment ID';
+$string['tooldetailsmailtosubject'] = 'LTI Tool Configuration';
+$string['tooldetailsmodalemail'] = 'Email';
+$string['tooldetailsmodallink'] = 'View Configuration Details';
+$string['tooldetailsmodaltitle'] = 'Tool Configuration Details';
+$string['tooldetailsplatformid'] = 'Platform ID';
+$string['tooldetailspublickeyseturl'] = 'Public Keyset URL';
 $string['toolisbeingused'] = 'This tool is being used {$a} times';
 $string['toolisnotbeingused'] = 'This tool has not yet been used';
 $string['toolproxy'] = 'External tool registrations';
index ca27416..6da25d5 100644 (file)
@@ -55,6 +55,20 @@ $triggerview = optional_param('triggerview', 1, PARAM_BOOL);
 
 $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST);
 $lti = $DB->get_record('lti', array('id' => $cm->instance), '*', MUST_EXIST);
+
+$typeid = $lti->typeid;
+if ($typeid) {
+    $config = lti_get_type_type_config($typeid);
+    if ($config->lti_ltiversion === LTI_VERSION_1P3) {
+        if (!isset($SESSION->lti_initiatelogin_status)) {
+            echo lti_initiate_login($cm->course, $id, $lti, $config);
+            exit;
+        } else {
+            unset($SESSION->lti_initiatelogin_status);
+        }
+    }
+}
+
 $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
 
 $context = context_module::instance($cm->id);
index 295581b..3bd9a2a 100644 (file)
@@ -52,12 +52,14 @@ defined('MOODLE_INTERNAL') || die;
 
 // TODO: Switch to core oauthlib once implemented - MDL-30149.
 use moodle\mod\lti as lti;
+use Firebase\JWT\JWT as JWT;
 
 global $CFG;
 require_once($CFG->dirroot.'/mod/lti/OAuth.php');
 require_once($CFG->libdir.'/weblib.php');
 require_once($CFG->dirroot . '/course/modlib.php');
 require_once($CFG->dirroot . '/mod/lti/TrivialStore.php');
+require_once($CFG->libdir . '/php-jwt/src/JWT.php');
 
 define('LTI_URL_DOMAIN_REGEX', '/(?:https?:\/\/)?(?:www\.)?([^\/]+)(?:\/|$)/i');
 
@@ -88,32 +90,438 @@ define('LTI_COURSEVISIBLE_ACTIVITYCHOOSER', 2);
 
 define('LTI_VERSION_1', 'LTI-1p0');
 define('LTI_VERSION_2', 'LTI-2p0');
+define('LTI_VERSION_1P3', '1.3.0');
+
+define('LTI_ACCESS_TOKEN_LIFE', 3600);
+
+// Standard prefix for JWT claims.
+define('LTI_JWT_CLAIM_PREFIX', 'https://purl.imsglobal.org/spec/lti');
+
+/**
+ * Return the mapping for standard message types to JWT message_type claim.
+ *
+ * @return array
+ */
+function lti_get_jwt_message_type_mapping() {
+    return array(
+        'basic-lti-launch-request' => 'LtiResourceLinkRequest',
+        'ContentItemSelectionRequest' => 'LtiDeepLinkingRequest',
+        'LtiDeepLinkingResponse' => 'ContentItemSelection',
+    );
+}
+
+/**
+ * Return the mapping for standard message parameters to JWT claim.
+ *
+ * @return array
+ */
+function lti_get_jwt_claim_mapping() {
+    return array(
+        'accept_copy_advice' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'accept_copy_advice',
+            'isarray' => false
+        ],
+        'accept_media_types' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'accept_media_types',
+            'isarray' => true
+        ],
+        'accept_multiple' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'accept_multiple',
+            'isarray' => false
+        ],
+        'accept_presentation_document_targets' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'accept_presentation_document_targets',
+            'isarray' => true
+        ],
+        'accept_types' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'accept_types',
+            'isarray' => true
+        ],
+        'accept_unsigned' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'accept_unsigned',
+            'isarray' => false
+        ],
+        'auto_create' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'auto_create',
+            'isarray' => false
+        ],
+        'can_confirm' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'can_confirm',
+            'isarray' => false
+        ],
+        'content_item_return_url' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'deep_link_return_url',
+            'isarray' => false
+        ],
+        'content_items' => [
+            'suffix' => 'dl',
+            'group' => '',
+            'claim' => 'content_items',
+            'isarray' => true
+        ],
+        'data' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'data',
+            'isarray' => false
+        ],
+        'text' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'text',
+            'isarray' => false
+        ],
+        'title' => [
+            'suffix' => 'dl',
+            'group' => 'deep_linking_settings',
+            'claim' => 'title',
+            'isarray' => false
+        ],
+        'lti_msg' => [
+            'suffix' => 'dl',
+            'group' => '',
+            'claim' => 'msg',
+            'isarray' => false
+        ],
+        'lti_log' => [
+            'suffix' => 'dl',
+            'group' => '',
+            'claim' => 'log',
+            'isarray' => false
+        ],
+        'lti_errormsg' => [
+            'suffix' => 'dl',
+            'group' => '',
+            'claim' => 'errormsg',
+            'isarray' => false
+        ],
+        'lti_errorlog' => [
+            'suffix' => 'dl',
+            'group' => '',
+            'claim' => 'errorlog',
+            'isarray' => false
+        ],
+        'context_id' => [
+            'suffix' => '',
+            'group' => 'context',
+            'claim' => 'id',
+            'isarray' => false
+        ],
+        'context_label' => [
+            'suffix' => '',
+            'group' => 'context',
+            'claim' => 'label',
+            'isarray' => false
+        ],
+        'context_title' => [
+            'suffix' => '',
+            'group' => 'context',
+            'claim' => 'title',
+            'isarray' => false
+        ],
+        'context_type' => [
+            'suffix' => '',
+            'group' => 'context',
+            'claim' => 'type',
+            'isarray' => true
+        ],
+        'lis_course_offering_sourcedid' => [
+            'suffix' => '',
+            'group' => 'lis',
+            'claim' => 'course_offering_sourcedid',
+            'isarray' => false
+        ],
+        'lis_course_section_sourcedid' => [
+            'suffix' => '',
+            'group' => 'lis',
+            'claim' => 'course_section_sourcedid',
+            'isarray' => false
+        ],
+        'launch_presentation_css_url' => [
+            'suffix' => '',
+            'group' => 'launch_presentation',
+            'claim' => 'css_url',
+            'isarray' => false
+        ],
+        'launch_presentation_document_target' => [
+            'suffix' => '',
+            'group' => 'launch_presentation',
+            'claim' => 'document_target',
+            'isarray' => false
+        ],
+        'launch_presentation_height' => [
+            'suffix' => '',
+            'group' => 'launch_presentation',
+            'claim' => 'height',
+            'isarray' => false
+        ],
+        'launch_presentation_locale' => [
+            'suffix' => '',
+            'group' => 'launch_presentation',
+            'claim' => 'locale',
+            'isarray' => false
+        ],
+        'launch_presentation_return_url' => [
+            'suffix' => '',
+            'group' => 'launch_presentation',
+            'claim' => 'return_url',
+            'isarray' => false
+        ],
+        'launch_presentation_width' => [
+            'suffix' => '',
+            'group' => 'launch_presentation',
+            'claim' => 'width',
+            'isarray' => false
+        ],
+        'lis_person_contact_email_primary' => [
+            'suffix' => '',
+            'group' => null,
+            'claim' => 'email',
+            'isarray' => false
+        ],
+        'lis_person_name_family' => [
+            'suffix' => '',
+            'group' => null,
+            'claim' => 'family_name',
+            'isarray' => false
+        ],
+        'lis_person_name_full' => [
+            'suffix' => '',
+            'group' => null,
+            'claim' => 'name',
+            'isarray' => false
+        ],
+        'lis_person_name_given' => [
+            'suffix' => '',
+            'group' => null,
+            'claim' => 'given_name',
+            'isarray' => false
+        ],
+        'lis_person_sourcedid' => [
+            'suffix' => '',
+            'group' => 'lis',
+            'claim' => 'person_sourcedid',
+            'isarray' => false
+        ],
+        'user_id' => [
+            'suffix' => '',
+            'group' => null,
+            'claim' => 'sub',
+            'isarray' => false
+        ],
+        'user_image' => [
+            'suffix' => '',
+            'group' => null,
+            'claim' => 'picture',
+            'isarray' => false
+        ],
+        'roles' => [
+            'suffix' => '',
+            'group' => '',
+            'claim' => 'roles',
+            'isarray' => true
+        ],
+        'role_scope_mentor' => [
+            'suffix' => '',
+            'group' => '',
+            'claim' => 'role_scope_mentor',
+            'isarray' => false
+        ],
+        'deployment_id' => [
+            'suffix' => '',
+            'group' => '',
+            'claim' => 'deployment_id',
+            'isarray' => false
+        ],
+        'lti_message_type' => [
+            'suffix' => '',
+            'group' => '',
+            'claim' => 'message_type',
+            'isarray' => false
+        ],
+        'lti_version' => [
+            'suffix' => '',
+            'group' => '',
+            'claim' => 'version',
+            'isarray' => false
+        ],
+        'resource_link_description' => [
+            'suffix' => '',
+            'group' => 'resource_link',
+            'claim' => 'description',
+            'isarray' => false
+        ],
+        'resource_link_id' => [
+            'suffix' => '',
+            'group' => 'resource_link',
+            'claim' => 'id',
+            'isarray' => false
+        ],
+        'resource_link_title' => [
+            'suffix' => '',
+            'group' => 'resource_link',
+            'claim' => 'title',
+            'isarray' => false
+        ],
+        'tool_consumer_info_product_family_code' => [
+            'suffix' => '',
+            'group' => 'tool_platform',
+            'claim' => 'family_code',
+            'isarray' => false
+        ],
+        'tool_consumer_info_version' => [
+            'suffix' => '',
+            'group' => 'tool_platform',
+            'claim' => 'version',
+            'isarray' => false
+        ],
+        'tool_consumer_instance_contact_email' => [
+            'suffix' => '',
+            'group' => 'tool_platform',
+            'claim' => 'contact_email',
+            'isarray' => false
+        ],
+        'tool_consumer_instance_description' => [
+            'suffix' => '',
+            'group' => 'tool_platform',
+            'claim' => 'description',
+            'isarray' => false
+        ],
+        'tool_consumer_instance_guid' => [
+            'suffix' => '',
+            'group' => 'tool_platform',
+            'claim' => 'guid',
+            'isarray' => false
+        ],
+        'tool_consumer_instance_name' => [
+            'suffix' => '',
+            'group' => 'tool_platform',
+            'claim' => 'name',
+            'isarray' => false
+        ],
+        'tool_consumer_instance_url' => [
+            'suffix' => '',
+            'group' => 'tool_platform',
+            'claim' => 'url',
+            'isarray' => false
+        ],
+        'custom_context_memberships_url' => [
+            'suffix' => 'nrps',
+            'group' => 'namesroleservice',
+            'claim' => 'context_memberships_url',
+            'isarray' => false
+        ],
+        'custom_context_memberships_versions' => [
+            'suffix' => 'nrps',
+            'group' => 'namesroleservice',
+            'claim' => 'service_versions',
+            'isarray' => true
+        ],
+        'custom_gradebookservices_scope' => [
+            'suffix' => 'ags',
+            'group' => 'endpoint',
+            'claim' => 'scope',
+            'isarray' => true
+        ],
+        'custom_lineitems_url' => [
+            'suffix' => 'ags',
+            'group' => 'endpoint',
+            'claim' => 'lineitems',
+            'isarray' => false
+        ],
+        'custom_lineitem_url' => [
+            'suffix' => 'ags',
+            'group' => 'endpoint',
+            'claim' => 'lineitem',
+            'isarray' => false
+        ],
+        'custom_results_url' => [
+            'suffix' => 'ags',
+            'group' => 'endpoint',
+            'claim' => 'results',
+            'isarray' => false
+        ],
+        'custom_result_url' => [
+            'suffix' => 'ags',
+            'group' => 'endpoint',
+            'claim' => 'result',
+            'isarray' => false
+        ],
+        'custom_scores_url' => [
+            'suffix' => 'ags',
+            'group' => 'endpoint',
+            'claim' => 'scores',
+            'isarray' => false
+        ],
+        'custom_score_url' => [
+            'suffix' => 'ags',
+            'group' => 'endpoint',
+            'claim' => 'score',
+            'isarray' => false
+        ],
+        'lis_outcome_service_url' => [
+            'suffix' => 'bos',
+            'group' => 'basicoutcomesservice',
+            'claim' => 'lis_outcome_service_url',
+            'isarray' => false
+        ],
+        'lis_result_sourcedid' => [
+            'suffix' => 'bos',
+            'group' => 'basicoutcomesservice',
+            'claim' => 'lis_result_sourcedid',
+            'isarray' => false
+        ],
+    );
+}
 
 /**
  * Return the launch data required for opening the external tool.
  *
  * @param  stdClass $instance the external tool activity settings
+ * @param  string $nonce  the nonce value to use (applies to LTI 1.3 only)
  * @return array the endpoint URL and parameters (including the signature)
  * @since  Moodle 3.0
  */
-function lti_get_launch_data($instance) {
+function lti_get_launch_data($instance, $nonce = '') {
     global $PAGE, $CFG, $USER;
 
     if (empty($instance->typeid)) {
         $tool = lti_get_tool_by_url_match($instance->toolurl, $instance->course);
         if ($tool) {
             $typeid = $tool->id;
+            $ltiversion = $tool->ltiversion;
         } else {
             $tool = lti_get_tool_by_url_match($instance->securetoolurl,  $instance->course);
             if ($tool) {
                 $typeid = $tool->id;
+                $ltiversion = $tool->ltiversion;
             } else {
                 $typeid = null;
+                $ltiversion = LTI_VERSION_1;
             }
         }
     } else {
         $typeid = $instance->typeid;
         $tool = lti_get_type($typeid);
+        $ltiversion = $tool->ltiversion;
     }
 
     if ($typeid) {
@@ -145,6 +553,8 @@ function lti_get_launch_data($instance) {
         $toolproxy = null;
         if (!empty($instance->resourcekey)) {
             $key = $instance->resourcekey;
+        } else if ($ltiversion === LTI_VERSION_1P3) {
+            $key = $tool->clientid;
         } else if (!empty($typeconfig['resourcekey'])) {
             $key = $typeconfig['resourcekey'];
         } else {
@@ -190,7 +600,7 @@ function lti_get_launch_data($instance) {
     } else {
         $requestparams = $allparams;
     }
-    $requestparams = array_merge($requestparams, lti_build_standard_request($instance, $orgid, $islti2));
+    $requestparams = array_merge($requestparams, lti_build_standard_message($instance, $orgid, $ltiversion));
     $customstr = '';
     if (isset($typeconfig['customparameters'])) {
         $customstr = $typeconfig['customparameters'];
@@ -231,14 +641,15 @@ function lti_get_launch_data($instance) {
 
     $requestparams['launch_presentation_return_url'] = $returnurl;
 
-    // Add the parameters configured by the LTI advantage services.
+    // Add the parameters configured by the LTI services.
     if ($typeid && !$islti2) {
         $services = lti_get_services();
         foreach ($services as $service) {
-            $ltiadvantageparameters = $service->get_launch_parameters('basic-lti-launch-request',
+            $serviceparameters = $service->get_launch_parameters('basic-lti-launch-request',
                     $course->id, $USER->id , $typeid, $instance->id);
-            foreach ($ltiadvantageparameters as $ltiadvantagekey => $ltiadvantagevalue) {
-                $requestparams[$ltiadvantagekey] = $ltiadvantagevalue;
+            foreach ($serviceparameters as $paramkey => $paramvalue) {
+                $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,
+                    $islti2);
             }
         }
     }
@@ -254,8 +665,12 @@ function lti_get_launch_data($instance) {
         }
     }
 
-    if (!empty($key) && !empty($secret)) {
-        $parms = lti_sign_parameters($requestparams, $endpoint, "POST", $key, $secret);
+    if ((!empty($key) && !empty($secret)) || ($ltiversion === LTI_VERSION_1P3)) {
+        if ($ltiversion !== LTI_VERSION_1P3) {
+            $parms = lti_sign_parameters($requestparams, $endpoint, 'POST', $key, $secret);
+        } else {
+            $parms = lti_sign_jwt($requestparams, $endpoint, $key, $typeid, $nonce);
+        }
 
         $endpointurl = new \moodle_url($endpoint);
         $endpointparams = $endpointurl->params();
@@ -515,8 +930,29 @@ function lti_build_request_lti2($tool, $params) {
  * @param string    $messagetype    The request message type. Defaults to basic-lti-launch-request if empty.
  *
  * @return array                    Request details
+ * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
+ * @see lti_build_standard_message()
  */
 function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = 'basic-lti-launch-request') {
+    if (!$islti2) {
+        $ltiversion = LTI_VERSION_1;
+    } else {
+        $ltiversion = LTI_VERSION_2;
+    }
+    return lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype);
+}
+
+/**
+ * This function builds the standard parameters for an LTI message that must be sent to the tool producer
+ *
+ * @param stdClass  $instance       Basic LTI instance object
+ * @param string    $orgid          Organisation ID
+ * @param boolean   $ltiversion     LTI version to be used for tool messages
+ * @param string    $messagetype    The request message type. Defaults to basic-lti-launch-request if empty.
+ *
+ * @return array                    Message parameters
+ */
+function lti_build_standard_message($instance, $orgid, $ltiversion, $messagetype = 'basic-lti-launch-request') {
     global $CFG;
 
     $requestparams = array();
@@ -538,11 +974,7 @@ function lti_build_standard_request($instance, $orgid, $islti2, $messagetype = '
     // Add oauth_callback to be compliant with the 1.0A spec.
     $requestparams['oauth_callback'] = 'about:blank';
 
-    if (!$islti2) {
-        $requestparams['lti_version'] = 'LTI-1p0';
-    } else {
-        $requestparams['lti_version'] = 'LTI-2p0';
-    }
+    $requestparams['lti_version'] = $ltiversion;
     $requestparams['lti_message_type'] = $messagetype;
 
     if ($orgid) {
@@ -623,13 +1055,14 @@ function lti_build_custom_parameters($toolproxy, $tool, $instance, $params, $cus
  *                       TC without further interaction from the user. False by default.
  * @param bool $canconfirm Flag for can_confirm parameter. False by default.
  * @param bool $copyadvice Indicates whether the TC is able and willing to make a local copy of a content item. False by default.
+ * @param string $nonce
  * @return stdClass The object containing the signed request parameters and the URL to the TP's Content-Item selection interface.
  * @throws moodle_exception When the LTI tool type does not exist.`
  * @throws coding_exception For invalid media type and presentation target parameters.
  */
 function lti_build_content_item_selection_request($id, $course, moodle_url $returnurl, $title = '', $text = '', $mediatypes = [],
                                                   $presentationtargets = [], $autocreate = false, $multiple = false,
-                                                  $unsigned = false, $canconfirm = false, $copyadvice = false) {
+                                                  $unsigned = false, $canconfirm = false, $copyadvice = false, $nonce = '') {
     global $USER;
 
     $tool = lti_get_type($id);
@@ -653,14 +1086,18 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu
     $key = '';
     $secret = '';
     $islti2 = false;
+    $islti13 = false;
     if (isset($tool->toolproxyid)) {
         $islti2 = true;
         $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
         $key = $toolproxy->guid;
         $secret = $toolproxy->secret;
     } else {
+        $islti13 = $tool->ltiversion === LTI_VERSION_1P3;
         $toolproxy = null;
-        if (!empty($typeconfig['resourcekey'])) {
+        if ($islti13 && !empty($tool->clientid)) {
+            $key = $tool->clientid;
+        } else if (!$islti13 && !empty($typeconfig['resourcekey'])) {
             $key = $typeconfig['resourcekey'];
         }
         if (!empty($typeconfig['password'])) {
@@ -708,21 +1145,9 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu
         $requestparams = array_merge($requestparams, $lti2params);
     }
 
-    // Add the parameters configured by the LTI advantage services.
-    if ($id && !$islti2) {
-        $services = lti_get_services();
-        foreach ($services as $service) {
-            $ltiadvantageparameters = $service->get_launch_parameters('ContentItemSelectionRequest',
-                    $course->id, $USER->id , $id);
-            foreach ($ltiadvantageparameters as $ltiadvantagekey => $ltiadvantagevalue) {
-                $requestparams[$ltiadvantagekey] = $ltiadvantagevalue;
-            }
-        }
-    }
-
     // Get standard request parameters and merge to the request parameters.
     $orgid = !empty($typeconfig['organizationid']) ? $typeconfig['organizationid'] : '';
-    $standardparams = lti_build_standard_request(null, $orgid, $islti2, 'ContentItemSelectionRequest');
+    $standardparams = lti_build_standard_message(null, $orgid, $tool->ltiversion, 'ContentItemSelectionRequest');
     $requestparams = array_merge($requestparams, $standardparams);
 
     // Get custom request parameters and merge to the request parameters.
@@ -733,6 +1158,19 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu
     $customparams = lti_build_custom_parameters($toolproxy, $tool, $instance, $requestparams, $customstr, '', $islti2);
     $requestparams = array_merge($requestparams, $customparams);
 
+    // Add the parameters configured by the LTI services.
+    if ($id && !$islti2) {
+        $services = lti_get_services();
+        foreach ($services as $service) {
+            $serviceparameters = $service->get_launch_parameters('ContentItemSelectionRequest',
+                    $course->id, $USER->id , $id);
+            foreach ($serviceparameters as $paramkey => $paramvalue) {
+                $requestparams['custom_' . $paramkey] = lti_parse_custom_parameter($toolproxy, $tool, $requestparams, $paramvalue,
+                    $islti2);
+            }
+        }
+    }
+
     // Allow request params to be updated by sub-plugins.
     $plugins = core_component::get_plugin_list('ltisource');
     foreach (array_keys($plugins) as $plugin) {
@@ -743,13 +1181,18 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu
         }
     }
 
-    // Media types. Set to ltilink by default if empty.
-    if (empty($mediatypes)) {
-        $mediatypes = [
-            'application/vnd.ims.lti.v1.ltilink',
-        ];
+    if (!$islti13) {
+        // Media types. Set to ltilink by default if empty.
+        if (empty($mediatypes)) {
+            $mediatypes = [
+                'application/vnd.ims.lti.v1.ltilink',
+            ];
+        }
+        $requestparams['accept_media_types'] = implode(',', $mediatypes);
+    } else {
+        // Only LTI links are currently supported.
+        $requestparams['accept_types'] = 'ltiResourceLink';
     }
-    $requestparams['accept_media_types'] = implode(',', $mediatypes);
 
     // Presentation targets. Supports frame, iframe, window by default if empty.
     if (empty($presentationtargets)) {
@@ -770,7 +1213,11 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu
     $requestparams['content_item_return_url'] = $returnurl->out(false);
     $requestparams['title'] = $title;
     $requestparams['text'] = $text;
-    $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret);
+    if (!$islti13) {
+        $signedparams = lti_sign_parameters($requestparams, $toolurlout, 'POST', $key, $secret);
+    } else {
+        $signedparams = lti_sign_jwt($requestparams, $toolurlout, $key, $id, $nonce);
+    }
     $toolurlparams = $toolurl->params();
 
     // Strip querystring params in endpoint url from $signedparams to avoid duplication.
@@ -805,40 +1252,27 @@ function lti_build_content_item_selection_request($id, $course, moodle_url $retu
 }
 
 /**
- * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the
- * selected content item. This configuration data can be then used when adding a tool into the course.
+ * Verifies the OAuth signature of an incoming message.
  *
  * @param int $typeid The tool type ID.
- * @param string $messagetype The value for the lti_message_type parameter.
- * @param string $ltiversion The value for the lti_version parameter.
  * @param string $consumerkey The consumer key.
- * @param string $contentitemsjson The JSON string for the content_items parameter.
- * @return stdClass The array of module information objects.
+ * @return stdClass Tool type
  * @throws moodle_exception
  * @throws lti\OAuthException
  */
-function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) {
+function lti_verify_oauth_signature($typeid, $consumerkey) {
     $tool = lti_get_type($typeid);
     // Validate parameters.
     if (!$tool) {
         throw new moodle_exception('errortooltypenotfound', 'mod_lti');
     }
-    // Check lti_message_type. Show debugging if it's not set to ContentItemSelection.
-    // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment.
-    if ($messagetype !== 'ContentItemSelection') {
-        debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.",
-            DEBUG_DEVELOPER);
-    }
-
     $typeconfig = lti_get_type_config($typeid);
 
     if (isset($tool->toolproxyid)) {
-        $islti2 = true;
         $toolproxy = lti_get_tool_proxy($tool->toolproxyid);
         $key = $toolproxy->guid;
         $secret = $toolproxy->secret;
     } else {
-        $islti2 = false;
         $toolproxy = null;
         if (!empty($typeconfig['resourcekey'])) {
             $key = $typeconfig['resourcekey'];
@@ -852,17 +1286,6 @@ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiver
         }
     }
 
-    // Check LTI versions from our side and the response's side. Show debugging if they don't match.
-    // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment.
-    $expectedversion = LTI_VERSION_1;
-    if ($islti2) {
-        $expectedversion = LTI_VERSION_2;
-    }
-    if ($ltiversion !== $expectedversion) {
-        debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," .
-            " Response: {$ltiversion}", DEBUG_DEVELOPER);
-    }
-
     if ($consumerkey !== $key) {
         throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
     }
@@ -879,6 +1302,86 @@ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiver
         throw new lti\OAuthException("OAuth signature failed: " . $e->getMessage());
     }
 
+    return $tool;
+}
+
+/**
+ * Verifies the JWT signature of an incoming message.
+ *
+ * @param int $typeid The tool type ID.
+ * @param string $consumerkey The consumer key.
+ * @param string $jwtparam JWT parameter value
+ *
+ * @return stdClass Tool type
+ * @throws moodle_exception
+ * @throws UnexpectedValueException     Provided JWT was invalid
+ * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
+ * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
+ * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
+ * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
+ */
+function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
+    $tool = lti_get_type($typeid);
+    // Validate parameters.
+    if (!$tool) {
+        throw new moodle_exception('errortooltypenotfound', 'mod_lti');
+    }
+    if (isset($tool->toolproxyid)) {
+        throw new moodle_exception('JWT security not supported with LTI 2');
+    }
+
+    $typeconfig = lti_get_type_config($typeid);
+
+    $key = $tool->clientid ?? '';
+    $publickey = $typeconfig['publickey'] ?? '';
+
+    if ($consumerkey !== $key) {
+        throw new moodle_exception('errorincorrectconsumerkey', 'mod_lti');
+    }
+    if (empty($publickey)) {
+        throw new moodle_exception('No public key configured');
+    }
+
+    JWT::decode($jwtparam, $publickey, array('RS256'));
+
+    return $tool;
+}
+
+/**
+ * Processes the tool provider's response to the ContentItemSelectionRequest and builds the configuration data from the
+ * selected content item. This configuration data can be then used when adding a tool into the course.
+ *
+ * @param int $typeid The tool type ID.
+ * @param string $messagetype The value for the lti_message_type parameter.
+ * @param string $ltiversion The value for the lti_version parameter.
+ * @param string $consumerkey The consumer key.
+ * @param string $contentitemsjson The JSON string for the content_items parameter.
+ * @return stdClass The array of module information objects.
+ * @throws moodle_exception
+ * @throws lti\OAuthException
+ */
+function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiversion, $consumerkey, $contentitemsjson) {
+    $tool = lti_get_type($typeid);
+    // Validate parameters.
+    if (!$tool) {
+        throw new moodle_exception('errortooltypenotfound', 'mod_lti');
+    }
+    // Check lti_message_type. Show debugging if it's not set to ContentItemSelection.
+    // No need to throw exceptions for now since lti_message_type does not seem to be used in this processing at the moment.
+    if ($messagetype !== 'ContentItemSelection') {
+        debugging("lti_message_type is invalid: {$messagetype}. It should be set to 'ContentItemSelection'.",
+            DEBUG_DEVELOPER);
+    }
+
+    // Check LTI versions from our side and the response's side. Show debugging if they don't match.
+    // No need to throw exceptions for now since LTI version does not seem to be used in this processing at the moment.
+    $expectedversion = $tool->ltiversion;
+    $islti2 = ($expectedversion === LTI_VERSION_2);
+    if ($ltiversion !== $expectedversion) {
+        debugging("lti_version from response does not match the tool's configuration. Tool: {$expectedversion}," .
+            " Response: {$ltiversion}", DEBUG_DEVELOPER);
+    }
+
     $items = json_decode($contentitemsjson);
     if (empty($items)) {
         throw new moodle_exception('errorinvaliddata', 'mod_lti', '', $contentitemsjson);
@@ -890,6 +1393,7 @@ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiver
     $config = null;
     if (!empty($items->{'@graph'})) {
         $item = $items->{'@graph'}[0];
+        $typeconfig = lti_get_type_type_config($tool->id);
 
         $config = new stdClass();
         $config->name = '';
@@ -919,11 +1423,11 @@ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiver
             $config->toolurl = $url->out(false);
             $config->typeid = 0;
         } else {
-            $config->typeid = $typeid;
+            $config->typeid = $tool->id;
         }
         $config->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
-        if (!$islti2 && isset($typeconfig['acceptgrades'])) {
-            $acceptgrades = $typeconfig['acceptgrades'];
+        if (!$islti2 && isset($typeconfig->lti_acceptgrades)) {
+            $acceptgrades = $typeconfig->lti_acceptgrades;
             if ($acceptgrades == LTI_SETTING_ALWAYS) {
                 // We create a line item regardless if the definition contains one or not.
                 $config->instructorchoiceacceptgrades = LTI_SETTING_ALWAYS;
@@ -972,6 +1476,92 @@ function lti_tool_configuration_from_content_item($typeid, $messagetype, $ltiver
     return $config;
 }
 
+/**
+ * Converts the new Deep-Linking format for Content-Items to the old format.
+ *
+ * @param string $param JSON string representing new Deep-Linking format
+ * @return string  JSON representation of content-items
+ */
+function lti_convert_content_items($param) {
+    $items = array();
+    $json = json_decode($param);
+    if (!empty($json) && is_array($json)) {
+        foreach ($json as $item) {
+            if (isset($item->type)) {
+                $newitem = clone $item;
+                switch ($item->type) {
+                    case 'ltiResourceLink':
+                        $newitem->{'@type'} = 'LtiLinkItem';
+                        $newitem->mediaType = 'application\/vnd.ims.lti.v1.ltilink';
+                        break;
+                    case 'link':
+                    case 'rich':
+                        $newitem->{'@type'} = 'ContentItem';
+                        $newitem->mediaType = 'text/html';
+                        break;
+                    case 'file':
+                        $newitem->{'@type'} = 'FileItem';
+                        break;
+                }
+                unset($newitem->type);
+                if (isset($item->html)) {
+                    $newitem->text = $item->html;
+                    unset($newitem->html);
+                }
+                if (isset($item->presentation)) {
+                    $newitem->placementAdvice = new stdClass();
+                    if (isset($item->presentation->documentTarget)) {
+                        $newitem->placementAdvice->presentationDocumentTarget = $item->presentation->documentTarget;
+                    }
+                    if (isset($item->presentation->windowTarget)) {
+                        $newitem->placementAdvice->windowTarget = $item->presentation->windowTarget;
+                    }
+                    if (isset($item->presentation->width)) {
+                        $newitem->placementAdvice->dislayWidth = $item->presentation->width;
+                    }
+                    if (isset($item->presentation->height)) {
+                        $newitem->placementAdvice->dislayHeight = $item->presentation->height;
+                    }
+                    unset($newitem->presentation);
+                }
+                if (isset($item->icon) && isset($item->icon->url)) {
+                    $newitem->icon->{'@id'} = $item->icon->url;
+                    unset($newitem->icon->url);
+                }
+                if (isset($item->thumbnail) && isset($item->thumbnail->url)) {
+                    $newitem->thumbnail->{'@id'} = $item->thumbnail->url;
+                    unset($newitem->thumbnail->url);
+                }
+                if (isset($item->lineItem)) {
+                    unset($newitem->lineItem);
+                    $newitem->lineItem = new stdClass();
+                    $newitem->lineItem->{'@type'} = 'LineItem';
+                    $newitem->lineItem->reportingMethod = 'http://purl.imsglobal.org/ctx/lis/v2p1/Result#totalScore';
+                    if (isset($item->lineItem->label)) {
+                        $newitem->lineItem->label = $item->lineItem->label;
+                    }
+                    if (isset($item->lineItem->resourceId)) {
+                        $newitem->lineItem->assignedActivity = new stdClass();
+                        $newitem->lineItem->assignedActivity->activityId = $item->lineItem->resourceId;
+                    }
+                    if (isset($item->lineItem->scoreMaximum)) {
+                        $newitem->lineItem->scoreConstraints = new stdClass();
+                        $newitem->lineItem->scoreConstraints->{'@type'} = 'NumericLimits';
+                        $newitem->lineItem->scoreConstraints->totalMaximum = $item->lineItem->scoreMaximum;
+                    }
+                }
+                $items[] = $newitem;
+            }
+        }
+    }
+
+    $newitems = new stdClass();
+    $newitems->{'@context'} = 'http://purl.imsglobal.org/ctx/lti/v1/ContentItem';
+    $newitems->{'@graph'} = $items;
+
+    return json_encode($newitems);
+}
+
 function lti_get_tool_table($tools, $id) {
     global $OUTPUT;
     $html = '';
@@ -1188,20 +1778,22 @@ function lti_get_enabled_capabilities($tool) {
     } else {
         $enabledcapabilities = array();
     }
-    $paramstr = str_replace("\r\n", "\n", $tool->parameter);
-    $paramstr = str_replace("\n\r", "\n", $paramstr);
-    $paramstr = str_replace("\r", "\n", $paramstr);
-    $params = explode("\n", $paramstr);
-    foreach ($params as $param) {
-        $pos = strpos($param, '=');
-        if (($pos === false) || ($pos < 1)) {
-            continue;
-        }
-        $value = trim(core_text::substr($param, $pos + 1, strlen($param)));
-        if (substr($value, 0, 1) == '$') {
-            $value = substr($value, 1);
-            if (!in_array($value, $enabledcapabilities)) {
-                $enabledcapabilities[] = $value;
+    if (!empty($tool->parameter)) {
+        $paramstr = str_replace("\r\n", "\n", $tool->parameter);
+        $paramstr = str_replace("\n\r", "\n", $paramstr);
+        $paramstr = str_replace("\r", "\n", $paramstr);
+        $params = explode("\n", $paramstr);
+        foreach ($params as $param) {
+            $pos = strpos($param, '=');
+            if (($pos === false) || ($pos < 1)) {
+                continue;
+            }
+            $value = trim(core_text::substr($param, $pos + 1, strlen($param)));
+            if (substr($value, 0, 1) == '$') {
+                $value = substr($value, 1);
+                if (!in_array($value, $enabledcapabilities)) {
+                    $enabledcapabilities[] = $value;
+                }
             }
         }
     }
@@ -1231,12 +1823,11 @@ function lti_split_custom_parameters($toolproxy, $tool, $params, $customstr, $is
             continue;
         }
         $key = trim(core_text::substr($line, 0, $pos));
-        $key = lti_map_keyname($key, false);
         $val = trim(core_text::substr($line, $pos + 1, strlen($line)));
         $val = lti_parse_custom_parameter($toolproxy, $tool, $params, $val, $islti2);
         $key2 = lti_map_keyname($key);
         $retval['custom_'.$key2] = $val;
-        if ($key != $key2) {
+        if (($islti2 || ($tool->ltiversion === LTI_VERSION_1P3)) && ($key != $key2)) {
             $retval['custom_'.$key] = $val;
         }
     }
@@ -1304,11 +1895,12 @@ function lti_parse_custom_parameter($toolproxy, $tool, $params, $value, $islti2)
                     } else {
                         $value = lti_calculate_custom_parameter($value1);
                     }
-                } else if ($islti2) {
+                } else {
                     $val = $value;
                     $services = lti_get_services();
                     foreach ($services as $service) {
                         $service->set_tool_proxy($toolproxy);
+                        $service->set_type($tool);
                         $value = $service->parse_value($val);
                         if ($val != $value) {
                             break;
@@ -1346,16 +1938,18 @@ function lti_calculate_custom_parameter($value) {
  * @return string       Processed name
  */
 function lti_map_keyname($key, $tolower = true) {
-    $newkey = "";
     if ($tolower) {
+        $newkey = '';
         $key = core_text::strtolower(trim($key));
-    }
-    foreach (str_split($key) as $ch) {
-        if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') || (!$tolower && ($ch >= 'A' && $ch <= 'Z'))) {
-            $newkey .= $ch;
-        } else {
-            $newkey .= '_';
+        foreach (str_split($key) as $ch) {
+            if ( ($ch >= 'a' && $ch <= 'z') || ($ch >= '0' && $ch <= '9') ) {
+                $newkey .= $ch;
+            } else {
+                $newkey .= '_';
+            }
         }
+    } else {
+        $newkey = $key;
     }
     return $newkey;
 }
@@ -1697,6 +2291,7 @@ function lti_get_shared_secrets_by_key($key) {
 
     // Look up the shared secret for the specified key in both the types_config table (for configured tools)
     // And in the lti resource table for ad-hoc tools.
+    $lti13 = LTI_VERSION_1P3;
     $query = "SELECT t2.value
                 FROM {lti_types_config} t1
                 JOIN {lti_types_config} t2 ON t1.typeid = t2.typeid
@@ -1705,18 +2300,19 @@ function lti_get_shared_secrets_by_key($key) {
                 AND t1.value = :key1
                 AND t2.name = 'password'
                 AND type.state = :configured1
+                AND type.ltiversion <> :ltiversion
                UNION
               SELECT tp.secret AS value
                 FROM {lti_tool_proxies} tp
                 JOIN {lti_types} t ON tp.id = t.toolproxyid
               WHERE tp.guid = :key2
                 AND t.state = :configured2
-              UNION
-             SELECT password AS value
+               UNION
+              SELECT password AS value
                FROM {lti}
               WHERE resourcekey = :key3";
 
-    $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED,
+    $sharedsecrets = $DB->get_records_sql($query, array('configured1' => LTI_TOOL_STATE_CONFIGURED, 'ltiversion' => $lti13,
         'configured2' => LTI_TOOL_STATE_CONFIGURED, 'key1' => $key, 'key2' => $key, 'key3' => $key));
 
     $values = array_map(function($item) {
@@ -1830,6 +2426,11 @@ function lti_get_type_type_config($id) {
 
     $type->lti_toolurl = $basicltitype->baseurl;
 
+    $type->lti_ltiversion = $basicltitype->ltiversion;
+
+    $type->lti_clientid = $basicltitype->clientid;
+    $type->lti_clientid_disabled = $type->lti_clientid;
+
     $type->lti_description = $basicltitype->description;
 
     $type->lti_parameters = $basicltitype->parameter;
@@ -1844,6 +2445,15 @@ function lti_get_type_type_config($id) {
     if (isset($config['password'])) {
         $type->lti_password = $config['password'];
     }
+    if (isset($config['publickey'])) {
+        $type->lti_publickey = $config['publickey'];
+    }
+    if (isset($config['initiatelogin'])) {
+        $type->lti_initiatelogin = $config['initiatelogin'];
+    }
+    if (isset($config['redirectionuris'])) {
+        $type->lti_redirectionuris = $config['redirectionuris'];
+    }
 
     if (isset($config['sendname'])) {
         $type->lti_sendname = $config['sendname'];
@@ -1912,15 +2522,9 @@ function lti_get_type_type_config($id) {
     }
 
     // Get the parameters from the LTI services.
-    $services = lti_get_services();
-    $ltiserviceprefixlength = 11;
-    foreach ($services as $service) {
-        $configurationparameters = $service->get_configuration_parameter_names();
-        foreach ($configurationparameters as $ltiserviceparameter) {
-            $shortltiserviceparameter = substr($ltiserviceparameter, $ltiserviceprefixlength);
-            if (isset($config[$shortltiserviceparameter])) {
-                $type->$ltiserviceparameter = $config[$shortltiserviceparameter];
-            }
+    foreach ($config as $name => $value) {
+        if (strpos($name, 'ltiservice_') === 0) {
+            $type->{$name} = $config[$name];
         }
     }
 
@@ -1938,6 +2542,17 @@ function lti_prepare_type_for_save($type, $config) {
     if (isset($config->lti_typename)) {
         $type->name = $config->lti_typename;
     }
+    if (isset($config->lti_ltiversion)) {
+        $type->ltiversion = $config->lti_ltiversion;
+    }
+    if (isset($config->lti_clientid)) {
+        $type->clientid = $config->lti_clientid;
+    }
+    if ((!empty($type->ltiversion) && $type->ltiversion === LTI_VERSION_1P3) && empty($type->clientid)) {
+        $type->clientid = random_string(15);
+    } else if (empty($type->clientid)) {
+        $type->clientid = null;
+    }
     if (isset($config->lti_coursevisible)) {
         $type->coursevisible = $config->lti_coursevisible;
     }
@@ -1969,6 +2584,8 @@ function lti_prepare_type_for_save($type, $config) {
     unset ($config->lti_typename);
     unset ($config->lti_toolurl);
     unset ($config->lti_description);
+    unset ($config->lti_ltiversion);
+    unset ($config->lti_clientid);
     unset ($config->lti_icon);
     unset ($config->lti_secureicon);
 }
@@ -1997,7 +2614,7 @@ function lti_update_type($type, $config) {
             if (substr($key, 0, 11) == 'ltiservice_' && !is_null($value)) {
                 $record = new \StdClass();
                 $record->typeid = $type->id;
-                $record->name = substr($key, 11);
+                $record->name = $key;
                 $record->value = $value;
                 lti_update_config($record);
             }
@@ -2026,6 +2643,10 @@ function lti_add_type($type, $config) {
         $type->state = LTI_TOOL_STATE_PENDING;
     }
 
+    if (!isset($type->ltiversion)) {
+        $type->ltiversion = LTI_VERSION_1;
+    }
+
     if (!isset($type->timecreated)) {
         $type->timecreated = time();
     }
@@ -2048,12 +2669,13 @@ function lti_add_type($type, $config) {
     if ($id) {
         foreach ($config as $key => $value) {
             if (!is_null($value)) {
-                $fieldparts = preg_split("/(lti|ltiservice)_/i", $key);
-                // If array has only one element, it did not start with the pattern.
-                if (count($fieldparts) < 2) {
+                if (substr($key, 0, 4) === 'lti_') {
+                    $fieldname = substr($key, 4);
+                } else if (substr($key, 0, 11) !== 'ltiservice_') {
                     continue;
+                } else {
+                    $fieldname = $key;
                 }
-                $fieldname = $fieldparts[1];
 
                 $record = new \StdClass();
                 $record->typeid = $id;
@@ -2298,7 +2920,7 @@ function lti_update_config($config) {
 /**
  * Gets the tool settings
  *
- * @param int  $toolproxyid   Id of tool proxy record
+ * @param int  $toolproxyid   Id of tool proxy record (or tool ID if negative)
  * @param int  $courseid      Id of course (null if system settings)
  * @param int  $instanceid    Id of course module (null if system or context settings)
  *
@@ -2308,8 +2930,13 @@ function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = nul
     global $DB;
 
     $settings = array();
-    $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid,
-        'course' => $courseid, 'coursemoduleid' => $instanceid));
+    if ($toolproxyid > 0) {
+        $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('toolproxyid' => $toolproxyid,
+            'course' => $courseid, 'coursemoduleid' => $instanceid));
+    } else {
+        $settingsstr = $DB->get_field('lti_tool_settings', 'settings', array('typeid' => -$toolproxyid,
+            'course' => $courseid, 'coursemoduleid' => $instanceid));
+    }
     if ($settingsstr !== false) {
         $settings = json_decode($settingsstr, true);
     }
@@ -2320,7 +2947,7 @@ function lti_get_tool_settings($toolproxyid, $courseid = null, $instanceid = nul
  * Sets the tool settings (
  *
  * @param array  $settings      Array of settings
- * @param int    $toolproxyid   Id of tool proxy record
+ * @param int    $toolproxyid   Id of tool proxy record (or tool ID if negative)
  * @param int    $courseid      Id of course (null if system settings)
  * @param int    $instanceid    Id of course module (null if system or context settings)
  */
@@ -2328,13 +2955,22 @@ function lti_set_tool_settings($settings, $toolproxyid, $courseid = null, $insta
     global $DB;
 
     $json = json_encode($settings);
-    $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid,
-        'course' => $courseid, 'coursemoduleid' => $instanceid));
+    if ($toolproxyid >= 0) {
+        $record = $DB->get_record('lti_tool_settings', array('toolproxyid' => $toolproxyid,
+            'course' => $courseid, 'coursemoduleid' => $instanceid));
+    } else {
+        $record = $DB->get_record('lti_tool_settings', array('typeid' => -$toolproxyid,
+            'course' => $courseid, 'coursemoduleid' => $instanceid));
+    }
     if ($record !== false) {
         $DB->update_record('lti_tool_settings', (object)array('id' => $record->id, 'settings' => $json, 'timemodified' => time()));
     } else {
         $record = new \stdClass();
-        $record->toolproxyid = $toolproxyid;
+        if ($toolproxyid > 0) {
+            $record->toolproxyid = $toolproxyid;
+        } else {
+            $record->typeid = -$toolproxyid;
+        }
         $record->course = $courseid;
         $record->coursemoduleid = $instanceid;
         $record->settings = $json;
@@ -2371,6 +3007,183 @@ function lti_sign_parameters($oldparms, $endpoint, $method, $oauthconsumerkey, $
     return $newparms;
 }
 
+/**
+ * Converts the message paramters to their equivalent JWT claim and signs the payload to launch the external tool using JWT
+ *
+ * @param array  $parms        Parameters to be passed for signing
+ * @param string $endpoint     url of the external tool
+ * @param string $oauthconsumerkey
+ * @param string $typeid       ID of LTI tool type
+ * @param string $nonce        Nonce value to use
+ * @return array|null
+ */
+function lti_sign_jwt($parms, $endpoint, $oauthconsumerkey, $typeid = 0, $nonce = '') {
+
+    if (empty($typeid)) {
+        $typeid = 0;
+    }
+    $messagetypemapping = lti_get_jwt_message_type_mapping();
+    if (isset($parms['lti_message_type']) && array_key_exists($parms['lti_message_type'], $messagetypemapping)) {
+        $parms['lti_message_type'] = $messagetypemapping[$parms['lti_message_type']];
+    }
+    if (isset($parms['roles'])) {
+        $roles = explode(',', $parms['roles']);
+        $newroles = array();
+        foreach ($roles as $role) {
+            if (strpos($role, 'urn:lti:role:ims/lis/') === 0) {
+                $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . substr($role, 21);
+            } else if (strpos($role, 'urn:lti:instrole:ims/lis/') === 0) {
+                $role = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#' . substr($role, 25);
+            } else if (strpos($role, 'urn:lti:sysrole:ims/lis/') === 0) {
+                $role = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#' . substr($role, 24);
+            } else if ((strpos($role, '://') === false) && (strpos($role, 'urn:') !== 0)) {
+                $role = "http://purl.imsglobal.org/vocab/lis/v2/membership#{$role}";
+            }
+            $newroles[] = $role;
+        }
+        $parms['roles'] = implode(',', $newroles);
+    }
+
+    $now = time();
+    if (empty($nonce)) {
+        $nonce = bin2hex(openssl_random_pseudo_bytes(10));
+    }
+    $claimmapping = lti_get_jwt_claim_mapping();
+    $payload = array(
+        'nonce' => $nonce,
+        'iat' => $now,
+        'exp' => $now + 60,
+    );
+    $payload['iss'] = get_config('mod_lti', 'platformid');
+    $payload['aud'] = $oauthconsumerkey;
+    $payload[LTI_JWT_CLAIM_PREFIX . '/claim/deployment_id'] = strval($typeid);
+    $payload[LTI_JWT_CLAIM_PREFIX . '/claim/target_link_uri'] = $endpoint;
+
+    foreach ($parms as $key => $value) {
+        $claim = LTI_JWT_CLAIM_PREFIX;
+        if (array_key_exists($key, $claimmapping)) {
+            $mapping = $claimmapping[$key];
+            if ($mapping['isarray']) {
+                $value = explode(',', $value);
+                sort($value);
+            }
+            if (!empty($mapping['suffix'])) {
+                $claim .= "-{$mapping['suffix']}";
+            }
+            $claim .= '/claim/';
+            if (is_null($mapping['group'])) {
+                $payload[$mapping['claim']] = $value;
+            } else if (empty($mapping['group'])) {
+                $payload["{$claim}{$mapping['claim']}"] = $value;
+            } else {
+                $claim .= $mapping['group'];
+                $payload[$claim][$mapping['claim']] = $value;
+            }
+        } else if (strpos($key, 'custom_') === 0) {
+            $payload["{$claim}/claim/custom"][substr($key, 7)] = $value;
+        } else if (strpos($key, 'ext_') === 0) {
+            $payload["{$claim}/claim/ext"][substr($key, 4)] = $value;
+        }
+    }
+
+    $privatekey = get_config('mod_lti', 'privatekey');
+    $kid = get_config('mod_lti', 'kid');
+    $jwt = JWT::encode($payload, $privatekey, 'RS256', $kid);
+
+    $newparms = array();
+    $newparms['id_token'] = $jwt;
+
+    return $newparms;
+}
+
+/**
+ * Verfies the JWT and converts its claims to their equivalent message parameter.
+ *
+ * @param int    $typeid
+ * @param string $jwtparam   JWT parameter
+ *
+ * @return array  message parameters
+ * @throws moodle_exception
+ */
+function lti_convert_from_jwt($typeid, $jwtparam) {
+
+    $params = array();
+    $parts = explode('.', $jwtparam);
+    $ok = (count($parts) === 3);
+    if ($ok) {
+        $payload = JWT::urlsafeB64Decode($parts[1]);
+        $claims = json_decode($payload, true);
+        $ok = !is_null($claims) && !empty($claims['iss']);
+    }
+    if ($ok) {
+        lti_verify_jwt_signature($typeid, $claims['iss'], $jwtparam);
+        $params['oauth_consumer_key'] = $claims['iss'];
+        foreach (lti_get_jwt_claim_mapping() as $key => $mapping) {
+            $claim = LTI_JWT_CLAIM_PREFIX;
+            if (!empty($mapping['suffix'])) {
+                $claim .= "-{$mapping['suffix']}";
+            }
+            $claim .= '/claim/';
+            if (is_null($mapping['group'])) {
+                $claim = $mapping['claim'];
+            } else if (empty($mapping['group'])) {
+                $claim .= $mapping['claim'];
+            } else {
+                $claim .= $mapping['group'];
+            }
+            if (isset($claims[$claim])) {
+                $value = null;
+                if (empty($mapping['group'])) {
+                    $value = $claims[$claim];
+                } else {
+                    $group = $claims[$claim];
+                    if (is_array($group) && array_key_exists($mapping['claim'], $group)) {
+                        $value = $group[$mapping['claim']];
+                    }
+                }
+                if (!empty($value) && $mapping['isarray']) {
+                    if (is_array($value)) {
+                        if (is_array($value[0])) {
+                            $value = json_encode($value);
+                        } else {
+                            $value = implode(',', $value);
+                        }
+                    }
+                }
+                if (!is_null($value) && is_string($value) && (strlen($value) > 0)) {
+                    $params[$key] = $value;
+                }
+            }
+            $claim = LTI_JWT_CLAIM_PREFIX . '/claim/custom';
+            if (isset($claims[$claim])) {
+                $custom = $claims[$claim];
+                if (is_array($custom)) {
+                    foreach ($custom as $key => $value) {
+                        $params["custom_{$key}"] = $value;
+                    }
+                }
+            }
+            $claim = LTI_JWT_CLAIM_PREFIX . '/claim/ext';
+            if (isset($claims[$claim])) {
+                $ext = $claims[$claim];
+                if (is_array($ext)) {
+                    foreach ($ext as $key => $value) {
+                        $params["ext_{$key}"] = $value;
+                    }
+                }
+            }
+        }
+    }
+    if (isset($params['content_items'])) {
+        $params['content_items'] = lti_convert_content_items($params['content_items']);
+    }
+    $messagetypemapping = lti_get_jwt_message_type_mapping();
+    if (isset($params['lti_message_type']) && array_key_exists($params['lti_message_type'], $messagetypemapping)) {
+        $params['lti_message_type'] = $messagetypemapping[$params['lti_message_type']];
+    }
+    return $params;
+}
+
 /**
  * Posts the launch petition HTML
  *
@@ -2437,6 +3250,67 @@ function lti_post_launch_html($newparms, $endpoint, $debug=false) {
     return $r;
 }
 
+/**
+ * Generate the form for initiating a login request for an LTI 1.3 message
+ *
+ * @param int            $courseid  Course ID
+ * @param int            $id        LTI instance ID
+ * @param stdClass|null  $instance  LTI instance
+ * @param stdClass       $config    Tool type configuration
+ * @param string         $messagetype   LTI message type
+ * @param string         $title     Title of content item
+ * @param string         $text      Description of content item
+ * @return string
+ */
+function lti_initiate_login($courseid, $id, $instance, $config, $messagetype = 'basic-lti-launch-request', $title = '',
+        $text = '') {
+    global $SESSION, $USER;
+
+    if (!empty($instance)) {
+        $endpoint = !empty($instance->toolurl) ? $instance->toolurl : $config->lti_toolurl;
+    } else {
+        $endpoint = $config->lti_toolurl;
+        if (($messagetype === 'ContentItemSelectionRequest') && !empty($config->lti_toolurl_ContentItemSelectionRequest)) {
+            $endpoint = $config->lti_toolurl_ContentItemSelectionRequest;
+        }
+    }
+    $endpoint = trim($endpoint);
+
+    // If SSL is forced make sure https is on the normal launch URL.
+    if (isset($config->lti_forcessl) && ($config->lti_forcessl == '1')) {
+        $endpoint = lti_ensure_url_is_https($endpoint);
+    } else if (!strstr($endpoint, '://')) {
+        $endpoint = 'http://' . $endpoint;
+    }
+
+    $params = array();
+    $params['iss'] = get_config('mod_lti', 'platformid');
+    $params['target_link_uri'] = $endpoint;
+    $params['login_hint'] = $USER->id;
+    $params['lti_message_hint'] = $id;
+    $SESSION->lti_message_hint = "{$courseid},{$config->typeid},{$id}," . base64_encode($title) . ',' .
+        base64_encode($text);
+
+    $r = "<form action=\"" . $config->lti_initiatelogin .
+        "\" name=\"ltiInitiateLoginForm\" id=\"ltiInitiateLoginForm\" method=\"post\" " .
+        "encType=\"application/x-www-form-urlencoded\">\n";
+
+    foreach ($params as $key => $value) {
+        $key = htmlspecialchars($key);
+        $value = htmlspecialchars($value);
+        $r .= "  <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
+    }
+    $r .= "</form>\n";
+
+    $r .= "<script type=\"text/javascript\">\n" .
+        "//<![CDATA[\n" .
+        "document.ltiInitiateLoginForm.submit();\n" .
+        "//]]>\n" .
+        "</script>\n";
+
+    return $r;
+}
+
 function lti_get_type($typeid) {
     global $DB;
 
@@ -2668,6 +3542,8 @@ function lti_get_capabilities() {
        'Membership.role' => 'roles',
        'Result.sourcedId' => 'lis_result_sourcedid',
        'Result.autocreate' => 'lis_outcome_service_url',
+       'BasicOutcome.sourcedId' => 'lis_result_sourcedid',
+       'BasicOutcome.url' => 'lis_outcome_service_url',
        'Moodle.Person.userGroupIds' => null);
 
     return $capabilities;
@@ -2735,6 +3611,31 @@ function lti_get_service_by_resource_id($services, $resourceid) {
 
 }
 
+/**
+ * Initializes an array with the scopes for services supported by the LTI module
+ *
+ * @param object $type  LTI tool type
+ * @param array  $typeconfig  LTI tool type configuration
+ *
+ * @return array List of scopes
+ */
+function lti_get_permitted_service_scopes($type, $typeconfig) {
+
+    $services = lti_get_services();
+    $scopes = array();
+    foreach ($services as $service) {
+        $service->set_type($type);
+        $service->set_typeconfig($typeconfig);
+        $servicescopes = $service->get_permitted_scopes();
+        if (!empty($servicescopes)) {
+            $scopes = array_merge($scopes, $servicescopes);
+        }
+    }
+
+    return $scopes;
+
+}
+
 /**
  * Extracts the named contexts from a tool proxy
  *
@@ -2861,6 +3762,13 @@ function get_tool_type_urls(stdClass $type) {
         $urls['course'] = $courseurl;
     }
 
+    $url = new moodle_url('/mod/lti/certs.php');
+    $urls['publickeyset'] = $url->out();
+    $url = new moodle_url('/mod/lti/token.php');
+    $urls['accesstoken'] = $url->out();
+    $url = new moodle_url('/mod/lti/auth.php');
+    $urls['authrequest'] = $url->out();
+
     return $urls;
 }
 
@@ -2923,6 +3831,36 @@ function get_tool_type_state_info(stdClass $type) {
     );
 }
 
+/**
+ * Returns information on the configuration of the tool type
+ *
+ * @param stdClass $type The tool type
+ *
+ * @return array An array with configuration details
+ */
+function get_tool_type_config($type) {
+    $platformid = get_config('mod_lti', 'platformid');
+    $clientid = $type->clientid;
+    $deploymentid = $type->id;
+    $publickeyseturl = new moodle_url('/mod/lti/certs.php');
+    $publickeyseturl = $publickeyseturl->out();
+
+    $accesstokenurl = new moodle_url('/mod/lti/token.php');
+    $accesstokenurl = $accesstokenurl->out();
+
+    $authrequesturl = new moodle_url('/mod/lti/auth.php');
+    $authrequesturl = $authrequesturl->out();
+
+    return array(
+        'platformid' => $platformid,
+        'clientid' => $clientid,
+        'deploymentid' => $deploymentid,
+        'publickeyseturl' => $publickeyseturl,
+        'accesstokenurl' => $accesstokenurl,
+        'authrequesturl' => $authrequesturl
+    );
+}
+
 /**
  * Returns a summary of each LTI capability this tool type requires in plain language
  *
@@ -2984,6 +3922,8 @@ function get_tool_type_instance_ids($type) {
  * @return array An array of values representing this type
  */
 function serialise_tool_type(stdClass $type) {
+    global $CFG;
+
     $capabilitygroups = get_tool_type_capability_groups($type);
     $instanceids = get_tool_type_instance_ids($type);
     // Clean the name. We don't want tags here.
@@ -3000,6 +3940,9 @@ function serialise_tool_type(stdClass $type) {
         'description' => $description,
         'urls' => get_tool_type_urls($type),
         'state' => get_tool_type_state_info($type),
+        'platformid' => get_config('mod_lti', 'platformid'),
+        'clientid' => $type->clientid,
+        'deploymentid' => $type->id,
         'hascapabilitygroups' => !empty($capabilitygroups),
         'capabilitygroups' => $capabilitygroups,
         // Course ID of 1 means it's not linked to a course.
@@ -3269,3 +4212,38 @@ function get_tag($tagname, $xpath, $attribute = null) {
     }
     return null;
 }
+
+/**
+ * Create a new access token.
+ *
+ * @param int $typeid Tool type ID
+ * @param string[] $scopes Scopes permitted for new token
+ *
+ * @return stdClass Access token
+ */
+function lti_new_access_token($typeid, $scopes) {
+    global $DB;
+
+    // Make sure the token doesn't exist (even if it should be almost impossible with the random generation).
+    $numtries = 0;
+    do {
+        $numtries ++;
+        $generatedtoken = md5(uniqid(rand(), 1));
+        if ($numtries > 5) {
+            throw new moodle_exception('Failed to generate LTI access token');
+        }
+    } while ($DB->record_exists('lti_access_tokens', array('token' => $generatedtoken)));
+    $newtoken = new stdClass();
+    $newtoken->typeid = $typeid;
+    $newtoken->scope = json_encode(array_values($scopes));
+    $newtoken->token = $generatedtoken;
+
+    $newtoken->timecreated = time();
+    $newtoken->validuntil = $newtoken->timecreated + LTI_ACCESS_TOKEN_LIFE;
+    $newtoken->lastaccess = null;
+
+    $DB->insert_record('lti_access_tokens', $newtoken);
+
+    return $newtoken;
+
+}
index 163e923..bc3ba4e 100644 (file)
@@ -112,7 +112,7 @@ class mod_lti_mod_form extends moodleform_mod {
                 $toolproxy[] = $type->id;
                 $attributes = array( 'globalTool' => 1, 'toolproxy' => 1);
                 $enabledcapabilities = explode("\n", $type->enabledcapability);
-                if (!in_array('Result.autocreate', $enabledcapabilities)) {
+                if (!in_array('Result.autocreate', $enabledcapabilities) || in_array('BasicOutcome.url', $enabledcapabilities)) {
                     $attributes['nogrades'] = 1;
                 }
                 if (!in_array('Person.name.full', $enabledcapabilities) && !in_array('Person.name.family', $enabledcapabilities) &&
index a5a5df4..d10e57d 100644 (file)
@@ -33,6 +33,7 @@ require_once($CFG->dirroot.'/mod/lti/servicelib.php');
 // TODO: Switch to core oauthlib once implemented - MDL-30149.
 use mod_lti\service_exception_handler;
 use moodle\mod\lti as lti;
+use ltiservice_basicoutcomes\local\service\basicoutcomes;
 
 $rawbody = file_get_contents("php://input");
 
@@ -46,24 +47,26 @@ if ($logrequests) {
     lti_log_request($rawbody);
 }
 
-foreach (lti\OAuthUtil::get_headers() as $name => $value) {
-    if ($name === 'Authorization') {
-        // TODO: Switch to core oauthlib once implemented - MDL-30149.
-        $oauthparams = lti\OAuthUtil::split_header($value);
-
-        $consumerkey = $oauthparams['oauth_consumer_key'];
-        break;
+$ok = true;
+$type = null;
+$toolproxy = false;
+
+$consumerkey = lti\get_oauth_key_from_headers(null, array(basicoutcomes::SCOPE_BASIC_OUTCOMES));
+if ($consumerkey === false) {
+    throw new Exception('Missing or invalid consumer key or access token.');
+} else if (is_string($consumerkey)) {
+    $toolproxy = lti_get_tool_proxy_from_guid($consumerkey);
+    if ($toolproxy !== false) {
+        $secrets = array($toolproxy->secret);
+    } else if (!empty($tool)) {
+        $secrets = array($typeconfig['password']);
+    } else {
+        $secrets = lti_get_shared_secrets_by_key($consumerkey);
+    }
+    $sharedsecret = lti_verify_message($consumerkey, lti_get_shared_secrets_by_key($consumerkey), $rawbody);
+    if ($sharedsecret === false) {
+        throw new Exception('Message signature not valid');
     }
-}
-
-if (empty($consumerkey)) {
-    throw new Exception('Consumer key is missing.');
-}
-
-$sharedsecret = lti_verify_message($consumerkey, lti_get_shared_secrets_by_key($consumerkey), $rawbody);
-
-if ($sharedsecret === false) {
-    throw new Exception('Message signature not valid');
 }
 
 // TODO MDL-46023 Replace this code with a call to the new library.
diff --git a/mod/lti/service/basicoutcomes/classes/local/resources/basicoutcomes.php b/mod/lti/service/basicoutcomes/classes/local/resources/basicoutcomes.php
new file mode 100644 (file)
index 0000000..3d9c02e
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains a class definition for the Basic Outcomes resource
+ *
+ * @package    ltiservice_basicoutcomes
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace ltiservice_basicoutcomes\local\resources;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A resource implementing the Basic Outcomes service.
+ *
+ * @package    ltiservice_basicoutcomes
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class basicoutcomes extends \mod_lti\local\ltiservice\resource_base {
+
+    /**
+     * Class constructor.
+     *
+     * @param \mod_lti\local\ltiservice\service_base $service Service instance
+     */
+    public function __construct($service) {
+
+        parent::__construct($service);
+        $this->id = 'Outcomes.LTI1';
+        $this->template = '';
+        $this->formats[] = 'application/vnd.ims.lti.v1.outcome+xml';
+        $this->methods[] = 'POST';
+
+    }
+
+    /**
+     * Get the resource fully qualified endpoint.
+     *
+     * @return string
+     */
+    public function get_endpoint() {
+
+        $url = new \moodle_url('/mod/lti/service.php');
+        return $url->out(false);
+
+    }
+
+    /**
+     * Execute the request for this resource.
+     *
+     * @param \mod_lti\local\ltiservice\response $response  Response object for this request.
+     */
+    public function execute($response) {
+        // Should never be called as the endpoint sends requests to the LTI 1 service endpoint.
+    }
+
+}
diff --git a/mod/lti/service/basicoutcomes/classes/local/service/basicoutcomes.php b/mod/lti/service/basicoutcomes/classes/local/service/basicoutcomes.php
new file mode 100644 (file)
index 0000000..32c2a47
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains a class definition for the Basic Outcomes service
+ *
+ * @package    ltiservice_basicoutcomes
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace ltiservice_basicoutcomes\local\service;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A service implementing Basic Outcomes.
+ *
+ * @package    ltiservice_basicoutcomes
+ * @since      Moodle 3.7
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class basicoutcomes extends \mod_lti\local\ltiservice\service_base {
+
+    /** Scope for accessing the service */
+    const SCOPE_BASIC_OUTCOMES = 'https://purl.imsglobal.org/spec/lti-bo/scope/basicoutcome';
+
+    /**
+     * Class constructor.
+     */
+    public function __construct() {
+
+        parent::__construct();
+        $this->id = 'basicoutcomes';
+        $this->name = 'Basic Outcomes';
+
+    }
+
+    /**
+     * Get the resources for this service.
+     *
+     * @return array
+     */
+    public function get_resources() {
+
+        if (empty($this->resources)) {
+            $this->resources = array();
+            $this->resources[] = new \ltiservice_basicoutcomes\local\resources\basicoutcomes($this);
+        }
+
+        return $this->resources;
+
+    }
+    /**
+     * Get the scope(s) permitted for the tool relevant to this service.
+     *
+     * @return array
+     */
+    public function get_permitted_scopes() {
+
+        $scopes = array();
+        if (!isset($this->get_typeconfig()['acceptgrades']) || ($this->get_typeconfig()['acceptgrades'] != LTI_SETTING_NEVER)) {
+            $scopes[] = self::SCOPE_BASIC_OUTCOMES;
+        }
+
+        return $scopes;
+
+    }
+
+}
diff --git a/mod/lti/service/basicoutcomes/classes/privacy/provider.php b/mod/lti/service/basicoutcomes/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..a31ea9b
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy Subsystem implementation for ltiservice_basicoutcomes.
+ *
+ * @package    ltiservice_basicoutcomes
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace ltiservice_basicoutcomes\privacy;
+
+use \core_privacy\local\metadata\collection;
+use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\userlist;
+use \core_privacy\local\request\approved_userlist;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for ltiservice_basicoutcomes.
+ *
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+        \core_privacy\local\metadata\provider,
+        \core_privacy\local\request\core_userlist_provider,
+        \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @param collection $collection The initialised collection to add items to.
+     * @return collection A listing of user data stored through this system.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        $collection->link_external_location('External LTI provider.', [
+            'userid' => 'privacy:metadata:userid',
+            'grade' => 'privacy:metadata:grade',
+        ], 'privacy:metadata:externalpurpose');
+
+        return $collection;
+    }
+
+    /**
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param int $userid The user to search.
+     * @return contextlist The contextlist containing the list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        return new contextlist();
+    }
+
+    /**
+     * Get the list of users who have data within a context.
+     *
+     * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
+     */
+    public static function get_users_in_context(userlist $userlist) {
+    }
+
+    /**
+     * Export all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts to export information for.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+    }
+
+    /**
+     * Delete all user data which matches the specified context.
+     *
+     * @param \context $context A user context.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+    }
+
+    /**
+     * Delete multiple users within a single context.
+     *
+     * @param approved_userlist $userlist The approved context and user information to delete information for.
+     */
+    public static function delete_data_for_users(approved_userlist $userlist) {
+    }
+
+    /**
+     * Delete all user data for the specified user, in the specified contexts.
+     *
+     * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+    }
+}
diff --git a/mod/lti/service/basicoutcomes/lang/en/ltiservice_basicoutcomes.php b/mod/lti/service/basicoutcomes/lang/en/ltiservice_basicoutcomes.php
new file mode 100644 (file)
index 0000000..3c660ac
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'ltiservice_basicoutcomes', language 'en'
+ *
+ * @package    ltiservice_basicoutcomes
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['allow'] = 'Use this service to accept grades from the tool';
+$string['ltiservice_basicoutcomes'] = 'Basic Outcomes';
+$string['ltiservice_basicoutcomes_help'] = 'Allow the tool to save and retrieve its grades to a gradebook column associated with each link.';
+$string['notallow'] = 'Do not use this service';
+$string['pluginname'] = 'Basic Outcomes Service';
+$string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.';
+$string['privacy:metadata:grade'] = 'The tool\'s grades of the user using the LTI consumer.';
+$string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.';
diff --git a/mod/lti/service/basicoutcomes/version.php b/mod/lti/service/basicoutcomes/version.php
new file mode 100644 (file)
index 0000000..ce05dce
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Version information for the ltiservice_basicoutcomes service.
+ *
+ * @package    ltiservice_basicoutcomes
+ * @copyright  2019 Stephen Vickers
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+$plugin->version   = 2019030500;
+$plugin->requires  = 2018112800;
+$plugin->component = 'ltiservice_basicoutcomes';
index 16a3620..a713173 100644 (file)
@@ -68,86 +68,51 @@ class lineitem extends resource_base {
         $params = $this->parse_template();
         $contextid = $params['context_id'];
         $itemid = $params['item_id'];
-        if ($response->get_request_method() === 'GET') {
-            $contenttype = $response->get_accept();
-        } else {
-            $contenttype = $response->get_content_type();
-        }
+        $isget = $response->get_request_method() === self::HTTP_GET;
         // We will receive typeid when working with LTI 1.x, if not then we are in LTI 2.
-        $typeid = optional_param('type_id', null, PARAM_ALPHANUM);
-        if (is_null($typeid)) {
-            if (!$this->check_tool_proxy(null, $response->get_request_data())) {
-                $response->set_code(403);
-                $response->set_reason("Invalid tool proxy specified.");
-                return;
+        $typeid = optional_param('type_id', null, PARAM_INT);
+
+        $scopes = array(gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM);
+        if ($response->get_request_method() === self::HTTP_GET) {
+            $scopes[] = gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ;
+        }
+
+        try {
+            if (!$this->check_tool($typeid, $response->get_request_data(), $scopes)) {
+                throw new \Exception(null, 401);
             }
-        } else {
+            $typeid = $this->get_service()->get_type()->id;
+            if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) {
+                throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404);
+            }
+            if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) {
+                throw new \Exception('Not allowed in context', 403);
+            }
+            if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
+                throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404);
+            }
+            $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
+            if ($item === false) {
+                throw new \Exception('Line item does not exist', 404);
+            }
+            require_once($CFG->libdir.'/gradelib.php');
             switch ($response->get_request_method()) {
                 case self::HTTP_GET:
-                    if (!$this->check_type($typeid, $contextid, 'LineItem.item:get', $response->get_request_data())) {
-                        $response->set_code(403);
-                        $response->set_reason("This resource does not support GET requests.");
-                        return;
-                    }
+                    $this->get_request($response, $item, $typeid);
                     break;
                 case self::HTTP_PUT:
-                    if (!$this->check_type($typeid, $contextid, 'LineItem.item:put', $response->get_request_data())) {
-                        $response->set_code(403);
-                        $response->set_reason("This resource does not support PUT requests.");
-                        return;
-                    }
+                    $json = $this->process_put_request($response->get_request_data(), $item, $typeid);
+                    $response->set_body($json);
+                    $response->set_code(200);
                     break;
                 case self::HTTP_DELETE:
-                    if (!$this->check_type($typeid, $contextid, 'LineItem.item:delete', $response->get_request_data())) {
-                        $response->set_code(403);
-                        $response->set_reason("This resource does not support DELETE requests.");
-                        return;
-                    }
+                    $this->process_delete_request($item);
+                    $response->set_code(204);
                     break;
-                default:  // Should not be possible.
-                    $response->set_code(405);
-                    return;
             }
-        }
-        if (empty($contextid) || (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
-            $response->set_code(400);
-            $response->set_reason("Invalid request made.");
-            return;
-        }
-        if (!$DB->record_exists('course', array('id' => $contextid))) {
-            $response->set_code(404);
-            $response->set_reason("Not Found: Course $contextid doesn't exist.");
-            return;
-        }
-        if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
-            $response->set_code(404);
-            $response->set_reason("Not Found: Grade item $itemid doesn't exist.");
-            return;
-        }
-        $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
-        if ($item === false) {
-            $response->set_code(403);
-            $response->set_reason("Line item does not exist.");
-            return;
-        }
-        require_once($CFG->libdir.'/gradelib.php');
-        switch ($response->get_request_method()) {
-            case 'GET':
-                $this->get_request($response, $item, $typeid);
-                break;
-            case 'PUT':
-                $json = $this->process_put_request($response->get_request_data(), $item, $typeid);
-                $response->set_body($json);
-                $response->set_code(200);
-                break;
-            case 'DELETE':
-                $this->process_delete_request($item);
-                $response->set_code(204);
-                break;
-            default:  // Should not be possible.
-                $response->set_code(405);
-                $response->set_reason("Invalid request method specified.");
-                return;
+        } catch (\Exception $e) {
+            $response->set_code($e->getCode());
+            $response->set_reason($e->getMessage());
         }
     }
 
@@ -218,7 +183,24 @@ class lineitem extends resource_base {
             $gbs->tag = $tag;
         }
         $ltilinkid = null;
-        if (isset($json->ltiLinkId)) {
+        if (isset($json->resourceLinkId)) {
+            if (is_numeric($json->resourceLinkId)) {
+                $ltilinkid = $json->resourceLinkId;
+                if ($gbs) {
+                    if (intval($gbs->ltilinkid) !== intval($json->resourceLinkId)) {
+                        $gbs->ltilinkid = $json->resourceLinkId;
+                        $upgradegradebookservices = true;
+                    }
+                } else {
+                    if (intval($item->iteminstance) !== intval($json->resourceLinkId)) {
+                        $item->iteminstance = intval($json->resourceLinkId);
+                        $updategradeitem = true;
+                    }
+                }
+            } else {
+                throw new \Exception(null, 400);
+            }
+        } else if (isset($json->ltiLinkId)) {
             if (is_numeric($json->ltiLinkId)) {
                 $ltilinkid = $json->ltiLinkId;
                 if ($gbs) {
@@ -316,24 +298,6 @@ class lineitem extends resource_base {
         }
     }
 
-    /**
-     * Get permissions from the config of the tool for that resource
-     *
-     * @param int $typeid
-     *
-     * @return array with the permissions related to this resource by the $lti_type or null if none.
-     */
-    public function get_permissions($typeid) {
-        $tool = lti_get_type_type_config($typeid);
-        if ($tool->ltiservice_gradesynchronization == '1') {
-            return array('LineItem.item:get');
-        } else if ($tool->ltiservice_gradesynchronization == '2') {
-            return array('LineItem.item:get', 'LineItem.item:put', 'LineItem.item:delete');
-        } else {
-            return array();
-        }
-    }
-
     /**
      * Parse a value for custom parameter substitution variables.
      *
@@ -348,7 +312,13 @@ class lineitem extends resource_base {
             require_once($CFG->libdir . '/gradelib.php');
 
             $this->params['context_id'] = $COURSE->id;
+            if ($tool = $this->get_service()->get_type()) {
+                $this->params['tool_code'] = $tool->id;
+            }
             $id = optional_param('id', 0, PARAM_INT); // Course Module ID.
+            if (empty($id)) {
+                $id = optional_param('lti_message_hint', 0, PARAM_INT);
+            }
             if (!empty($id)) {
                 $cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST);
                 $id = $cm->instance;
@@ -356,6 +326,7 @@ class lineitem extends resource_base {
                 if ($item && $item->items) {
                     $this->params['item_id'] = $item->items[0]->id;
                     $resolved = parent::get_endpoint();
+                    $resolved .= "?type_id={$tool->id}";
                 }
             }
             $value = str_replace('$LineItem.url', $resolved, $value);
index cd70180..2efc688 100644 (file)
@@ -74,51 +74,35 @@ class lineitems extends resource_base {
             $contenttype = $response->get_content_type();
         }
         $container = empty($contenttype) || ($contenttype === $this->formats[0]);
-        // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2.
-        $typeid = optional_param('type_id', null, PARAM_ALPHANUM);
-        if (is_null($typeid)) {
-            if (!$this->check_tool_proxy(null, $response->get_request_data())) {
-                $response->set_code(403);
-                $response->set_reason("Invalid tool proxy specified.");
-                return;
+        // We will receive typeid when working with LTI 1.x, if not then we are in LTI 2.
+        $typeid = optional_param('type_id', null, PARAM_INT);
+
+        $scopes = array(gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM);
+        if ($response->get_request_method() === self::HTTP_GET) {
+            $scopes[] = gradebookservices::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ;
+        }
+
+        try {
+            if (!$this->check_tool($typeid, $response->get_request_data(), $scopes)) {
+                throw new \Exception(null, 401);
             }
-        } else {
-            switch ($response->get_request_method()) {
-                case self::HTTP_GET:
-                    if (!$this->check_type($typeid, $contextid, 'LineItem.collection:get', $response->get_request_data())) {
-                        $response->set_code(403);
-                        $response->set_reason("This resource does not support GET requests.");
-                        return;
-                    }
-                    break;
-                case self::HTTP_POST:
-                    if (!$this->check_type($typeid, $contextid, 'LineItem.collection:post', $response->get_request_data())) {
-                        $response->set_code(403);
-                        $response->set_reason("This resource does not support POST requests.");
-                        return;
-                    }
-                    break;
-                default:  // Should not be possible.
-                    $response->set_code(405);
-                    $response->set_reason("Invalid request method specified.");
-                    return;
+            $typeid = $this->get_service()->get_type()->id;
+            if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) ||
+                    (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
+                    throw new \Exception('No context or unsupported content type', 400);
             }
-        }
-        if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) ||
-                (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
-            $response->set_code(400);
-            $response->set_reason("Invalid request made.");
-            return;
-        }
-        if (!$DB->record_exists('course', array('id' => $contextid))) {
-            $response->set_code(404);
-            $response->set_reason("Not Found: Course $contextid doesn't exist.");
-            return;
-        }
-        switch ($response->get_request_method()) {
-            case self::HTTP_GET:
+            if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) {
+                throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404);
+            }
+            if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) {
+                throw new \Exception('Not allowed in context', 403);
+            }
+            if ($response->get_request_method() !== self::HTTP_POST) {
                 $resourceid = optional_param('resource_id', null, PARAM_TEXT);
-                $ltilinkid = optional_param('lti_link_id', null, PARAM_TEXT);
+                $ltilinkid = optional_param('resource_link_id', null, PARAM_TEXT);
+                if (is_null($ltilinkid)) {
+                    $ltilinkid = optional_param('lti_link_id', null, PARAM_TEXT);
+                }
                 $tag = optional_param('tag', null, PARAM_TEXT);
                 $limitnum = optional_param('limit', 0, PARAM_INT);
                 $limitfrom = optional_param('from', 0, PARAM_INT);
@@ -129,23 +113,18 @@ class lineitems extends resource_base {
                 $json = $this->get_json_for_get_request($items, $resourceid, $ltilinkid, $tag, $limitfrom,
                         $limitnum, $totalcount, $typeid, $response);
                 $response->set_content_type($this->formats[0]);
-                break;
-            case self::HTTP_POST:
-                try {
-                    $json = $this->get_json_for_post_request($response->get_request_data(), $contextid, $typeid);
-                    $response->set_code(201);
-                    $response->set_content_type($this->formats[1]);
-                } catch (\Exception $e) {
-                    $response->set_code($e->getCode());
-                    $response->set_reason($e->getMessage());
-                }
-                break;
-            default:  // Should not be possible.
-                $response->set_code(405);
-                $response->set_reason("Invalid request method specified.");
-                return;
+            } else {
+                $json = $this->get_json_for_post_request($response->get_request_data(), $contextid, $typeid);
+                $response->set_code(201);
+                $response->set_content_type($this->formats[1]);
+            }
+            $response->set_body($json);
+
+        } catch (\Exception $e) {
+            $response->set_code($e->getCode());
+            $response->set_reason($e->getMessage());
         }
-        $response->set_body($json);
+
     }
 
     /**
@@ -186,7 +165,7 @@ class lineitems extends resource_base {
                 $baseurl->param('resource_id', $resourceid);
             }
             if (isset($ltilinkid)) {
-                $baseurl->param('lti_link_id', $ltilinkid);
+                $baseurl->param('resource_link_id', $ltilinkid);
             }
             if (isset($tag)) {
                 $baseurl->param('tag', $tag);
@@ -217,7 +196,7 @@ class lineitems extends resource_base {
             }
         }
 
-        $jsonitems=[];
+        $jsonitems = [];
         $endpoint = parent::get_endpoint();
         foreach ($items as $item) {
             array_push($jsonitems, gradebookservices::item_for_json($item, $endpoint, $typeid));
@@ -255,7 +234,7 @@ class lineitems extends resource_base {
         if (empty($json) ||
                 !isset($json->scoreMaximum) ||
                 !isset($json->label)) {
-            throw new \Exception(null, 400);
+            throw new \Exception('No label or Score Maximum', 400);
         }
         if (is_numeric($json->scoreMaximum)) {
             $max = $json->scoreMaximum;
@@ -264,7 +243,10 @@ class lineitems extends resource_base {
         }
         require_once($CFG->libdir.'/gradelib.php');
         $resourceid = (isset($json->resourceId)) ? $json->resourceId : '';
-        $ltilinkid = (isset($json->ltiLinkId)) ? $json->ltiLinkId : null;
+        $ltilinkid = (isset($json->resourceLinkId)) ? $json->resourceLinkId : null;
+        if ($ltilinkid == null) {
+            $ltilinkid = (isset($json->ltiLinkId)) ? $json->ltiLinkId : null;
+        }
         if ($ltilinkid != null) {
             if (is_null($typeid)) {
                 if (!gradebookservices::check_lti_id($ltilinkid, $contextid, $this->get_service()->get_tool_proxy()->id)) {
@@ -313,24 +295,6 @@ class lineitems extends resource_base {
 
     }
 
-    /**
-     * get permissions from the config of the tool for that resource
-     *
-     * @param string $typeid
-     *
-     * @return array with the permissions related to this resource by the lti type or null if none.
-     */
-    public function get_permissions($typeid) {
-        $tool = lti_get_type_type_config($typeid);
-        if ($tool->ltiservice_gradesynchronization == '1') {
-            return array('LineItem.collection:get');
-        } else if ($tool->ltiservice_gradesynchronization == '2') {
-            return array('LineItem.collection:get', 'LineItem.collection:post');
-        } else {
-            return array();
-        }
-    }
-
     /**
      * Parse a value for custom parameter substitution variables.
      *
@@ -343,7 +307,11 @@ class lineitems extends resource_base {
 
         if (strpos($value, '$LineItems.url') !== false) {
             $this->params['context_id'] = $COURSE->id;
-            $value = str_replace('$LineItems.url', parent::get_endpoint(), $value);
+            $query = '';
+            if (($tool = $this->get_service()->get_type())) {
+                $query = "?type_id={$tool->id}";
+            }
+            $value = str_replace('$LineItems.url', parent::get_endpoint() . $query, $value);
         }
 
         return $value;
index 445c1dc..61a6560 100644 (file)
@@ -31,7 +31,7 @@ use mod_lti\local\ltiservice\resource_base;
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * A resource implementing LISResults container.
+ * A resource implementing LISResult container.
  *
  * @package    ltiservice_gradebookservices
  * @copyright  2017 Cengage Learning http://www.cengage.com
@@ -66,90 +66,76 @@ class results extends resource_base {
         $contextid = $params['context_id'];
         $itemid = $params['item_id'];
 
-        $isget = $response->get_request_method() === 'GET';
-        if ($isget) {
-            $contenttype = $response->get_accept();
-        } else {
-            $contenttype = $response->get_content_type();
-        }
+        $isget = $response->get_request_method() === self::HTTP_GET;
         // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2.
-        $typeid = optional_param('type_id', null, PARAM_ALPHANUM);
-        if (is_null($typeid)) {
-            if (!$this->check_tool_proxy(null, $response->get_request_data())) {
-                $response->set_code(403);
-                $response->set_reason("Invalid tool proxy specified.");
-                return;
+        $typeid = optional_param('type_id', null, PARAM_INT);
+
+        $scope = gradebookservices::SCOPE_GRADEBOOKSERVICES_RESULT_READ;
+
+        try {
+            if (!$this->check_tool($typeid, $response->get_request_data(), array($scope))) {
+                throw new \Exception(null, 401);
             }
-        } else {
-            if (!$this->check_type($typeid, $contextid, 'Result.collection:get', $response->get_request_data())) {
-                $response->set_code(403);
-                $response->set_reason("This resource does not support GET requests.");
-                return;
+            $typeid = $this->get_service()->get_type()->id;
+            if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) {
+                throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404);
             }
-        }
-        if (empty($contextid) || (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
-            $response->set_code(400);
-            $response->set_reason("Invalid request made.");
-            return;
-        }
-        if (!$DB->record_exists('course', array('id' => $contextid))) {
-            $response->set_code(404);
-            $response->set_reason("Not Found: Course $contextid doesn't exist.");
-            return;
-        }
-        if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
-            $response->set_code(404);
-            $response->set_reason("Not Found: Grade item $itemid doesn't exist.");
-            return;
-        }
-        $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
-        if ($item === false) {
-            $response->set_code(403);
-            $response->set_reason("Line item does not exist.");
-            return;
-        }
-        $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid);
-        $ltilinkid = null;
-        if (isset($item->iteminstance)) {
-            $ltilinkid = $item->iteminstance;
-        } else if ($gbs && isset($gbs->ltilinkid)) {
-            $ltilinkid = $gbs->ltilinkid;
-        }
-        if ($ltilinkid != null) {
-            if (is_null($typeid)) {
-                if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid,
-                        $this->get_service()->get_tool_proxy()->id))) {
-                    $response->set_code(403);
-                    $response->set_reason("Invalid LTI id supplied.");
-                    return;
+            if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) {
+                throw new \Exception('Not allowed in context', 403);
+            }
+            if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
+                throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404);
+            }
+            $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
+            if ($item === false) {
+                throw new \Exception('Line item does not exist', 404);
+            }
+            $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid);
+            $ltilinkid = null;
+            if (isset($item->iteminstance)) {
+                $ltilinkid = $item->iteminstance;
+            } else if ($gbs && isset($gbs->ltilinkid)) {
+                $ltilinkid = $gbs->ltilinkid;
+            }
+            if ($ltilinkid != null) {
+                if (is_null($typeid)) {
+                    if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid,
+                            $this->get_service()->get_tool_proxy()->id))) {
+                        $response->set_code(403);
+                        $response->set_reason("Invalid LTI id supplied.");
+                        return;
+                    }
+                } else {
+                    if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
+                            $typeid))) {
+                        $response->set_code(403);
+                        $response->set_reason("Invalid LTI id supplied.");
+                        return;
+                    }
                 }
-            } else {
-                if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
-                        $typeid))) {
-                    $response->set_code(403);
-                    $response->set_reason("Invalid LTI id supplied.");
+            }
+            require_once($CFG->libdir.'/gradelib.php');
+            switch ($response->get_request_method()) {
+                case 'GET':
+                    $useridfilter = optional_param('user_id', 0, PARAM_INT);
+                    $limitnum = optional_param('limit', 0, PARAM_INT);
+                    $limitfrom = optional_param('from', 0, PARAM_INT);
+                    $typeid = optional_param('type_id', null, PARAM_TEXT);
+                    $json = $this->get_json_for_get_request($item->id, $limitfrom, $limitnum,
+                            $useridfilter, $typeid, $response);
+                    $response->set_content_type($this->formats[0]);
+                    $response->set_body($json);
+                    break;
+                default:  // Should not be possible.
+                    $response->set_code(405);
+                    $response->set_reason("Invalid request method specified.");
                     return;
-                }
             }
+            $response->set_body($json);
+        } catch (\Exception $e) {
+            $response->set_code($e->getCode());
+            $response->set_reason($e->getMessage());
         }
-        require_once($CFG->libdir.'/gradelib.php');
-        switch ($response->get_request_method()) {
-            case 'GET':
-                $useridfilter = optional_param('user_id', 0, PARAM_INT);
-                $limitnum = optional_param('limit', 0, PARAM_INT);
-                $limitfrom = optional_param('from', 0, PARAM_INT);
-                $typeid = optional_param('type_id', null, PARAM_TEXT);
-                $json = $this->get_json_for_get_request($item->id, $limitfrom, $limitnum,
-                        $useridfilter, $typeid, $response);
-                $response->set_content_type($this->formats[0]);
-                $response->set_body($json);
-                break;
-            default:  // Should not be possible.
-                $response->set_code(405);
-                $response->set_reason("Invalid request method specified.");
-                return;
-        }
-        $response->set_body($json);
     }
 
     /**
@@ -255,24 +241,6 @@ class results extends resource_base {
         return json_encode($jsonresults);
     }
 
-    /**
-     * get permissions from the config of the tool for that resource
-     *
-     * @param int $typeid
-     *
-     * @return array with the permissions related to this resource by the $lti_type or null if none.
-     */
-    public function get_permissions($typeid) {
-        $tool = lti_get_type_type_config($typeid);
-        if ($tool->ltiservice_gradesynchronization == '1') {
-            return array('Result.collection:get');
-        } else if ($tool->ltiservice_gradesynchronization == '2') {
-            return array('Result.collection:get');
-        } else {
-            return array();
-        }
-    }
-
     /**
      * Parse a value for custom parameter substitution variables.
      *
index afe63ea..4bc4e2a 100644 (file)
@@ -80,96 +80,82 @@ class scores extends resource_base {
         $container = empty($contenttype) || ($contenttype === $this->formats[0]);
         // We will receive typeid when working with LTI 1.x, if not the we are in LTI 2.
         $typeid = optional_param('type_id', null, PARAM_ALPHANUM);
-        if (is_null($typeid)) {
-            if (!$this->check_tool_proxy(null, $response->get_request_data())) {
-                $response->set_code(403);
-                return;
+
+        $scope = gradebookservices::SCOPE_GRADEBOOKSERVICES_SCORE;
+
+        try {
+            if (!$this->check_tool($typeid, $response->get_request_data(), array($scope))) {
+                throw new \Exception(null, 401);
             }
-        } else {
+            $typeid = $this->get_service()->get_type()->id;
+            if (empty($contextid) || !($container ^ ($response->get_request_method() === self::HTTP_POST)) ||
+                    (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
+                throw new \Exception('No context or unsupported content type', 400);
+            }
+            if (!($course = $DB->get_record('course', array('id' => $contextid), 'id', IGNORE_MISSING))) {
+                throw new \Exception("Not Found: Course {$contextid} doesn't exist", 404);
+            }
+            if (!$this->get_service()->is_allowed_in_context($typeid, $course->id)) {
+                throw new \Exception('Not allowed in context', 403);
+            }
+            if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
+                throw new \Exception("Not Found: Grade item {$itemid} doesn't exist", 404);
+            }
+            $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
+            if ($item === false) {
+                throw new \Exception('Line item does not exist', 404);
+            }
+            $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid);
+            $ltilinkid = null;
+            if (isset($item->iteminstance)) {
+                $ltilinkid = $item->iteminstance;
+            } else if ($gbs && isset($gbs->ltilinkid)) {
+                $ltilinkid = $gbs->ltilinkid;
+            }
+            if ($ltilinkid != null) {
+                if (is_null($typeid)) {
+                    if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid,
+                            $this->get_service()->get_tool_proxy()->id))) {
+                        $response->set_code(403);
+                        $response->set_reason("Invalid LTI id supplied.");
+                        return;
+                    }
+                } else {
+                    if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
+                            $typeid))) {
+                        $response->set_code(403);
+                        $response->set_reason("Invalid LTI id supplied.");
+                        return;
+                    }
+                }
+            }
+            $json = '[]';
+            require_once($CFG->libdir.'/gradelib.php');
             switch ($response->get_request_method()) {
                 case 'GET':
                     $response->set_code(405);
                     $response->set_reason("GET requests are not allowed.");
-                    return;
+                    break;
                 case 'POST':
-                    if (!$this->check_type($typeid, $contextid, 'Score.collection:post', $response->get_request_data())) {
-                        $response->set_code(401);
-                        $response->set_reason("This resource does not support POST requests.");
-                        return;
+                    try {
+                        $json = $this->get_json_for_post_request($response, $response->get_request_data(), $item, $contextid,
+                            $typeid);
+                        $response->set_content_type($this->formats[1]);
+                    } catch (\Exception $e) {
+                        $response->set_code($e->getCode());
+                        $response->set_reason($e->getMessage());
                     }
                     break;
                 default:  // Should not be possible.
                     $response->set_code(405);
+                    $response->set_reason("Invalid request method specified.");
                     return;
             }
+            $response->set_body($json);
+        } catch (\Exception $e) {
+            $response->set_code($e->getCode());
+            $response->set_reason($e->getMessage());
         }
-        if (empty($contextid) || !($container ^ ($response->get_request_method() === 'POST')) ||
-                (!empty($contenttype) && !in_array($contenttype, $this->formats))) {
-            $response->set_code(400);
-            return;
-        }
-        if (!$DB->record_exists('course', array('id' => $contextid))) {
-            $response->set_code(404);
-            $response->set_reason("Not Found: Course $contextid doesn't exist.");
-            return;
-        }
-        if (!$DB->record_exists('grade_items', array('id' => $itemid))) {
-            $response->set_code(404);
-            $response->set_reason("Not Found: Grade item $itemid doesn't exist.");
-            return;
-        }
-        $item = $this->get_service()->get_lineitem($contextid, $itemid, $typeid);
-        if ($item === false) {
-            $response->set_code(403);
-            $response->set_reason("Line item does not exist.");
-            return;
-        }
-        $gbs = gradebookservices::find_ltiservice_gradebookservice_for_lineitem($itemid);
-        $ltilinkid = null;
-        if (isset($item->iteminstance)) {
-            $ltilinkid = $item->iteminstance;
-        } else if ($gbs && isset($gbs->ltilinkid)) {
-            $ltilinkid = $gbs->ltilinkid;
-        }
-        if ($ltilinkid != null) {
-            if (is_null($typeid)) {
-                if (isset($item->iteminstance) && (!gradebookservices::check_lti_id($ltilinkid, $item->courseid,
-                        $this->get_service()->get_tool_proxy()->id))) {
-                    $response->set_code(403);
-                    $response->set_reason("Invalid LTI id supplied.");
-                    return;
-                }
-            } else {
-                if (isset($item->iteminstance) && (!gradebookservices::check_lti_1x_id($ltilinkid, $item->courseid,
-                        $typeid))) {
-                    $response->set_code(403);
-                    $response->set_reason("Invalid LTI id supplied.");
-                    return;
-                }
-            }
-        }
-        $json = '[]';
-        require_once($CFG->libdir.'/gradelib.php');
-        switch ($response->get_request_method()) {
-            case 'GET':
-                $response->set_code(405);
-                $response->set_reason("GET requests are not allowed.");
-                break;
-            case 'POST':
-                try {
-                    $json = $this->get_json_for_post_request($response, $response->get_request_data(), $item, $contextid, $typeid);
-                    $response->set_content_type($this->formats[1]);
-                } catch (\Exception $e) {
-                    $response->set_code($e->getCode());
-                    $response->set_reason($e->getMessage());
-                }
-                break;
-            default:  // Should not be possible.
-                $response->set_code(405);
-                $response->set_reason("Invalid request method specified.");
-                return;
-        }
-        $response->set_body($json);
     }
 
     /**
@@ -193,10 +179,11 @@ class scores extends resource_base {
                 !isset($score->timestamp) ||
                 isset($score->timestamp) && !gradebookservices::validate_iso8601_date($score->timestamp) ||
                 (isset($score->scoreGiven) && !is_numeric($score->scoreGiven)) ||
+                (isset($score->scoreGiven) && !isset($score->scoreMaximum)) ||
                 (isset($score->scoreMaximum) && !is_numeric($score->scoreMaximum)) ||
                 (!gradebookservices::is_user_gradable_in_course($contextid, $score->userId))
                 ) {
-            throw new \Exception('Incorrect score received' . $score, 400);
+            throw new \Exception('Incorrect score received' . $body, 400);
         }
         $score->timemodified = intval($score->timestamp);
 
@@ -216,25 +203,7 @@ class scores extends resource_base {
                 $score->scoreGiven = null;
             }
         }
-        gradebookservices::save_score($item, $score, $score->userId, $typeid);
-    }
-
-    /**
-     * get permissions from the config of the tool for that resource
-     *
-     * @param int $typeid
-     *
-     * @return array with the permissions related to this resource by the $lti_type or null if none.
-     */
-    public function get_permissions($typeid) {
-        $tool = lti_get_type_type_config($typeid);
-        if ($tool->ltiservice_gradesynchronization == '1') {
-            return array('Score.collection:post');
-        } else if ($tool->ltiservice_gradesynchronization == '2') {
-            return array('Score.collection:post');
-        } else {
-            return array();
-        }
+        $this->get_service()->save_grade_item($item, $score, $score->userId);
     }
 
     /**
index e6914c3..2a7c001 100644 (file)
@@ -43,8 +43,19 @@ defined('MOODLE_INTERNAL') || die();
  */
 class gradebookservices extends service_base {
 
-    /** Internal service name */
-    const SERVICE_NAME = 'ltiservice_gradebookservices';
+    /** Read-only access to Gradebook services */
+    const GRADEBOOKSERVICES_READ = 1;
+    /** Full access to Gradebook services */
+    const GRADEBOOKSERVICES_FULL = 2;
+    /** Scope for full access to Lineitem service */
+    const SCOPE_GRADEBOOKSERVICES_LINEITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem';
+    /** Scope for full access to Lineitem service */
+    const SCOPE_GRADEBOOKSERVICES_LINEITEM_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly';
+    /** Scope for access to Result service */
+    const SCOPE_GRADEBOOKSERVICES_RESULT_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly';
+    /** Scope for access to Score service */
+    const SCOPE_GRADEBOOKSERVICES_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score';
+
 
     /**
      * Class constructor.
@@ -53,7 +64,7 @@ class gradebookservices extends service_base {
 
         parent::__construct();
         $this->id = 'gradebookservices';
-        $this->name = $this->get_string('servicename');
+        $this->name = get_string($this->get_component_id(), $this->get_component_id());
 
     }
 
@@ -77,6 +88,30 @@ class gradebookservices extends service_base {
         return $this->resources;
     }
 
+    /**
+     * Get the scope(s) permitted for this service.
+     *
+     * @return array
+     */
+    public function get_permitted_scopes() {
+
+        $scopes = array();
+        $ok = !empty($this->get_type());
+        if ($ok && isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) {
+            if (!empty($setting = $this->get_typeconfig()['ltiservice_gradesynchronization'])) {
+                $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM_READ;
+                $scopes[] = self::SCOPE_GRADEBOOKSERVICES_RESULT_READ;
+                $scopes[] = self::SCOPE_GRADEBOOKSERVICES_SCORE;
+                if ($setting == self::GRADEBOOKSERVICES_FULL) {
+                    $scopes[] = self::SCOPE_GRADEBOOKSERVICES_LINEITEM;
+                }
+            }
+        }
+
+        return $scopes;
+
+    }
+
     /**
      * Adds form elements for gradebook sync add/edit page.
      *
@@ -87,35 +122,15 @@ class gradebookservices extends service_base {
         $selectelementname = 'ltiservice_gradesynchronization';
         $identifier = 'grade_synchronization';
         $options = [
-            $this->get_string('nevergs'),
-            $this->get_string('partialgs'),
-            $this->get_string('alwaysgs')
+            get_string('nevergs', $this->get_component_id()),
+            get_string('partialgs', $this->get_component_id()),
+            get_string('alwaysgs', $this->get_component_id())
         ];
 
-        $mform->addElement('select', $selectelementname, $this->get_string($identifier), $options);
+        $mform->addElement('select', $selectelementname, get_string($identifier, $this->get_component_id()), $options);
         $mform->setType($selectelementname, 'int');
         $mform->setDefault($selectelementname, 0);
-        $mform->addHelpButton($selectelementname, $identifier, self::SERVICE_NAME);
-    }
-
-    /**
-     * Retrieves string from lang file
-     *
-     * @param string $identifier
-     * @return string
-     */
-    private function get_string($identifier) {
-        return get_string($identifier, self::SERVICE_NAME);
-    }
-
-    /**
-     * Return an array with the names of the parameters that the service will be saving in the configuration
-     *
-     * @return array with the names of the parameters that the service will be saving in the configuration
-     *
-     */
-    public function get_configuration_parameter_names() {
-        return array('ltiservice_gradesynchronization');
+        $mform->addHelpButton($selectelementname, $identifier, $this->get_component_id());
     }
 
     /**
@@ -134,15 +149,15 @@ class gradebookservices extends service_base {
      */
     public function get_launch_parameters($messagetype, $courseid, $user, $typeid, $modlti = null) {
         global $DB;
-
         $launchparameters = array();
-        $tool = lti_get_type_type_config($typeid);
+        $this->set_type(lti_get_type($typeid));
+        $this->set_typeconfig(lti_get_type_config($typeid));
         // Only inject parameters if the service is enabled for this tool.
-        if (isset($tool->ltiservice_gradesynchronization)) {
-            if ($tool->ltiservice_gradesynchronization == '1' || $tool->ltiservice_gradesynchronization == '2') {
+        if (isset($this->get_typeconfig()['ltiservice_gradesynchronization'])) {
+            if ($this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_READ ||
+                $this->get_typeconfig()['ltiservice_gradesynchronization'] == self::GRADEBOOKSERVICES_FULL) {
                 // Check for used in context is only needed because there is no explicit site tool - course relation.
                 if ($this->is_allowed_in_context($typeid, $courseid)) {
-                    $endpoint = $this->get_service_path() . "/{$courseid}/lineitems";
                     if (is_null($modlti)) {
                         $id = null;
                     } else {
@@ -164,9 +179,10 @@ class gradebookservices extends service_base {
                             $id = null;
                         }
                     }
-                    $launchparameters['custom_lineitems_url'] = $endpoint . "?type_id={$typeid}";
+                    $launchparameters['gradebookservices_scope'] = implode(',', $this->get_permitted_scopes());
+                    $launchparameters['lineitems_url'] = '$LineItems.url';
                     if (!is_null($id)) {
-                        $launchparameters['custom_lineitem_url'] = $endpoint . "/{$id}/lineitem?type_id={$typeid}";
+                        $launchparameters['lineitem_url'] = '$LineItem.url';
                     }
                 }
             }
@@ -316,10 +332,26 @@ class gradebookservices extends service_base {
      * @param int $userid User ID
      *
      * @throws \Exception
+     * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
+     * @see gradebookservices::save_grade_item($gradeitem, $score, $userid)
      */
     public static function save_score($gradeitem, $score, $userid) {
+        $service = new gradebookservices();
+        $service->save_grade_item($gradeitem, $score, $userid);
+    }
+
+    /**
+     * Set a grade item.
+     *
+     * @param object $gradeitem Grade Item record
+     * @param object $score Result object
+     * @param int $userid User ID
+     *
+     * @throws \Exception
+     */
+    public function save_grade_item($gradeitem, $score, $userid) {
         global $DB, $CFG;
-        $source = 'mod' . self::SERVICE_NAME;
+        $source = 'mod' . $this->get_component_id();
         if ($DB->get_record('user', array('id' => $userid)) === false) {
             throw new \Exception(null, 400);
         }
@@ -406,11 +438,13 @@ class gradebookservices extends service_base {
         if ($gbs) {
             $lineitem->tag = (!empty($gbs->tag)) ? $gbs->tag : '';
             if (isset($gbs->ltilinkid)) {
+                $lineitem->resourceLinkId = strval($gbs->ltilinkid);
                 $lineitem->ltiLinkId = strval($gbs->ltilinkid);
             }
         } else {
             $lineitem->tag = '';
             if (isset($item->iteminstance)) {
+                $lineitem->resourceLinkId = strval($item->iteminstance);
                 $lineitem->ltiLinkId = strval($item->iteminstance);
             }
         }
index 7d9a5d1..af5e189 100644 (file)
@@ -30,6 +30,7 @@ $string['grade_synchronization_help'] = 'Whether to use the IMS LTI Assignment a
 * **Do not use this service** -  Basic Outcomes features and configuration will be used
 * **Use this service for grade sync only** - The service will populate the grades in an already existing gradebook column, but it will not be able to create new columns
 * **Use this service for grade sync and column management** -  The service will be able to create and update gradebook columns and manage the grades.';
+$string['ltiservice_gradebookservices'] = 'IMS LTI Assignment and Grade Services';
 $string['modulename'] = 'LTI Grades';
 $string['nevergs'] = 'Do not use this service';
 $string['partialgs'] = 'Use this service for grade sync only';
@@ -40,5 +41,4 @@ $string['privacy:metadata:grade'] = 'The grade the user received in Moodle for t
 $string['privacy:metadata:maxgrade'] = 'The max grade that can be achieved for this LTI activity.';
 $string['privacy:metadata:timemodified'] = 'The last time the grade was updated';
 $string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.';
-$string['servicename'] = 'LTI Assignment and Grade Services';
 $string['taskcleanup'] = 'LTI Assignment and Grade Services table cleanup';
index 8536934..0ced868 100644 (file)
@@ -54,7 +54,8 @@ class contextmemberships extends resource_base {
         $this->template = '/{context_type}/{context_id}/bindings/{tool_code}/memberships';
         $this->variables[] = 'ToolProxyBinding.memberships.url';
         $this->formats[] = 'application/vnd.ims.lis.v2.membershipcontainer+json';
-        $this->methods[] = 'GET';
+        $this->formats[] = 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json';
+        $this->methods[] = self::HTTP_GET;
 
     }
 
@@ -79,19 +80,23 @@ class contextmemberships extends resource_base {
         }
 
         try {
-            if (!($course = $DB->get_record('course', array('id' => $params['context_id']), 'id', IGNORE_MISSING))) {
-                throw new \Exception(null, 404);
+            if (!$this->check_tool($params['tool_code'], $response->get_request_data(),
+                array(memberships::SCOPE_MEMBERSHIPS_READ))) {
+                throw new \Exception(null, 401);
             }
-            if (!($context = \context_course::instance($course->id))) {
-                throw new \Exception(null, 404);
+            if (!($course = $DB->get_record('course', array('id' => $params['context_id']), 'id,shortname,fullname',
+                IGNORE_MISSING))) {
+                throw new \Exception("Not Found: Course {$params['context_id']} doesn't exist", 404);
             }
-            if (!($tool = $DB->get_record('lti_types', array('id' => $params['tool_code']),
-                                    'id,toolproxyid,enabledcapability,parameter', IGNORE_MISSING))) {
+            if (!$this->get_service()->is_allowed_in_context($params['tool_code'], $course->id)) {
                 throw new \Exception(null, 404);
             }
+            if (!($context = \context_course::instance($course->id))) {
+                throw new \Exception("Not Found: Course instance {$course->id} doesn't exist", 404);
+            }
             if (!empty($linkid)) {
                 if (!($lti = $DB->get_record('lti', array('id' => $linkid), 'id,course,typeid,servicesalt', IGNORE_MISSING))) {
-                    throw new \Exception(null, 404);
+                    throw new \Exception("Not Found: LTI link {$linkid} doesn't exist", 404);
                 }
                 $modinfo = get_fast_modinfo($course);
                 $cm = get_coursemodule_from_instance('lti', $linkid, $lti->course, false, MUST_EXIST);
@@ -101,39 +106,15 @@ class contextmemberships extends resource_base {
                     $modinfo = null;
                 }
             }
-            if ($tool->toolproxyid == 0) {
-                if (!$this->check_type($params['tool_code'], $params['context_id'],
-                        'ToolProxyBinding.memberships.url:get', null)) {
-                    throw new \Exception(null, 403);
-                }
-            } else {
-                $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $tool->toolproxyid), 'guid', IGNORE_MISSING);
-                if (!$this->check_tool_proxy($toolproxy->guid, $response->get_request_data())) {
-                    throw new \Exception(null, 403);
-                }
-            }
-            $json = memberships::get_users_json($this, $context, $course->id, $tool, $role, $limitfrom, $limitnum, $lti, $modinfo);
 
-            $response->set_content_type($this->formats[0]);
+            $json = $this->get_service()->get_members_json($this, $context, $course, $role, $limitfrom, $limitnum, $lti,
+                $modinfo, $response);
+
             $response->set_body($json);
 
         } catch (\Exception $e) {
             $response->set_code($e->getCode());
-        }
-    }
-
-    /**
-     * get permissions from the config of the tool for that resource
-     *
-     * @param int $typeid
-     * @return array with the permissions related to this resource by the $lti_type or null if none.
-     */
-    public function get_permissions($typeid) {
-        $tool = lti_get_type_type_config($typeid);
-        if ($tool->ltiservice_memberships == '1') {
-            return array('ToolProxyBinding.memberships.url:get');
-        } else {
-            return array();
+            $response->set_reason($e->getMessage());
         }
     }
 
@@ -154,14 +135,8 @@ class contextmemberships extends resource_base {
                 $this->params['context_type'] = 'CourseSection';
             }
             $this->params['context_id'] = $COURSE->id;
-
-            $id = optional_param('id', 0, PARAM_INT); // Course Module ID.
-            if (!empty($id)) {
-                $cm = get_coursemodule_from_id('lti', $id, 0, false, IGNORE_MISSING);
-                $lti = $DB->get_record('lti', array('id' => $cm->instance), 'typeid', IGNORE_MISSING);
-                if ($lti && !empty($lti->typeid)) {
-                    $this->params['tool_code'] = $lti->typeid;
-                }
+            if ($tool = $this->get_service()->get_type()) {
+                $this->params['tool_code'] = $tool->id;
             }
             $value = str_replace('$ToolProxyBinding.memberships.url', parent::get_endpoint(), $value);
         }
index bcbb01a..619efb7 100644 (file)
@@ -79,6 +79,10 @@ class linkmemberships extends resource_base {
             $limitfrom = 0;
         }
 
+        if (!$this->check_tool(null, $response->get_request_data(), memberships::SCOPE_MEMBERSHIPS_READ)) {
+            $response->set_code(403);
+            return;
+        }
         if (empty($linkid)) {
             $response->set_code(404);
             return;
@@ -87,19 +91,6 @@ class linkmemberships extends resource_base {
             $response->set_code(404);
             return;
         }
-        $tool = $DB->get_record('lti_types', array('id' => $lti->typeid));
-        if ($tool->toolproxyid == 0) { // We wil use the same permission for this and contextmembers.
-            if (!$this->check_type($lti->typeid, $lti->course, 'ToolProxyBinding.memberships.url:get', null)) {
-                $response->set_code(403);
-                return;
-            }
-        } else {
-            $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $tool->toolproxyid));
-            if (!$this->check_tool_proxy($toolproxy->guid, $response->get_request_data())) {
-                $response->set_code(403);
-                return;
-            }
-        }
         if (!($course = $DB->get_record('course', array('id' => $lti->course), 'id', IGNORE_MISSING))) {
             $response->set_code(404);
             return;
@@ -115,7 +106,8 @@ class linkmemberships extends resource_base {
         if ($info->is_available_for_all()) {
             $info = null;
         }
-        $json = memberships::get_users_json($this, $context, $lti->course, $tool, $role, $limitfrom, $limitnum, $lti, $info);
+        $json = $this->get_service()->get_members_json($this, $context, $lti->course, $role,
+                                                       $limitfrom, $limitnum, $lti, $info, $response);
 
         $response->set_content_type($this->formats[0]);
         $response->set_body($json);
index 751a185..6b9a9f5 100644 (file)
@@ -45,10 +45,6 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
     const CONTEXT_ROLE_LEARNER = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner';
     /** Capability used to identify Instructors */
     const INSTRUCTOR_CAPABILITY = 'moodle/course:manageactivities';
-    /** Name of LTI service component */
-    const LTI_SERVICE_COMPONENT = 'ltiservice_memberships';
-    /** Membership services enabled */
-    const MEMBERSHIP_ENABLED = 1;
     /** Always include field */
     const ALWAYS_INCLUDE_FIELD = 1;
     /** Allow the instructor to decide if included */
@@ -57,6 +53,8 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
     const INSTRUCTOR_INCLUDED = 1;
     /** Instructor delegated and approved for include */
     const INSTRUCTOR_DELEGATE_INCLUDED = array(self::DELEGATE_TO_INSTRUCTOR && self::INSTRUCTOR_INCLUDED);
+    /** Scope for reading membership data */
+    const SCOPE_MEMBERSHIPS_READ = 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly';
 
     /**
      * Class constructor.
@@ -65,7 +63,7 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
 
         parent::__construct();
         $this->id = 'memberships';
-        $this->name = get_string('servicename', self::LTI_SERVICE_COMPONENT);
+        $this->name = get_string($this->get_component_id(), $this->get_component_id());
 
     }
 
@@ -86,6 +84,24 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
 
     }
 
+    /**
+     * Get the scope(s) permitted for the tool relevant to this service.
+     *
+     * @return array
+     */
+    public function get_permitted_scopes() {
+
+        $scopes = array();
+        $ok = !empty($this->get_type());
+        if ($ok && isset($this->get_typeconfig()[$this->get_component_id()]) &&
+            ($this->get_typeconfig()[$this->get_component_id()] == parent::SERVICE_ENABLED)) {
+            $scopes[] = self::SCOPE_MEMBERSHIPS_READ;
+        }
+
+        return $scopes;
+
+    }
+
     /**
      * Get the JSON for members.
      *
@@ -101,8 +117,44 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
      * for LTI instance (null if context-level request)
      *
      * @return string
+     * @deprecated since Moodle 3.7 MDL-62599 - please do not use this function any more.
+     * @see memberships::get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response)
      */
     public static function get_users_json($resource, $context, $contextid, $tool, $role, $limitfrom, $limitnum, $lti, $info) {
+        global $DB;
+
+        debugging('get_users_json() has been deprecated, ' .
+                  'please use memberships::get_members_json() instead.', DEBUG_DEVELOPER);
+
+        $course = $DB->get_record('course', array('id' => $contextid), 'id,shortname,fullname', IGNORE_MISSING);
+
+        $memberships = new memberships();
+        $memberships->check_tool($tool->id, null, array(self::SCOPE_MEMBERSHIPS_READ));
+
+        $response = new \mod_lti\local\ltiservice\response();
+
+        $json = $memberships->get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response);
+
+        return $json;
+    }
+
+    /**
+     * Get the JSON for members.
+     *
+     * @param \mod_lti\local\ltiservice\resource_base $resource       Resource handling the request
+     * @param \context_course   $context    Course context
+     * @param \course           $course     Course
+     * @param string            $role       User role requested (empty if none)
+     * @param int               $limitfrom  Position of first record to be returned
+     * @param int               $limitnum   Maximum number of records to be returned
+     * @param object            $lti        LTI instance record
+     * @param \core_availability\info_module $info Conditional availability information
+     *      for LTI instance (null if context-level request)
+     * @param \mod_lti\local\ltiservice\response $response       Response object for the request
+     *
+     * @return string
+     */
+    public function get_members_json($resource, $context, $course, $role, $limitfrom, $limitnum, $lti, $info, $response) {
 
         $withcapability = '';
         $exclude = array();
@@ -117,18 +169,20 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
                                                          null, null, null, true));
             }
         }
-        $users = get_enrolled_users($context, $withcapability, 0, 'u.*', null, $limitfrom, $limitnum, true);
-        if (count($users) < $limitnum) {
-            $limitfrom = 0;
-            $limitnum = 0;
+        $users = get_enrolled_users($context, $withcapability, 0, 'u.*', null, 0, 0, true);
+        if (($response->get_accept() === 'application/vnd.ims.lti-nrps.v2.membershipcontainer+json') ||
+            (($response->get_accept() !== 'application/vnd.ims.lis.v2.membershipcontainer+json') &&
+            ($this->get_type()->ltiversion === LTI_VERSION_1P3))) {
+            $json = $this->users_to_json($resource, $users, $course, $exclude, $limitfrom, $limitnum, $lti, $info, $response);
+        } else {
+            $json = $this->users_to_jsonld($resource, $users, $course->id, $exclude, $limitfrom, $limitnum, $lti, $info, $response);
         }
-        $json = self::users_to_json($resource, $users, $contextid, $tool, $exclude, $limitfrom, $limitnum, $lti, $info);
 
         return $json;
     }
 
     /**
-     * Get the JSON representation of the users.
+     * Get the JSON-LD representation of the users.
      *
      * Note that when a limit is set and the exclude array is not empty, then the number of memberships
      * returned may be less than the limit.
@@ -136,34 +190,28 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
      * @param \mod_lti\local\ltiservice\resource_base $resource       Resource handling the request
      * @param array  $users               Array of user records
      * @param string $contextid           Course ID
-     * @param object $tool                Tool instance object
      * @param array  $exclude             Array of user records to be excluded from the response
      * @param int    $limitfrom           Position of first record to be returned
      * @param int    $limitnum            Maximum number of records to be returned
      * @param object $lti                 LTI instance record
-     * @param \core_availability\info_module  $info     Conditional availability information for LTI instance
+     * @param \core_availability\info_module $info Conditional availability information
+     *      for LTI instance (null if context-level request)
+     * @param \mod_lti\local\ltiservice\response $response       Response object for the request
      *
      * @return string
      */
-    private static function users_to_json($resource, $users, $contextid, $tool, $exclude, $limitfrom, $limitnum,
-            $lti, $info) {
+    private function users_to_jsonld($resource, $users, $contextid, $exclude, $limitfrom, $limitnum,
+            $lti, $info, $response) {
         global $DB;
 
+        $tool = $this->get_type();
+        $toolconfig = $this->get_typeconfig();
         $arrusers = [
             '@context' => 'http://purl.imsglobal.org/ctx/lis/v2/MembershipContainer',
             '@type' => 'Page',
             '@id' => $resource->get_endpoint(),
         ];
 
-        if ($limitnum > 0) {
-            $limitfrom += $limitnum;
-            $nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$limitfrom}";
-            if (!is_null($lti)) {
-                $nextpage .= "&rlid={$lti->id}";
-            }
-            $arrusers['nextPage'] = $nextpage;
-        }
-
         $arrusers['pageOf'] = [
             '@type' => 'LISMembershipContainer',
             'membershipSubject' => [
@@ -175,6 +223,8 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
 
         $enabledcapabilities = lti_get_enabled_capabilities($tool);
         $islti2 = $tool->toolproxyid > 0;
+        $n = 0;
+        $more = false;
         foreach ($users as $user) {
             if (in_array($user->id, $exclude)) {
                 continue;
@@ -182,6 +232,16 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
             if (!empty($info) && !$info->is_user_visible($info->get_course_module(), $user->id)) {
                 continue;
             }
+            $n++;
+            if ($limitnum > 0) {
+                if ($n <= $limitfrom) {
+                    continue;
+                }
+                if (count($arrusers['pageOf']['membershipSubject']['membership']) >= $limitnum) {
+                    $more = true;
+                    break;
+                }
+            }
 
             $member = new \stdClass();
             $member->{"@type" } = 'LISPerson';
@@ -189,13 +249,12 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
             $membership->status = 'Active';
             $membership->role = explode(',', lti_get_ims_role($user->id, null, $contextid, true));
 
-            $toolconfig = lti_get_type_type_config($tool->id);
             $instanceconfig = null;
             if (!is_null($lti)) {
                 $instanceconfig = lti_get_type_config_from_instance($lti->id);
             }
             $isallowedlticonfig = self::is_allowed_field_set($toolconfig, $instanceconfig,
-                                    ['name' => 'lti_sendname', 'email' => 'lti_sendemailaddr']);
+                                    ['name' => 'sendname', 'email' => 'sendemailaddr']);
 
             $includedcapabilities = [
                 'User.id'              => ['type' => 'id',
@@ -208,7 +267,7 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
                                             'member.field' => 'name',
                                             'source.value' => format_string("{$user->firstname} {$user->lastname}")],
                 'Person.name.given'    => ['type' => 'name',
-                                            'member.field' => 'givenName',
+                                            'member.field' => 'giveName',
                                             'source.value' => format_string($user->firstname)],
                 'Person.name.family'   => ['type' => 'name',
                                             'member.field' => 'familyName',
@@ -253,13 +312,168 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
 
             $arrusers['pageOf']['membershipSubject']['membership'][] = $membership;
         }
+        if ($more) {
+            $nextlimitfrom = $limitfrom + $limitnum;
+            $nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$nextlimitfrom}";
+            if (!is_null($lti)) {
+                $nextpage .= "&rlid={$lti->id}";
+            }
+            $arrusers['nextPage'] = $nextpage;
+        }
+
+        $response->set_content_type('application/vnd.ims.lis.v2.membershipcontainer+json');
+
+        return json_encode($arrusers);
+    }
+
+    /**
+     * Get the NRP service JSON representation of the users.
+     *
+     * Note that when a limit is set and the exclude array is not empty, then the number of memberships
+     * returned may be less than the limit.
+     *
+     * @param \mod_lti\local\ltiservice\resource_base $resource       Resource handling the request
+     * @param array   $users               Array of user records
+     * @param \course $course              Course
+     * @param array   $exclude             Array of user records to be excluded from the response
+     * @param int     $limitfrom           Position of first record to be returned
+     * @param int     $limitnum            Maximum number of records to be returned
+     * @param object  $lti                 LTI instance record
+     * @param \core_availability\info_module  $info     Conditional availability information for LTI instance
+     * @param \mod_lti\local\ltiservice\response $response       Response object for the request
+     *
+     * @return string
+     */
+    private function users_to_json($resource, $users, $course, $exclude, $limitfrom, $limitnum,
+            $lti, $info, $response) {
+        global $DB, $CFG;
+
+        $tool = $this->get_type();
+        $toolconfig = $this->get_typeconfig();
+
+        $context = new \stdClass();
+        $context->id = $course->id;
+        $context->label = trim(html_to_text($course->shortname, 0));
+        $context->title = trim(html_to_text($course->fullname, 0));
+
+        $arrusers = [
+            'id' => $resource->get_endpoint(),
+            'context' => $context,
+            'members' => []
+        ];
+
+        $islti2 = $tool->toolproxyid > 0;
+        $n = 0;
+        $more = false;
+        foreach ($users as $user) {
+            if (in_array($user->id, $exclude)) {
+                continue;
+            }
+            if (!empty($info) && !$info->is_user_visible($info->get_course_module(), $user->id)) {
+                continue;
+            }
+            $n++;
+            if ($limitnum > 0) {
+                if ($n <= $limitfrom) {
+                    continue;
+                }
+                if (count($arrusers['members']) >= $limitnum) {
+                    $more = true;
+                    break;
+                }
+            }
+
+            $member = new \stdClass();
+            $member->status = 'Active';
+            $member->roles = explode(',', lti_get_ims_role($user->id, null, $course->id, true));
+
+            $instanceconfig = null;
+            if (!is_null($lti)) {
+                $instanceconfig = lti_get_type_config_from_instance($lti->id);
+            }
+            if (!$islti2) {
+                $isallowedlticonfig = self::is_allowed_field_set($toolconfig, $instanceconfig,
+                                        ['name' => 'sendname', 'givenname' => 'sendname', 'familyname' => 'sendname',
+                                         'email' => 'sendemailaddr']);
+            } else {
+                $isallowedlticonfig = self::is_allowed_capability_set($tool,
+                                        ['name' => 'Person.name.full', 'givenname' => 'Person.name.given',
+                                         'familyname' => 'Person.name.family', 'email' => 'Person.email.primary']);
+            }
+            $includedcapabilities = [
+                'User.id'              => ['type' => 'id',
+                                            'member.field' => 'user_id',
+                                            'source.value' => $user->id],
+                'Person.sourcedId'     => ['type' => 'id',
+                                            'member.field' => 'lis_person_sourcedid',
+                                            'source.value' => format_string($user->idnumber)],
+                'Person.name.full'     => ['type' => 'name',
+                                            'member.field' => 'name',
+                                            'source.value' => format_string("{$user->firstname} {$user->lastname}")],
+                'Person.name.given'    => ['type' => 'givenname',
+                                            'member.field' => 'given_name',
+                                            'source.value' => format_string($user->firstname)],
+                'Person.name.family'   => ['type' => 'familyname',
+                                            'member.field' => 'family_name',
+                                            'source.value' => format_string($user->lastname)],
+                'Person.email.primary' => ['type' => 'email',
+                                            'member.field' => 'email',
+                                            'source.value' => format_string($user->email)]
+            ];
+
+            if (!is_null($lti)) {
+                $message = new \stdClass();
+                $message->{'https://purl.imsglobal.org/spec/lti/claim/message_type'} = 'LtiResourceLinkRequest';
+                $conditions = array('courseid' => $course->id, 'itemtype' => 'mod',
+                        'itemmodule' => 'lti', 'iteminstance' => $lti->id);
+
+                if (!empty($lti->servicesalt) && $DB->record_exists('grade_items', $conditions)) {
+                    $basicoutcome = new \stdClass();
+                    $basicoutcome->lis_result_sourcedid = json_encode(lti_build_sourcedid($lti->id,
+                                                                                     $user->id,
+                                                                                     $lti->servicesalt,
+                                                                                     $lti->typeid));
+                    // Add outcome service URL.
+                    $serviceurl = new \moodle_url('/mod/lti/service.php');
+                    $serviceurl = $serviceurl->out();
+                    $forcessl = false;
+                    if (!empty($CFG->mod_lti_forcessl)) {
+                        $forcessl = true;
+                    }
+                    if ((isset($toolconfig['forcessl']) && ($toolconfig['forcessl'] == '1')) or $forcessl) {
+                        $serviceurl = lti_ensure_url_is_https($serviceurl);
+                    }
+                    $basicoutcome->lis_outcome_service_url = $serviceurl;
+                    $message->{'https://purl.imsglobal.org/spec/lti-bos/claim/basicoutcomesservice'} = $basicoutcome;
+                }
+                $member->message = [$message];
+            }
+
+            foreach ($includedcapabilities as $capabilityname => $capability) {
+                if (($capability['type'] === 'id') || $isallowedlticonfig[$capability['type']]) {
+                    $member->{$capability['member.field']} = $capability['source.value'];
+                }
+            }
+
+            $arrusers['members'][] = $member;
+        }
+        if ($more) {
+            $nextlimitfrom = $limitfrom + $limitnum;
+            $nextpage = "{$resource->get_endpoint()}?limit={$limitnum}&from={$nextlimitfrom}";
+            if (!is_null($lti)) {
+                $nextpage .= "&rlid={$lti->id}";
+            }
+            $response->add_additional_header("Link: {$nextpage};rel=next");
+        }
+
+        $response->set_content_type('application/vnd.ims.lti-nrps.v2.membershipcontainer+json');
 
         return json_encode($arrusers);
     }
 
     /**
      * Determines whether a user attribute may be used as part of LTI membership
-     * @param object            $toolconfig      Tool config
+     * @param array             $toolconfig      Tool config
      * @param object            $instanceconfig  Tool instance config
      * @param array             $fields          Set of fields to return if allowed or not
      * @return array Verification which associates an attribute with a boolean (allowed or not)
@@ -267,11 +481,11 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
     private static function is_allowed_field_set($toolconfig, $instanceconfig, $fields) {
         $isallowedstate = [];
         foreach ($fields as $key => $field) {
-            $allowed = self::ALWAYS_INCLUDE_FIELD == $toolconfig->{$field};
-            if (!$allowed) {
-                if (self::DELEGATE_TO_INSTRUCTOR == $toolconfig->{$field} && !is_null($instanceconfig)) {
-                    $allowed = $instanceconfig->{$field} == self::INSTRUCTOR_INCLUDED;
-                }
+            $allowed = isset($toolconfig[$field]) && (self::ALWAYS_INCLUDE_FIELD == $toolconfig[$field]);
+            if (!$allowed && isset($toolconfig[$field]) && (self::DELEGATE_TO_INSTRUCTOR == $toolconfig[$field]) &&
+                !is_null($instanceconfig)) {
+                $allowed = isset($instanceconfig->{"lti_{$field}"}) &&
+                          ($instanceconfig->{"lti_{$field}"} == self::INSTRUCTOR_INCLUDED);
             }
             $isallowedstate[$key] = $allowed;
         }
@@ -284,26 +498,16 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
      * @param \MoodleQuickForm $mform
      */
     public function get_configuration_options(&$mform) {
-        $elementname = 'ltiservice_memberships';
+        $elementname = $this->get_component_id();
         $options = [
-            get_string('notallow', self::LTI_SERVICE_COMPONENT),
-            get_string('allow', self::LTI_SERVICE_COMPONENT)
+            get_string('notallow', $this->get_component_id()),
+            get_string('allow', $this->get_component_id())
         ];
 
-        $mform->addElement('select', $elementname, get_string($elementname, self::LTI_SERVICE_COMPONENT), $options);
+        $mform->addElement('select', $elementname, get_string($elementname, $this->get_component_id()), $options);
         $mform->setType($elementname, 'int');
         $mform->setDefault($elementname, 0);
-        $mform->addHelpButton($elementname, $elementname, self::LTI_SERVICE_COMPONENT);
-    }
-
-    /**
-     * Return an array with the names of the parameters that the service will be saving in the configuration
-     *
-     * @return array with the names of the parameters that the service will be saving in the configuration
-     *
-     */
-    public function get_configuration_parameter_names() {
-        return array(self::LTI_SERVICE_COMPONENT);
+        $mform->addHelpButton($elementname, $elementname, $this->get_component_id());
     }
 
     /**
@@ -325,16 +529,10 @@ class memberships extends \mod_lti\local\ltiservice\service_base {
 
         $launchparameters = array();
         $tool = lti_get_type_type_config($typeid);
-        if (isset($tool->ltiservice_memberships)) {
-            if ($tool->ltiservice_memberships == '1' && $this->is_used_in_context($typeid, $courseid)) {
-                $endpoint = $this->get_service_path();
-                if ($COURSE->id === SITEID) {
-                    $contexttype = 'Group';
-                } else {
-                    $contexttype = 'CourseSection';
-                }
-                $launchparameters['custom_context_memberships_url'] = $endpoint .
-                    "/{$contexttype}/{$courseid}/bindings/{$typeid}/memberships";
+        if (isset($tool->{$this->get_component_id()})) {
+            if ($tool->{$this->get_component_id()} == parent::SERVICE_ENABLED && $this->is_used_in_context($typeid, $courseid)) {
+                $launchparameters['context_memberships_url'] = '$ToolProxyBinding.memberships.url';
+                $launchparameters['context_memberships_versions'] = '1.0,2.0';
             }
         }
         return $launchparameters;
index c077a14..f7d74ac 100644 (file)
  */
 
 $string['allow'] = 'Use this service to retrieve members\' information as per privacy settings';
-$string['ltiservice_memberships'] = 'IMS LTI Membership: ';
-$string['ltiservice_memberships_help'] = 'Allow the tool to retrieve member\'s info from the course using the IMS LTI Membership Service. The privacy settings will apply.';
+$string['ltiservice_memberships'] = 'IMS LTI Names and Role Provisioning';
+$string['ltiservice_memberships_help'] = 'Allow the tool to retrieve member\'s info from the course ' .
+  'using the IMS LTI Names and Role Provisioning Service. The privacy settings will apply.  For course-level ' .
+  'requests these will be based on the tool confguration settings.  If you wish to always send such details ' .
+  'do not delegate the choice to teachers.  Link-level requests will always use the privacy settings which ' .
+  'apply to the link.';
 $string['notallow'] = 'Do not use this service';
-$string['pluginname'] = 'Memberships LTI Service';
+$string['pluginname'] = 'Names and Role Provisioning LTI Service';
 $string['privacy:metadata:email'] = 'The email of the user using the LTI consumer.';
 $string['privacy:metadata:externalpurpose'] = 'This information is sent to an external LTI provider.';
 $string['privacy:metadata:firstname'] = 'The firstname of the user using the LTI consumer.';
@@ -35,4 +39,3 @@ $string['privacy:metadata:fullname'] = 'The fullname of the user using the LTI c
 $string['privacy:metadata:lastname'] = 'The lastname of the user using the LTI consumer.';
 $string['privacy:metadata:userid'] = 'The ID of the user using the LTI consumer.';
 $string['privacy:metadata:useridnumber'] = 'The ID number of the user using the LTI consumer';
-$string['servicename'] = 'Memberships';
index bd55ac4..53fef57 100644 (file)
@@ -83,13 +83,15 @@ class profile extends \mod_lti\local\ltiservice\resource_base {
 
         $version = service_base::LTI_VERSION2P0;
         $params = $this->parse_template();
-        $ok = $this->get_service()->check_tool_proxy($params['tool_proxy_id']);
-        if (!$ok) {
-            $response->set_code(404);
-        } else if (optional_param('lti_version', '', PARAM_ALPHANUMEXT) != $version) {
+        if (optional_param('lti_version', service_base::LTI_VERSION2P0, PARAM_ALPHANUMEXT) != $version) {
+            $ok = false;
             $response->set_code(400);
         } else {
-            $toolproxy = $this->get_service()->get_tool_proxy();
+            $toolproxy = lti_get_tool_proxy_from_guid($params['tool_proxy_id']);
+            $ok = $toolproxy !== false;
+        }
+        if ($ok) {
+            $this->get_service()->set_tool_proxy($toolproxy);
             $response->set_content_type($this->formats[0]);
 
             $servicepath = $this->get_service()->get_service_path();
@@ -111,7 +113,12 @@ class profile extends \mod_lti\local\ltiservice\resource_base {
                         $formats = implode("\", \"", $resource->get_formats());
                         $methods = implode("\", \"", $resource->get_methods());
                         $capabilityofferedarr = array_merge($capabilityofferedarr, $resource->get_variables());
-                        $path = $servicepath . preg_replace('/\{?.*\}$/', '', $resource->get_path());
+                        $template = $resource->get_path();
+                        if (!empty($template)) {
+                            $path = $servicepath . preg_replace('/[\(\)]/', '', $template);
+                        } else {
+                            $path = $resource->get_endpoint();
+                        }
                         $serviceoffered .= <<< EOD
 {$sep}
     {
@@ -199,17 +206,6 @@ EOD;
         }
     }
 
-    /**
-     * Get the resource fully qualified endpoint.
-     *
-     * @return string
-     */
-    public function get_endpoint() {
-
-        return parent::get_endpoint() . '?lti_version=' . service_base::LTI_VERSION2P0;
-
-    }
-
     /**
      * Parse a value for custom parameter substitution variables.
      *
@@ -218,7 +214,7 @@ EOD;
      * @return string
      */
     public function parse_value($value) {
-        if (strpos($value, '$ToolConsumerProfile.url') !== false) {
+        if (!empty($this->get_service()->get_tool_proxy()) && (strpos($value, '$ToolConsumerProfile.url') !== false)) {
             $value = str_replace('$ToolConsumerProfile.url', $this->get_endpoint(), $value);
         }
         return $value;
index ccb2e05..16f1f39 100644 (file)
@@ -66,10 +66,12 @@ class toolproxy extends \mod_lti\local\ltiservice\resource_base {
      */
     public function execute($response) {
 
-        $ok = $this->check_tool_proxy(null, $response->get_request_data());
+        $ok = $this->check_tool(null, $response->get_request_data());
+        $ok = $ok && ($this->get_service()->get_tool_proxy());
         if ($ok) {
             $toolproxy = $this->get_service()->get_tool_proxy();
-        } else {
+        }
+        if (!$ok) {
             $toolproxy = null;
             $response->set_code(401);
         }
@@ -118,6 +120,7 @@ class toolproxy extends \mod_lti\local\ltiservice\resource_base {
         }
 
         // Check all services requested were offered (only tool services currently supported).
+        $requestsbasicoutcomes = false;
         if ($ok && isset($toolproxyjson->security_contract->tool_service)) {
             $contexts = lti_get_contexts($toolproxyjson);
             $profileservice = lti_get_service_by_name('profile');
@@ -129,6 +132,7 @@ class toolproxy extends \mod_lti\local\ltiservice\resource_base {
             $errors = array();
             foreach ($tpservices as $service) {
                 $fqid = lti_get_fqid($contexts, $service->service);
+                $requestsbasicoutcomes = $requestsbasicoutcomes || (substr($fqid, -13) === 'Outcomes.LTI1');
                 if (substr($fqid, 0, strlen($context)) !== $context) {
                     $errors[] = $service->service;
                 } else {
@@ -219,7 +223,15 @@ class toolproxy extends \mod_lti\local\ltiservice\resource_base {
 
                 $type = new \stdClass();
                 $type->state = LTI_TOOL_STATE_PENDING;
+                $type->ltiversion = LTI_VERSION_2;
                 $type->toolproxyid = $toolproxy->id;
+                // Ensure gradebook column is created.
+                if ($requestsbasicoutcomes && !in_array('BasicOutcome.url', $launchrequest->enabled_capability)) {
+                    $launchrequest->enabled_capability[] = 'BasicOutcome.url';
+                }
+                if ($requestsbasicoutcomes && !in_array('BasicOutcome.sourcedId', $launchrequest->enabled_capability)) {
+                    $launchrequest->enabled_capability[] = 'BasicOutcome.sourcedId';
+                }
                 $type->enabledcapability = implode("\n", $launchrequest->enabled_capability);
                 $type->parameter = self::lti_extract_parameters($launchrequest->parameter);
 
index e7c7b5c..3a9b2fd 100644 (file)
@@ -49,7 +49,7 @@ class contextsettings extends \mod_lti\local\ltiservice\resource_base {
 
         parent::__construct($service);
         $this->id = 'ToolProxyBindingSettings';
-        $this->template = '/{context_type}/{context_id}/bindings/{vendor_code}/{product_code}';
+        $this->template = '/{context_type}/{context_id}/bindings/{vendor_code}/{product_code}(/custom)';
         $this->variables[] = 'ToolProxyBinding.custom.url';
         $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings+json';
         $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings.simple+json';
@@ -71,86 +71,107 @@ class contextsettings extends \mod_lti\local\ltiservice\resource_base {
         $vendorcode = $params['vendor_code'];
         $productcode = $params['product_code'];
         $bubble = optional_param('bubble', '', PARAM_ALPHA);
+        $typeid = null;
+        if (($vendorcode === 'tool') && is_numeric($productcode)) {
+            $typeid = $productcode;
+        }
         $ok = !empty($contexttype) && !empty($contextid) &&
               !empty($vendorcode) && !empty($productcode) &&
-              $this->check_tool_proxy($productcode, $response->get_request_data());
+              $this->check_tool($typeid, $response->get_request_data(),
+                  array(toolsettings::SCOPE_TOOL_SETTINGS));
         if (!$ok) {
             $response->set_code(401);
-        }
-        $contenttype = $response->get_accept();
-        $simpleformat = !empty($contenttype) && ($contenttype == $this->formats[1]);
-        if ($ok) {
-            $ok = (empty($bubble) || ((($bubble == 'distinct') || ($bubble == 'all')))) &&
-                 (!$simpleformat || empty($bubble) || ($bubble != 'all')) &&
-                 (empty($bubble) || ($response->get_request_method() == 'GET'));
-        }
-
-        if (!$ok) {
-            $response->set_code(404);
         } else {
-            $systemsetting = null;
-            $contextsettings = lti_get_tool_settings($this->get_service()->get_tool_proxy()->id, $contextid);
-            if (!empty($bubble)) {
-                $systemsetting = new systemsettings($this->get_service());
-                $systemsetting->params['tool_proxy_id'] = $productcode;
-                $systemsettings = lti_get_tool_settings($this->get_service()->get_tool_proxy()->id);
-                if ($bubble == 'distinct') {
-                    toolsettings::distinct_settings($systemsettings, $contextsettings, null);
-                }
+            $toolproxy = $this->get_service()->get_tool_proxy();
+            if (!empty($toolproxy)) {
+                $ok = $toolproxy->guid === $productcode;
+                $typeid = null;
+                $id = $toolproxy->id;
             } else {
-                $systemsettings = null;
+                $ok = $vendorcode === 'tool';
+                $typeid = intval($productcode);
+                $id = -$typeid;
             }
-            if ($response->get_request_method() == 'GET') {
-                $json = '';
-                if ($simpleformat) {
-                    $response->set_content_type($this->formats[1]);
-                    $json .= "{";
-                } else {
-                    $response->set_content_type($this->formats[0]);
-                    $json .= "{\n  \"@context\":\"http://purl.imsglobal.org/ctx/lti/v2/ToolSettings\",\n  \"@graph\":[\n";
-                }
-                $settings = toolsettings::settings_to_json($systemsettings, $simpleformat, 'ToolProxy', $systemsetting);
-                $json .= $settings;
-                $isfirst = strlen($settings) <= 0;
-                $settings = toolsettings::settings_to_json($contextsettings, $simpleformat, 'ToolProxyBinding', $this);
-                if ((strlen($settings) > 0) && !$isfirst) {
-                    $json .= ",";
-                }
-                $json .= $settings;
-                if ($simpleformat) {
-                    $json .= "\n}";
+            $contenttype = $response->get_accept();
+            $simpleformat = !empty($contenttype) && ($contenttype == $this->formats[1]);
+            if ($ok) {
+                $ok = (empty($bubble) || ((($bubble == 'distinct') || ($bubble == 'all')))) &&
+                     (!$simpleformat || empty($bubble) || ($bubble != 'all')) &&
+                     (empty($bubble) || ($response->get_request_method() == 'GET'));
+            }
+
+            if (!$ok) {
+                $response->set_code(404);
+            } else {
+                $systemsetting = null;
+                $contextsettings = lti_get_tool_settings($id, $contextid);
+                if (!empty($bubble)) {
+                    $systemsetting = new systemsettings($this->get_service());
+                    $systemsetting->params['tool_proxy_id'] = $productcode;
+                    if ($id >= 0) {
+                        $systemsetting->params['config_type'] = 'toolproxy';
+                    } else {
+                        $systemsetting->params['config_type'] = 'tool';
+                    }
+                    $systemsettings = lti_get_tool_settings($id);
+                    if ($bubble == 'distinct') {
+                        toolsettings::distinct_settings($systemsettings, $contextsettings, null);
+                    }
                 } else {
-                    $json .= "\n  ]\n}";
+                    $systemsettings = null;
                 }
-                $response->set_body($json);
-            } else { // PUT.
-                $settings = null;
-                if ($response->get_content_type() == $this->formats[0]) {
-                    $json = json_decode($response->get_request_data());
-                    $ok = !empty($json);
-                    if ($ok) {
-                        $ok = isset($json->{"@graph"}) && is_array($json->{"@graph"}) && (count($json->{"@graph"}) == 1) &&
-                              ($json->{"@graph"}[0]->{"@type"} == 'ToolProxyBinding');
+                if ($response->get_request_method() == 'GET') {
+                    $json = '';
+                    if ($simpleformat) {
+                        $response->set_content_type($this->formats[1]);
+                        $json .= "{";
+                    } else {
+                        $response->set_content_type($this->formats[0]);
+                        $json .= "{\n  \"@context\":\"http://purl.imsglobal.org/ctx/lti/v2/ToolSettings\",\n  \"@graph\":[\n";
                     }
-                    if ($ok) {
-                        $settings = $json->{"@graph"}[0]->custom;
-                        unset($settings->{'@id'});
+                    $settings = toolsettings::settings_to_json($systemsettings, $simpleformat, 'ToolProxy', $systemsetting);
+                    $json .= $settings;
+                    $isfirst = strlen($settings) <= 0;
+                    $settings = toolsettings::settings_to_json($contextsettings, $simpleformat, 'ToolProxyBinding', $this);
+                    if ((strlen($settings) > 0) && !$isfirst) {
+                        $json .= ",";
                     }
-                } else {  // Simple JSON.
-                    $json = json_decode($response->get_request_data(), true);
-                    $ok = !empty($json);
-                    if ($ok) {
-                        $ok = is_array($json);
+                    $json .= $settings;
+                    if ($simpleformat) {
+                        $json .= "\n}";
+                    } else {
+                        $json .= "\n  ]\n}";
+                    }
+                    $response->set_body($json);
+                } else { // PUT.
+                    $settings = null;
+                    if ($response->get_content_type() == $this->formats[0]) {
+                        $json = json_decode($response->get_request_data());
+                        $ok = !empty($json);
+                        if ($ok) {
+                            $ok = isset($json->{"@graph"}) && is_array($json->{"@graph"}) && (count($json->{"@graph"}) == 1) &&
+                                  ($json->{"@graph"}[0]->{"@type"} == 'ToolProxyBinding');
+                        }
+                        if ($ok) {
+                            $settings = $json->{"@graph"}[0]->custom;
+                            unset($settings->{'@id'});
+                        }
+                    } else {  // Simple JSON.
+                        $json = json_decode($response->get_request_data(), true);
+                        $ok = !empty($json);
+                        if ($ok) {
+                            $ok = is_array($json);
+                        }
+                        if ($ok) {
+                            $settings = $json;
+                        }
                     }
                     if ($ok) {
-                        $settings = $json;
+                        lti_set_tool_settings($settings, $id, $contextid);
+                    } else {
+                        $response->set_code(406);
                     }
                 }
-                if ($ok) {
-                    lti_set_tool_settings($settings, $this->get_service()->get_tool_proxy()->id, $contextid);
-                } else {
-                    $response->set_code(406);
-                }
             }
         }
     }
@@ -172,8 +193,13 @@ class contextsettings extends \mod_lti\local\ltiservice\resource_base {
                 $this->params['context_type'] = 'CourseSection';
             }
             $this->params['context_id'] = $COURSE->id;
-            $this->params['vendor_code'] = $this->get_service()->get_tool_proxy()->vendorcode;
-            $this->params['product_code'] = $this->get_service()->get_tool_proxy()->guid;
+            if (!empty($this->get_service()->get_tool_proxy())) {
+                $this->params['vendor_code'] = $this->get_service()->get_tool_proxy()->vendorcode;
+                $this->params['product_code'] = $this->get_service()->get_tool_proxy()->guid;
+            } else {
+                $this->params['vendor_code'] = 'tool';
+                $this->params['product_code'] = $this->get_service()->get_type()->id;
+            }
             $value = str_replace('$ToolProxyBinding.custom.url', parent::get_endpoint(), $value);
         }
         return $value;
index 2c77006..f162399 100644 (file)
@@ -48,7 +48,7 @@ class linksettings extends \mod_lti\local\ltiservice\resource_base {
 
         parent::__construct($service);
         $this->id = 'LtiLinkSettings';
-        $this->template = '/links/{link_id}';
+        $this->template = '/links/{link_id}(/custom)';
         $this->variables[] = 'LtiLink.custom.url';
         $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings+json';
         $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings.simple+json';
@@ -72,7 +72,7 @@ class linksettings extends \mod_lti\local\ltiservice\resource_base {
         $simpleformat = !empty($contenttype) && ($contenttype == $this->formats[1]);
         $ok = (empty($bubble) || ((($bubble == 'distinct') || ($bubble == 'all')))) &&
              (!$simpleformat || empty($bubble) || ($bubble != 'all')) &&
-             (empty($bubble) || ($response->get_request_method() == 'GET'));
+             (empty($bubble) || ($response->get_request_method() == self::HTTP_GET));
         if (!$ok) {
             $response->set_code(406);
         }
@@ -84,38 +84,51 @@ class linksettings extends \mod_lti\local\ltiservice\resource_base {
             $ok = !empty($linkid);
             if ($ok) {
                 $lti = $DB->get_record('lti', array('id' => $linkid), 'course,typeid', MUST_EXIST);
-                $ltitype = $DB->get_record('lti_types', array('id' => $lti->typeid));
-                $toolproxy = $DB->get_record('lti_tool_proxies', array('id' => $ltitype->toolproxyid));
-                $ok = $this->check_tool_proxy($toolproxy->guid, $response->get_request_data());
+                $ok = $this->check_tool($lti->typeid, $response->get_request_data(),
+                    array(toolsettings::SCOPE_TOOL_SETTINGS));
             }
             if (!$ok) {
                 $response->set_code(401);
             }
         }
         if ($ok) {
-            $linksettings = lti_get_tool_settings($this->get_service()->get_tool_proxy()->id, $lti->course, $linkid);
-            if (!empty($bubble)) {
-                $contextsetting = new contextsettings($this->get_service());
-                if ($COURSE == 'site') {
-                    $contextsetting->params['context_type'] = 'Group';
-                } else {
-                    $contextsetting->params['context_type'] = 'CourseSection';
-                }
-                $contextsetting->params['context_id'] = $lti->course;
-                $contextsetting->params['vendor_code'] = $this->get_service()->get_tool_proxy()->vendorcode;
-                $contextsetting->params['product_code'] = $this->get_service()->get_tool_proxy()->id;
-                $contextsettings = lti_get_tool_settings($this->get_service()->get_tool_proxy()->id, $lti->course);
-                $systemsetting = new systemsettings($this->get_service());
-                $systemsetting->params['tool_proxy_id'] = $this->get_service()->get_tool_proxy()->id;
-                $systemsettings = lti_get_tool_settings($this->get_service()->get_tool_proxy()->id);
-                if ($bubble == 'distinct') {
-                    toolsettings::distinct_settings($systemsettings, $contextsettings, $linksettings);
-                }
+            if (!empty($this->get_service()->get_tool_proxy())) {
+                $id = $this->get_service()->get_tool_proxy()->id;
             } else {
-                $contextsettings = null;
-                $systemsettings = null;
+                $id = -$this->get_service()->get_type()->id;
             }
             if ($response->get_request_method() == 'GET') {
+                $linksettings = lti_get_tool_settings($id, $lti->course, $linkid);
+                if (!empty($bubble)) {
+                    $contextsetting = new contextsettings($this->get_service());
+                    if ($COURSE == 'site') {
+                        $contextsetting->params['context_type'] = 'Group';
+                    } else {
+                        $contextsetting->params['context_type'] = 'CourseSection';
+                    }
+                    $contextsetting->params['context_id'] = $lti->course;
+                    if ($id >= 0) {
+                        $contextsetting->params['vendor_code'] = $this->get_service()->get_tool_proxy()->vendorcode;
+                    } else {
+                        $contextsetting->params['vendor_code'] = 'tool';
+                    }
+                    $contextsetting->params['product_code'] = abs($id);
+                    $contextsettings = lti_get_tool_settings($id, $lti->course);
+                    $systemsetting = new systemsettings($this->get_service());
+                    if ($id >= 0) {
+                        $systemsetting->params['config_type'] = 'toolproxy';
+                    } else {
+                        $systemsetting->params['config_type'] = 'tool';
+                    }
+                    $systemsetting->params['tool_proxy_id'] = abs($id);
+                    $systemsettings = lti_get_tool_settings($id);
+                    if ($bubble == 'distinct') {
+                        toolsettings::distinct_settings($systemsettings, $contextsettings, $linksettings);
+                    }
+                } else {
+                    $contextsettings = null;
+                    $systemsettings = null;
+                }
                 $json = '';
                 if ($simpleformat) {
                     $response->set_content_type($this->formats[1]);
@@ -176,7 +189,7 @@ class linksettings extends \mod_lti\local\ltiservice\resource_base {
                     }
                 }
                 if ($ok) {
-                    lti_set_tool_settings($settings, $this->get_service()->get_tool_proxy()->id, $lti->course, $linkid);
+                    lti_set_tool_settings($settings, $id, $lti->course, $linkid);
                 } else {
                     $response->set_code(406);
                 }
index b6be719..6d25009 100644 (file)
@@ -50,12 +50,12 @@ class systemsettings extends resource_base {
 
         parent::__construct($service);
         $this->id = 'ToolProxySettings';
-        $this->template = '/toolproxy/{tool_proxy_id}';
+        $this->template = '/{config_type}/{tool_proxy_id}(/custom)';
         $this->variables[] = 'ToolProxy.custom.url';
         $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings+json';
         $this->formats[] = 'application/vnd.ims.lti.v2.toolsettings.simple+json';
-        $this->methods[] = 'GET';
-        $this->methods[] = 'PUT';
+        $this->methods[] = self::HTTP_GET;
+        $this->methods[] = self::HTTP_PUT;
 
     }
 
@@ -68,10 +68,27 @@ class systemsettings extends resource_base {
 
         $params = $this->parse_template();
         $tpid = $params['tool_proxy_id'];
-        $bubble = optional_param('bubble', '', PARAM_ALPHA);
-        $ok = !empty($tpid) && $this->check_tool_proxy($tpid, $response->get_request_data());
+        $configtype = $params['config_type'];
+        $ok = (in_array($configtype, array('toolproxy', 'tool')));
+        if ($ok) {
+            $typeid = null;
+            if (($configtype === 'tool') && is_numeric($tpid)) {
+                $typeid = $tpid;
+            }
+            $bubble = optional_param('bubble', '', PARAM_ALPHA);
+            $ok = !empty($tpid) && $this->check_tool($typeid, $response->get_request_data(),
+                array(toolsettings::SCOPE_TOOL_SETTINGS));
+        }
         if (!$ok) {
             $response->set_code(401);
+        } else if (!empty($this->get_service()->get_tool_proxy())) {
+            $ok = ($this->get_service()->get_tool_proxy()->guid === $tpid);
+            $id = $this->get_service()->get_tool_proxy()->id;
+        } else if (!empty($typeid)) {
+            $id = -$typeid;
+        } else {
+            $ok = false;
+            $response->set_code(404);
         }
         $contenttype = $response->get_accept();
         $simpleformat = !empty($contenttype) && ($contenttype == $this->formats[1]);
@@ -85,7 +102,7 @@ class systemsettings extends resource_base {
         }
 
         if ($ok) {
-            $systemsettings = lti_get_tool_settings($this->get_service()->get_tool_proxy()->id);
+            $systemsettings = lti_get_tool_settings($id);
             if ($response->get_request_method() == 'GET') {
                 $json = '';
                 if ($simpleformat) {
@@ -127,7 +144,7 @@ class systemsettings extends resource_base {
                     }
                 }
                 if ($ok) {
-                    lti_set_tool_settings($settings, $this->get_service()->get_tool_proxy()->id);
+                    lti_set_tool_settings($settings, $id);
                 } else {
                     $response->set_code(406);
                 }
index f7969a9..31ca1d6 100644 (file)
@@ -38,6 +38,9 @@ defined('MOODLE_INTERNAL') || die();
  */
 class toolsettings extends \mod_lti\local\ltiservice\service_base {
 
+    /** Scope for managing tool settings */
+    const SCOPE_TOOL_SETTINGS = 'https://purl.imsglobal.org/spec/lti-ts/scope/toolsetting';
+
     /**
      * Class constructor.
      */
@@ -67,6 +70,24 @@ class toolsettings extends \mod_lti\local\ltiservice\service_base {
 
     }
 
+    /**
+     * Get the scope(s) permitted for the tool relevant to this service.
+     *
+     * @return array
+     */
+    public function get_permitted_scopes() {
+
+        $scopes = array();
+        $ok = !empty($this->get_type());
+        if ($ok && isset($this->get_typeconfig()[$this->get_component_id()]) &&
+            ($this->get_typeconfig()[$this->get_component_id()] == parent::SERVICE_ENABLED)) {
+            $scopes[] = self::SCOPE_TOOL_SETTINGS;
+        }
+
+        return $scopes;
+
+    }
+
     /**
      * Get the distinct settings from each level by removing any duplicates from higher levels.
      *
@@ -111,11 +132,10 @@ class toolsettings extends \mod_lti\local\ltiservice\service_base {
             if (!$simpleformat) {
                 $json .= "    {\n      \"@type\":\"{$type}\",\n";
                 $json .= "      \"@id\":\"{$resource->get_endpoint()}\",\n";
-                $json .= "      \"custom\":{\n";
-                $json .= "        \"@id\":\"{$resource->get_endpoint()}/custom\"";
+                $json .= "      \"custom\":{";
                 $indent = '      ';
             }
-            $isfirst = $simpleformat;
+            $isfirst = true;
             if (!empty($settings)) {
                 foreach ($settings as $key => $value) {
                     if (!$isfirst) {
@@ -135,4 +155,53 @@ class toolsettings extends \mod_lti\local\ltiservice\service_base {
 
     }
 
+    /**
+     * Adds form elements for membership add/edit page.
+     *
+     * @param \MoodleQuickForm $mform
+     */
+    public function get_configuration_options(&$mform) {
+        $elementname = $this->get_component_id();
+        $options = [
+            get_string('notallow', $this->get_component_id()),
+            get_string('allow', $this->get_component_id())
+        ];
+
+        $mform->addElement('select', $elementname, get_string($elementname, $this->get_component_id()), $options);
+        $mform->setType($elementname, 'int');
+        $mform->setDefault($elementname, 0);
+        $mform->addHelpButton($elementname, $elementname, $this->get_component_id());
+    }
+