Merge branch 'MDL-67886_master' of git://github.com/mdjnelson/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 22 Apr 2020 23:36:40 +0000 (01:36 +0200)
committerAdrian Greeve <abgreeve@gmail.com>
Thu, 23 Apr 2020 07:01:40 +0000 (15:01 +0800)
52 files changed:
.github/FUNDING.yml [new file with mode: 0644]
.travis.yml
admin/message.php
composer.json
composer.lock
config-dist.php
h5p/classes/player.php
lang/en/h5p.php
lib/ajax/getnavbranch.php
lib/ajax/service.php
lib/classes/filetypes.php
lib/classes/session/database.php
lib/classes/session/handler.php
lib/classes/session/manager.php
lib/classes/session/memcached.php
lib/classes/session/redis.php
lib/db/services.php
lib/db/upgrade.php
lib/externallib.php
lib/php-jwt/README.md
lib/php-jwt/composer.json
lib/php-jwt/src/BeforeValidException.php
lib/php-jwt/src/ExpiredException.php
lib/php-jwt/src/JWK.php [new file with mode: 0644]
lib/php-jwt/src/JWT.php
lib/php-jwt/src/SignatureInvalidException.php
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/build/local/dynamic/repository.min.js
lib/table/amd/build/local/dynamic/repository.min.js.map
lib/table/amd/src/dynamic.js
lib/table/amd/src/local/dynamic/repository.js
lib/table/classes/external/dynamic/fetch.php
lib/table/tests/external/dynamic/fetch_test.php
lib/tablelib.php
lib/tests/behat/behat_hooks.php
lib/tests/session_manager_test.php
lib/tests/session_redis_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
message/output/popup/db/services.php
mod/lti/db/caches.php [new file with mode: 0644]
mod/lti/edit_form.php
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/tests/fixtures/test_keyset [new file with mode: 0644]
mod/lti/tests/locallib_test.php
mod/lti/token.php
mod/lti/version.php
user/classes/table/participants.php [moved from user/classes/participants_table.php with 99% similarity]
user/index.php
version.php

diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644 (file)
index 0000000..adb6942
--- /dev/null
@@ -0,0 +1,2 @@
+# Primary donations pages.
+custom: ["https://moodle.com/donations/", moodle.org]
index 321c133..245d7fc 100644 (file)
@@ -2,8 +2,6 @@
 # process (which uses our internal CI system) this file is here for the benefit
 # of community developers git clones - see MDL-51458.
 
-sudo: required
-
 # We currently disable Travis notifications entirely until https://github.com/travis-ci/travis-ci/issues/4976
 # is fixed.
 notifications:
@@ -11,6 +9,8 @@ notifications:
 
 language: php
 
+os: linux
+
 dist: xenial
 
 services:
@@ -41,7 +41,7 @@ env:
     # Perform an upgrade test too.
     - DB=pgsql    TASK=UPGRADE
 
-matrix:
+jobs:
     # Enable fast finish.
     # This will fail the build if a single job fails (except those in allow_failures).
     # It will not stop the jobs from running.
index d004e9e..b5c31d7 100644 (file)
@@ -83,7 +83,7 @@ if (($form = data_submitted()) && confirm_sesskey()) {
                 }
             } else {
                 $newsettings = array();
-                if (array_key_exists($componentprovidersetting, $form)) {
+                if (property_exists($form, $componentprovidersetting)) {
                     // We must be processing loggedin or loggedoff checkboxes.
                     // Store defained comma-separated processors as setting value.
                     // Using array_filter eliminates elements set to 0 above.
index b58e8c0..9a3f1d0 100644 (file)
@@ -13,7 +13,7 @@
     "require-dev": {
         "phpunit/phpunit": "7.5.*",
         "phpunit/dbunit": "4.0.*",
-        "moodlehq/behat-extension": "3.39.1",
+        "moodlehq/behat-extension": "3.39.2",
         "mikey179/vfsstream": "^1.6",
         "instaclick/php-webdriver": "dev-local as 1.x-dev"
     }
index bd2613c..7fd4234 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "7d4095e9af1e9ef59e2a74273a5c55b2",
+    "content-hash": "a2329ee2d14a351b74f99322f42722da",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "behat/gherkin",
-            "version": "v4.6.0",
+            "version": "v4.6.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Gherkin.git",
-                "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07"
+                "reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Gherkin/zipball/ab0a02ea14893860bca00f225f5621d351a3ad07",
-                "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07",
+                "url": "https://api.github.com/repos/Behat/Gherkin/zipball/51ac4500c4dc30cbaaabcd2f25694299df666a31",
+                "reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31",
                 "shasum": ""
             },
             "require": {
                 "gherkin",
                 "parser"
             ],
-            "time": "2019-01-16T14:22:17+00:00"
+            "time": "2020-03-17T14:03:26+00:00"
         },
         {
             "name": "behat/mink",
-            "version": "v1.7.1",
+            "version": "v1.8.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/minkphp/Mink.git",
-                "reference": "e6930b9c74693dff7f4e58577e1b1743399f3ff9"
+                "reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/minkphp/Mink/zipball/e6930b9c74693dff7f4e58577e1b1743399f3ff9",
-                "reference": "e6930b9c74693dff7f4e58577e1b1743399f3ff9",
+                "url": "https://api.github.com/repos/minkphp/Mink/zipball/07c6a9fe3fa98c2de074b25d9ed26c22904e3887",
+                "reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.1",
-                "symfony/css-selector": "~2.1|~3.0"
+                "symfony/css-selector": "^2.7|^3.0|^4.0|^5.0"
             },
             "require-dev": {
-                "symfony/phpunit-bridge": "~2.7|~3.0"
+                "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20",
+                "symfony/debug": "^2.7|^3.0|^4.0",
+                "symfony/phpunit-bridge": "^3.4.38 || ^5.0.5"
             },
             "suggest": {
                 "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)",
                 "behat/mink-goutte-driver": "fast headless driver for any app without JS emulation",
                 "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)",
-                "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)"
+                "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)",
+                "dmore/chrome-mink-driver": "fast and JS-enabled driver for any app (requires chromium or google chrome)"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.7.x-dev"
+                    "dev-master": "1.8.x-dev"
                 }
             },
             "autoload": {
                 "testing",
                 "web"
             ],
-            "time": "2016-03-05T08:26:18+00:00"
+            "time": "2020-03-11T15:45:53+00:00"
         },
         {
             "name": "behat/mink-browserkit-driver",
-            "version": "1.3.3",
+            "version": "v1.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/minkphp/MinkBrowserKitDriver.git",
-                "reference": "1b9a7ce903cfdaaec5fb32bfdbb26118343662eb"
+                "reference": "e3b90840022ebcd544c7b394a3c9597ae242cbee"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/1b9a7ce903cfdaaec5fb32bfdbb26118343662eb",
-                "reference": "1b9a7ce903cfdaaec5fb32bfdbb26118343662eb",
+                "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/e3b90840022ebcd544c7b394a3c9597ae242cbee",
+                "reference": "e3b90840022ebcd544c7b394a3c9597ae242cbee",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "mink/driver-testsuite": "dev-master",
+                "symfony/debug": "^2.7|^3.0|^4.0",
                 "symfony/http-kernel": "~2.3|~3.0|~4.0"
             },
             "type": "mink-driver",
                 "browser",
                 "testing"
             ],
-            "time": "2018-05-02T09:25:31+00:00"
+            "time": "2020-03-11T09:49:45+00:00"
         },
         {
             "name": "behat/mink-extension",
         },
         {
             "name": "behat/mink-selenium2-driver",
-            "version": "v1.3.1",
+            "version": "v1.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/minkphp/MinkSelenium2Driver.git",
-                "reference": "473a9f3ebe0c134ee1e623ce8a9c852832020288"
+                "reference": "312a967dd527f28980cce40850339cd5316da092"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/473a9f3ebe0c134ee1e623ce8a9c852832020288",
-                "reference": "473a9f3ebe0c134ee1e623ce8a9c852832020288",
+                "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/312a967dd527f28980cce40850339cd5316da092",
+                "reference": "312a967dd527f28980cce40850339cd5316da092",
                 "shasum": ""
             },
             "require": {
                 "behat/mink": "~1.7@dev",
                 "instaclick/php-webdriver": "~1.1",
-                "php": ">=5.3.1"
+                "php": ">=5.4"
             },
             "require-dev": {
-                "symfony/phpunit-bridge": "~2.7"
+                "mink/driver-testsuite": "dev-master"
             },
             "type": "mink-driver",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.3.x-dev"
+                    "dev-master": "1.4.x-dev"
                 }
             },
             "autoload": {
                 "MIT"
             ],
             "authors": [
-                {
-                    "name": "Konstantin Kudryashov",
-                    "email": "ever.zet@gmail.com",
-                    "homepage": "http://everzet.com"
-                },
                 {
                     "name": "Pete Otaqui",
                     "email": "pete@otaqui.com",
                     "homepage": "https://github.com/pete-otaqui"
+                },
+                {
+                    "name": "Konstantin Kudryashov",
+                    "email": "ever.zet@gmail.com",
+                    "homepage": "http://everzet.com"
                 }
             ],
             "description": "Selenium2 (WebDriver) driver for Mink framework",
                 "testing",
                 "webdriver"
             ],
-            "time": "2016-03-05T09:10:18+00:00"
+            "time": "2020-03-11T14:43:21+00:00"
         },
         {
             "name": "behat/transliterator",
-            "version": "v1.2.0",
+            "version": "v1.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Transliterator.git",
-                "reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c"
+                "reference": "3c4ec1d77c3d05caa1f0bf8fb3aae4845005c7fc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Transliterator/zipball/826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c",
-                "reference": "826ce7e9c2a6664c0d1f381cbb38b1fb80a7ee2c",
+                "url": "https://api.github.com/repos/Behat/Transliterator/zipball/3c4ec1d77c3d05caa1f0bf8fb3aae4845005c7fc",
+                "reference": "3c4ec1d77c3d05caa1f0bf8fb3aae4845005c7fc",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "chuyskywalker/rolling-curl": "^3.1",
-                "php-yaoi/php-yaoi": "^1.0"
+                "php-yaoi/php-yaoi": "^1.0",
+                "phpunit/phpunit": "^4.8.36|^6.3"
             },
             "type": "library",
             "extra": {
                 }
             },
             "autoload": {
-                "psr-0": {
-                    "Behat\\Transliterator": "src/"
+                "psr-4": {
+                    "Behat\\Transliterator\\": "src/Behat/Transliterator"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 "slug",
                 "transliterator"
             ],
-            "time": "2017-04-04T11:38:05+00:00"
+            "time": "2020-01-14T16:39:13+00:00"
         },
         {
             "name": "container-interop/container-interop",
         },
         {
             "name": "fabpot/goutte",
-            "version": "v3.2.3",
+            "version": "v3.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfPHP/Goutte.git",
-                "reference": "3f0eaf0a40181359470651f1565b3e07e3dd31b8"
+                "reference": "4ab5199e3ec0ffde0ee0b5ecf568a4fb8398dbae"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/3f0eaf0a40181359470651f1565b3e07e3dd31b8",
-                "reference": "3f0eaf0a40181359470651f1565b3e07e3dd31b8",
+                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/4ab5199e3ec0ffde0ee0b5ecf568a4fb8398dbae",
+                "reference": "4ab5199e3ec0ffde0ee0b5ecf568a4fb8398dbae",
                 "shasum": ""
             },
             "require": {
                 "guzzlehttp/guzzle": "^6.0",
-                "php": ">=5.5.0",
-                "symfony/browser-kit": "~2.1|~3.0|~4.0",
-                "symfony/css-selector": "~2.1|~3.0|~4.0",
-                "symfony/dom-crawler": "~2.1|~3.0|~4.0"
+                "php": "^7.1.3",
+                "symfony/browser-kit": "^4.4|^5.0",
+                "symfony/css-selector": "^4.4|^5.0",
+                "symfony/dom-crawler": "^4.4|^5.0"
             },
             "require-dev": {
-                "symfony/phpunit-bridge": "^3.3 || ^4"
+                "symfony/phpunit-bridge": "^5.0"
             },
             "type": "application",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.2-dev"
+                    "dev-master": "3.3-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "scraper"
             ],
-            "time": "2018-06-29T15:13:57+00:00"
+            "time": "2019-12-06T13:11:18+00:00"
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.5.0",
+            "version": "6.5.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5"
+                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5",
-                "reference": "dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82",
+                "reference": "43ece0e75098b7ecd8d13918293029e555a50f82",
                 "shasum": ""
             },
             "require": {
                 "rest",
                 "web service"
             ],
-            "time": "2019-12-07T18:20:45+00:00"
+            "time": "2019-12-23T11:57:10+00:00"
         },
         {
             "name": "guzzlehttp/promises",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.39.1",
+            "version": "v3.39.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "e61855d292200e7d324241f49ae4c03bad607e70"
+                "reference": "7a2df2124ba8a85ccf21e517d18c78f932bdbbce"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/e61855d292200e7d324241f49ae4c03bad607e70",
-                "reference": "e61855d292200e7d324241f49ae4c03bad607e70",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/7a2df2124ba8a85ccf21e517d18c78f932bdbbce",
+                "reference": "7a2df2124ba8a85ccf21e517d18c78f932bdbbce",
                 "shasum": ""
             },
             "require": {
                 "behat/behat": "3.5.*",
-                "behat/mink": "~1.7",
+                "behat/mink": "~1.8",
                 "behat/mink-extension": "~2.2",
                 "behat/mink-goutte-driver": "~1.2",
                 "behat/mink-selenium2-driver": "~1.3",
                 "Behat",
                 "moodle"
             ],
-            "time": "2019-12-05T23:18:23+00:00"
+            "time": "2020-04-09T16:06:14+00:00"
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.9.4",
+            "version": "1.9.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7"
+                "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/579bb7356d91f9456ccd505f24ca8b667966a0a7",
-                "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef",
+                "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef",
                 "shasum": ""
             },
             "require": {
                 "object",
                 "object graph"
             ],
-            "time": "2019-12-15T19:12:40+00:00"
+            "time": "2020-01-17T21:11:47+00:00"
         },
         {
             "name": "phar-io/manifest",
         },
         {
             "name": "phpdocumentor/reflection-docblock",
-            "version": "4.3.2",
+            "version": "5.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e"
+                "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
-                "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e",
+                "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.0",
-                "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
-                "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
-                "webmozart/assert": "^1.0"
+                "ext-filter": "^7.1",
+                "php": "^7.2",
+                "phpdocumentor/reflection-common": "^2.0",
+                "phpdocumentor/type-resolver": "^1.0",
+                "webmozart/assert": "^1"
             },
             "require-dev": {
-                "doctrine/instantiator": "^1.0.5",
-                "mockery/mockery": "^1.0",
-                "phpunit/phpunit": "^6.4"
+                "doctrine/instantiator": "^1",
+                "mockery/mockery": "^1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "4.x-dev"
+                    "dev-master": "5.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "phpDocumentor\\Reflection\\": [
-                        "src/"
-                    ]
+                    "phpDocumentor\\Reflection\\": "src"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
                 {
                     "name": "Mike van Riel",
                     "email": "me@mikevanriel.com"
+                },
+                {
+                    "name": "Jaap van Otterdijk",
+                    "email": "account@ijaap.nl"
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "time": "2019-09-12T14:27:41+00:00"
+            "time": "2020-02-22T12:28:44+00:00"
         },
         {
             "name": "phpdocumentor/type-resolver",
-            "version": "1.0.1",
+            "version": "1.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
+                "reference": "7462d5f123dfc080dfdf26897032a6513644fc95"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
-                "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/7462d5f123dfc080dfdf26897032a6513644fc95",
+                "reference": "7462d5f123dfc080dfdf26897032a6513644fc95",
                 "shasum": ""
             },
             "require": {
-                "php": "^7.1",
+                "php": "^7.2",
                 "phpdocumentor/reflection-common": "^2.0"
             },
             "require-dev": {
-                "ext-tokenizer": "^7.1",
-                "mockery/mockery": "~1",
-                "phpunit/phpunit": "^7.0"
+                "ext-tokenizer": "^7.2",
+                "mockery/mockery": "~1"
             },
             "type": "library",
             "extra": {
                 }
             ],
             "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
-            "time": "2019-08-22T18:11:29+00:00"
+            "time": "2020-02-18T18:59:58+00:00"
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.10.0",
+            "version": "v1.10.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "d638ebbb58daba25a6a0dc7969e1358a0e3c6682"
+                "reference": "451c3cd1418cf640de218914901e51b064abb093"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d638ebbb58daba25a6a0dc7969e1358a0e3c6682",
-                "reference": "d638ebbb58daba25a6a0dc7969e1358a0e3c6682",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
+                "reference": "451c3cd1418cf640de218914901e51b064abb093",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.0.2",
                 "php": "^5.3|^7.0",
                 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
-                "sebastian/comparator": "^1.2.3|^2.0|^3.0",
-                "sebastian/recursion-context": "^1.0|^2.0|^3.0"
+                "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
+                "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
             },
             "require-dev": {
                 "phpspec/phpspec": "^2.5 || ^3.2",
                 "spy",
                 "stub"
             ],
-            "time": "2019-12-17T16:54:23+00:00"
+            "time": "2020-03-05T15:02:03+00:00"
         },
         {
             "name": "phpunit/dbunit",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "7.5.18",
+            "version": "7.5.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "fcf6c4bfafaadc07785528b06385cce88935474d"
+                "reference": "9467db479d1b0487c99733bb1e7944d32deded2c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fcf6c4bfafaadc07785528b06385cce88935474d",
-                "reference": "fcf6c4bfafaadc07785528b06385cce88935474d",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c",
+                "reference": "9467db479d1b0487c99733bb1e7944d32deded2c",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2019-12-06T05:14:37+00:00"
+            "time": "2020-01-08T08:45:45+00:00"
         },
         {
             "name": "psr/container",
         },
         {
             "name": "psr/log",
-            "version": "1.1.2",
+            "version": "1.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/php-fig/log.git",
-                "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801"
+                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801",
-                "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
+                "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
                 "shasum": ""
             },
             "require": {
                 "psr",
                 "psr-3"
             ],
-            "time": "2019-11-01T11:05:21+00:00"
+            "time": "2020-03-23T09:12:05+00:00"
         },
         {
             "name": "ralouphie/getallheaders",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v4.4.1",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "e19e465c055137938afd40cfddd687e7511bbbf0"
+                "reference": "e4b0dc1b100bf75b5717c5b451397f230a618a42"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/e19e465c055137938afd40cfddd687e7511bbbf0",
-                "reference": "e19e465c055137938afd40cfddd687e7511bbbf0",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/e4b0dc1b100bf75b5717c5b451397f230a618a42",
+                "reference": "e4b0dc1b100bf75b5717c5b451397f230a618a42",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2019-10-28T20:30:34+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-28T10:15:50+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.4.36",
+            "version": "v3.4.39",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
-                "reference": "e212b06996819a2bce026a63da03b7182d05a690"
+                "reference": "e4636a4f23f157278a19e5db160c63de0da297d8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/e212b06996819a2bce026a63da03b7182d05a690",
-                "reference": "e212b06996819a2bce026a63da03b7182d05a690",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/e4636a4f23f157278a19e5db160c63de0da297d8",
+                "reference": "e4636a4f23f157278a19e5db160c63de0da297d8",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony ClassLoader Component",
             "homepage": "https://symfony.com",
-            "time": "2019-08-20T13:31:17+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-15T09:38:08+00:00"
         },
         {
             "name": "symfony/config",
-            "version": "v4.4.1",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "7aa5817f1b7a8ed377752b90fcc47dfb3c67b40c"
+                "reference": "3f4a3de1af498ed0ea653d4dc2317794144e6ca4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/7aa5817f1b7a8ed377752b90fcc47dfb3c67b40c",
-                "reference": "7aa5817f1b7a8ed377752b90fcc47dfb3c67b40c",
+                "url": "https://api.github.com/repos/symfony/config/zipball/3f4a3de1af498ed0ea653d4dc2317794144e6ca4",
+                "reference": "3f4a3de1af498ed0ea653d4dc2317794144e6ca4",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2019-12-01T10:50:45+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-27T16:54:36+00:00"
         },
         {
             "name": "symfony/console",
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.4.36",
+            "version": "v5.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "f819f71ae3ba6f396b4c015bd5895de7d2f1f85f"
+                "reference": "5f8d5271303dad260692ba73dfa21777d38e124e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/f819f71ae3ba6f396b4c015bd5895de7d2f1f85f",
-                "reference": "f819f71ae3ba6f396b4c015bd5895de7d2f1f85f",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/5f8d5271303dad260692ba73dfa21777d38e124e",
+                "reference": "5f8d5271303dad260692ba73dfa21777d38e124e",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8"
+                "php": "^7.2.5"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.4-dev"
+                    "dev-master": "5.0-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2019-10-01T11:57:37+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-27T16:56:45+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.4.36",
+            "version": "v3.4.39",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "f72e33fdb1170b326e72c3157f0cd456351dd086"
+                "reference": "ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/f72e33fdb1170b326e72c3157f0cd456351dd086",
-                "reference": "f72e33fdb1170b326e72c3157f0cd456351dd086",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29",
+                "reference": "ce9f3b5e8e1c50f849fded59b3a1b6bc3562ec29",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2019-10-24T15:33:53+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-23T10:22:40+00:00"
         },
         {
             "name": "symfony/dependency-injection",
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v4.4.1",
+            "version": "v4.4.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "36bbcab9369fc2f583220890efd43bf262d563fd"
+                "reference": "4d0fb3374324071ecdd94898367a3fa4b5563162"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/36bbcab9369fc2f583220890efd43bf262d563fd",
-                "reference": "36bbcab9369fc2f583220890efd43bf262d563fd",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/4d0fb3374324071ecdd94898367a3fa4b5563162",
+                "reference": "4d0fb3374324071ecdd94898367a3fa4b5563162",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2019-10-29T11:38:30+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-29T19:12:22+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.36",
+            "version": "v3.4.39",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "f9031c22ec127d4a2450760f81a8677fe8a10177"
+                "reference": "9d4e22943b73acc1ba50595b7de1a01fe9dbad48"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f9031c22ec127d4a2450760f81a8677fe8a10177",
-                "reference": "f9031c22ec127d4a2450760f81a8677fe8a10177",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9d4e22943b73acc1ba50595b7de1a01fe9dbad48",
+                "reference": "9d4e22943b73acc1ba50595b7de1a01fe9dbad48",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2019-10-24T15:33:53+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-15T09:38:08+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v5.0.1",
+            "version": "v5.0.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "1d71f670bc5a07b9ccc97dc44f932177a322d4e6"
+                "reference": "ca3b87dd09fff9b771731637f5379965fbfab420"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/1d71f670bc5a07b9ccc97dc44f932177a322d4e6",
-                "reference": "1d71f670bc5a07b9ccc97dc44f932177a322d4e6",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/ca3b87dd09fff9b771731637f5379965fbfab420",
+                "reference": "ca3b87dd09fff9b771731637f5379965fbfab420",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2019-11-26T23:25:11+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-27T16:56:45+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
-            "version": "v1.13.1",
+            "version": "v1.15.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3"
+                "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
-                "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14",
+                "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.13-dev"
+                    "dev-master": "1.15-dev"
                 }
             },
             "autoload": {
                 "polyfill",
                 "portable"
             ],
-            "time": "2019-11-27T13:56:44+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-02-27T09:26:54+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.13.1",
+            "version": "v1.15.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f"
+                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7b4aab9743c30be783b73de055d24a39cf4b954f",
-                "reference": "7b4aab9743c30be783b73de055d24a39cf4b954f",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
+                "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.13-dev"
+                    "dev-master": "1.15-dev"
                 }
             },
             "autoload": {
                 "portable",
                 "shim"
             ],
-            "time": "2019-11-27T14:18:11+00:00"
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2020-03-09T19:04:49+00:00"
         },
         {
             "name": "symfony/process",
         },
         {
             "name": "webmozart/assert",
-            "version": "1.6.0",
+            "version": "1.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/webmozart/assert.git",
-                "reference": "573381c0a64f155a0d9a23f4b0c797194805b925"
+                "reference": "aed98a490f9a8f78468232db345ab9cf606cf598"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925",
-                "reference": "573381c0a64f155a0d9a23f4b0c797194805b925",
+                "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598",
+                "reference": "aed98a490f9a8f78468232db345ab9cf606cf598",
                 "shasum": ""
             },
             "require": {
                 "check",
                 "validate"
             ],
-            "time": "2019-11-24T13:36:37+00:00"
+            "time": "2020-02-14T12:15:55+00:00"
         }
     ],
     "aliases": [
     "prefer-stable": false,
     "prefer-lowest": false,
     "platform": [],
-    "platform-dev": []
+    "platform-dev": [],
+    "plugin-api-version": "1.1.0"
 }
index 51f7e55..fb45533 100644 (file)
@@ -628,6 +628,13 @@ $CFG->admin = 'admin';
 //
 //      $CFG->debugsessionlock = 5;
 //
+// There are times when a session lock is not required during a request. For a page/service to opt-in whether or not a
+// session lock is required this setting must first be set to 'true'.
+// This is an experimental issue. The session store can not be in the session, please
+// see https://docs.moodle.org/en/Session_handling#Read_only_sessions.
+//
+//      $CFG->enable_read_only_sessions = true;
+//
 // Uninstall plugins from CLI only. This stops admins from uninstalling plugins from the graphical admin
 // user interface, and forces plugins to be uninstalled from the Command Line tool only, found at
 // admin/cli/plugin_uninstall.php.
index 9633388..e8c68a6 100644 (file)
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
 
 use core_h5p\local\library\autoloader;
 use core_xapi\local\statement\item_activity;
+use core\lock\lock_config;
 
 /**
  * H5P player class, for displaying any local H5P content.
@@ -337,8 +338,20 @@ class player {
             // content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content.
             $onlyupdatelibs = !helper::can_update_library($file);
 
-            // Validate and store the H5P content before displaying it.
-            $h5pid = helper::save_h5p($this->factory, $file, $config, $onlyupdatelibs, false);
+            // Start lock to prevent synchronous access to save the same h5p.
+            $lockfactory = lock_config::get_lock_factory('core_h5p');
+            $lockkey = 'core_h5p_' . $pathnamehash;
+            if ($lock = $lockfactory->get_lock($lockkey, 10)) {
+                try {
+                    // Validate and store the H5P content before displaying it.
+                    $h5pid = helper::save_h5p($this->factory, $file, $config, $onlyupdatelibs, false);
+                } finally {
+                    $lock->release();
+                }
+            } else {
+                $this->core->h5pF->setErrorMessage(get_string('lockh5pdeploy', 'core_h5p'));
+                return false;
+            };
             if (!$h5pid && $file->get_userid() != $USER->id && has_capability('moodle/h5p:updatelibraries', $this->context)) {
                 // The user has permission to update libraries but the package has been uploaded by a different
                 // user without this permission. Check if there is some missing required library error.
index c7235eb..b9858b5 100644 (file)
@@ -137,6 +137,7 @@ $string['licenseV3'] = 'Version 3';
 $string['licensee'] = 'Licensee';
 $string['licenseextras'] = 'License extras';
 $string['licenseversion'] = 'License version';
+$string['lockh5pdeploy'] = 'This H5P content cannot be accessed because it is being deployed. Please try again later.';
 $string['missingcontentfolder'] = 'A valid content folder is missing';
 $string['missingcoreversion'] = 'The system was unable to install the {$a->%component} component from the package, as it requires a newer version of the H5P plugin. This site is currently running version {$a->%current}, whereas the required version is {$a->%required} or higher. Please upgrade and then try again.';
 $string['missingdependency'] = 'Missing dependency {$a->@dep} required by {$a->@lib}.';
index 17b2668..b55ebf8 100644 (file)
@@ -26,6 +26,7 @@
  */
 
 define('AJAX_SCRIPT', true);
+define('READ_ONLY_SESSION', true);
 
 /** Include config */
 require_once(__DIR__ . '/../../config.php');
index 3bcb8e9..977b5e4 100644 (file)
@@ -28,6 +28,8 @@
  */
 
 define('AJAX_SCRIPT', true);
+// Services can declare 'readonlysession' in their config located in db/services.php, if not present will default to false.
+define('READ_ONLY_SESSION', true);
 
 if (!empty($_GET['nosessionupdate'])) {
     define('NO_SESSION_UPDATE', true);
index 1ed83a9..e1e0416 100644 (file)
@@ -115,7 +115,7 @@ abstract class core_filetypes {
             'gzip' => array('type' => 'application/g-zip', 'icon' => 'archive',
                     'groups' => array('archive'), 'string' => 'archive'),
             'h' => array('type' => 'text/plain', 'icon' => 'sourcecode'),
-            'h5p' => array('type' => 'application/zip', 'icon' => 'h5p', 'string' => 'archive'),
+            'h5p' => array('type' => 'application/zip.h5p', 'icon' => 'h5p', 'string' => 'archive'),
             'hpp' => array('type' => 'text/plain', 'icon' => 'sourcecode'),
             'hqx' => array('type' => 'application/mac-binhex40', 'icon' => 'archive',
                     'groups' => array('archive'), 'string' => 'archive'),
index ab19a78..c8c842f 100644 (file)
@@ -172,7 +172,9 @@ class database extends handler {
             }
             if (!$this->recordid) {
                 // Lock session if exists and not already locked.
-                $this->database->get_session_lock($record->id, $this->acquiretimeout);
+                if ($this->requires_write_lock()) {
+                    $this->database->get_session_lock($record->id, $this->acquiretimeout);
+                }
                 $this->recordid = $record->id;
             }
         } catch (\dml_sessionwait_exception $ex) {
index 276f553..ca3ee97 100644 (file)
@@ -34,6 +34,9 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class handler {
+    /** @var boolean $requireswritelock does the session need and/or have a lock? */
+    protected $requireswritelock = false;
+
     /**
      * Start the session.
      * @return bool success
@@ -42,6 +45,49 @@ abstract class handler {
         return session_start();
     }
 
+    /**
+     * Write the session and release lock. If the session was not intentionally opened
+     * with a write lock, then we will abort the session instead if able.
+     */
+    public function write_close() {
+        if ($this->requires_write_lock()) {
+            session_write_close();
+            $this->requireswritelock = false;
+        } else {
+            $this->abort();
+        }
+    }
+
+    /**
+     * Release lock on the session without writing it.
+     * May not be possible in older versions of PHP. If so, session may be written anyway
+     * so that any locks are released.
+     */
+    public function abort() {
+        session_abort();
+        $this->requireswritelock = false;
+    }
+
+    /**
+     * This is called after init() and before start() to indicate whether the session
+     * opened should be writable or not. This is intentionally captured even if your
+     * handler doesn't support non-locking sessions, so that behavior (upon session close)
+     * matches closely between handlers.
+     * @param bool $requireswritelock true if needs to be open for writing
+     */
+    public function set_requires_write_lock($requireswritelock) {
+        $this->requireswritelock = $requireswritelock;
+    }
+
+    /**
+     * Has this session been opened with a writelock? Your handler should call this during
+     * start() if you support read-only sessions.
+     * @return bool true if session is intended to have a write lock.
+     */
+    public function requires_write_lock() {
+        return $this->requireswritelock;
+    }
+
     /**
      * Init session handler.
      */
index 7510dcc..c3b0baa 100644 (file)
@@ -57,6 +57,27 @@ class manager {
     /** @var string $logintokenkey Key used to get and store request protection for login form. */
     protected static $logintokenkey = 'core_auth_login';
 
+    /** @var array Stores the the SESSION before a request is performed, used to check incorrect read-only modes */
+    private static $priorsession = [];
+
+    /**
+     * If the current session is not writeable, abort it, and re-open it
+     * requesting (and blocking) until a write lock is acquired.
+     * If current session was already opened with an intentional write lock,
+     * this call will not do anything.
+     * NOTE: Even when using a session handler that does not support non-locking sessions,
+     * if the original session was not opened with the explicit intention of being locked,
+     * this will still restart your session so that code behaviour matches as closely
+     * as practical across environments.
+     */
+    public static function restart_with_write_lock() {
+        if (self::$sessionactive && !self::$handler->requires_write_lock()) {
+            @self::$handler->abort();
+            self::$sessionactive = false;
+            self::start_session(true);
+        }
+    }
+
     /**
      * Start user session.
      *
@@ -82,8 +103,26 @@ class manager {
             return;
         }
 
+        if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) {
+            $requireslock = !READ_ONLY_SESSION;
+        } else {
+            $requireslock = true; // For backwards compatibility, we default to assuming that a lock is needed.
+        }
+        self::start_session($requireslock);
+    }
+
+    /**
+     * Handles starting a session.
+     *
+     * @param bool $requireslock If this is false then no write lock will be acquired,
+     *                           and the session will be read-only.
+     */
+    private static function start_session(bool $requireslock) {
+        global $PERF;
+
         try {
             self::$handler->init();
+            self::$handler->set_requires_write_lock($requireslock);
             self::prepare_cookies();
             $isnewsession = empty($_COOKIE[session_name()]);
 
@@ -99,6 +138,10 @@ class manager {
             self::$sessionactive = true; // Set here, so the session can be cleared if the security check fails.
             self::check_security();
 
+            if (!$requireslock) {
+                self::$priorsession = (array) $_SESSION['SESSION'];
+            }
+
             // Link global $USER and $SESSION,
             // this is tricky because PHP does not allow references to references
             // and global keyword uses internally once reference to the $GLOBALS array.
@@ -633,9 +676,7 @@ class manager {
         $DB->delete_records('sessions', array('sid'=>$sid));
         self::init_empty_session();
         self::add_session_record($_SESSION['USER']->id); // Do not use $USER here because it may not be set up yet.
-        session_write_close();
-        self::$sessionactive = false;
-
+        self::write_close();
         self::append_samesite_cookie_attribute();
     }
 
@@ -655,27 +696,46 @@ class manager {
             $PERF->sessionlock['url'] = me();
             self::update_recent_session_locks($PERF->sessionlock);
             self::sessionlock_debugging();
-        }
 
-        if (version_compare(PHP_VERSION, '5.6.0', '>=')) {
-            // More control over whether session data
-            // is persisted or not.
-            if (self::$sessionactive && session_id()) {
-                // Write session and release lock only if
-                // indication session start was clean.
-                session_write_close();
-            } else {
-                // Otherwise, if possibile lock exists want
-                // to clear it, but do not write session.
-                @session_abort();
+            if (!self::$handler->requires_write_lock()) {
+                // Compare the array of the earlier session data with the array now, if
+                // there is a difference then a lock is required.
+                $arraydiff = self::array_session_diff(
+                    self::$priorsession,
+                    (array) $_SESSION['SESSION']
+                );
+
+                if ($arraydiff) {
+                    if (isset($arraydiff['cachestore_session'])) {
+                        throw new \moodle_exception('The session store can not be in the session when '
+                            . 'enable_read_only_sessions is enabled');
+                    }
+
+                    error_log('This session was started as a read-only session but writes have been detected.');
+                    error_log('The following SESSION params were either added, or were updated.');
+                    foreach ($arraydiff as $key => $value) {
+                        error_log('SESSION key: ' . $key);
+                    }
+                }
             }
+        }
+
+        // More control over whether session data
+        // is persisted or not.
+        if (self::$sessionactive && session_id()) {
+            // Write session and release lock only if
+            // indication session start was clean.
+            self::$handler->write_close();
         } else {
-            // Any indication session was started, attempt
-            // to close it.
-            if (self::$sessionactive || session_id()) {
-                session_write_close();
+            // Otherwise, if possible lock exists want
+            // to clear it, but do not write session.
+            // If the $handler has not been set then
+            // there is no session to abort.
+            if (isset(self::$handler)) {
+                @self::$handler->abort();
             }
         }
+
         self::$sessionactive = false;
     }
 
@@ -1317,4 +1377,27 @@ class manager {
             }
         }
     }
+
+    /**
+     * Compares two arrays outputs the difference.
+     *
+     * Note this does not use array_diff_assoc due to
+     * the use of stdClasses in Moodle sessions.
+     *
+     * @param array $array1
+     * @param array $array2
+     * @return array
+     */
+    private static function array_session_diff(array $array1, array $array2) : array {
+        $difference = [];
+        foreach ($array1 as $key => $value) {
+            if (!isset($array2[$key])) {
+                $difference[$key] = $value;
+            } else if ($array2[$key] !== $value) {
+                $difference[$key] = $value;
+            }
+        }
+
+        return $difference;
+    }
 }
index 643e10e..58307a3 100644 (file)
@@ -101,6 +101,8 @@ class memcached extends handler {
      * @return bool success
      */
     public function start() {
+        ini_set('memcached.sess_locking', $this->requires_write_lock() ? '1' : '0');
+
         // NOTE: memcached before 2.2.0 expires session locks automatically after max_execution_time,
         //       this leads to major difference compared to other session drivers that timeout
         //       and stop the second request execution instead.
@@ -148,7 +150,6 @@ class memcached extends handler {
         ini_set('session.save_handler', 'memcached');
         ini_set('session.save_path', $this->savepath);
         ini_set('memcached.sess_prefix', $this->prefix);
-        ini_set('memcached.sess_locking', '1'); // Locking is required!
         ini_set('memcached.sess_lock_expire', $this->lockexpire);
 
         if (version_compare($version, '3.0.0-dev') >= 0) {
index d475867..b54639a 100644 (file)
@@ -255,9 +255,11 @@ class redis extends handler {
      */
     public function handler_read($id) {
         try {
-            $this->lock_session($id);
+            if ($this->requires_write_lock()) {
+                $this->lock_session($id);
+            }
             $sessiondata = $this->connection->get($id);
-            if ($sessiondata === false) {
+            if ($sessiondata === false && $this->requires_write_lock()) {
                 $this->unlock_session($id);
                 return '';
             }
index a9c8106..6d997e7 100644 (file)
@@ -748,6 +748,7 @@ $functions = array(
         'type' => 'read',
         'loginrequired' => false,
         'ajax' => true,
+        'readonlysession' => false, // Fetching removes from stack.
     ),
     'core_session_touch' => array(
         'classname' => 'core\session\external',
@@ -1374,6 +1375,7 @@ $functions = array(
         'type' => 'read',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+        'readonlysession' => true, // We don't modify the session.
     ),
     'core_message_mark_all_notifications_as_read' => array(
         'classname' => 'core_message_external',
index e27a541..f71d4d4 100644 (file)
@@ -2291,5 +2291,20 @@ function xmldb_main_upgrade($oldversion) {
         // Main savepoint reached.
         upgrade_main_savepoint(true, 2020041500.00);
     }
+
+    if ($oldversion < 2020041700.01) {
+        // Upgrade h5p MIME type for existing h5p files.
+        $select = $DB->sql_like('filename', '?', false);
+        $DB->set_field_select(
+            'files',
+            'mimetype',
+            'application/zip.h5p',
+            $select,
+            array('%.h5p')
+        );
+
+        upgrade_main_savepoint(true, 2020041700.01);
+    }
+
     return true;
 }
index 24002e0..002a99d 100644 (file)
@@ -161,6 +161,11 @@ class external_api {
             } else {
                 $function->loginrequired = true;
             }
+            if (isset($functions[$function->name]['readonlysession'])) {
+                $function->readonlysession = $functions[$function->name]['readonlysession'];
+            } else {
+                $function->readonlysession = false;
+            }
         }
 
         return $function;
@@ -184,6 +189,12 @@ class external_api {
 
         $externalfunctioninfo = static::external_function_info($function);
 
+        // Eventually this should shift into the various handlers and not be handled via config.
+        $readonlysession = $externalfunctioninfo->readonlysession ?? false;
+        if (!$readonlysession || empty($CFG->enable_read_only_sessions)) {
+            \core\session\manager::restart_with_write_lock();
+        }
+
         $currentpage = $PAGE;
         $currentcourse = $COURSE;
         $response = array();
index b1a7a3a..9c8b545 100644 (file)
@@ -23,7 +23,7 @@ Example
 use \Firebase\JWT\JWT;
 
 $key = "example_key";
-$token = array(
+$payload = array(
     "iss" => "http://example.org",
     "aud" => "http://example.com",
     "iat" => 1356999524,
@@ -36,7 +36,7 @@ $token = array(
  * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
  * for a list of spec-compliant algorithms.
  */
-$jwt = JWT::encode($token, $key);
+$jwt = JWT::encode($payload, $key);
 $decoded = JWT::decode($jwt, $key, array('HS256'));
 
 print_r($decoded);
@@ -93,14 +93,14 @@ ehde/zUxo6UvS7UrBQIDAQAB
 -----END PUBLIC KEY-----
 EOD;
 
-$token = array(
+$payload = array(
     "iss" => "example.org",
     "aud" => "example.com",
     "iat" => 1356999524,
     "nbf" => 1357000000
 );
 
-$jwt = JWT::encode($token, $privateKey, 'RS256');
+$jwt = JWT::encode($payload, $privateKey, 'RS256');
 echo "Encode:\n" . print_r($jwt, true) . "\n";
 
 $decoded = JWT::decode($jwt, $publicKey, array('RS256'));
index b76ffd1..25d1cfa 100644 (file)
@@ -2,6 +2,10 @@
     "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",
+    "keywords": [
+        "php",
+        "jwt"
+    ],
     "authors": [
         {
             "name": "Neuman Vong",
@@ -24,6 +28,6 @@
         }
     },
     "require-dev": {
-        "phpunit/phpunit": " 4.8.35"
+        "phpunit/phpunit": ">=4.8 <=9"
     }
 }
index a6ee2f7..fdf82bd 100644 (file)
@@ -3,5 +3,4 @@ namespace Firebase\JWT;
 
 class BeforeValidException extends \UnexpectedValueException
 {
-
 }
index 3597370..7f7d056 100644 (file)
@@ -3,5 +3,4 @@ namespace Firebase\JWT;
 
 class ExpiredException extends \UnexpectedValueException
 {
-
 }
diff --git a/lib/php-jwt/src/JWK.php b/lib/php-jwt/src/JWK.php
new file mode 100644 (file)
index 0000000..1d27391
--- /dev/null
@@ -0,0 +1,171 @@
+<?php
+
+namespace Firebase\JWT;
+
+use DomainException;
+use UnexpectedValueException;
+
+/**
+ * JSON Web Key implementation, based on this spec:
+ * https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41
+ *
+ * PHP version 5
+ *
+ * @category Authentication
+ * @package  Authentication_JWT
+ * @author   Bui Sy Nguyen <nguyenbs@gmail.com>
+ * @license  http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
+ * @link     https://github.com/firebase/php-jwt
+ */
+class JWK
+{
+    /**
+     * Parse a set of JWK keys
+     *
+     * @param array $jwks The JSON Web Key Set as an associative array
+     *
+     * @return array An associative array that represents the set of keys
+     *
+     * @throws InvalidArgumentException     Provided JWK Set is empty
+     * @throws UnexpectedValueException     Provided JWK Set was invalid
+     * @throws DomainException              OpenSSL failure
+     *
+     * @uses parseKey
+     */
+    public static function parseKeySet(array $jwks)
+    {
+        $keys = array();
+
+        if (!isset($jwks['keys'])) {
+            throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
+        }
+        if (empty($jwks['keys'])) {
+            throw new InvalidArgumentException('JWK Set did not contain any keys');
+        }
+
+        foreach ($jwks['keys'] as $k => $v) {
+            $kid = isset($v['kid']) ? $v['kid'] : $k;
+            if ($key = self::parseKey($v)) {
+                $keys[$kid] = $key;
+            }
+        }
+
+        if (0 === \count($keys)) {
+            throw new UnexpectedValueException('No supported algorithms found in JWK Set');
+        }
+
+        return $keys;
+    }
+
+    /**
+     * Parse a JWK key
+     *
+     * @param array $jwk An individual JWK
+     *
+     * @return resource|array An associative array that represents the key
+     *
+     * @throws InvalidArgumentException     Provided JWK is empty
+     * @throws UnexpectedValueException     Provided JWK was invalid
+     * @throws DomainException              OpenSSL failure
+     *
+     * @uses createPemFromModulusAndExponent
+     */
+    private static function parseKey(array $jwk)
+    {
+        if (empty($jwk)) {
+            throw new InvalidArgumentException('JWK must not be empty');
+        }
+        if (!isset($jwk['kty'])) {
+            throw new UnexpectedValueException('JWK must contain a "kty" parameter');
+        }
+
+        switch ($jwk['kty']) {
+            case 'RSA':
+                if (\array_key_exists('d', $jwk)) {
+                    throw new UnexpectedValueException('RSA private keys are not supported');
+                }
+                if (!isset($jwk['n']) || !isset($jwk['e'])) {
+                    throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
+                }
+
+                $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
+                $publicKey = \openssl_pkey_get_public($pem);
+                if (false === $publicKey) {
+                    throw new DomainException(
+                        'OpenSSL error: ' . \openssl_error_string()
+                    );
+                }
+                return $publicKey;
+            default:
+                // Currently only RSA is supported
+                break;
+        }
+    }
+
+    /**
+     * Create a public key represented in PEM format from RSA modulus and exponent information
+     *
+     * @param string $n The RSA modulus encoded in Base64
+     * @param string $e The RSA exponent encoded in Base64
+     *
+     * @return string The RSA public key represented in PEM format
+     *
+     * @uses encodeLength
+     */
+    private static function createPemFromModulusAndExponent($n, $e)
+    {
+        $modulus = JWT::urlsafeB64Decode($n);
+        $publicExponent = JWT::urlsafeB64Decode($e);
+
+        $components = array(
+            'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus),
+            'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent)
+        );
+
+        $rsaPublicKey = \pack(
+            'Ca*a*a*',
+            48,
+            self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])),
+            $components['modulus'],
+            $components['publicExponent']
+        );
+
+        // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
+        $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
+        $rsaPublicKey = \chr(0) . $rsaPublicKey;
+        $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
+
+        $rsaPublicKey = \pack(
+            'Ca*a*',
+            48,
+            self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
+            $rsaOID . $rsaPublicKey
+        );
+
+        $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" .
+            \chunk_split(\base64_encode($rsaPublicKey), 64) .
+            '-----END PUBLIC KEY-----';
+
+        return $rsaPublicKey;
+    }
+
+    /**
+     * DER-encode the length
+     *
+     * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4.  See
+     * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
+     *
+     * @param int $length
+     * @return string
+     */
+    private static function encodeLength($length)
+    {
+        if ($length <= 0x7F) {
+            return \chr($length);
+        }
+
+        $temp = \ltrim(\pack('N', $length), \chr(0));
+
+        return \pack('Ca*', 0x80 | \strlen($temp), $temp);
+    }
+}
index 33e2ed4..4ccc1a9 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 namespace Firebase\JWT;
+
 use \DomainException;
 use \InvalidArgumentException;
 use \UnexpectedValueException;
@@ -21,6 +22,9 @@ use \DateTime;
  */
 class JWT
 {
+    const ASN1_INTEGER = 0x02;
+    const ASN1_SEQUENCE = 0x10;
+    const ASN1_BIT_STRING = 0x03;
 
     /**
      * When checking nbf, iat or expiration times,
@@ -38,9 +42,10 @@ class JWT
     public static $timestamp = null;
 
     public static $supported_algs = array(
+        'ES256' => array('openssl', 'SHA256'),
         'HS256' => array('hash_hmac', 'SHA256'),
-        'HS512' => array('hash_hmac', 'SHA512'),
         'HS384' => array('hash_hmac', 'SHA384'),
+        'HS512' => array('hash_hmac', 'SHA512'),
         'RS256' => array('openssl', 'SHA256'),
         'RS384' => array('openssl', 'SHA384'),
         'RS512' => array('openssl', 'SHA512'),
@@ -49,11 +54,11 @@ class JWT
     /**
      * 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'
+     * @param string                    $jwt            The JWT
+     * @param string|array|resource     $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 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
      *
      * @return object The JWT's payload as a PHP object
      *
@@ -68,13 +73,13 @@ class JWT
      */
     public static function decode($jwt, $key, array $allowed_algs = array())
     {
-        $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp;
+        $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) {
+        $tks = \explode('.', $jwt);
+        if (\count($tks) != 3) {
             throw new UnexpectedValueException('Wrong number of segments');
         }
         list($headb64, $bodyb64, $cryptob64) = $tks;
@@ -93,10 +98,15 @@ class JWT
         if (empty(static::$supported_algs[$header->alg])) {
             throw new UnexpectedValueException('Algorithm not supported');
         }
-        if (!in_array($header->alg, $allowed_algs)) {
+        if (!\in_array($header->alg, $allowed_algs)) {
             throw new UnexpectedValueException('Algorithm not allowed');
         }
-        if (is_array($key) || $key instanceof \ArrayAccess) {
+        if ($header->alg === 'ES256') {
+            // OpenSSL expects an ASN.1 DER sequence for ES256 signatures
+            $sig = self::signatureToDER($sig);
+        }
+
+        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');
@@ -112,11 +122,11 @@ class JWT
             throw new SignatureInvalidException('Signature verification failed');
         }
 
-        // Check if the nbf if it is defined. This is the time that the
+        // Check 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)
+                'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf)
             );
         }
 
@@ -125,7 +135,7 @@ class JWT
         // 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)
+                'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat)
             );
         }
 
@@ -144,7 +154,7 @@ class JWT
      * @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'
+     *                                  Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
      * @param mixed         $keyId
      * @param array         $head       An array with header elements to attach
      *
@@ -159,18 +169,18 @@ class JWT
         if ($keyId !== null) {
             $header['kid'] = $keyId;
         }
-        if ( isset($head) && is_array($head) ) {
-            $header = array_merge($head, $header);
+        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);
+        $signing_input = \implode('.', $segments);
 
         $signature = static::sign($signing_input, $key, $alg);
         $segments[] = static::urlsafeB64Encode($signature);
 
-        return implode('.', $segments);
+        return \implode('.', $segments);
     }
 
     /**
@@ -179,7 +189,7 @@ class JWT
      * @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'
+     *                                  Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
      *
      * @return string An encrypted message
      *
@@ -191,15 +201,18 @@ class JWT
             throw new DomainException('Algorithm not supported');
         }
         list($function, $algorithm) = static::$supported_algs[$alg];
-        switch($function) {
+        switch ($function) {
             case 'hash_hmac':
-                return hash_hmac($algorithm, $msg, $key, true);
+                return \hash_hmac($algorithm, $msg, $key, true);
             case 'openssl':
                 $signature = '';
-                $success = openssl_sign($msg, $signature, $key, $algorithm);
+                $success = \openssl_sign($msg, $signature, $key, $algorithm);
                 if (!$success) {
                     throw new DomainException("OpenSSL unable to sign data");
                 } else {
+                    if ($alg === 'ES256') {
+                        $signature = self::signatureFromDER($signature, 256);
+                    }
                     return $signature;
                 }
         }
@@ -225,9 +238,9 @@ class JWT
         }
 
         list($function, $algorithm) = static::$supported_algs[$alg];
-        switch($function) {
+        switch ($function) {
             case 'openssl':
-                $success = openssl_verify($msg, $signature, $key, $algorithm);
+                $success = \openssl_verify($msg, $signature, $key, $algorithm);
                 if ($success === 1) {
                     return true;
                 } elseif ($success === 0) {
@@ -235,19 +248,19 @@ class JWT
                 }
                 // returns 1 on success, 0 on failure, -1 on error.
                 throw new DomainException(
-                    'OpenSSL error: ' . openssl_error_string()
+                    '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);
+                $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));
+                $len = \min(static::safeStrlen($signature), static::safeStrlen($hash));
 
                 $status = 0;
                 for ($i = 0; $i < $len; $i++) {
-                    $status |= (ord($signature[$i]) ^ ord($hash[$i]));
+                    $status |= (\ord($signature[$i]) ^ \ord($hash[$i]));
                 }
                 $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash));
 
@@ -266,23 +279,23 @@ class JWT
      */
     public static function jsonDecode($input)
     {
-        if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
+        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);
+            $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);
+            $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()) {
+        if ($errno = \json_last_error()) {
             static::handleJsonError($errno);
         } elseif ($obj === null && $input !== 'null') {
             throw new DomainException('Null result with non-null input');
@@ -301,8 +314,8 @@ class JWT
      */
     public static function jsonEncode($input)
     {
-        $json = json_encode($input);
-        if (function_exists('json_last_error') && $errno = json_last_error()) {
+        $json = \json_encode($input);
+        if ($errno = \json_last_error()) {
             static::handleJsonError($errno);
         } elseif ($json === 'null' && $input !== null) {
             throw new DomainException('Null result with non-null input');
@@ -319,12 +332,12 @@ class JWT
      */
     public static function urlsafeB64Decode($input)
     {
-        $remainder = strlen($input) % 4;
+        $remainder = \strlen($input) % 4;
         if ($remainder) {
             $padlen = 4 - $remainder;
-            $input .= str_repeat('=', $padlen);
+            $input .= \str_repeat('=', $padlen);
         }
-        return base64_decode(strtr($input, '-_', '+/'));
+        return \base64_decode(\strtr($input, '-_', '+/'));
     }
 
     /**
@@ -336,7 +349,7 @@ class JWT
      */
     public static function urlsafeB64Encode($input)
     {
-        return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
+        return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
     }
 
     /**
@@ -365,15 +378,135 @@ class JWT
     /**
      * Get the number of bytes in cryptographic strings.
      *
-     * @param string
+     * @param string $str
      *
      * @return int
      */
     private static function safeStrlen($str)
     {
-        if (function_exists('mb_strlen')) {
-            return mb_strlen($str, '8bit');
+        if (\function_exists('mb_strlen')) {
+            return \mb_strlen($str, '8bit');
         }
-        return strlen($str);
+        return \strlen($str);
+    }
+
+    /**
+     * Convert an ECDSA signature to an ASN.1 DER sequence
+     *
+     * @param   string $sig The ECDSA signature to convert
+     * @return  string The encoded DER object
+     */
+    private static function signatureToDER($sig)
+    {
+        // Separate the signature into r-value and s-value
+        list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2));
+
+        // Trim leading zeros
+        $r = \ltrim($r, "\x00");
+        $s = \ltrim($s, "\x00");
+
+        // Convert r-value and s-value from unsigned big-endian integers to
+        // signed two's complement
+        if (\ord($r[0]) > 0x7f) {
+            $r = "\x00" . $r;
+        }
+        if (\ord($s[0]) > 0x7f) {
+            $s = "\x00" . $s;
+        }
+
+        return self::encodeDER(
+            self::ASN1_SEQUENCE,
+            self::encodeDER(self::ASN1_INTEGER, $r) .
+            self::encodeDER(self::ASN1_INTEGER, $s)
+        );
+    }
+
+    /**
+     * Encodes a value into a DER object.
+     *
+     * @param   int     $type DER tag
+     * @param   string  $value the value to encode
+     * @return  string  the encoded object
+     */
+    private static function encodeDER($type, $value)
+    {
+        $tag_header = 0;
+        if ($type === self::ASN1_SEQUENCE) {
+            $tag_header |= 0x20;
+        }
+
+        // Type
+        $der = \chr($tag_header | $type);
+
+        // Length
+        $der .= \chr(\strlen($value));
+
+        return $der . $value;
+    }
+
+    /**
+     * Encodes signature from a DER object.
+     *
+     * @param   string  $der binary signature in DER format
+     * @param   int     $keySize the number of bits in the key
+     * @return  string  the signature
+     */
+    private static function signatureFromDER($der, $keySize)
+    {
+        // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
+        list($offset, $_) = self::readDER($der);
+        list($offset, $r) = self::readDER($der, $offset);
+        list($offset, $s) = self::readDER($der, $offset);
+
+        // Convert r-value and s-value from signed two's compliment to unsigned
+        // big-endian integers
+        $r = \ltrim($r, "\x00");
+        $s = \ltrim($s, "\x00");
+
+        // Pad out r and s so that they are $keySize bits long
+        $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
+        $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
+
+        return $r . $s;
+    }
+
+    /**
+     * Reads binary DER-encoded data and decodes into a single object
+     *
+     * @param string $der the binary data in DER format
+     * @param int $offset the offset of the data stream containing the object
+     * to decode
+     * @return array [$offset, $data] the new offset and the decoded object
+     */
+    private static function readDER($der, $offset = 0)
+    {
+        $pos = $offset;
+        $size = \strlen($der);
+        $constructed = (\ord($der[$pos]) >> 5) & 0x01;
+        $type = \ord($der[$pos++]) & 0x1f;
+
+        // Length
+        $len = \ord($der[$pos++]);
+        if ($len & 0x80) {
+            $n = $len & 0x1f;
+            $len = 0;
+            while ($n-- && $pos < $size) {
+                $len = ($len << 8) | \ord($der[$pos++]);
+            }
+        }
+
+        // Value
+        if ($type == self::ASN1_BIT_STRING) {
+            $pos++; // Skip the first contents octet (padding indicator)
+            $data = \substr($der, $pos, $len - 1);
+            $pos += $len - 1;
+        } elseif (!$constructed) {
+            $data = \substr($der, $pos, $len);
+            $pos += $len;
+        } else {
+            $data = null;
+        }
+
+        return array($pos, $data);
     }
 }
index 27332b2..87cb34d 100644 (file)
@@ -3,5 +3,4 @@ namespace Firebase\JWT;
 
 class SignatureInvalidException extends \UnexpectedValueException
 {
-
 }
index 26453f9..96cc2ca 100644 (file)
Binary files a/lib/table/amd/build/dynamic.min.js and b/lib/table/amd/build/dynamic.min.js differ
index 15c863a..d457b4f 100644 (file)
Binary files a/lib/table/amd/build/dynamic.min.js.map and b/lib/table/amd/build/dynamic.min.js.map differ
index 439fe85..db1dc71 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/repository.min.js and b/lib/table/amd/build/local/dynamic/repository.min.js differ
index 4806057..d1d3f8b 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/repository.min.js.map and b/lib/table/amd/build/local/dynamic/repository.min.js.map differ
index 5f11a38..c3f264f 100644 (file)
@@ -66,6 +66,7 @@ export const refreshTableContent = tableRoot => {
     const filterset = getFiltersetFromTable(tableRoot);
 
     return fetchTableData(
+        tableRoot.dataset.tableComponent,
         tableRoot.dataset.tableHandler,
         tableRoot.dataset.tableUniqueid,
         {
index 5ef99c4..9083c6f 100644 (file)
@@ -28,6 +28,7 @@ import {call as fetchMany} from 'core/ajax';
  * Fetch table view.
  *
  * @method fetch
+ * @param {String} component The component
  * @param {String} handler The name of the handler
  * @param {String} uniqueid The unique id of the table
  * @param {Object} filters The filters to apply when searching
@@ -38,7 +39,7 @@ import {call as fetchMany} from 'core/ajax';
  * @param {Number} params parameters to request table
  * @return {Promise} Resolved with requested table view
  */
-export const fetch = (handler, uniqueid, {
+export const fetch = (component, handler, uniqueid, {
         sortBy = null,
         sortOrder = null,
         joinType = null,
@@ -52,6 +53,7 @@ export const fetch = (handler, uniqueid, {
     return fetchMany([{
         methodname: `core_table_dynamic_fetch`,
         args: {
+            component,
             handler,
             uniqueid,
             sortby: sortBy,
index 194822f..f49b6b4 100644 (file)
@@ -51,9 +51,14 @@ class fetch extends external_api {
      */
     public static function execute_parameters(): external_function_parameters {
         return new external_function_parameters ([
+            'component' => new external_value(
+                PARAM_COMPONENT,
+                'Component',
+                VALUE_REQUIRED
+            ),
             'handler' => new external_value(
                 // Note: We do not have a PARAM_CLASSNAME which would have been ideal.
-                PARAM_RAW,
+                PARAM_ALPHANUMEXT,
                 'Handler',
                 VALUE_REQUIRED
             ),
@@ -116,6 +121,7 @@ class fetch extends external_api {
     /**
      * External function to fetch a table view.
      *
+     * @param string $component The component.
      * @param string $handler Dynamic table class name.
      * @param string $uniqueid Unique ID for the container.
      * @param string $sortby The name of a sortable column.
@@ -126,11 +132,11 @@ class fetch extends external_api {
      * @param string $lastinitial The last name initial to filter on
      * @param int $pagenumber The page number.
      * @param int $pagesize The number of records.
-     * @param string $jointype The join type.
      *
      * @return array
      */
     public static function execute(
+        string $component,
         string $handler,
         string $uniqueid,
         string $sortby,
@@ -145,11 +151,8 @@ class fetch extends external_api {
 
         global $PAGE;
 
-        if (!class_exists($handler) || !is_subclass_of($handler, \core_table\dynamic::class)) {
-            throw new \UnexpectedValueException('Unknown table handler, or table handler does not support dynamic updating.');
-        }
-
         [
+            'component' => $component,
             'handler' => $handler,
             'uniqueid' => $uniqueid,
             'sortby' => $sortby,
@@ -161,6 +164,7 @@ class fetch extends external_api {
             'pagenumber' => $pagenumber,
             'pagesize' => $pagesize,
         ] = self::validate_parameters(self::execute_parameters(), [
+            'component' => $component,
             'handler' => $handler,
             'uniqueid' => $uniqueid,
             'sortby' => $sortby,
@@ -173,7 +177,22 @@ class fetch extends external_api {
             'pagesize' => $pagesize,
         ]);
 
-        $filterset = new \core_user\table\participants_filterset();
+        $tableclass = "\\{$component}\\table\\{$handler}";
+        if (!class_exists($tableclass)) {
+            throw new \UnexpectedValueException("Table handler class {$tableclass} not found. " .
+                "Please make sure that your table handler class is under the \\{$component}\\table namespace.");
+        }
+
+        if (!is_subclass_of($tableclass, \core_table\dynamic::class)) {
+            throw new \UnexpectedValueException("Table handler class {$tableclass} does not support dynamic updating.");
+        }
+
+        $filtersetclass = "{$tableclass}_filterset";
+        if (!class_exists($filtersetclass)) {
+            throw new \UnexpectedValueException("The filter specified ({$filtersetclass}) is invalid.");
+        }
+
+        $filterset = new $filtersetclass();
         foreach ($filters as $rawfilter) {
             $filterset->add_filter_from_params(
                 $rawfilter['name'],
@@ -182,7 +201,7 @@ class fetch extends external_api {
             );
         }
 
-        $instance = new $handler($uniqueid);
+        $instance = new $tableclass($uniqueid);
         $instance->set_filterset($filterset);
         $instance->set_sorting($sortby, $sortorder);
 
@@ -208,11 +227,11 @@ class fetch extends external_api {
 
         ob_start();
         $instance->out($pagesize, true);
-        $participanttablehtml = ob_get_contents();
+        $tablehtml = ob_get_contents();
         ob_end_clean();
 
         return [
-            'html' => $participanttablehtml,
+            'html' => $tablehtml,
             'warnings' => []
         ];
     }
index 123b0ec..b5ae343 100644 (file)
@@ -48,6 +48,26 @@ class fetch_test extends advanced_testcase {
         require_once("{$CFG->libdir}/externallib.php");
     }
 
+    /**
+     * Test execute invalid component format.
+     */
+    public function test_execute_invalid_component_format(): void {
+        $this->resetAfterTest();
+
+        $this->expectException(\invalid_parameter_exception::class);
+        fetch::execute("core-user", "participants", "", "email", "4", [], "1");
+    }
+
+    /**
+     * Test execute invalid component.
+     */
+    public function test_execute_invalid_component(): void {
+        $this->resetAfterTest();
+
+        $this->expectException(\UnexpectedValueException::class);
+        fetch::execute("core_users", "participants", "", "email", "4", [], "1");
+    }
+
     /**
      * Test execute invalid handler.
      */
@@ -55,10 +75,11 @@ class fetch_test extends advanced_testcase {
         $this->resetAfterTest();
 
         $this->expectException('UnexpectedValueException');
-        $this->expectExceptionMessage("Unknown table handler, or table handler does not support dynamic updating.");
+        $handler = "\\core_user\\table\\users_participants_table";
+        $this->expectExceptionMessage("Table handler class {$handler} not found. Please make sure that your table handler class is under the \\core_user\\table namespace.");
 
         // Tests that invalid users_participants_table class gets an exception.
-        fetch::execute("core_user\users_participants_table", "", "email", "4", [], "1");
+        fetch::execute("core_user", "users_participants_table", "", "email", "4", [], "1");
     }
 
     /**
@@ -77,13 +98,12 @@ class fetch_test extends advanced_testcase {
                 'values' => [(int)$course->id]
             ]
         ];
-        $this->expectException('invalid_parameter_exception');
+        $this->expectException(\invalid_parameter_exception::class);
         $this->expectExceptionMessage("Invalid parameter value detected (filters => Invalid parameter value detected " .
         "(Missing required key in single structure: name): Missing required key in single structure: name");
 
-        fetch::execute("core_user\participants_table",
-            "user-index-participants-{$course->id}", "firstname", "4", $filter, (string)filter::JOINTYPE_ANY);
-
+        fetch::execute("core_user", "participants", "user-index-participants-{$course->id}",
+        "firstname", "4", $filter, (string)filter::JOINTYPE_ANY);
     }
 
     /**
@@ -108,7 +128,7 @@ class fetch_test extends advanced_testcase {
             ]
         ];
 
-        $participantstable = fetch::execute("core_user\participants_table",
+        $participantstable = fetch::execute("core_user", "participants",
             "user-index-participants-{$course->id}", "firstname", "4", $filter, (string)filter::JOINTYPE_ANY);
         $html = $participantstable['html'];
 
index 3f79a59..33375eb 100644 (file)
@@ -1499,6 +1499,26 @@ class flexible_table {
         ];
     }
 
+    /**
+     * Get dynamic class component.
+     *
+     * @return string
+     */
+    protected function get_component() {
+        $tableclass = explode("\\", get_class($this));
+        return reset($tableclass);
+    }
+
+    /**
+     * Get dynamic class handler.
+     *
+     * @return string
+     */
+    protected function get_handler() {
+        $tableclass = explode("\\", get_class($this));
+        return end($tableclass);
+    }
+
     /**
      * Get the dynamic table start wrapper.
      * If this is not a dynamic table, then an empty string is returned making this safe to blindly call.
@@ -1510,7 +1530,8 @@ class flexible_table {
             $sortdata = $this->get_sort_order();
             return html_writer::start_tag('div', [
                 'data-region' => 'core_table/dynamic',
-                'data-table-handler' => get_class($this),
+                'data-table-handler' => $this->get_handler(),
+                'data-table-component' => $this->get_component(),
                 'data-table-uniqueid' => $this->uniqueid,
                 'data-table-filters' => json_encode($this->get_filterset()),
                 'data-table-sort-by' => $sortdata['sortby'],
index 0695b14..c7b53df 100644 (file)
@@ -307,6 +307,9 @@ class behat_hooks extends behat_base {
         $driverexceptionmsg = 'Selenium server is not running, you need to start it to run tests that involve Javascript. ' . $moreinfo;
         try {
             $session = $this->getSession();
+            if (!$session->isStarted()) {
+                $session->start();
+            }
         } catch (CurlExec $e) {
             // Exception thrown by WebDriver, so only @javascript tests will be caugth; in
             // behat_util::check_server_status() we already checked that the server is running.
index bbafd02..a71d1ac 100644 (file)
@@ -854,4 +854,61 @@ class core_session_manager_testcase extends advanced_testcase {
         $this->assertCount(1, $SESSION->recentsessionlocks);
         $this->assertEquals('/good.php?id=4', $SESSION->recentsessionlocks[0]['url']);
     }
+
+    public function test_array_session_diff_same_array() {
+        $a = [];
+        $a['c'] = new stdClass();
+        $a['c']->o = new stdClass();
+        $a['c']->o->o = new stdClass();
+        $a['c']->o->o->l = 'cool';
+
+        $class = new ReflectionClass('\core\session\manager');
+        $method = $class->getMethod('array_session_diff');
+        $method->setAccessible(true);
+
+        $result = $method->invokeArgs(null, [$a, $a]);
+
+        $this->assertEmpty($result);
+    }
+
+    public function test_array_session_diff_first_array_larger() {
+        $a = [];
+        $a['stdClass'] = new stdClass();
+        $a['stdClass']->attribute = 'This is an attribute';
+        $a['array'] = ['array', 'contents'];
+
+        $b = [];
+        $b['array'] = ['array', 'contents'];
+
+        $class = new ReflectionClass('\core\session\manager');
+        $method = $class->getMethod('array_session_diff');
+        $method->setAccessible(true);
+
+        $result = $method->invokeArgs(null, [$a, $b]);
+
+        $expected = [];
+        $expected['stdClass'] = new stdClass();
+        $expected['stdClass']->attribute = 'This is an attribute';
+        $this->assertEquals($expected, $result);
+    }
+
+    public function test_array_session_diff_second_array_larger() {
+        $a = [];
+        $a['array'] = ['array', 'contents'];
+
+        $b = [];
+        $b['stdClass'] = new stdClass();
+        $b['stdClass']->attribute = 'This is an attribute';
+        $b['array'] = ['array', 'contents'];
+
+        $class = new ReflectionClass('\core\session\manager');
+        $method = $class->getMethod('array_session_diff');
+        $method->setAccessible(true);
+
+        $result = $method->invokeArgs(null, [$a, $b]);
+
+        // It's empty because the first array contains all the contents of the second.
+        $expected = [];
+        $this->assertEquals($expected, $result);
+    }
 }
index ff9ddd3..938491a 100644 (file)
@@ -94,6 +94,7 @@ class core_session_redis_testcase extends advanced_testcase {
     public function test_normal_session_start_stop_works() {
         $sess = new \core\session\redis();
         $sess->init();
+        $sess->set_requires_write_lock(true);
         $this->assertTrue($sess->handler_open('Not used', 'Not used'));
         $this->assertSame('', $sess->handler_read('sess1'));
         $this->assertTrue($sess->handler_write('sess1', 'DATA'));
@@ -110,6 +111,7 @@ class core_session_redis_testcase extends advanced_testcase {
     public function test_session_blocks_with_existing_session() {
         $sess = new \core\session\redis();
         $sess->init();
+        $sess->set_requires_write_lock(true);
         $this->assertTrue($sess->handler_open('Not used', 'Not used'));
         $this->assertSame('', $sess->handler_read('sess1'));
         $this->assertTrue($sess->handler_write('sess1', 'DATA'));
@@ -121,6 +123,7 @@ class core_session_redis_testcase extends advanced_testcase {
 
         $sessblocked = new \core\session\redis();
         $sessblocked->init();
+        $sessblocked->set_requires_write_lock(true);
         $this->assertTrue($sessblocked->handler_open('Not used', 'Not used'));
 
         // Trap the error log and send it to stdOut so we can expect output at the right times.
@@ -143,6 +146,7 @@ class core_session_redis_testcase extends advanced_testcase {
     public function test_session_is_destroyed_when_it_does_not_exist() {
         $sess = new \core\session\redis();
         $sess->init();
+        $sess->set_requires_write_lock(true);
         $this->assertTrue($sess->handler_open('Not used', 'Not used'));
         $this->assertTrue($sess->handler_destroy('sess-destroy'));
         $this->assertSessionNoLocks();
@@ -151,6 +155,7 @@ class core_session_redis_testcase extends advanced_testcase {
     public function test_session_is_destroyed_when_we_have_it_open() {
         $sess = new \core\session\redis();
         $sess->init();
+        $sess->set_requires_write_lock(true);
         $this->assertTrue($sess->handler_open('Not used', 'Not used'));
         $this->assertSame('', $sess->handler_read('sess-destroy'));
         $this->assertTrue($sess->handler_destroy('sess-destroy'));
@@ -160,8 +165,10 @@ class core_session_redis_testcase extends advanced_testcase {
 
     public function test_multiple_sessions_do_not_interfere_with_each_other() {
         $sess1 = new \core\session\redis();
+        $sess1->set_requires_write_lock(true);
         $sess1->init();
         $sess2 = new \core\session\redis();
+        $sess2->set_requires_write_lock(true);
         $sess2->init();
 
         // Initialize session 1.
@@ -203,6 +210,7 @@ class core_session_redis_testcase extends advanced_testcase {
     public function test_multiple_sessions_work_with_a_single_instance() {
         $sess = new \core\session\redis();
         $sess->init();
+        $sess->set_requires_write_lock(true);
 
         // Initialize session 1.
         $this->assertTrue($sess->handler_open('Not used', 'Not used'));
@@ -223,6 +231,7 @@ class core_session_redis_testcase extends advanced_testcase {
     public function test_session_exists_returns_valid_values() {
         $sess = new \core\session\redis();
         $sess->init();
+        $sess->set_requires_write_lock(true);
 
         $this->assertTrue($sess->handler_open('Not used', 'Not used'));
         $this->assertSame('', $sess->handler_read('sess1'));
index 9c01c71..f90da3d 100644 (file)
     <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>
+    <version>5.2.0</version>
     <licenseversion>3-Clause</licenseversion>
   </library>
   <library>
index ea13839..f9908eb 100644 (file)
@@ -38,6 +38,10 @@ information provided here is intended especially for developers.
 * H5P libraries have been moved from /lib/h5p to h5p/h5plib as an h5plib plugintype.
 * mdn-polyfills has been renamed to polyfills. The reason there is no polyfill from the MDN is
   because there is no example polyfills on the MDN for this functionality.
+* AJAX pages can be called without requiring a session lock if they set READ_ONLY_SESSION to true, eg.
+  define('READ_ONLY_SESSION', true); Note - this also requires $CFG->enable_read_only_sessions to be set to true.
+* External functions can be called without requiring a session lock if they define 'readonlysession' => true in
+  db/services.php. Note - this also requires $CFG->enable_read_only_sessions to be set to true.
 * database_manager::check_database_schema() now checks for missing and extra indexes.
 
 === 3.8 ===
index 89809e4..b2521fd 100644 (file)
@@ -41,5 +41,6 @@ $functions = array(
         'type' => 'read',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+        'readonlysession' => true,
     ),
 );
diff --git a/mod/lti/db/caches.php b/mod/lti/db/caches.php
new file mode 100644 (file)
index 0000000..9d4f4e7
--- /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/>.
+
+/**
+ * This file contains the cache definitions for the lti plugin
+ *
+ * @package    mod_lti
+ * @copyright 2020 Carlos Vinícius Monteiro Costa
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Added definition for keyset cache.
+$definitions = [
+    'keyset' => [
+        'mode' => cache_store::MODE_APPLICATION
+    ]
+];
\ No newline at end of file
index 85507c0..83361f5 100644 (file)
@@ -129,12 +129,30 @@ class mod_lti_edit_types_form extends moodleform {
                 $mform->setType('lti_clientid', PARAM_TEXT);
             }
 
-            $mform->addElement('textarea', 'lti_publickey', get_string('publickey', 'lti'), array('rows' => 8, 'cols' => 60));
+            $keyoptions = [
+                LTI_RSA_KEY => get_string('keytype_rsa', 'lti'),
+                LTI_JWK_KEYSET => get_string('keytype_keyset', 'lti'),
+            ];
+            $mform->addElement('select', 'lti_keytype', get_string('keytype', 'lti'), $keyoptions);
+            $mform->setType('lti_keytype', PARAM_TEXT);
+            $mform->addHelpButton('lti_keytype', 'keytype', 'lti');
+            $mform->setDefault('lti_keytype', LTI_JWK_KEYSET);
+            $mform->hideIf('lti_keytype', 'lti_ltiversion', 'neq', LTI_VERSION_1P3);
+
+            $mform->addElement('textarea', 'lti_publickey', get_string('publickey', 'lti'), ['rows' => 8, 'cols' => 60]);
             $mform->setType('lti_publickey', PARAM_TEXT);
             $mform->addHelpButton('lti_publickey', 'publickey', 'lti');
+            $mform->hideIf('lti_publickey', 'lti_keytype', 'neq', LTI_RSA_KEY);
             $mform->hideIf('lti_publickey', 'lti_ltiversion', 'neq', LTI_VERSION_1P3);
             $mform->setForceLtr('lti_publickey');
 
+            $mform->addElement('text', 'lti_publickeyset', get_string('publickeyset', 'lti'), ['size' => '64']);
+            $mform->setType('lti_publickeyset', PARAM_TEXT);
+            $mform->addHelpButton('lti_publickeyset', 'publickeyset', 'lti');
+            $mform->hideIf('lti_publickeyset', 'lti_keytype', 'neq', LTI_JWK_KEYSET);
+            $mform->hideIf('lti_publickeyset', 'lti_ltiversion', 'neq', LTI_VERSION_1P3);
+            $mform->setForceLtr('lti_publickeyset');
+
             $mform->addElement('text', 'lti_initiatelogin', get_string('initiatelogin', 'lti'), array('size' => '64'));
             $mform->setType('lti_initiatelogin', PARAM_URL);
             $mform->addHelpButton('lti_initiatelogin', 'initiatelogin', 'lti');
index 4995d5c..1d1284b 100644 (file)
@@ -86,6 +86,7 @@ $string['basicltifieldset'] = 'Custom example fieldset';
 $string['basicltiintro'] = 'Activity description';
 $string['basicltiname'] = 'Activity name';
 $string['basicltisettings'] = 'Basic Learning Tool Interoperability (LTI) settings';
+$string['cachedef_keyset'] = 'Caches the keyset information of tools';
 $string['cancel'] = 'Cancel';
 $string['cancelled'] = 'Cancelled';
 $string['cannot_delete'] = 'You may not delete this tool configuration.';
@@ -230,6 +231,10 @@ $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['keytype'] = 'Public key type';
+$string['keytype_help'] = 'The authentication method used to validate the tool.';
+$string['keytype_keyset'] = 'Keyset Url';
+$string['keytype_rsa'] = 'RSA Key';
 $string['launch_in_moodle'] = 'Launch tool in Moodle';
 $string['launch_in_popup'] = 'Launch tool in a pop-up';
 $string['launch_url'] = 'Tool URL';
@@ -398,6 +403,8 @@ $string['privacy:metadata:userid'] = 'The ID of the user accessing the LTI Consu
 $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['publickeyset'] = 'Public keyset';
+$string['publickeyset_help'] = 'Public keyset from where moodle will retrieve the tool\'s public key to allow signatures of incoming messages and service requests to be verified.';
 $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.';
index 7632f47..da61f6e 100644 (file)
@@ -52,7 +52,8 @@ 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;
+use Firebase\JWT\JWT;
+use Firebase\JWT\JWK;
 
 global $CFG;
 require_once($CFG->dirroot.'/mod/lti/OAuth.php');
@@ -90,6 +91,8 @@ 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_RSA_KEY', 'RSA_KEY');
+define('LTI_JWK_KEYSET', 'JWK_KEYSET');
 
 define('LTI_DEFAULT_ORGID_SITEID', 'SITEID');
 define('LTI_DEFAULT_ORGID_SITEHOST', 'SITEHOST');
@@ -1319,6 +1322,45 @@ function lti_verify_oauth_signature($typeid, $consumerkey) {
     return $tool;
 }
 
+/**
+ * Verifies the JWT signature using a JWK keyset.
+ *
+ * @param string $jwtparam JWT parameter value.
+ * @param string $keyseturl The tool keyseturl.
+ * @param string $clientid The tool client id.
+ *
+ * @return object The JWT's payload as a PHP object
+ * @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_with_keyset($jwtparam, $keyseturl, $clientid) {
+    // Attempts to retrieve cached keyset.
+    $cache = cache::make('mod_lti', 'keyset');
+    $keyset = $cache->get($clientid);
+
+    try {
+        if (empty($keyset)) {
+            throw new moodle_exception('errornocachedkeysetfound', 'mod_lti');
+        }
+        $keysetarr = json_decode($keyset, true);
+        $keys = JWK::parseKeySet($keysetarr);
+        $jwt = JWT::decode($jwtparam, $keys, ['RS256']);
+    } catch (Exception $e) {
+        // Something went wrong, so attempt to update cached keyset and then try again.
+        $keyset = file_get_contents($keyseturl);
+        $keysetarr = json_decode($keyset, true);
+        $keys = JWK::parseKeySet($keysetarr);
+        $jwt = JWT::decode($jwtparam, $keys, ['RS256']);
+        // If sucessful, updates the cached keyset.
+        $cache->set($clientid, $keyset);
+    }
+    return $jwt;
+}
+
 /**
  * Verifies the JWT signature of an incoming message.
  *
@@ -1336,6 +1378,7 @@ function lti_verify_oauth_signature($typeid, $consumerkey) {
  */
 function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
     $tool = lti_get_type($typeid);
+
     // Validate parameters.
     if (!$tool) {
         throw new moodle_exception('errortooltypenotfound', 'mod_lti');
@@ -1347,16 +1390,28 @@ function lti_verify_jwt_signature($typeid, $consumerkey, $jwtparam) {
     $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'));
+    if (empty($typeconfig['keytype']) || $typeconfig['keytype'] === LTI_RSA_KEY) {
+        $publickey = $typeconfig['publickey'] ?? '';
+        if (empty($publickey)) {
+            throw new moodle_exception('No public key configured');
+        }
+        // Attemps to verify jwt with RSA key.
+        JWT::decode($jwtparam, $publickey, ['RS256']);
+    } else if ($typeconfig['keytype'] === LTI_JWK_KEYSET) {
+        $keyseturl = $typeconfig['publickeyset'] ?? '';
+        if (empty($keyseturl)) {
+            throw new moodle_exception('No public keyset configured');
+        }
+        // Attempts to verify jwt with jwk keyset.
+        lti_verify_with_keyset($jwtparam, $keyseturl, $tool->clientid);
+    } else {
+        throw new moodle_exception('Invalid public key type');
+    }
 
     return $tool;
 }
@@ -2476,6 +2531,12 @@ function lti_get_type_type_config($id) {
     if (isset($config['publickey'])) {
         $type->lti_publickey = $config['publickey'];
     }
+    if (isset($config['publickeyset'])) {
+        $type->lti_publickeyset = $config['publickeyset'];
+    }
+    if (isset($config['keytype'])) {
+        $type->lti_keytype = $config['keytype'];
+    }
     if (isset($config['initiatelogin'])) {
         $type->lti_initiatelogin = $config['initiatelogin'];
     }
diff --git a/mod/lti/tests/fixtures/test_keyset b/mod/lti/tests/fixtures/test_keyset
new file mode 100644 (file)
index 0000000..6d8f60e
--- /dev/null
@@ -0,0 +1 @@
+{"keys":[{"kty":"RSA","kid":"701feb7a2901164add6576bfced23510","n":"tFqL_TBjryeXRp4SMLxpW7cDWuw9nag1tN8m3aLRnHj9SECzavBdOQIlXiPeKIV2i95TCTdFAxdjIoDXGqy_MUX0BRdQrWHA4pF-4bj3WgciwieJ9AVV1QH8dEgkV8vlSWQ9vuD0qYsr24ZfznMtKXXdToLtwN6Za1c0RtIJC1s8rSFIaFEQ8EZW0IgJYvYn-HJvbBL0ZZXBetTb-kKHAdhqWJs8MooehG9OjB7bvur25uc_Q32NfMvMYEA-oWcs9n5PuxgmCAgQHAHzQH4l0oWwF7nnCddhNGskIUGT4VqtbZU-ZSr3jqXg5eHhcKowP-Yl32ugUm1kKzf5RDZQG7Ci4bkhaCzliBFUpiaSezXOqeVe2rgq0pBBJZFjL6ECCWLDQMzYzUtZrAIt3qQeTcVnEgmbXQmXjPbpF5rj7xYeyZoMKY5Qe1NUZNEdFFuODIA6PZFUmOO3tUwZs9Zmk0OUX9dZzlJIa-cyyez0kben_MLnZ64T4Z3dPDzI2rmCJGoexzDXDFb-_bAZTVdGbohDBkkBEnBG2jjBnWlZdjzuRGENDkSKw8lVm2g-uc4cGIkdLfW8BpGeIOZsT3A7-o5R3D0U7hlykd-weF99QF_ZcflE71iZN80u_J-xB7DA2pdTi7TF6yDjEFaG9kYYgmvabx2qTKIcfAkOKVP4YvU","e":"AQAB","alg":"RS256","use":"sig"},{"kty":"RSA","kid":"57c1177d2d53a021c73756491c137b17","n":"tkeuoQIsfQzW8_wrmI4qCLPYccqNc9iMD5_uwy6JTVx6PQAIwlSGeAPkWpxV9RJmXKWhZ6dMxZ-vCEPqDSMI3IIvYPdVOuu-jdlxFtGfodIu0R1Nk38Q4TPnBQ3WXKaBvwpsBLdoURiHxAprFIyLy4m95-e5qB3dW0kFYbtbSHz3rz28byJ3t0SQBlSO36f2uBbn3jWC3-IMkIFiST6Ndvdj0Z7Q08qALXWG3k6R5y8oEJpNrxhyUgJypeCKsMt938tNBPDGXNVyo0dUK6DL2jJX7UpNCmv62mblfDiGrh4LCJJEcY-Mn2EtGhYczRGYVOhq8-7_GfYa7Dor7fzi-57M0cJ-ROB99YwQ415XcE-wnhSKy8Wr_K8CEPK04o8l5mUraBwRIm7N3hfMC8kez7Pu7mcA9u5Z1V5wtj6ltztZhTKjJVe-azurLjVpz8zbKl_tc6rD-mkhaXpyFTStk0jf9uYVf6fsEq3btPoxmNZgpNW1FeB5ied5ndhydDVtj8cSl4vVc4PzTKwHb99EcVC7ka_327dp7wE3ewMkPinxdWVUrtilWglVUOO6O9K6iOr5e0zHFw7l7-LMOod6mqj4RfWqvvZRaUsB89WMTvT0i5Y-Wx0ysidIEKNfFekBLOb57eb160ysoEdPfVNQ7nCJHlLjVxwO0Ez1OOwnnIE","e":"AQAB","alg":"RS256","use":"sig"}]}
\ No newline at end of file
index 2b2f59e..1a9800a 100644 (file)
@@ -1078,6 +1078,8 @@ V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
 MwIDAQAB
 -----END PUBLIC KEY-----';
 
+        $config->lti_keytype = LTI_RSA_KEY;
+
         $typeid = lti_add_type($type, $config);
 
         lti_verify_jwt_signature($typeid, '', 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4g' .
@@ -1087,6 +1089,46 @@ MwIDAQAB
             'v7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA');
     }
 
+    /**
+     * Test lti_verify_jwt_signature_jwk().
+     */
+    public function test_lti_verify_jwt_signature_jwk() {
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+
+        // Create a tool type, associated with that proxy.
+        $type = new stdClass();
+        $type->state = LTI_TOOL_STATE_CONFIGURED;
+        $type->name = "Test tool";
+        $type->description = "Example description";
+        $type->baseurl = $this->getExternalTestFileUrl('/test.html');
+
+        $config = new stdClass();
+        $config->lti_publickeyset = dirname(__FILE__) . '/fixtures/test_keyset';
+
+        $config->lti_keytype = LTI_JWK_KEYSET;
+
+        $typeid = lti_add_type($type, $config);
+
+        $jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjU3YzExNzdkMmQ1M2EwMjFjNzM';
+        $jwt .= '3NTY0OTFjMTM3YjE3In0.eyJpc3MiOiJnclJvbkd3RTd1WjRwZ28iLCJzdWIiOiJnclJvb';
+        $jwt .= 'kd3RTd1WjRwZ28iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0L21vb2RsZS9tb2QvbHRpL3R';
+        $jwt .= 'va2VuLnBocCIsImp0aSI6IjFlMUJPVEczVFJjbFdUem00dERsMGc9PSIsImlhdCI6MTU4M';
+        $jwt .= 'Dg1NTUwNX0.Lowhc9ovNAXRb2rkAnv1oozDXlRD54Mz2JS1i8Zx4yGWQzmXzam-La19_g0';
+        $jwt .= 'CTnwlKM6gxaInnRKFRAcwhJVcWec389liLAjMbna6d6iTWYTZr7q_4BIe3CT_oTMWASGta';
+        $jwt .= 'Paaq53ch1rO4YdueEtmtd1K47ibo4Lhu1jmP_icc3lxjfnqiv4vIYdy7W2JQEzpk1ImuQr';
+        $jwt .= 'AlO1xR3fZ6bgcJhVIaw5xoaZD3ZgEjuZOQXMkywv1bL-mL17RX336CzHd8rYZg82QXrBzb';
+        $jwt .= 'NWzAlaZxv9VSug8t6mORvM6TkYYWjqEBKemgkD5rNh1BHrPcjWP7vy2Jz7YMjLsmuvDuLK';
+        $jwt .= '_PHYIKL--s4gcXWoYmOu1vj-SgoPczTJPoiBD35hAKqVHy5ggHaYHBy95_bbcFd8H1smHw';
+        $jwt .= 'pejrAFj1QAwGyTISLzUm08oq7Ak0tSxRKKXw4lpZAka1MmYxO3tJ_3-MXw6Bwz12bNgitJ';
+        $jwt .= 'lQd6n3kkGLCJAmANeRkPsH6eZVwF0n2cjh2O1JAwyNcMD2vs4I8ftM1EqqoE2M3r6kt3AC';
+        $jwt .= 'EscmqzizI3j80USBCLUUb1UTsfJb2g7oyApJAp-13Q3InR3QyvWO8unG5VraFE7IL5I28h';
+        $jwt .= 'MkQAHuCI90DFmXB4leflAu7wNlIK_U8xkGl8X8Mnv6MWgg94Ki8jgIq_kA85JAqI';
+
+        lti_verify_jwt_signature($typeid, '', $jwt);
+    }
+
     /**
      * Test lti_verify_jwt_signature().
      */
@@ -1155,6 +1197,7 @@ MwIDAQAB
         $type->baseurl = $this->getExternalTestFileUrl('/test.html');
 
         $config = new stdClass();
+        $config->lti_keytype = LTI_RSA_KEY;
         $typeid = lti_add_type($type, $config);
 
         $this->expectExceptionMessage('No public key configured');
@@ -1272,6 +1315,7 @@ e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
 V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
 MwIDAQAB
 -----END PUBLIC KEY-----';
+        $config->lti_keytype = LTI_RSA_KEY;
 
         $typeid = lti_add_type($type, $config);
 
index 3d90ca8..1dc2abb 100644 (file)
 define('NO_DEBUG_DISPLAY', true);
 define('NO_MOODLE_COOKIES', true);
 
-use Firebase\JWT\JWT as JWT;
+use Firebase\JWT\JWT;
 
 require_once(__DIR__ . '/../../config.php');
 require_once($CFG->dirroot . '/mod/lti/locallib.php');
 
-
 $response = new \mod_lti\local\ltiservice\response();
 
 $contenttype = isset($_SERVER['CONTENT_TYPE']) ? explode(';', $_SERVER['CONTENT_TYPE'], 2)[0] : '';
@@ -66,19 +65,17 @@ if ($ok) {
 }
 
 if ($ok) {
-    $error = 'invalid_client';
     $tool = $DB->get_record('lti_types', array('clientid' => $claims['sub']));
     if ($tool) {
-        $typeconfig = lti_get_type_config($tool->id);
-        if (!empty($typeconfig['publickey'])) {
-            try {
-                $jwt = JWT::decode($clientassertion, $typeconfig['publickey'], array('RS256'));
-                $ok = true;
-            } catch (Exception $e) {
-                $ok = false;
-            }
+        try {
+            lti_verify_jwt_signature($tool->id, $claims['sub'], $clientassertion);
+            $ok = true;
+        } catch (Exception $e) {
+            $error = $e->getMessage();
+            $ok = false;
         }
     } else {
+        $error = 'invalid_client';
         $ok = false;
     }
 }
@@ -86,6 +83,7 @@ if ($ok) {
 if ($ok) {
     $scopes = array();
     $requestedscopes = explode(' ', $scope);
+    $typeconfig = lti_get_type_config($tool->id);
     $permittedscopes = lti_get_permitted_service_scopes($tool, $typeconfig);
     $scopes = array_intersect($requestedscopes, $permittedscopes);
     $ok = !empty($scopes);
@@ -115,4 +113,4 @@ EOD;
 
 $response->set_body($body);
 
-$response->send();
+$response->send();
\ No newline at end of file
index 42ae637..e26af0d 100644 (file)
@@ -48,7 +48,7 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2020010800;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2020022200;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2019111200;    // Requires this Moodle version.
 $plugin->component = 'mod_lti';     // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 0;
similarity index 99%
rename from user/classes/participants_table.php
rename to user/classes/table/participants.php
index ab46598..2f9c5ca 100644 (file)
@@ -21,8 +21,9 @@
  * @copyright  2017 Mark Nelson <markn@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+declare(strict_types=1);
 
-namespace core_user;
+namespace core_user\table;
 
 use DateTime;
 use context;
@@ -45,7 +46,7 @@ require_once($CFG->dirroot . '/user/lib.php');
  * @copyright  2017 Mark Nelson <markn@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class participants_table extends \table_sql implements dynamic_table {
+class participants extends \table_sql implements dynamic_table {
 
     /**
      * @var int $courseid The course id
index ce18893..1082bbc 100644 (file)
@@ -33,7 +33,6 @@ require_once($CFG->dirroot.'/enrol/locallib.php');
 use core_table\local\filter\filter;
 use core_table\local\filter\integer_filter;
 use core_table\local\filter\string_filter;
-use core_user\participants_table;
 
 define('DEFAULT_PAGE_SIZE', 20);
 define('SHOW_ALL_PAGE_SIZE', 5000);
@@ -276,7 +275,7 @@ if (count($keywordfilter)) {
     $filterset->add_filter($keywordfilter);
 }
 
-$participanttable = new participants_table(participants_table::get_unique_id_from_argument($course->id));
+$participanttable = new \core_user\table\participants(\core_user\table\participants::get_unique_id_from_argument($course->id));
 $participanttable->set_selectall($selectall);
 $participanttable->set_filterset($filterset);
 $participanttable->define_baseurl($baseurl);
index 69a1c24..9150a38 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2020041700.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2020041700.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 $release  = '3.9dev (Build: 20200417)'; // Human-friendly version name