<?php /** * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; under version 2 * of the License (non-upgradable). * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * Copyright (c) 2002-2008 (original work) Public Research Centre Henri Tudor & University of Luxembourg (under the project TAO & TAO2); * 2008-2010 (update and modification) Deutsche Institut für Internationale Pädagogische Forschung (under the project TAO-TRANSFER); * 2009-2012 (update and modification) Public Research Centre Henri Tudor (under the project TAO-SUSTAIN & TAO-DEV); * 2013-2017 (update and modification) Open Assessment Technologies SA (under the project TAO-PRODUCT); */ use oat\generis\persistence\DriverConfigurationFeeder; use oat\tao\helpers\InstallHelper; use oat\oatbox\install\Installer; use oat\oatbox\service\ServiceManager; use oat\tao\model\OperatedByService; use oat\generis\persistence\sql\DbCreator; use oat\generis\persistence\sql\SetupDb; use oat\generis\persistence\PersistenceManager; use oat\generis\model\data\Ontology; use oat\tao\model\TaoOntology; use oat\generis\model\GenerisRdf; use oat\tao\model\user\TaoRoles; use oat\tao\model\service\ApplicationService; use oat\oatbox\service\ServiceNotFoundException; /** * * * Installation main class * * @access public * @author Jérôme Bogaerts, <jerome@taotesting.com> * @package tao */ class tao_install_Installator { // Adding container and logger. use \oat\oatbox\log\ContainerLoggerTrait; /** * Installator related dependencies will be reached under this offset. */ const CONTAINER_INDEX = 'taoInstallInstallator'; protected $options = []; private $log = []; private $escapedChecks = []; private $oatBoxInstall = null; public function __construct($options) { // Using the container if it's necessary with automatic dependency returning. $options = $this->initContainer($options, static::CONTAINER_INDEX); if (!isset($options['root_path'])) { throw new tao_install_utils_Exception("root_path option must be defined to perform installation."); } if (!isset($options['install_path'])) { throw new tao_install_utils_Exception("install_path option must be defined to perform installation."); } $this->options = $options; $this->options['root_path'] = rtrim($this->options['root_path'], '/\\') . DIRECTORY_SEPARATOR; $this->options['install_path'] = rtrim($this->options['install_path'], '/\\') . DIRECTORY_SEPARATOR; $this->oatBoxInstall = new Installer(); } /** * Run the TAO install from the given data * @throws tao_install_utils_Exception * @param $installData array data coming from the install form * @param $callback callable|null post install callback */ public function install(array $installData, callable $callback = null) { try { /** * It's a quick hack for solving reinstall issue. * Should be a better option. */ @unlink($this->options['root_path'] . 'config/generis.conf.php'); /* * 0 - Check input parameters. */ $this->log('i', "Checking install data"); self::checkInstallData($installData); $this->log('i', "Starting TAO install"); // Sanitize $installData if needed. if (!preg_match("/\/$/", $installData['module_url'])) { $installData['module_url'] .= '/'; } // Define the ROOT_URL constant if not defined (can be used in manifest files) if (!defined('ROOT_URL')) { define('ROOT_URL', $installData['module_url']); } if (isset($installData['extensions'])) { $extensionIDs = is_array($installData['extensions']) ? $installData['extensions'] : explode(',', $installData['extensions']); } else { $extensionIDs = ['taoCe']; } $this->log('d', 'Extensions to be installed: ' . var_export($extensionIDs, true)); $installData['file_path'] = rtrim($installData['file_path'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; /* * 1 - Check configuration with checks described in the manifest. */ $configChecker = tao_install_utils_ChecksHelper::getConfigChecker($extensionIDs); // Silence checks to have to be escaped. foreach ($configChecker->getComponents() as $c) { if (method_exists($c, 'getName') && in_array($c->getName(), $this->getEscapedChecks())) { $configChecker->silent($c); } } $reports = $configChecker->check(); foreach ($reports as $r) { $msg = $r->getMessage(); $component = $r->getComponent(); $this->log('i', $msg); if ($r->getStatus() !== common_configuration_Report::VALID && !$component->isOptional()) { throw new tao_install_utils_Exception($msg); } } /* * X - Setup Oatbox */ $this->log('d', 'Removing old config'); $consistentOptions = array_merge($installData, $this->options); $consistentOptions['config_path'] = $this->getConfigPath(); $this->oatBoxInstall->setOptions($consistentOptions); $this->oatBoxInstall->install(); $this->log('d', 'Oatbox was installed!'); ServiceManager::setServiceManager($this->getServiceManager()); /* * 2 - Setup RDS persistence */ if (!$this->getServiceManager()->has(DriverConfigurationFeeder::SERVICE_ID)) { $this->getServiceManager()->register( DriverConfigurationFeeder::SERVICE_ID, new DriverConfigurationFeeder( [ DriverConfigurationFeeder::OPTION_DRIVER_OPTIONS => [] ] ) ); } if ($this->getServiceManager()->has(PersistenceManager::SERVICE_ID)) { $persistenceManager = $this->getServiceManager()->get(PersistenceManager::SERVICE_ID); } else { $this->log('i', "Spawning new PersistenceManager"); $persistenceManager = new PersistenceManager(); } if (!$persistenceManager->hasPersistence('default')) { $this->log('i', "Register default Persistence"); $dbalConfigCreator = new tao_install_utils_DbalConfigCreator(); $persistenceManager->registerPersistence('default', $dbalConfigCreator->createDbalConfig($installData)); $this->getServiceManager()->register(PersistenceManager::SERVICE_ID, $persistenceManager); } $dbCreator = new SetupDb(); $dbCreator->setLogger($this->logger); $dbCreator->setupDatabase($persistenceManager->getPersistenceById('default')); /* * 4 - Create the generis config files */ $this->log('d', 'Writing generis config'); $generisConfigWriter = new tao_install_utils_ConfigWriter( $this->options['root_path'] . 'generis/config/sample/generis.conf.php', $this->getGenerisConfig() ); $session_name = (isset($installData['session_name'])) ? $installData['session_name'] : self::generateSessionName(); $generisConfigWriter->createConfig(); $constants = [ 'LOCAL_NAMESPACE' => $installData['module_namespace'], 'GENERIS_INSTANCE_NAME' => $installData['instance_name'], 'GENERIS_SESSION_NAME' => $session_name, 'ROOT_PATH' => $this->options['root_path'], 'FILES_PATH' => $installData['file_path'], 'ROOT_URL' => $installData['module_url'], 'DEFAULT_LANG' => $installData['module_lang'], 'DEBUG_MODE' => ($installData['module_mode'] == 'debug') ? true : false, 'TIME_ZONE' => $installData['timezone'] ]; $constants['DEFAULT_ANONYMOUS_INTERFACE_LANG'] = (isset($installData['anonymous_lang'])) ? $installData['anonymous_lang'] : $installData['module_lang']; $generisConfigWriter->writeConstants($constants); $this->log('d', 'The following constants were written in generis config:' . PHP_EOL . var_export($constants, true)); /* * 4b - Prepare the file/cache folder (FILES_PATH) not yet defined) * @todo solve this more elegantly */ $file_path = $installData['file_path']; if (is_dir($file_path)) { $this->log('i', 'Data from previous install found and will be removed'); if (!helpers_File::emptyDirectory($file_path, true)) { throw new common_exception_Error('Unable to empty ' . $file_path . ' folder.'); } } else { if (mkdir($file_path, 0700, true)) { $this->log('d', $file_path . ' directory was created!'); } else { throw new Exception($file_path . ' directory creation was failed!'); } } $cachePath = $file_path . 'generis' . DIRECTORY_SEPARATOR . 'cache'; if (mkdir($cachePath, 0700, true)) { $this->log('d', $cachePath . ' directory was created!'); } else { throw new Exception($cachePath . ' directory creation was failed!'); } foreach ((array)$installData['extra_persistences'] as $k => $persistence) { $persistenceManager->registerPersistence($k, $persistence); } /* * 5 - Run the extensions bootstrap */ $this->log('d', 'Running the extensions bootstrap'); common_Config::load($this->getGenerisConfig()); /* * 5b - Create cache persistence */ $this->log('d', 'Creating cache persistence..'); $persistenceManager->registerPersistence('cache', [ 'driver' => 'phpfile' ]); $persistenceManager->getPersistenceById('cache')->purge(); $this->getServiceManager()->register(PersistenceManager::SERVICE_ID, $persistenceManager); /* * 6 - Finish Generis Install */ $this->log('d', 'Finishing generis install..'); $generis = common_ext_ExtensionsManager::singleton()->getExtensionById('generis'); $generisInstaller = new common_ext_GenerisInstaller($generis, true); $generisInstaller->initContainer($this->getContainer()); $generisInstaller->install(); /* * 7 - Add languages */ $this->log('d', 'Adding languages..'); $ontology = $this->getServiceManager()->get(Ontology::SERVICE_ID); $langModel = \tao_models_classes_LanguageService::singleton()->getLanguageDefinition(); $rdfModel = $ontology->getRdfInterface(); foreach ($langModel as $triple) { $rdfModel->add($triple); } /* * 8 - Install the extensions */ InstallHelper::initContainer($this->container); $installed = InstallHelper::installRecursively($extensionIDs, $installData); $this->log('ext', $installed); /* * 8b - Generates client side translation bundles (depends on extension install) */ $this->log('i', 'Generates client side translation bundles'); tao_models_classes_LanguageService::singleton()->generateAll(); /* * 9 - Insert Super User */ $this->log('i', 'Spawning SuperUser ' . $installData['user_login']); $userClass = $ontology->getClass(TaoOntology::CLASS_URI_TAO_USER); $userid = $installData['module_namespace'] . TaoOntology::DEFAULT_USER_URI_SUFFIX; $userpwd = core_kernel_users_Service::getPasswordHash()->encrypt($installData['user_pass1']); $userLang = 'http://www.tao.lu/Ontologies/TAO.rdf#Lang' . $installData['module_lang']; $superUser = $userClass->createInstance('Super User', 'super user created during the TAO installation', $userid); $superUser->setPropertiesValues([ GenerisRdf::PROPERTY_USER_ROLES => [ TaoRoles::GLOBAL_MANAGER, TaoRoles::SYSTEM_ADMINISTRATOR ], TaoOntology::PROPERTY_USER_FIRST_TIME => GenerisRdf::GENERIS_TRUE, GenerisRdf::PROPERTY_USER_LOGIN => $installData['user_login'], GenerisRdf::PROPERTY_USER_PASSWORD => $userpwd, GenerisRdf::PROPERTY_USER_LASTNAME => $installData['user_lastname'], GenerisRdf::PROPERTY_USER_FIRSTNAME => $installData['user_firstname'], GenerisRdf::PROPERTY_USER_MAIL => $installData['user_email'], GenerisRdf::PROPERTY_USER_DEFLG => $userLang, GenerisRdf::PROPERTY_USER_UILG => $userLang, GenerisRdf::PROPERTY_USER_TIMEZONE => TIME_ZONE ]); /* * 10 - Secure the install for production mode */ if ($installData['module_mode'] == 'production') { $extensions = common_ext_ExtensionsManager::singleton()->getInstalledExtensions(); $this->log('i', 'Securing tao for production'); // 11.0 Protect TAO dist $shield = new tao_install_utils_Shield(array_keys($extensions)); $shield->disableRewritePattern(["!/test/", "!/doc/"]); $shield->denyAccessTo([ 'views/sass', 'views/js/test', 'views/build' ]); $shield->protectInstall(); } /* * 11 - Create the version file */ $this->log('d', 'Creating TAO version file'); file_put_contents($installData['file_path'] . 'version', TAO_VERSION); /* * 12 - Register Information about organization operating the system */ $this->log('t', 'Registering information about the organization operating the system'); $operatedByService = $this->getServiceManager()->get(OperatedByService::SERVICE_ID); if (!empty($installData['operated_by_name'])) { $operatedByService->setName($installData['operated_by_name']); } if (!empty($installData['operated_by_email'])) { $operatedByService->setEmail($installData['operated_by_email']); } $this->getServiceManager()->register(OperatedByService::SERVICE_ID, $operatedByService); if ($callback) { $callback(); } $this->recreateDependencyInjectionContainerCache(); $this->setInstallationFinished(); } catch (Exception $e) { if ($this->retryInstallation($e)) { return; } // In any case, we transmit a single exception type (at the moment) // for a clearer API for client code. $this->log('e', 'Error Occurs : ' . $e->getMessage() . PHP_EOL . $e->getTraceAsString()); throw new tao_install_utils_Exception($e->getMessage(), 0, $e); } } public function getServiceManager() { return $this->oatBoxInstall->setupServiceManager($this->getConfigPath()); } private function retryInstallation($exception) { $returnValue = false; $err = $exception->getMessage(); if (strpos($err, 'cannot construct the resource because the uri cannot be empty') === 0 && $this->isWindows()) { /* * a known issue * @see http://forge.taotesting.com/issues/3014 * this issue can only be fixed by an administrator * changing the thread_stack system variable in my.ini as following: * '256K' on 64bit windows * '192K' on 32bit windows */ $this->log('e', 'Error Occurs : ' . $err . PHP_EOL . $exception->getTraceAsString()); throw new tao_install_utils_Exception("Error in mysql system variable 'thread_stack':<br>It is required to change its value in my.ini as following<br>'192K' on 32bit windows<br>'256K' on 64bit windows.<br><br>Note that such configuration changes will only take effect after server restart.<br><br>", 0, $exception); } if (!$returnValue) { return false; } // it is a known issue, go ahead to retry with the issue fixer $this->install($this->config); return true; } private function isWindows() { return strtoupper(substr(PHP_OS, 0, 3)) == 'WIN'; } /** * Generate an alphanum token to be used as a PHP session name. * * @access public * @author Jerome Bogaerts, <jerome.bogaerts@tudor.lu> * @return string */ public static function generateSessionName() { return 'tao_' . helpers_Random::generateString(8); } /** * Check the install data information such as * - instance name * - database driver * - ... * * If a parameter of the $installData is not valid regarding the install * business rules, an MalformedInstall * * @param array $installData */ public static function checkInstallData(array $installData) { // instance name if (empty($installData['instance_name'])) { $msg = "Missing install parameter 'instance_name'."; throw new tao_install_utils_MalformedParameterException($msg); } elseif (!is_string($installData['instance_name'])) { $msg = "Malformed install parameter 'instance_name'. It must be a string."; throw new tao_install_utils_MalformedParameterException($msg); } elseif (1 === preg_match('/\s/u', $installData['instance_name'])) { $msg = "Malformed install parameter 'instance_name'. It cannot contain spacing characters (tab, backspace)."; throw new tao_install_utils_MalformedParameterException($msg); } } /** * Tell the Installator instance to not take into account * a Configuration Check with ID = $id. * * @param string $id The identifier of the check to escape. */ public function escapeCheck($id) { $checks = $this->getEscapedChecks(); array_push($checks, $id); $checks = array_unique($checks); $this->setEscapedChecks($checks); } /** * Obtain an array of Configuration Check IDs to be escaped by * the Installator. * * @return array */ public function getEscapedChecks() { return $this->escapedChecks; } /** * Set the array of Configuration Check IDs to be escaped by * the Installator. * * @param array $escapedChecks An array of strings. * @return void */ public function setEscapedChecks(array $escapedChecks) { $this->escapedChecks = $escapedChecks; } /** * Informs you if a given Configuration Check ID corresponds * to a Check that has to be escaped. */ public function isEscapedCheck($id) { return in_array($id, $this->getEscapedChecks()); } /** * Log message and add it to $this->log array; * @see common_Logger class * @param string $logLevel * <ul> * <li>'w' - warning</li> * <li>'t' - trace</li> * <li>'d' - debug</li> * <li>'i' - info</li> * <li>'e' - error</li> * <li>'f' - fatal</li> * <li>'ext' - installed extensions</li> * </ul> * @param string $message * @param array $tags */ public function log($logLevel, $message, $tags = []) { if (!is_array($tags)) { $tags = [$tags]; } if ($this->getLogger() instanceof \Psr\Log\LoggerInterface) { if ($logLevel === 'ext') { $this->logNotice('Installed extensions: ' . implode(', ', $message)); } else { $this->getLogger()->log( common_log_Logger2Psr::getPsrLevelFromCommon($logLevel), $message ); } } if (method_exists('common_Logger', $logLevel)) { call_user_func('common_Logger::' . $logLevel, $message, $tags); } if (is_array($message)) { $this->log[$logLevel] = (isset($this->log[$logLevel])) ? array_merge($this->log[$logLevel], $message) : $message; } else { $this->log[$logLevel][] = $message; } } /** * Get array of log messages * @return array */ public function getLog() { return $this->log; } /** * Get the config file platform e.q. generis.conf.php * * @return string */ protected function getGenerisConfig() { return $this->getConfigPath() . 'generis.conf.php'; } /** * Get the config path for installation * If options have installation_config_path, it's taken otherwise it's root_path * * @return string */ protected function getConfigPath() { if (isset($this->options['installation_config_path'])) { return $this->options['installation_config_path']; } else { return $this->options['root_path'] . 'config' . DIRECTORY_SEPARATOR; } } /** * Mark application as ready to be used (all extensions installed and post scripts executed) * @throws common_Exception */ private function setInstallationFinished() { $applicationService = $this->getServiceManager()->get(ApplicationService::SERVICE_ID); $applicationService->setOption(ApplicationService::OPTION_INSTALLATION_FINISHED, true); $this->getServiceManager()->register(ApplicationService::SERVICE_ID, $applicationService); } private function recreateDependencyInjectionContainerCache(): void { ServiceManager::getServiceManager() ->getContainerBuilder() ->forceBuild(); } }