Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 237
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
tao_install_Installator
0.00% covered (danger)
0.00%
0 / 237
0.00% covered (danger)
0.00%
0 / 17
3306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 install
0.00% covered (danger)
0.00%
0 / 167
0.00% covered (danger)
0.00%
0 / 1
702
 getServiceManager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 retryInstallation
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 isWindows
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 generateSessionName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkInstallData
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 escapeCheck
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getEscapedChecks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setEscapedChecks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEscapedCheck
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 log
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 getLog
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGenerisConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getConfigPath
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setInstallationFinished
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 recreateDependencyInjectionContainerCache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2002-2008 (original work) Public Research Centre Henri Tudor & University of Luxembourg
19 *                         (under the project TAO & TAO2);
20 *               2008-2010 (update and modification) Deutsche Institut für Internationale Pädagogische Forschung
21 *                         (under the project TAO-TRANSFER);
22 *               2009-2012 (update and modification) Public Research Centre Henri Tudor
23 *                         (under the project TAO-SUSTAIN & TAO-DEV);
24 *               2013-2017 (update and modification) Open Assessment Technologies SA (under the project TAO-PRODUCT);
25 */
26
27use oat\generis\persistence\DriverConfigurationFeeder;
28use oat\oatbox\cache\SetupFileCache;
29use oat\tao\helpers\InstallHelper;
30use oat\oatbox\install\Installer;
31use oat\oatbox\service\ServiceManager;
32use oat\tao\model\OperatedByService;
33use oat\generis\persistence\sql\DbCreator;
34use oat\generis\persistence\sql\SetupDb;
35use oat\generis\persistence\PersistenceManager;
36use oat\generis\model\data\Ontology;
37use oat\tao\model\TaoOntology;
38use oat\generis\model\GenerisRdf;
39use oat\tao\model\user\TaoRoles;
40use oat\tao\model\service\ApplicationService;
41use oat\oatbox\service\ServiceNotFoundException;
42
43/**
44 *
45 *
46 * Installation main class
47 *
48 * @access public
49 * @author Jérôme Bogaerts, <jerome@taotesting.com>
50 * @package tao
51 */
52
53class tao_install_Installator
54{
55    // Adding container and logger.
56    use \oat\oatbox\log\ContainerLoggerTrait;
57
58    /**
59     * Installator related dependencies will be reached under this offset.
60     */
61    public const CONTAINER_INDEX = 'taoInstallInstallator';
62
63    protected $options = [];
64
65    private $log = [];
66
67    private $escapedChecks = [];
68
69    private $oatBoxInstall = null;
70
71    public function __construct($options)
72    {
73        // Using the container if it's necessary with automatic dependency returning.
74        $options = $this->initContainer($options, static::CONTAINER_INDEX);
75
76        if (!isset($options['root_path'])) {
77            throw new tao_install_utils_Exception("root_path option must be defined to perform installation.");
78        }
79        if (!isset($options['install_path'])) {
80            throw new tao_install_utils_Exception("install_path option must be defined to perform installation.");
81        }
82
83        $this->options = $options;
84
85        $this->options['root_path'] = rtrim($this->options['root_path'], '/\\') . DIRECTORY_SEPARATOR;
86        $this->options['install_path'] = rtrim($this->options['install_path'], '/\\') . DIRECTORY_SEPARATOR;
87
88        $this->oatBoxInstall = new Installer();
89    }
90
91    /**
92     * Run the TAO install from the given data
93     * @throws tao_install_utils_Exception
94     * @param $installData array data coming from the install form
95     * @param $callback callable|null post install callback
96     */
97    public function install(array $installData, callable $callback = null)
98    {
99        try {
100            /**
101             * It's a quick hack for solving reinstall issue.
102             * Should be a better option.
103             */
104            @unlink($this->options['root_path'] . 'config/generis.conf.php');
105
106            /*
107             * 0 - Check input parameters.
108             */
109            $this->log('i', "Checking install data");
110            self::checkInstallData($installData);
111
112            $this->log('i', "Starting TAO install");
113
114            // Sanitize $installData if needed.
115            if (!preg_match("/\/$/", $installData['module_url'])) {
116                $installData['module_url'] .= '/';
117            }
118
119            // Define the ROOT_URL constant if not defined (can be used in manifest files)
120            if (!defined('ROOT_URL')) {
121                define('ROOT_URL', $installData['module_url']);
122            }
123
124            if (isset($installData['extensions'])) {
125                $extensionIDs = is_array($installData['extensions'])
126                 ? $installData['extensions']
127                 : explode(',', $installData['extensions']);
128            } else {
129                $extensionIDs = ['taoCe'];
130            }
131
132            $this->log('d', 'Extensions to be installed: ' . var_export($extensionIDs, true));
133
134            $installData['file_path'] = rtrim($installData['file_path'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
135
136            /*
137             *  1 - Check configuration with checks described in the manifest.
138             */
139            $configChecker = tao_install_utils_ChecksHelper::getConfigChecker($extensionIDs);
140
141            // Silence checks to have to be escaped.
142            foreach ($configChecker->getComponents() as $c) {
143                if (method_exists($c, 'getName') && in_array($c->getName(), $this->getEscapedChecks())) {
144                    $configChecker->silent($c);
145                }
146            }
147
148            $reports = $configChecker->check();
149            foreach ($reports as $r) {
150                $msg = $r->getMessage();
151                $component = $r->getComponent();
152                $this->log('i', $msg);
153
154                if ($r->getStatus() !== common_configuration_Report::VALID && !$component->isOptional()) {
155                    throw new tao_install_utils_Exception($msg);
156                }
157            }
158
159            /*
160             *  X - Setup Oatbox
161             */
162
163            $this->log('d', 'Removing old config');
164            $consistentOptions = array_merge($installData, $this->options);
165            $consistentOptions['config_path'] = $this->getConfigPath();
166            $this->oatBoxInstall->setOptions($consistentOptions);
167            $this->oatBoxInstall->install();
168            $this->log('d', 'Oatbox was installed!');
169
170            ServiceManager::setServiceManager($this->getServiceManager());
171
172            /*
173             *  2 - Setup RDS persistence
174             */
175            if (!$this->getServiceManager()->has(DriverConfigurationFeeder::SERVICE_ID)) {
176                $this->getServiceManager()->register(
177                    DriverConfigurationFeeder::SERVICE_ID,
178                    new DriverConfigurationFeeder(
179                        [
180                            DriverConfigurationFeeder::OPTION_DRIVER_OPTIONS => []
181                        ]
182                    )
183                );
184            }
185
186            if ($this->getServiceManager()->has(PersistenceManager::SERVICE_ID)) {
187                $persistenceManager = $this->getServiceManager()->get(PersistenceManager::SERVICE_ID);
188            } else {
189                $this->log('i', "Spawning new PersistenceManager");
190                $persistenceManager = new PersistenceManager();
191            }
192            if (!$persistenceManager->hasPersistence('default')) {
193                $this->log('i', "Register default Persistence");
194                $dbalConfigCreator = new tao_install_utils_DbalConfigCreator();
195                $persistenceManager->registerPersistence('default', $dbalConfigCreator->createDbalConfig($installData));
196                $this->getServiceManager()->register(PersistenceManager::SERVICE_ID, $persistenceManager);
197            }
198
199            $dbCreator = new SetupDb();
200            $dbCreator->setLogger($this->logger);
201            $dbCreator->setupDatabase($persistenceManager->getPersistenceById('default'));
202
203            /*
204             *  4 - Create the generis config files
205             */
206
207            $this->log('d', 'Writing generis config');
208            $generisConfigWriter = new tao_install_utils_ConfigWriter(
209                $this->options['root_path'] . 'generis/config/sample/generis.conf.php',
210                $this->getGenerisConfig()
211            );
212
213            $session_name = $installData['session_name'] ?? self::generateSessionName();
214            $generisConfigWriter->createConfig();
215            $constants = [
216                'LOCAL_NAMESPACE'           => $installData['module_namespace'],
217                'GENERIS_INSTANCE_NAME'     => $installData['instance_name'],
218                'GENERIS_SESSION_NAME'      => $session_name,
219                'ROOT_PATH'                 => $this->options['root_path'],
220                'FILES_PATH'                => $installData['file_path'],
221                'ROOT_URL'                  => $installData['module_url'],
222                'DEFAULT_LANG'              => $installData['module_lang'],
223                'DEBUG_MODE'                => ($installData['module_mode'] == 'debug') ? true : false,
224                'TIME_ZONE'                 => $installData['timezone']
225            ];
226
227            $constants['DEFAULT_ANONYMOUS_INTERFACE_LANG'] = $installData['anonymous_lang']
228                ?? $installData['module_lang'];
229
230            $generisConfigWriter->writeConstants($constants);
231            $this->log(
232                'd',
233                'The following constants were written in generis config:' . PHP_EOL . var_export($constants, true)
234            );
235
236            /*
237             * 4b - Prepare the file/cache folder (FILES_PATH) not yet defined)
238             * @todo solve this more elegantly
239             */
240            $file_path = $installData['file_path'];
241            if (is_dir($file_path)) {
242                $this->log('i', 'Data from previous install found and will be removed');
243                if (!helpers_File::emptyDirectory($file_path, true)) {
244                    throw new common_exception_Error('Unable to empty ' . $file_path . ' folder.');
245                }
246            } else {
247                if (mkdir($file_path, 0700, true)) {
248                    $this->log('d', $file_path . ' directory was created!');
249                } else {
250                    throw new Exception($file_path . ' directory creation was failed!');
251                }
252            }
253
254            $setupFileCache = $this->getServiceManager()->get(SetupFileCache::class);
255            $cachePath = $file_path . 'generis' . DIRECTORY_SEPARATOR . 'cache';
256            $setupFileCache->createDirectory($cachePath);
257            $this->log('d', $cachePath . ' directory was created!');
258
259            foreach ((array)$installData['extra_persistences'] as $k => $persistence) {
260                $persistenceManager->registerPersistence($k, $persistence);
261            }
262
263            /*
264             * 5 - Run the extensions bootstrap
265             */
266            $this->log('d', 'Running the extensions bootstrap');
267            common_Config::load($this->getGenerisConfig());
268
269            /*
270             * 5b - Create cache persistence
271            */
272            $this->log('d', 'Creating cache persistence..');
273            $setupFileCache->createPersistence();
274
275            /*
276             * 6 - Finish Generis Install
277             */
278
279            $this->log('d', 'Finishing generis install..');
280            $generis = common_ext_ExtensionsManager::singleton()->getExtensionById('generis');
281
282            $generisInstaller = new common_ext_GenerisInstaller($generis, true);
283            $generisInstaller->initContainer($this->getContainer());
284            $generisInstaller->install();
285
286            /*
287             * 7 - Add languages
288             */
289            $this->log('d', 'Adding languages..');
290            $ontology = $this->getServiceManager()->get(Ontology::SERVICE_ID);
291            $langModel = \tao_models_classes_LanguageService::singleton()->getLanguageDefinition();
292            $rdfModel = $ontology->getRdfInterface();
293            foreach ($langModel as $triple) {
294                $rdfModel->add($triple);
295            }
296
297            /*
298             * 8 - Install the extensions
299             */
300            InstallHelper::initContainer($this->container);
301            $installed = InstallHelper::installRecursively($extensionIDs, $installData);
302            $this->log('ext', $installed);
303
304            /*
305             *  8b - Generates client side translation bundles (depends on extension install)
306             */
307            $this->log('i', 'Generates client side translation bundles');
308
309            tao_models_classes_LanguageService::singleton()->generateAll();
310
311            /*
312             *  9 - Insert Super User
313             */
314            $this->log('i', 'Spawning SuperUser ' . $installData['user_login']);
315
316            $userClass = $ontology->getClass(TaoOntology::CLASS_URI_TAO_USER);
317            $userid = $installData['module_namespace'] . TaoOntology::DEFAULT_USER_URI_SUFFIX;
318            $userpwd = core_kernel_users_Service::getPasswordHash()->encrypt($installData['user_pass1']);
319            $userLang = 'http://www.tao.lu/Ontologies/TAO.rdf#Lang' . $installData['module_lang'];
320
321            $superUser = $userClass->createInstance(
322                'Super User',
323                'super user created during the TAO installation',
324                $userid
325            );
326            $superUser->setPropertiesValues([
327                GenerisRdf::PROPERTY_USER_ROLES => [
328                    TaoRoles::GLOBAL_MANAGER,
329                    TaoRoles::SYSTEM_ADMINISTRATOR
330                ],
331                TaoOntology::PROPERTY_USER_FIRST_TIME => GenerisRdf::GENERIS_TRUE,
332                GenerisRdf::PROPERTY_USER_LOGIN => $installData['user_login'],
333                GenerisRdf::PROPERTY_USER_PASSWORD => $userpwd,
334                GenerisRdf::PROPERTY_USER_LASTNAME => $installData['user_lastname'],
335                GenerisRdf::PROPERTY_USER_FIRSTNAME => $installData['user_firstname'],
336                GenerisRdf::PROPERTY_USER_MAIL => $installData['user_email'],
337                GenerisRdf::PROPERTY_USER_DEFLG => $userLang,
338                GenerisRdf::PROPERTY_USER_UILG => $userLang,
339                GenerisRdf::PROPERTY_USER_TIMEZONE => TIME_ZONE
340            ]);
341
342            /*
343             *  10 - Secure the install for production mode
344             */
345            if ($installData['module_mode'] == 'production') {
346                $extensions = common_ext_ExtensionsManager::singleton()->getInstalledExtensions();
347                $this->log('i', 'Securing tao for production');
348
349                // 11.0 Protect TAO dist
350                $shield = new tao_install_utils_Shield(array_keys($extensions));
351                $shield->disableRewritePattern(["!/test/", "!/doc/"]);
352                $shield->denyAccessTo([
353                    'views/sass',
354                    'views/js/test',
355                    'views/build'
356                ]);
357                $shield->protectInstall();
358            }
359
360            /*
361             *  11 - Create the version file
362             */
363            $this->log('d', 'Creating TAO version file');
364            file_put_contents($installData['file_path'] . 'version', TAO_VERSION);
365
366            /*
367             * 12 - Register Information about organization operating the system
368             */
369            $this->log('t', 'Registering information about the organization operating the system');
370            $operatedByService = $this->getServiceManager()->get(OperatedByService::SERVICE_ID);
371
372            if (!empty($installData['operated_by_name'])) {
373                $operatedByService->setName($installData['operated_by_name']);
374            }
375
376            if (!empty($installData['operated_by_email'])) {
377                $operatedByService->setEmail($installData['operated_by_email']);
378            }
379
380            $this->getServiceManager()->register(OperatedByService::SERVICE_ID, $operatedByService);
381            if ($callback) {
382                $callback();
383            }
384
385            $this->recreateDependencyInjectionContainerCache();
386            $this->setInstallationFinished();
387        } catch (Exception $e) {
388            if ($this->retryInstallation($e)) {
389                return;
390            }
391
392            // In any case, we transmit a single exception type (at the moment)
393            // for a clearer API for client code.
394            $this->log('e', 'Error Occurs : ' . $e->getMessage() . PHP_EOL . $e->getTraceAsString());
395            throw new tao_install_utils_Exception($e->getMessage(), 0, $e);
396        }
397    }
398
399    public function getServiceManager()
400    {
401        return $this->oatBoxInstall->setupServiceManager($this->getConfigPath());
402    }
403
404    private function retryInstallation($exception)
405    {
406        $returnValue = false;
407        $err = $exception->getMessage();
408
409        if (strpos($err, 'cannot construct the resource because the uri cannot be empty') === 0 && $this->isWindows()) {
410            /*
411             * a known issue
412             * @see http://forge.taotesting.com/issues/3014
413             * this issue can only be fixed by an administrator
414             * changing the thread_stack system variable in my.ini as following:
415             * '256K' on 64bit windows
416             * '192K' on 32bit windows
417             */
418
419            $this->log('e', 'Error Occurs : ' . $err . PHP_EOL . $exception->getTraceAsString());
420            throw new tao_install_utils_Exception(
421                "Error in mysql system variable 'thread_stack':<br>It is required to change its value in "
422                    . "my.ini as following<br>'192K' on 32bit windows<br>'256K' on 64bit windows.<br><br>Note that "
423                    . "such configuration changes will only take effect after server restart.<br><br>",
424                0,
425                $exception
426            );
427        }
428
429        if (!$returnValue) {
430            return false;
431        }
432
433        // it is a known issue, go ahead to retry with the issue fixer
434        $this->install($this->config);
435        return true;
436    }
437
438    private function isWindows()
439    {
440        return strtoupper(substr(PHP_OS, 0, 3)) == 'WIN';
441    }
442
443    /**
444     * Generate an alphanum token to be used as a PHP session name.
445     *
446     * @access public
447     * @author Jerome Bogaerts, <jerome.bogaerts@tudor.lu>
448     * @return string
449     */
450    public static function generateSessionName()
451    {
452        return 'tao_' . helpers_Random::generateString(8);
453    }
454
455    /**
456     * Check the install data information such as
457     * - instance name
458     * - database driver
459     * - ...
460     *
461     * If a parameter of the $installData is not valid regarding the install
462     * business rules, an MalformedInstall
463     *
464     * @param array $installData
465     */
466    public static function checkInstallData(array $installData)
467    {
468        // instance name
469        if (empty($installData['instance_name'])) {
470            $msg = "Missing install parameter 'instance_name'.";
471            throw new tao_install_utils_MalformedParameterException($msg);
472        } elseif (!is_string($installData['instance_name'])) {
473            $msg = "Malformed install parameter 'instance_name'. It must be a string.";
474            throw new tao_install_utils_MalformedParameterException($msg);
475        } elseif (1 === preg_match('/\s/u', $installData['instance_name'])) {
476            $msg = "Malformed install parameter 'instance_name'. It cannot contain spacing characters "
477                . "(tab, backspace).";
478            throw new tao_install_utils_MalformedParameterException($msg);
479        }
480    }
481
482    /**
483     * Tell the Installator instance to not take into account
484     * a Configuration Check with ID = $id.
485     *
486     * @param string $id The identifier of the check to escape.
487     */
488    public function escapeCheck($id)
489    {
490        $checks = $this->getEscapedChecks();
491        array_push($checks, $id);
492        $checks = array_unique($checks);
493        $this->setEscapedChecks($checks);
494    }
495
496    /**
497     * Obtain an array of Configuration Check IDs to be escaped by
498     * the Installator.
499     *
500     * @return array
501     */
502    public function getEscapedChecks()
503    {
504        return $this->escapedChecks;
505    }
506
507    /**
508     * Set the array of Configuration Check IDs to be escaped by
509     * the Installator.
510     *
511     * @param array $escapedChecks An array of strings.
512     * @return void
513     */
514    public function setEscapedChecks(array $escapedChecks)
515    {
516        $this->escapedChecks = $escapedChecks;
517    }
518
519    /**
520     * Informs you if a given Configuration Check ID corresponds
521     * to a Check that has to be escaped.
522     */
523    public function isEscapedCheck($id)
524    {
525        return in_array($id, $this->getEscapedChecks());
526    }
527
528    /**
529     * Log message and add it to $this->log array;
530     * @see common_Logger class
531     * @param string $logLevel
532     * <ul>
533     *   <li>'w' - warning</li>
534     *   <li>'t' - trace</li>
535     *   <li>'d' - debug</li>
536     *   <li>'i' - info</li>
537     *   <li>'e' - error</li>
538     *   <li>'f' - fatal</li>
539     *   <li>'ext' - installed extensions</li>
540     * </ul>
541     * @param string $message
542     * @param array $tags
543     */
544    public function log($logLevel, $message, $tags = [])
545    {
546        if (!is_array($tags)) {
547            $tags = [$tags];
548        }
549        if ($this->getLogger() instanceof \Psr\Log\LoggerInterface) {
550            if ($logLevel === 'ext') {
551                $this->logNotice('Installed extensions: ' . implode(', ', $message));
552            } else {
553                $this->getLogger()->log(
554                    common_log_Logger2Psr::getPsrLevelFromCommon($logLevel),
555                    $message
556                );
557            }
558        }
559        if (method_exists('common_Logger', $logLevel)) {
560            call_user_func('common_Logger::' . $logLevel, $message, $tags);
561        }
562        if (is_array($message)) {
563            $this->log[$logLevel] = (isset($this->log[$logLevel]))
564                ? array_merge($this->log[$logLevel], $message)
565                : $message;
566        } else {
567            $this->log[$logLevel][] = $message;
568        }
569    }
570
571    /**
572     * Get array of log messages
573     * @return array
574     */
575    public function getLog()
576    {
577        return $this->log;
578    }
579
580    /**
581     * Get the config file platform e.q. generis.conf.php
582     *
583     * @return string
584     */
585    protected function getGenerisConfig()
586    {
587        return $this->getConfigPath() . 'generis.conf.php';
588    }
589
590    /**
591     * Get the config path for installation
592     * If options have installation_config_path, it's taken otherwise it's root_path
593     *
594     * @return string
595     */
596    protected function getConfigPath()
597    {
598        if (isset($this->options['installation_config_path'])) {
599            return $this->options['installation_config_path'];
600        } else {
601            return $this->options['root_path'] . 'config' . DIRECTORY_SEPARATOR;
602        }
603    }
604
605    /**
606     * Mark application as ready to be used (all extensions installed and post scripts executed)
607     * @throws common_Exception
608     */
609    private function setInstallationFinished()
610    {
611        $applicationService = $this->getServiceManager()->get(ApplicationService::SERVICE_ID);
612        $applicationService->setOption(ApplicationService::OPTION_INSTALLATION_FINISHED, true);
613        $this->getServiceManager()->register(ApplicationService::SERVICE_ID, $applicationService);
614    }
615
616    private function recreateDependencyInjectionContainerCache(): void
617    {
618        ServiceManager::getServiceManager()->rebuildContainer();
619    }
620}