diff --git a/public/lib/classes/component.php b/public/lib/classes/component.php index d55cac7669a..7ff10f1074a 100644 --- a/public/lib/classes/component.php +++ b/public/lib/classes/component.php @@ -204,6 +204,16 @@ class component { spl_autoload_register([self::class, 'classloader']); } + // Attempt to load the Composer autoloader from the Moodle root. + // In composer scaffolded installations this is a shim which delegates to the parent project. + global $CFG; + if (!empty($CFG->root)) { + $composerautoload = "{$CFG->root}/vendor/autoload.php"; + if (is_file($composerautoload)) { + require_once($composerautoload); + } + } + // Load any composer-driven autoload files. // This is intended to mimic the behaviour of the standard Composer Autoloader. foreach (static::$composerautoloadfiles as $file) { diff --git a/public/lib/classes/environment.php b/public/lib/classes/environment.php index 403dc7357f4..f1f5cca152b 100644 --- a/public/lib/classes/environment.php +++ b/public/lib/classes/environment.php @@ -24,6 +24,26 @@ namespace core; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class environment { + + /** + * Get the Composer root install path for the active Composer runtime. + * + * @return string|null + */ + protected static function get_composer_root_install_path(): ?string { + if (!class_exists(\Composer\InstalledVersions::class)) { + return null; + } + + $rootpackage = \Composer\InstalledVersions::getRootPackage(); + if (!is_array($rootpackage) || empty($rootpackage['install_path'])) { + return null; + } + + $realpath = realpath($rootpackage['install_path']); + return $realpath ?: null; + } + /** * Ensure that Composer dependencies are installed and the necessary files are present. * @@ -32,24 +52,7 @@ class environment { */ public static function check_composer_dependencies_installed(\environment_results $result): ?\environment_results { // Check if the composer vendor directory exists. - $vendorpath = static::get_vendor_path(); - if (!is_dir($vendorpath)) { - $result->setInfo('Composer vendor directory not found'); - $result->setFeedbackStr('composernotfound'); - return $result; - } - - // Check if the composer autoload file exists. - $autoloadpath = "{$vendorpath}/autoload.php"; - if (!is_file($autoloadpath)) { - $result->setInfo('Composer autoload file not found'); - $result->setFeedbackStr('composernotfound'); - return $result; - } - - // Check if the installed.php file exists in the composer directory. - $installedpath = "{$vendorpath}/composer/installed.php"; - if (!is_file($installedpath)) { + if (!class_exists(\Composer\InstalledVersions::class)) { $result->setInfo('Composer installed data not found'); $result->setFeedbackStr('composernotfound'); return $result; @@ -72,21 +75,34 @@ class environment { return null; // Skip this check in developer mode. } - $vendorpath = static::get_vendor_path(); - if (!is_dir($vendorpath)) { - return null; // No vendor directory, so no developer dependencies to check. + if (!class_exists(\Composer\InstalledVersions::class)) { + return null; // Composer not installed, so no developer dependencies to check. } - // Check if the installed.php file exists in the composer directory. - $installedpath = "{$vendorpath}/composer/installed.php"; - if (!is_file($installedpath)) { - return null; // No installed file, so no developer dependencies to check. + $installed = \Composer\InstalledVersions::getAllRawData(); + if (!is_array($installed)) { + return null; } - // Check if developer dependencies have been installed too. - $installed = include($installedpath); - if (is_array($installed) && array_key_exists('root', $installed)) { - if ($installed['root']['dev']) { + // Only consider the installed data set which matches the active Composer root. + // This reduces the risk of reporting false positives if multiple Composer autoloaders + // have been included in the same process. + $rootinstallpath = static::get_composer_root_install_path(); + if ($rootinstallpath === null) { + return null; + } + + foreach ($installed as $data) { + if (!is_array($data) || !array_key_exists('root', $data) || !is_array($data['root'])) { + continue; + } + + $installpath = $data['root']['install_path'] ?? null; + if (empty($installpath) || realpath($installpath) !== $rootinstallpath) { + continue; + } + + if (!empty($data['root']['dev'])) { $result->setInfo('Composer Developer dependencies are installed'); $result->setFeedbackStr('composerdeveloperdependenciesinstalled'); return $result; @@ -106,12 +122,27 @@ class environment { public static function check_composer_dependencies_optimised( \environment_results $result ): ?\environment_results { - $vendorpath = static::get_vendor_path(); - if (!is_dir($vendorpath)) { - return null; // No vendor directory, so no developer dependencies to check. + if (!class_exists(\Composer\InstalledVersions::class)) { + return null; // Composer not installed, so no developer dependencies to check. } - $autoloader = require("{$vendorpath}/autoload.php"); + $rootpackage = \Composer\InstalledVersions::getRootPackage(); + if (!is_array($rootpackage) || empty($rootpackage['install_path'])) { + return null; + } + + $rootpath = $rootpackage['install_path']; + $rootvendor = realpath("{$rootpath}/vendor"); + if (!$rootvendor) { + return null; + } + + $loaders = \Composer\Autoload\ClassLoader::getRegisteredLoaders(); + if (!array_key_exists($rootvendor, $loaders)) { + return null; // No autoloader for our vendor dir, so nothing to check. + } + + $autoloader = $loaders[$rootvendor]; if (static::is_developer_mode_enabled()) { if ($autoloader->isClassMapAuthoritative()) { diff --git a/public/lib/tests/classes/environment.php b/public/lib/tests/classes/environment.php index 025cfabee8b..d1566667d55 100644 --- a/public/lib/tests/classes/environment.php +++ b/public/lib/tests/classes/environment.php @@ -25,7 +25,7 @@ namespace core\tests; */ class environment extends \core\environment { /** @var string|null The path to the vendor dir if modified */ - protected static ?string $vendorpath; + protected static ?string $vendorpath = null; /** @var bool|null Whether developer mode is enabled, or defer to $CFG */ protected static ?bool $devmode = null; diff --git a/public/lib/tests/environment_test.php b/public/lib/tests/environment_test.php index fe8f946801e..23cd4dab493 100644 --- a/public/lib/tests/environment_test.php +++ b/public/lib/tests/environment_test.php @@ -28,50 +28,6 @@ use core\tests\environment as environment_tester; */ #[\PHPUnit\Framework\Attributes\CoversClass(environment::class)] final class environment_test extends \advanced_testcase { - #[\PHPUnit\Framework\Attributes\DataProvider('composer_error_states_provider')] - public function test_composer_not_installed_cases( - array $fs = [], - string $expectedfeedback = 'composernotfound' - ): void { - \org\bovigo\vfs\vfsStream::setup('root', null, $fs); - environment_tester::set_vendor_path(\org\bovigo\vfs\vfsStream::url('root/vendor')); - - $result = new \environment_results('custom_check'); - $result = environment_tester::check_composer_dependencies_installed($result); - $this->assertEquals($expectedfeedback, $result->getFeedbackStr()); - - // Check that the developer dependencies tests do not error in these conditions. - $result = new \environment_results('custom_check'); - $result = environment_tester::check_composer_developer_dependencies_not_installed($result); - $this->assertNull($result); - } - - /** - * Data provider for test_composer_not_installed_cases. - * - * @return \Generator - */ - public static function composer_error_states_provider(): \Generator { - yield 'composer vendor directory not found' => [ - 'fs' => [], - 'expectedfeedback' => 'composernotfound', - ]; - yield 'composer autoload file not found' => [ - 'fs' => [ - 'vendor' => [], - ], - 'expectedfeedback' => 'composernotfound', - ]; - yield 'composer installed data not found' => [ - 'fs' => [ - 'vendor' => [ - 'autoload.php' => '', - ], - ], - 'expectedfeedback' => 'composernotfound', - ]; - } - public function test_composer_installed(): void { \org\bovigo\vfs\vfsStream::setup('root', null, [ 'vendor' => [