From d9edb8b6ac46be513c26d3062b1ea6c71524e017 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Mon, 19 Feb 2024 22:49:10 +0800 Subject: [PATCH 1/2] Check the Category tag against APIs --- CHANGELOG.md | 1 + moodle/Sniffs/Commenting/CategorySniff.php | 76 ++++++++ moodle/Tests/MoodleCSBaseTestCase.php | 4 + .../Sniffs/Commenting/CategorySniffTest.php | 69 +++++++ .../Commenting/fixtures/category_tags.php | 22 +++ moodle/Tests/Util/DocblocksTest.php | 18 ++ moodle/Tests/Util/MoodleUtilTest.php | 177 ++++++++++++++++++ moodle/Util/Docblocks.php | 23 ++- moodle/Util/MoodleUtil.php | 58 +++++- 9 files changed, 444 insertions(+), 4 deletions(-) create mode 100644 moodle/Sniffs/Commenting/CategorySniff.php create mode 100644 moodle/Tests/Sniffs/Commenting/CategorySniffTest.php create mode 100644 moodle/Tests/Sniffs/Commenting/fixtures/category_tags.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c48f9..8068878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt ### Added - Add new moodle.Commenting.Package sniffs to replace those present in moodle-local_moodlecheck. +- Add new moodle.Commenting.Category sniffs to replace those present in moodle-local_moodlecheck. ### Fixed - The moodle.Files.MoodleInternal sniff no longer treats Attributes as side-effects. diff --git a/moodle/Sniffs/Commenting/CategorySniff.php b/moodle/Sniffs/Commenting/CategorySniff.php new file mode 100644 index 0000000..6f9537a --- /dev/null +++ b/moodle/Sniffs/Commenting/CategorySniff.php @@ -0,0 +1,76 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Sniffs\Commenting; + +// phpcs:disable moodle.NamingConventions + +use MoodleHQ\MoodleCS\moodle\Util\MoodleUtil; +use MoodleHQ\MoodleCS\moodle\Util\Docblocks; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; + +/** + * Checks that all test classes and global functions have appropriate @package tags. + * + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class CategorySniff implements Sniff { + + /** + * Register for open tag (only process once per file). + */ + public function register() { + return [ + T_DOC_COMMENT_OPEN_TAG, + ]; + } + + /** + * Processes php files and perform various checks with file. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position in the stack. + */ + public function process(File $phpcsFile, $stackPtr) { + $categoryTokens = Docblocks::getMatchingDocTags($phpcsFile, $stackPtr, '@category'); + if (empty($categoryTokens)) { + return; + } + + $tokens = $phpcsFile->getTokens(); + $apis = MoodleUtil::getMoodleApis($phpcsFile); + + $docblock = Docblocks::getDocBlock($phpcsFile, $stackPtr); + foreach ($categoryTokens as $tokenPtr) { + $categoryValuePtr = $phpcsFile->findNext( + T_DOC_COMMENT_STRING, + $tokenPtr, + $docblock['comment_closer'] + ); + $categoryValue = $tokens[$categoryValuePtr]['content']; + if (!in_array($categoryValue, $apis)) { + $phpcsFile->addError( + 'Invalid @category tag value "%s".', + $categoryValuePtr, + 'Invalid', + [$categoryValue] + ); + } + } + } +} diff --git a/moodle/Tests/MoodleCSBaseTestCase.php b/moodle/Tests/MoodleCSBaseTestCase.php index 8d6dd43..738eabd 100644 --- a/moodle/Tests/MoodleCSBaseTestCase.php +++ b/moodle/Tests/MoodleCSBaseTestCase.php @@ -86,6 +86,10 @@ public function set_component_mapping(array $mapping): void { \MoodleHQ\MoodleCS\moodle\Util\MoodleUtil::setMockedComponentMappings($mapping); } + public function set_api_mapping(array $mapping): void { + \MoodleHQ\MoodleCS\moodle\Util\MoodleUtil::setMockedApiMappings($mapping); + } + /** * Set the name of the standard to be tested. * diff --git a/moodle/Tests/Sniffs/Commenting/CategorySniffTest.php b/moodle/Tests/Sniffs/Commenting/CategorySniffTest.php new file mode 100644 index 0000000..029fd29 --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/CategorySniffTest.php @@ -0,0 +1,69 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Tests\Sniffs\Commenting; + +use MoodleHQ\MoodleCS\moodle\Tests\MoodleCSBaseTestCase; + +// phpcs:disable moodle.NamingConventions + +/** + * Test the CategorySniff sniff. + * + * @category test + * @copyright 2024 onwards Andrew Lyons + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\Commenting\CategorySniff + */ +class CategorySniffTest extends MoodleCSBaseTestCase +{ + /** + * @dataProvider provider + */ + public function test_category_correctness( + string $fixture, + array $errors, + array $warnings + ): void { + $this->set_standard('moodle'); + $this->set_sniff('moodle.Commenting.Category'); + $this->set_fixture(sprintf("%s/fixtures/%s.php", __DIR__, $fixture)); + $this->set_warnings($warnings); + $this->set_errors($errors); + $this->set_api_mapping([ + 'test' => [ + 'component' => 'core', + 'allowspread' => true, + 'allowlevel2' => false, + ], + ]); + + $this->verify_cs_results(); + } + + public static function provider(): array { + return [ + 'Standard fixes' => [ + 'fixture' => 'category_tags', + 'errors' => [ + 13 => 'Invalid @category tag value "core"', + ], + 'warnings' => [], + ], + ]; + } +} diff --git a/moodle/Tests/Sniffs/Commenting/fixtures/category_tags.php b/moodle/Tests/Sniffs/Commenting/fixtures/category_tags.php new file mode 100644 index 0000000..90899f7 --- /dev/null +++ b/moodle/Tests/Sniffs/Commenting/fixtures/category_tags.php @@ -0,0 +1,22 @@ +findNext(T_FUNCTION, $classPointer); $this->assertNull(Docblocks::getDocBlock($phpcsFile, $methodPointer)); $this->assertCount(0, Docblocks::getMatchingDocTags($phpcsFile, $methodPointer, '@property')); + + // Get the docblock from pointers at the start, middle, and end, of a docblock. + $tokens = $phpcsFile->getTokens(); + $startDocPointer = $phpcsFile->findNext(T_DOC_COMMENT_OPEN_TAG, 0); + $endDocPointer = $phpcsFile->findNext(T_DOC_COMMENT_CLOSE_TAG, $startDocPointer); + $middleDocPointer = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $startDocPointer, $endDocPointer); + + $docblock = Docblocks::getDocBlock($phpcsFile, $startDocPointer); + $this->assertIsArray($docblock); + $this->assertEquals($tokens[$startDocPointer], $docblock); + + $docblock = Docblocks::getDocBlock($phpcsFile, $middleDocPointer); + $this->assertIsArray($docblock); + $this->assertEquals($tokens[$startDocPointer], $docblock); + + $docblock = Docblocks::getDocBlock($phpcsFile, $endDocPointer); + $this->assertIsArray($docblock); + $this->assertEquals($tokens[$startDocPointer], $docblock); } public function testGetDocBlockClassOnly(): void { diff --git a/moodle/Tests/Util/MoodleUtilTest.php b/moodle/Tests/Util/MoodleUtilTest.php index 3d1b65f..fe41b3b 100644 --- a/moodle/Tests/Util/MoodleUtilTest.php +++ b/moodle/Tests/Util/MoodleUtilTest.php @@ -470,6 +470,14 @@ protected function cleanMoodleUtilCaches() { $moodleComponents = $moodleUtil->getProperty('moodleComponents'); $moodleComponents->setAccessible(true); $moodleUtil->setStaticPropertyValue('moodleComponents', []); + + $apiCache = $moodleUtil->getProperty('apis'); + $apiCache->setAccessible(true); + $apiCache->setValue(null, []); + + $apiCache = $moodleUtil->getProperty('mockedApisList'); + $apiCache->setAccessible(true); + $apiCache->setValue(null, []); } /** @@ -774,4 +782,173 @@ public function testGetTokensOnLine(): void $this->assertCount(count($expectedTokens), $tokens); $this->assertEquals($expectedTokens, $tokens); } + + public function testGetMoodleApis(): void { + $this->cleanMoodleUtilCaches(); + // Let's calculate moodleRoot. + $vfs = vfsStream::setup('root', null, []); + + $apis = [ + 'test' => [ + 'component' => 'core', + 'allowlevel2' => false, + 'allowspread' => false, + ], + 'time' => [ + 'component' => 'core', + 'allowlevel2' => false, + 'allowspread' => false, + ], + ]; + + vfsStream::copyFromFileSystem(__DIR__ . '/fixtures/moodleutil/complete', $vfs); + vfsStream::create( + [ + 'lib' => [ + 'apis.json' => json_encode( + $apis, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ), + 'example.php' => '' + ], + ], + $vfs + ); + + // We are passing a real File, prepare it. + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $file = new File($vfs->url() . '/lib/lib.php', $phpcsRuleset, $phpcsConfig); + + $this->assertEquals( + array_keys($apis), + MoodleUtil::getMoodleApis($file) + ); + } + + public function testGetMoodleApisNoApis(): void { + $this->cleanMoodleUtilCaches(); + + // Let's calculate moodleRoot. + $vfs = vfsStream::setup('root', null, []); + + vfsStream::copyFromFileSystem(__DIR__ . '/fixtures/moodleutil/complete', $vfs); + + // We are passing a real File, prepare it. + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $file = new File($vfs->url() . '/lib/lib.php', $phpcsRuleset, $phpcsConfig); + + $this->assertNull( + MoodleUtil::getMoodleApis($file) + ); + } + + public function testGetMoodleApisInvalidJson(): void { + $this->cleanMoodleUtilCaches(); + // Let's calculate moodleRoot. + $vfs = vfsStream::setup('root', null, []); + + vfsStream::copyFromFileSystem(__DIR__ . '/fixtures/moodleutil/complete', $vfs); + vfsStream::create( + [ + 'lib' => [ + 'apis.json' => 'invalid:"json"', + 'example.php' => '' + ], + ], + $vfs + ); + + // We are passing a real File, prepare it. + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $file = new File($vfs->url() . '/lib/lib.php', $phpcsRuleset, $phpcsConfig); + + $this->assertNull( + MoodleUtil::getMoodleApis($file) + ); + } + + + public function testGetMoodleApisNotAMoodle(): void { + $this->cleanMoodleUtilCaches(); + // Let's calculate moodleRoot. + $vfs = vfsStream::setup('root', null, []); + + $apis = [ + 'test' => [ + 'component' => 'core', + 'allowlevel2' => false, + 'allowspread' => false, + ], + 'time' => [ + 'component' => 'core', + 'allowlevel2' => false, + 'allowspread' => false, + ], + ]; + + vfsStream::create( + [ + 'lib' => [ + 'apis.json' => json_encode( + $apis, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ), + 'example.php' => '' + ], + ], + $vfs + ); + + // We are passing a real File, prepare it. + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $file = new File($vfs->url() . '/lib/lib.php', $phpcsRuleset, $phpcsConfig); + + $this->assertNull( + MoodleUtil::getMoodleApis($file) + ); + } + + public function testGetMoodleApisMocked(): void { + $this->cleanMoodleUtilCaches(); + // Let's calculate moodleRoot. + $apis = [ + 'test' => [ + 'component' => 'core', + 'allowlevel2' => false, + 'allowspread' => false, + ], + 'time' => [ + 'component' => 'core', + 'allowlevel2' => false, + 'allowspread' => false, + ], + ]; + + $vfs = vfsStream::setup('root', null, []); + vfsStream::create( + [ + 'lib' => [ + 'apis.json' => json_encode([]), + 'example.php' => '' + ], + ], + $vfs + ); + + $this->set_api_mapping($apis); + + // We are passing a real File, prepare it. + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $file = new File($vfs->url() . '/lib/lib.php', $phpcsRuleset, $phpcsConfig); + + $this->assertEquals( + array_keys($apis), + MoodleUtil::getMoodleApis($file) + ); + } } diff --git a/moodle/Util/Docblocks.php b/moodle/Util/Docblocks.php index 86b91f0..1bf55bd 100644 --- a/moodle/Util/Docblocks.php +++ b/moodle/Util/Docblocks.php @@ -44,6 +44,25 @@ public static function getDocBlock( int $stackPtr ): ?array { $tokens = $phpcsFile->getTokens(); + $token = $tokens[$stackPtr]; + + // Check if the passed pointer was for a doc. + $midDocBlockTokens = [ + T_DOC_COMMENT, + T_DOC_COMMENT_STAR, + T_DOC_COMMENT_WHITESPACE, + T_DOC_COMMENT_TAG, + T_DOC_COMMENT_STRING, + ]; + if ($token['code'] === T_DOC_COMMENT_OPEN_TAG) { + return $token; + } else if ($token['code'] === T_DOC_COMMENT_CLOSE_TAG) { + return $tokens[$token['comment_opener']]; + } else if (in_array($token['code'], $midDocBlockTokens)) { + $commentStart = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, $stackPtr); + return $commentStart ? $tokens[$commentStart] : null; + } + $find = [ T_ABSTRACT => T_ABSTRACT, T_FINAL => T_FINAL, @@ -86,8 +105,7 @@ public static function getDocBlock( } if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) { - $commentEnd = $tokens[$stackPtr]['comment_closer']; - $nextToken = $commentEnd; + $nextToken = $tokens[$stackPtr]['comment_closer']; while ($nextToken = $phpcsFile->findNext(T_WHITESPACE, $nextToken + 1, null, true)) { if ($nextToken && $tokens[$nextToken]['code'] === T_ATTRIBUTE) { $nextToken = $tokens[$nextToken]['attribute_closer'] + 1; @@ -105,7 +123,6 @@ public static function getDocBlock( return null; } - $previousContent = null; for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) { if (isset($find[$tokens[$commentEnd]['code']]) === true) { diff --git a/moodle/Util/MoodleUtil.php b/moodle/Util/MoodleUtil.php index 10298f5..3ddcb78 100644 --- a/moodle/Util/MoodleUtil.php +++ b/moodle/Util/MoodleUtil.php @@ -51,12 +51,18 @@ abstract class MoodleUtil { /** @var array A list of mocked component mappings for use in unit tests */ protected static $mockedComponentMappings = []; + /** @var array A cached list of APIs */ + protected static $apis = []; + + /** @var array A list of mocked API mappings for use in unit tests */ + protected static $mockedApisList = []; + /** * Mock component mappings for unit tests. * * @param array $mappings List of file path => component mappings * - * @throws Exception + * @throws \Exception */ public static function setMockedComponentMappings(array $mappings): void { if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) { @@ -66,6 +72,20 @@ public static function setMockedComponentMappings(array $mappings): void { self::$mockedComponentMappings = $mappings; } + /** + * Mock API mappings for unit tests. + * + * @param array $mappings + * @throws \Exception + */ + public static function setMockedApiMappings(array $mappings): void { + if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) { + throw new \Exception('Not running in a unit test'); // @codeCoverageIgnore + } + + self::$mockedApisList = $mappings; + } + /** * Load moodle core_component without needing an installed site. * @@ -239,6 +259,42 @@ public static function getMoodleComponent(File $file, $selfPath = true): ?string return null; } + /** + * Get the list of Moodle APIs. + * + * @param File $file + * @param bool $selfPath + * @return null|array + */ + public static function getMoodleApis(File $file, bool $selfPath = true): ?array { + if (defined('PHPUNIT_TEST') && PHPUNIT_TEST && !empty(self::$mockedApisList)) { + return array_keys(self::$mockedApisList); // @codeCoverageIgnore + } + + if (empty(self::$apis)) { + // Verify that we are able to find a valid moodle root. + if (!$moodleRoot = self::getMoodleRoot($file, $selfPath)) { + return null; + } + + // APIs are located in lib/apis.json. + $apisFile = $moodleRoot . '/lib/apis.json'; + + if (!is_readable($apisFile)) { + return null; + } + + $data = json_decode(file_get_contents($apisFile), true); + if (json_last_error() !== JSON_ERROR_NONE) { + return null; + } + + self::$apis = $data; + } + + return array_keys(self::$apis); + } + /** * Try to guess moodle branch (numeric) * From 491c94f479d5eb4736ee98dd05acb4971e2ad3be Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 23 Feb 2024 17:03:41 +0800 Subject: [PATCH 2/2] Revert to stored apis.json when Moodle version not available --- moodle/Tests/Util/MoodleUtilTest.php | 13 +- moodle/Util/MoodleUtil.php | 32 ++-- moodle/Util/apis.json | 272 +++++++++++++++++++++++++++ 3 files changed, 302 insertions(+), 15 deletions(-) create mode 100644 moodle/Util/apis.json diff --git a/moodle/Tests/Util/MoodleUtilTest.php b/moodle/Tests/Util/MoodleUtilTest.php index fe41b3b..1cb19ac 100644 --- a/moodle/Tests/Util/MoodleUtilTest.php +++ b/moodle/Tests/Util/MoodleUtilTest.php @@ -839,7 +839,9 @@ public function testGetMoodleApisNoApis(): void { $phpcsRuleset = new Ruleset($phpcsConfig); $file = new File($vfs->url() . '/lib/lib.php', $phpcsRuleset, $phpcsConfig); - $this->assertNull( + $apis = json_decode(file_get_contents(__DIR__ . '/../../Util/apis.json')); + $this->assertEquals( + array_keys((array) $apis), MoodleUtil::getMoodleApis($file) ); } @@ -865,7 +867,10 @@ public function testGetMoodleApisInvalidJson(): void { $phpcsRuleset = new Ruleset($phpcsConfig); $file = new File($vfs->url() . '/lib/lib.php', $phpcsRuleset, $phpcsConfig); - $this->assertNull( + // Revert to the stored version if the file is not readable. + $apis = json_decode(file_get_contents(__DIR__ . '/../../Util/apis.json')); + $this->assertEquals( + array_keys((array) $apis), MoodleUtil::getMoodleApis($file) ); } @@ -907,7 +912,9 @@ public function testGetMoodleApisNotAMoodle(): void { $phpcsRuleset = new Ruleset($phpcsConfig); $file = new File($vfs->url() . '/lib/lib.php', $phpcsRuleset, $phpcsConfig); - $this->assertNull( + $apis = json_decode(file_get_contents(__DIR__ . '/../../Util/apis.json')); + $this->assertEquals( + array_keys((array) $apis), MoodleUtil::getMoodleApis($file) ); } diff --git a/moodle/Util/MoodleUtil.php b/moodle/Util/MoodleUtil.php index 3ddcb78..6b7d47d 100644 --- a/moodle/Util/MoodleUtil.php +++ b/moodle/Util/MoodleUtil.php @@ -273,23 +273,31 @@ public static function getMoodleApis(File $file, bool $selfPath = true): ?array if (empty(self::$apis)) { // Verify that we are able to find a valid moodle root. - if (!$moodleRoot = self::getMoodleRoot($file, $selfPath)) { - return null; + if ($moodleRoot = self::getMoodleRoot($file, $selfPath)) { + // APIs are located in lib/apis.json. + $apisFile = $moodleRoot . '/lib/apis.json'; + + if (is_readable($apisFile)) { + $data = json_decode(file_get_contents($apisFile), true); + if (json_last_error() === JSON_ERROR_NONE) { + self::$apis = $data; + } + } } - // APIs are located in lib/apis.json. - $apisFile = $moodleRoot . '/lib/apis.json'; + if (empty(self::$apis)) { + // If there is no apis.json file, we can't load the current APIs. + // Load the version from the release of 4.2 when the file was introduced. + // TODO Remove after min requirement is >= Moodle 4.2 #115. + $apisFile = __DIR__ . '/apis.json'; - if (!is_readable($apisFile)) { - return null; - } + $data = json_decode(file_get_contents($apisFile), true); + if (json_last_error() !== JSON_ERROR_NONE) { + return null; // @codeCoverageIgnore + } - $data = json_decode(file_get_contents($apisFile), true); - if (json_last_error() !== JSON_ERROR_NONE) { - return null; + self::$apis = $data; } - - self::$apis = $data; } return array_keys(self::$apis); diff --git a/moodle/Util/apis.json b/moodle/Util/apis.json new file mode 100644 index 0000000..8d91b22 --- /dev/null +++ b/moodle/Util/apis.json @@ -0,0 +1,272 @@ +{ + "access": { + "component": "core_access", + "allowedlevel2": true, + "allowedspread": false + }, + "admin": { + "component": "core_admin", + "allowedlevel2": false, + "allowedspread": false + }, + "adminpresets": { + "component": "core_adminpresets", + "allowedlevel2": true, + "allowedspread": false + }, + "analytics": { + "component": "core_analytics", + "allowedlevel2": true, + "allowedspread": true + }, + "availability": { + "component": "core_availability", + "allowedlevel2": false, + "allowedspread": false + }, + "backup": { + "component": "core_backup", + "allowedlevel2": true, + "allowedspread": true + }, + "badges": { + "component": "core_badges", + "allowedlevel2": false, + "allowedspread": false + }, + "cache": { + "component": "core_cache", + "allowedlevel2": true, + "allowedspread": true + }, + "calendar": { + "component": "core_calendar", + "allowedlevel2": false, + "allowedspread": false + }, + "check": { + "component": "core", + "allowedlevel2": true, + "allowedspread": true + }, + "comment": { + "component": "core_comment", + "allowedlevel2": false, + "allowedspread": false + }, + "competency": { + "component": "core_competency", + "allowedlevel2": false, + "allowedspread": false + }, + "completion": { + "component": "core_completion", + "allowedlevel2": true, + "allowedspread": true + }, + "context": { + "component": "core", + "allowedlevel2": true, + "allowedspread": false + }, + "core": { + "component": null, + "allowedlevel2": false, + "allowedspread": false + }, + "customfield": { + "component": "core_customfield", + "allowedlevel2": true, + "allowedspread": true + }, + "ddl": { + "component": "core", + "allowedlevel2": true, + "allowedspread": false + }, + "dml": { + "component": "core", + "allowedlevel2": true, + "allowedspread": false + }, + "enrol": { + "component": "core_enrol", + "allowedlevel2": false, + "allowedspread": false + }, + "event": { + "component": "core", + "allowedlevel2": true, + "allowedspread": true + }, + "external": { + "component": "core", + "allowedlevel2": true, + "allowedspread": true + }, + "files": { + "component": "core_files", + "allowedlevel2": true, + "allowedspread": false + }, + "form": { + "component": "core_form", + "allowedlevel2": true, + "allowedspread": true + }, + "grade": { + "component": "core_grades", + "allowedlevel2": false, + "allowedspread": false + }, + "grading": { + "component": "core_grading", + "allowedlevel2": false, + "allowedspread": false + }, + "group": { + "component": "core_group", + "allowedlevel2": false, + "allowedspread": false + }, + "h5p": { + "component": "core_h5p", + "allowedlevel2": true, + "allowedspread": true + }, + "lock": { + "component": "core", + "allowedlevel2": true, + "allowedspread": false + }, + "log": { + "component": "core", + "allowedlevel2": true, + "allowedspread": true + }, + "media": { + "component": "core_media", + "allowedlevel2": false, + "allowedspread": false + }, + "message": { + "component": "core_message", + "allowedlevel2": true, + "allowedspread": true + }, + "moodlenet": { + "component": "core", + "allowedlevel2": false, + "allowedspread": false + }, + "navigation": { + "component": "core", + "allowedlevel2": true, + "allowedspread": true + }, + "oauth2": { + "component": "core", + "allowedlevel2": true, + "allowedspread": true + }, + "output": { + "component": "core", + "allowedlevel2": true, + "allowedspread": true + }, + "page": { + "component": "core", + "allowedlevel2": false, + "allowedspread": false + }, + "payment": { + "component": "core_payment", + "allowedlevel2": true, + "allowedspread": true + }, + "plagiarism": { + "component": "core_plagiarism", + "allowedlevel2": false, + "allowedspread": false + }, + "portfolio": { + "component": "core_portfolio", + "allowedlevel2": false, + "allowedspread": false + }, + "preference": { + "component": "core", + "allowedlevel2": false, + "allowedspread": false + }, + "privacy": { + "component": "core_privacy", + "allowedlevel2": true, + "allowedspread": true + }, + "question": { + "component": "core_question", + "allowedlevel2": true, + "allowedspread": true + }, + "rating": { + "component": "core_rating", + "allowedlevel2": false, + "allowedspread": false + }, + "reportbuilder": { + "component": "core_reportbuilder", + "allowedlevel2": true, + "allowedspread": true + }, + "rss": { + "component": "core_rss", + "allowedlevel2": false, + "allowedspread": false + }, + "search": { + "component": "core_search", + "allowedlevel2": true, + "allowedspread": true + }, + "string": { + "component": "core", + "allowedlevel2": false, + "allowedspread": false + }, + "tag": { + "component": "core_tag", + "allowedlevel2": false, + "allowedspread": false + }, + "task": { + "component": "core", + "allowedlevel2": true, + "allowedspread": true + }, + "test": { + "component": "core", + "allowedlevel2": false, + "allowedspread": false + }, + "time": { + "component": "core", + "allowedlevel2": false, + "allowedspread": false + }, + "upgrade": { + "component": "core", + "allowedlevel2": true, + "allowedspread": false + }, + "webservice": { + "component": "core_webservice", + "allowedlevel2": false, + "allowedspread": false + }, + "xapi": { + "component": "core_xapi", + "allowedlevel2": true, + "allowedspread": true + } +}