4 * ModulesApplication class.
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
24 * The application reference pointer for the zend mvc modules 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());
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`");
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`");
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']];
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.
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)
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
131 * @param $files The list of files to safely filter
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)) {
148 }, $files), function ($script) {
149 return !empty($script);
154 return $filteredFiles;