drop support for php 7.4 (#5740)
[openemr.git] / src / Core / ModulesApplication.php
blob52eaa50d11c559d59e32396aa68fa3975536e836
1 <?php
3 /**
4 * ModulesApplication class.
6 * @package OpenEMR
7 * @link http://www.open-emr.org
9 * @author Stephen Nielson <stephen@nielson.org>
10 * @copyright Copyright (c) 2019 Stephen Nielson <stephen@nielson.org>
11 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
14 namespace OpenEMR\Core;
16 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
17 use Laminas\Mvc\Application;
18 use Laminas\Mvc\Service\ServiceManagerConfig;
19 use Laminas\ServiceManager\ServiceManager;
21 class ModulesApplication
23 /**
24 * The application reference pointer for the zend mvc modules application
26 * @var Application
29 private $application;
31 const CUSTOM_MODULE_BOOSTRAP_NAME = 'openemr.bootstrap.php';
33 public function __construct(Kernel $kernel, $webRootPath, $modulePath, $zendModulePath)
35 // Beware: default module path ends in a slash. Really should not but have to refactor to change..
36 $zendConfigurationPath = $webRootPath . '/' . $modulePath . $zendModulePath;
37 $customModulePath = $webRootPath . '/' . $modulePath . "custom_modules" . '/';
38 $configuration = require $zendConfigurationPath . '/' . 'config/application.config.php';
40 // Prepare the service manager
41 // We customize this and skip using the static Laminas\Mvc\Application::init in order to inject the
42 // Symfony Kernel's EventListener that way we can bridge the two frameworks.
43 $smConfig = isset($configuration['service_manager']) ? $configuration['service_manager'] : [];
44 $smConfig = new ServiceManagerConfig($smConfig);
46 $serviceManager = new ServiceManager();
47 $smConfig->configureServiceManager($serviceManager);
48 $serviceManager->setService('ApplicationConfig', $configuration);
49 $serviceManager->setService(EventDispatcherInterface::class, $kernel->getEventDispatcher());
51 // Load modules
52 $serviceManager->get('ModuleManager')->loadModules();
54 // Prepare list of listeners to bootstrap
55 $listenersFromAppConfig = isset($configuration['listeners']) ? $configuration['listeners'] : [];
56 $config = $serviceManager->get('config');
57 $listenersFromConfigService = isset($config['listeners']) ? $config['listeners'] : [];
59 $listeners = array_unique(array_merge($listenersFromConfigService, $listenersFromAppConfig));
61 $this->application = $serviceManager->get('Application')->bootstrap($listeners);
63 // we skip the audit log as it has no bearing on user activity and is core system related...
64 $resultSet = sqlStatementNoLog($statement = "SELECT mod_name FROM modules WHERE mod_active = 1 AND type != 1 ORDER BY `mod_ui_order`, `date`");
65 $db_modules = [];
66 while ($row = sqlFetchArray($resultSet)) {
67 $db_modules[] = $row["mod_name"];
70 // let's get our autoloader that people can tie into if they need it
71 $autoloader = new ModulesClassLoader($webRootPath);
72 $this->bootstrapCustomModules($autoloader, $kernel->getEventDispatcher(), $customModulePath);
75 private function bootstrapCustomModules(ModulesClassLoader $classLoader, $eventDispatcher, $customModulePath)
77 // we skip the audit log as it has no bearing on user activity and is core system related...
78 $resultSet = sqlStatementNoLog($statement = "SELECT mod_name, mod_directory FROM modules WHERE mod_active = 1 AND type != 1 ORDER BY `mod_ui_order`, `date`");
79 $db_modules = [];
80 while ($row = sqlFetchArray($resultSet)) {
81 if (is_readable($customModulePath . $row['mod_directory'] . '/' . attr(self::CUSTOM_MODULE_BOOSTRAP_NAME))) {
82 $db_modules[] = ["name" => $row["mod_name"], "directory" => $row['mod_directory'], "path" => $customModulePath . $row['mod_directory']];
83 } else {
84 // no reason to try and include a missing bootstrap.
85 // notify user, turn off module and move on...
86 error_log("Custom module " . errorLogEscape($customModulePath . $row['mod_directory'])
87 . '/' . self::CUSTOM_MODULE_BOOSTRAP_NAME
88 . " is enabled but missing bootstrap.php script. Install and enable in module manager. This is the only warning.");
89 // disable to prevent flooding log with this error
90 $error = sqlQueryNoLog("UPDATE `modules` SET `mod_active` = '0' WHERE `modules`.`mod_name` = ? AND `modules`.`mod_directory` = ?", array($row['mod_name'], $row['mod_directory']));
91 // tell user we did it.
92 if (!$error) {
93 error_log("Custom module " . errorLogEscape($row['mod_name']) . " has been disabled");
97 foreach ($db_modules as $module) {
98 $this->loadCustomModule($classLoader, $module, $eventDispatcher);
100 // TODO: stephen we should fire an event saying we've now loaded all the modules here.
101 // Unsure who'd be listening or care.
104 private function loadCustomModule(ModulesClassLoader $classLoader, $module, $eventDispatcher)
106 try {
107 // the only thing in scope here is $module and $eventDispatcher which is ok for our bootstrap piece.
108 // do we really want to just include a file?? Should we go all zend and actually force a class instantiation
109 // here and then inject the EventDispatcher or even possibly the Symfony Kernel here?
110 include $module['path'] . '/' . attr(self::CUSTOM_MODULE_BOOSTRAP_NAME);
111 } catch (Exception $exception) {
112 error_log(errorLogEscape($exception->getMessage()));
116 public function run()
118 $this->application->run();
121 public function getServiceManager(): ServiceManager
123 return $this->application->getServiceManager();
127 * Given a list of module files (javascript, css, etc) make sure they are locked down to be just inside the modules
128 * folder. The intent is to prevent module writers from including files outside the modules installation directory.
129 * If the file exists and is inside the modules installation path it will be returned. Otherwise it is filtered out
130 * of the array list
131 * @param $files The list of files to safely filter
132 * @return array
134 public static function filterSafeLocalModuleFiles(array $files): array
136 if (is_array($files) && !empty($files)) {
137 // for safety we only allow the scripts to be from the local filesystem for now
138 $filteredFiles = array_filter(array_map(function ($scriptSrc) {
139 // scripts that have any kind of parameters in them such as a cache buster mess up finding the real path
140 // we need to strip that out and then check against the real path
141 $scriptSrcPath = parse_url($scriptSrc, PHP_URL_PATH);
142 $realPath = realpath($GLOBALS['fileroot'] . $scriptSrcPath);
143 // make sure we haven't left our root path ie interface folder
144 if (strpos($realPath, $GLOBALS['fileroot'] . '/interface/modules/') === 0 && file_exists($realPath)) {
145 return $scriptSrc;
147 return null;
148 }, $files), function ($script) {
149 return !empty($script);
151 } else {
152 $filteredFiles = [];
154 return $filteredFiles;