From 3fbe06d1998a48ee284b581b4c8330ae55c0f083 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 13 Nov 2024 12:11:51 +0800 Subject: [PATCH] Moodle: Prepare for an upcoming change in Moodle distribution This set of changes adds support for: - the 'public' directory introduced in Moodle 5.1 - the ability for Moodle to be installed as a Composer dependency - configuration of an installation-specific prefix for the Moodle 'core' path I've also: - updated the plugin type list for Moodle 5.1 - removed the `admin_report` plugin type which has not been a valid plugin type for about 15 years. --- src/Composer/Installers/MoodleInstaller.php | 206 ++++++++++++------ .../Installers/Test/MoodleInstallerTest.php | 126 +++++++++++ 2 files changed, 267 insertions(+), 65 deletions(-) create mode 100644 tests/Composer/Installers/Test/MoodleInstallerTest.php diff --git a/src/Composer/Installers/MoodleInstaller.php b/src/Composer/Installers/MoodleInstaller.php index eb2b8acf..8030e8db 100644 --- a/src/Composer/Installers/MoodleInstaller.php +++ b/src/Composer/Installers/MoodleInstaller.php @@ -5,69 +5,145 @@ class MoodleInstaller extends BaseInstaller { /** @var array */ - protected $locations = array( - 'mod' => 'mod/{$name}/', - 'admin_report' => 'admin/report/{$name}/', - 'atto' => 'lib/editor/atto/plugins/{$name}/', - 'tool' => 'admin/tool/{$name}/', - 'assignment' => 'mod/assignment/type/{$name}/', - 'assignsubmission' => 'mod/assign/submission/{$name}/', - 'assignfeedback' => 'mod/assign/feedback/{$name}/', - 'antivirus' => 'lib/antivirus/{$name}/', - 'auth' => 'auth/{$name}/', - 'availability' => 'availability/condition/{$name}/', - 'block' => 'blocks/{$name}/', - 'booktool' => 'mod/book/tool/{$name}/', - 'cachestore' => 'cache/stores/{$name}/', - 'cachelock' => 'cache/locks/{$name}/', - 'calendartype' => 'calendar/type/{$name}/', - 'communication' => 'communication/provider/{$name}/', - 'customfield' => 'customfield/field/{$name}/', - 'fileconverter' => 'files/converter/{$name}/', - 'format' => 'course/format/{$name}/', - 'coursereport' => 'course/report/{$name}/', - 'contenttype' => 'contentbank/contenttype/{$name}/', - 'customcertelement' => 'mod/customcert/element/{$name}/', - 'datafield' => 'mod/data/field/{$name}/', - 'dataformat' => 'dataformat/{$name}/', - 'datapreset' => 'mod/data/preset/{$name}/', - 'editor' => 'lib/editor/{$name}/', - 'enrol' => 'enrol/{$name}/', - 'filter' => 'filter/{$name}/', - 'forumreport' => 'mod/forum/report/{$name}/', - 'gradeexport' => 'grade/export/{$name}/', - 'gradeimport' => 'grade/import/{$name}/', - 'gradereport' => 'grade/report/{$name}/', - 'gradingform' => 'grade/grading/form/{$name}/', - 'h5plib' => 'h5p/h5plib/{$name}/', - 'local' => 'local/{$name}/', - 'logstore' => 'admin/tool/log/store/{$name}/', - 'ltisource' => 'mod/lti/source/{$name}/', - 'ltiservice' => 'mod/lti/service/{$name}/', - 'media' => 'media/player/{$name}/', - 'message' => 'message/output/{$name}/', - 'mlbackend' => 'lib/mlbackend/{$name}/', - 'mnetservice' => 'mnet/service/{$name}/', - 'paygw' => 'payment/gateway/{$name}/', - 'plagiarism' => 'plagiarism/{$name}/', - 'portfolio' => 'portfolio/{$name}/', - 'qbank' => 'question/bank/{$name}/', - 'qbehaviour' => 'question/behaviour/{$name}/', - 'qformat' => 'question/format/{$name}/', - 'qtype' => 'question/type/{$name}/', - 'quizaccess' => 'mod/quiz/accessrule/{$name}/', - 'quiz' => 'mod/quiz/report/{$name}/', - 'report' => 'report/{$name}/', - 'repository' => 'repository/{$name}/', - 'scormreport' => 'mod/scorm/report/{$name}/', - 'search' => 'search/engine/{$name}/', - 'theme' => 'theme/{$name}/', - 'tiny' => 'lib/editor/tiny/plugins/{$name}/', - 'tinymce' => 'lib/editor/tinymce/plugins/{$name}/', - 'profilefield' => 'user/profile/field/{$name}/', - 'webservice' => 'webservice/{$name}/', - 'workshopallocation' => 'mod/workshop/allocation/{$name}/', - 'workshopeval' => 'mod/workshop/eval/{$name}/', - 'workshopform' => 'mod/workshop/form/{$name}/' - ); + protected $locations = [ + // Core plugin and subplugin types. + 'core' => '{$prefix}', + 'aiplacement' => '{$prefix}{$public}ai/placement/{$name}/', + 'aiprovider' => '{$prefix}{$public}ai/provider/{$name}/', + 'antivirus' => '{$prefix}{$public}lib/antivirus/{$name}/', + 'assignfeedback' => '{$prefix}{$public}mod/assign/feedback/{$name}/', + 'assignsubmission' => '{$prefix}{$public}mod/assign/submission/{$name}/', + 'auth' => '{$prefix}{$public}auth/{$name}/', + 'availability' => '{$prefix}{$public}availability/condition/{$name}/', + 'bbbext' => '{$prefix}{$public}mod/bigbluebuttonbn/extension/{$name}/', + 'block' => '{$prefix}{$public}blocks/{$name}/', + 'booktool' => '{$prefix}{$public}mod/book/tool/{$name}/', + 'cachelock' => '{$prefix}{$public}cache/locks/{$name}/', + 'cachestore' => '{$prefix}{$public}cache/stores/{$name}/', + 'calendartype' => '{$prefix}{$public}calendar/type/{$name}/', + 'communication' => '{$prefix}{$public}communication/provider/{$name}/', + 'contenttype' => '{$prefix}{$public}contentbank/contenttype/{$name}/', + 'coursereport' => '{$prefix}{$public}course/report/{$name}/', + 'customfield' => '{$prefix}{$public}customfield/field/{$name}/', + 'datafield' => '{$prefix}{$public}mod/data/field/{$name}/', + 'dataformat' => '{$prefix}{$public}dataformat/{$name}/', + 'datapreset' => '{$prefix}{$public}mod/data/preset/{$name}/', + 'editor' => '{$prefix}{$public}lib/editor/{$name}/', + 'enrol' => '{$prefix}{$public}enrol/{$name}/', + 'factor' => '{$prefix}{$public}admin/tool/mfa/factor/{$name}/', + 'fileconverter' => '{$prefix}{$public}files/converter/{$name}/', + 'filter' => '{$prefix}{$public}filter/{$name}/', + 'format' => '{$prefix}{$public}course/format/{$name}/', + 'forumreport' => '{$prefix}{$public}mod/forum/report/{$name}/', + 'gradeexport' => '{$prefix}{$public}grade/export/{$name}/', + 'gradeimport' => '{$prefix}{$public}grade/import/{$name}/', + 'gradepenalty' => '{$prefix}{$public}grade/penalty/{$name}/', + 'gradereport' => '{$prefix}{$public}grade/report/{$name}/', + 'gradingform' => '{$prefix}{$public}grade/grading/form/{$name}/', + 'h5plib' => '{$prefix}{$public}h5p/h5plib/{$name}/', + 'local' => '{$prefix}{$public}local/{$name}/', + 'logstore' => '{$prefix}{$public}admin/tool/log/store/{$name}/', + 'ltiservice' => '{$prefix}{$public}mod/lti/service/{$name}/', + 'ltisource' => '{$prefix}{$public}mod/lti/source/{$name}/', + 'media' => '{$prefix}{$public}media/player/{$name}/', + 'message' => '{$prefix}{$public}message/output/{$name}/', + 'mlbackend' => '{$prefix}{$public}lib/mlbackend/{$name}/', + 'mnetservice' => '{$prefix}{$public}mnet/service/{$name}/', + 'mod' => '{$prefix}{$public}mod/{$name}/', + 'monitoringexporter' => '{$prefix}{$public}admin/tool/monitoring/exporter/{$name}/', + 'paygw' => '{$prefix}{$public}payment/gateway/{$name}/', + 'plagiarism' => '{$prefix}{$public}plagiarism/{$name}/', + 'portfolio' => '{$prefix}{$public}portfolio/{$name}/', + 'profilefield' => '{$prefix}{$public}user/profile/field/{$name}/', + 'qbank' => '{$prefix}{$public}question/bank/{$name}/', + 'qbehaviour' => '{$prefix}{$public}question/behaviour/{$name}/', + 'qformat' => '{$prefix}{$public}question/format/{$name}/', + 'qtype' => '{$prefix}{$public}question/type/{$name}/', + 'quiz' => '{$prefix}{$public}mod/quiz/report/{$name}/', + 'quizaccess' => '{$prefix}{$public}mod/quiz/accessrule/{$name}/', + 'report' => '{$prefix}{$public}report/{$name}/', + 'repository' => '{$prefix}{$public}repository/{$name}/', + 'scormreport' => '{$prefix}{$public}mod/scorm/report/{$name}/', + 'search' => '{$prefix}{$public}search/engine/{$name}/', + 'smsgateway' => '{$prefix}{$public}sms/gateway/{$name}/', + 'theme' => '{$prefix}{$public}theme/{$name}/', + 'tiny' => '{$prefix}{$public}lib/editor/tiny/plugins/{$name}/', + 'tool' => '{$prefix}{$public}admin/tool/{$name}/', + 'webservice' => '{$prefix}{$public}webservice/{$name}/', + 'workshopallocation' => '{$prefix}{$public}mod/workshop/allocation/{$name}/', + 'workshopeval' => '{$prefix}{$public}mod/workshop/eval/{$name}/', + 'workshopform' => '{$prefix}{$public}mod/workshop/form/{$name}/', + + // Community plugin subplugin types. + + // mod_customcert subplugin types. + 'customcertelement' => '{$prefix}{$public}mod/customcert/element/{$name}/', + + // tool_lifecycle subplugin types. + 'lifecycletrigger' => '{$prefix}{$public}admin/tool/lifecycle/trigger/', + 'lifecyclestep' => '{$prefix}{$public}admin/tool/lifecycle/step/', + + + // Legacy plugin and subplugin types which may be installed manually. + 'atto' => '{$prefix}{$public}lib/editor/atto/plugins/{$name}/', + 'assignment' => '{$prefix}{$public}mod/assignment/type/{$name}/', + 'tinymce' => '{$prefix}{$public}lib/editor/tinymce/plugins/{$name}/', + ]; + + /** + * {@inheritDoc} + */ + public function inflectPackageVars(array $vars): array + { + // Guess the package prefix and public directory. + $vars['prefix'] = $this->getRootPackagePath(); + $vars['public'] = $this->getPublicPath(); + + // Guess the name from the package name if not explicitly set. + $matches = []; + preg_match('/^moodle-(?([^_]*))_(?(.*))$/', $vars['name'], $matches); + + if ($matches) { + $vars['name'] = $matches['name']; + } + + return $vars; + } + + /** + * Get the install path for the root package. + * + * @return string + */ + protected function getRootPackagePath(): string + { + // To allow for migration from the legacy way of doing things, we + // check for an 'install-path' setting in the root package extra. + // This allows the root package to put the Moodle installation in + // a custom location. + // If there is no such setting, we assume the root package is a + // legacy package. + $rootPackage = $this->composer->getPackage(); + + $extra = $rootPackage->getExtra(); + return $extra['install-path'] ?? ''; + } + + /** + * Determine if Moodle uses a public directory. + * + * @return string + */ + protected function getPublicPath(): string + { + // The public directory setting is stored in the main Moodle package's. + // Legacy Moodle installs do not have this path, or any setting. + $moodlePackage = $this->composer->getRepositoryManager()->findPackage( + 'moodle/moodle', + '*' + ); + $extra = $moodlePackage ? $moodlePackage->getExtra() : $this->composer->getPackage()->getExtra(); + + return !empty($extra['haspublicdir']) ? 'public/' : ''; + } } diff --git a/tests/Composer/Installers/Test/MoodleInstallerTest.php b/tests/Composer/Installers/Test/MoodleInstallerTest.php new file mode 100644 index 00000000..79df8ba4 --- /dev/null +++ b/tests/Composer/Installers/Test/MoodleInstallerTest.php @@ -0,0 +1,126 @@ + $rootExtras + * @param array $moodleExtras + */ + public function testInflectPackageVars( + string $expectedPublic, + string $expectedRootPath, + string $expectedName, + string $expectedInstallPath, + string $composerType, + array $rootExtras, + array $moodleExtras, + string $packageName, + ): void { + $composer = $this->getComposer(); + $composer->getPackage()->setExtra($rootExtras); + + // Add the Moodle package to the repository manager. + $repository = new ArrayRepository(); + $moodlePackage = $this->getPackage('moodle/moodle', '1.0.0'); + $moodlePackage->setExtra($moodleExtras); + $repository->addPackage($moodlePackage); + $composer->getRepositoryManager()->addRepository($repository); + + // Create the installer. + $testPackage = $this->getPackage($packageName, '1.0.0'); + $testPackage->setType($composerType); + $installer = new MoodleInstaller( + $testPackage, + $composer, + $this->getMockIO() + ); + + $name = $testPackage->getPrettyName(); + $vendor = 'moodle'; + $type = $composerType; + $result = $installer->inflectPackageVars(compact('name', 'vendor', 'type')); + + $this->assertEquals($result['public'], $expectedPublic); + $this->assertEquals($result['prefix'], $expectedRootPath); + $this->assertEquals($result['name'], $expectedName); + $this->assertEquals($expectedInstallPath, $installer->getInstallPath($testPackage, 'moodle')); + } + + public static function inflectionProvider(): array + { + return array( + // Legacy install without public dir. + array( + '', // expected public path + '', // expected install path + 'custommod', // expected name + 'mod/custommod/', // expected install path + 'moodle-mod', // composer type + [], // root extras + [], // package extras + 'moodle-mod_custommod', // package name + ), + + // Modern install moodle/moodle. + array( + '', // expected public path + 'moodle/', // expected install path + 'moodle', // expected name + 'moodle/', // expected install path + 'moodle-core', // composer type + ['install-path' => 'moodle/'], // root extras + [], // package extras + 'moodle', // package name + ), + // Modern install with public dir and install path. + array( + 'public/', // expected public path + 'moodle/', // expected install path + 'customblock', // expected name + 'moodle/public/blocks/customblock/', // expected install path + 'moodle-block', // composer type + ['install-path' => 'moodle/'], // root extras + ['haspublicdir' => true], // package extras + 'moodle-block_customblock', // package name + ), + + // Modern install with public dir and no install path. + array( + 'public/', // expected public path + '', // expected install path + 'customblock', // expected name + 'public/blocks/customblock/', // expected install path + 'moodle-block', // composer type + [], // root extras + ['haspublicdir' => true], // package extras + 'moodle-block_customblock', // package name + ), + ); + } + + /** + * {@inheritDoc} + */ + protected function getComposer(): Composer + { + $composer = parent::getComposer(); + + $repositoryManager = new RepositoryManager( + $this->getMockIO(), + $composer->getConfig(), + $this->getMockBuilder(HttpDownloader::class)->disableOriginalConstructor()->getMock() + ); + $composer->setRepositoryManager($repositoryManager); + + return $composer; + } +}