* @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':
It is required to change its value in my.ini as following
'192K' on 32bit windows
'256K' on 64bit windows.

Note that such configuration changes will only take effect after server restart.

", 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, * @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 * * @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(); } }