installcommand.class.php 11.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
<?php
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2015-2018 Teclib' and contributors.
 *
 * http://glpi-project.org
 *
 * based on GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * GLPI 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * GLPI 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 GLPI. If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
 */

namespace Glpi\Console\Database;

if (!defined('GLPI_ROOT')) {
   die("Sorry. You can't access this file directly");
}

39
use Glpi\Application\LocalConfigurationManager;
40
use Glpi\Console\Command\ForceNoPluginsOptionCommandInterface;
41 42 43 44 45 46 47 48
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
49 50
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Yaml\Yaml;
Johan Cwiklinski's avatar
Johan Cwiklinski committed
51 52 53 54
use Config;
use DBConnection;
use PDO;
use Toolbox;
55

56
class InstallCommand extends Command implements ForceNoPluginsOptionCommandInterface {
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

   /**
    * Error code returned if DB connection initialization fails.
    *
    * @var integer
    */
   const ERROR_DB_CONNECTION_FAILED = 1;

   /**
    * Error code returned if DB engine is unsupported.
    *
    * @var integer
    */
   const ERROR_DB_ENGINE_UNSUPPORTED = 2;

   /**
    * Error code returned when trying to install and having a DB config already set.
    *
    * @var integer
    */
   const ERROR_DB_CONFIG_ALREADY_SET = 3;

   /**
    * Error code returned when failing to create database.
    *
    * @var integer
    */
   const ERROR_DB_CREATION_FAILED = 4;

   /**
    * Error code returned when failing to save database configuration file.
    *
    * @var integer
    */
   const ERROR_DB_CONFIG_FILE_NOT_SAVED = 5;

   /**
    * Error code returned when failing to create database schema.
    *
    * @var integer
    */
   const ERROR_SCHEMA_CREATION_FAILED = 6;

Johan Cwiklinski's avatar
Johan Cwiklinski committed
100 101 102 103 104 105 106
   /**
    * Error code returned when failing to select database.
    *
    * @var integer
    */
   const ERROR_DB_SELECT_FAILED = 7;

107 108 109 110 111 112 113
   /**
    * Error code returned when failing to save local configuration file.
    *
    * @var integer
    */
   const ERROR_LOCAL_CONFIG_FILE_NOT_SAVED = 8;

114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
   protected function configure() {
      parent::configure();

      $this->setName('glpi:database:install');
      $this->setAliases(['db:install']);
      $this->setDescription('Install database schema');

      $this->addOption(
         'db-host',
         'H',
         InputOption::VALUE_OPTIONAL,
         __('Database host'),
         'localhost'
      );

      $this->addOption(
         'db-name',
         'd',
         InputOption::VALUE_REQUIRED,
         __('Database name')
      );

      $this->addOption(
         'db-password',
         'p',
         InputOption::VALUE_OPTIONAL,
         __('Database password (will be prompted for value if option passed without value)'),
         '' // Empty string by default (enable detection of null if passed without value)
      );

      $this->addOption(
         'db-port',
         'P',
         InputOption::VALUE_OPTIONAL,
         __('Database port')
      );

      $this->addOption(
         'db-user',
         'u',
         InputOption::VALUE_REQUIRED,
         __('Database user')
      );

      $this->addOption(
         'default-language',
         'L',
         InputOption::VALUE_REQUIRED,
         __('Default language of GLPI')
      );

      $this->addOption(
         'force',
         'f',
         InputOption::VALUE_NONE,
         __('Force execution of installation, overriding existing database and configuration')
      );
   }

   protected function interact(InputInterface $input, OutputInterface $output) {

175 176 177 178
      $questions = [
         'db-name'     => new Question(__('Database name:'), ''), // Required
         'db-user'     => new Question(__('Database user:'), ''), // Required
         'db-password' => new Question(__('Database password:'), ''), // Prompt if null (passed without value)
179
      ];
180 181 182
      $questions['db-password']->setHidden(true); // Make password input hidden

      foreach ($questions as $name => $question) {
183
         if (null === $input->getOption($name)) {
Cédric Anne's avatar
Cédric Anne committed
184
            /** @var \Symfony\Component\Console\Helper\QuestionHelper $question_helper */
185
            $question_helper = $this->getHelper('question');
186
            $value = $question_helper->ask($input, $output, $question);
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
            $input->setOption($name, $value);
         }
      }
   }

   protected function execute(InputInterface $input, OutputInterface $output) {

      $db_pass     = $input->getOption('db-password');
      $db_host     = $input->getOption('db-host');
      $db_name     = $input->getOption('db-name');
      $db_port     = $input->getOption('db-port');
      $db_user     = $input->getOption('db-user');
      $db_hostport = $db_host . (!empty($db_port) ? ':' . $db_port : '');

      $default_language = $input->getOption('default-language');

      $force          = $input->getOption('force');
      $no_interaction = $input->getOption('no-interaction'); // Base symfony/console option

Johan Cwiklinski's avatar
Johan Cwiklinski committed
206
      if (file_exists(GLPI_CONFIG_DIR . '/db.yaml') && !$force) {
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
         // Prevent overriding of existing DB
         $output->writeln(
            '<error>' . __('Database configuration already exists. Use --force option to override existing database and configuration.') . '</error>'
         );
         return self::ERROR_DB_CONFIG_ALREADY_SET;
      }

      if (empty($db_name)) {
         throw new InvalidArgumentException(
            __('Database name defined by --db-name option cannot be empty.')
         );
      }

      if (null === $db_pass) {
         // Will be null if option used without value and without interaction
         throw new InvalidArgumentException(
            __('--db-password option value cannot be null.')
         );
      }

      if (!$no_interaction) {
         // Ask for confirmation (unless --no-interaction)

         $informations = new Table($output);
         $informations->addRow([__('Database host'), $db_hostport]);
         $informations->addRow([__('Database name'), $db_name]);
         $informations->addRow([__('Database user'), $db_user]);
         $informations->render();

         /** @var QuestionHelper $question_helper */
         $question_helper = $this->getHelper('question');
         $run = $question_helper->ask(
            $input,
            $output,
            new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
         );
         if (!$run) {
            $output->writeln(
               '<comment>' . __('Installation aborted.') . '</comment>',
               OutputInterface::VERBOSITY_VERBOSE
            );
            return 0;
         }
      }

Johan Cwiklinski's avatar
Johan Cwiklinski committed
252 253 254 255 256 257 258 259
      $hostport = explode(":", $db_host);
      if (count($hostport) < 2 || intval($hostport[1]) > 0) {
         // "host" or "host:port"
         $dsn = "mysql:host=$db_host";
      } else {
         // ":socket"
         $dsn = "mysql:unix_socket={$hostport[1]}";
      }
260

Johan Cwiklinski's avatar
Johan Cwiklinski committed
261 262 263 264 265 266 267 268 269 270 271
      try {
         $dbh = new PDO(
            $dsn,
            $db_user,
            $db_pass
         );
         $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
         if (GLPI_FORCE_EMPTY_SQL_MODE) {
            $dbh->query("SET SESSION sql_mode = ''");
         }
      } catch (\PDOException $e) {
272
         $message = sprintf(
Johan Cwiklinski's avatar
Johan Cwiklinski committed
273 274 275
            __('Database connection failed with message "(%s)\n%s".'),
            $e->getMessage(),
            $e->getTraceAsString()
276 277 278
         );
         $output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
         return self::ERROR_DB_CONNECTION_FAILED;
Johan Cwiklinski's avatar
Johan Cwiklinski committed
279

280 281 282
      }

      ob_start();
Johan Cwiklinski's avatar
Johan Cwiklinski committed
283 284
      $db_version = $dbh->query('SELECT version()')->fetchColumn();
      $checkdb = Config::displayCheckDbEngine(false, $db_version);
285 286 287 288 289 290
      $message = ob_get_clean();
      if ($checkdb > 0) {
         $output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
         return self::ERROR_DB_ENGINE_UNSUPPORTED;
      }

Johan Cwiklinski's avatar
Johan Cwiklinski committed
291
      $db_name = str_replace('`', '``', $db_name); // Escape backquotes
292 293 294 295 296

      $output->writeln(
         '<comment>' . __('Creating the database...') . '</comment>',
         OutputInterface::VERBOSITY_VERBOSE
      );
Johan Cwiklinski's avatar
Johan Cwiklinski committed
297 298 299 300 301 302 303 304 305 306 307 308
      if (!$dbh->query('CREATE DATABASE IF NOT EXISTS `' . $db_name .'`')) {
         $error = $dbh->errorInfo();
         $message = sprintf(
            __("Database creation failed with message \"(%s)\n%s\"."),
            $error[0],
            $error[2]
         );
         $output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
         return self::ERROR_DB_CREATION_FAILED;
      }
      if (false === $dbh->exec('USE `' . $db_name .'`')) {
         $error = $dbh->errorInfo();
309
         $message = sprintf(
Johan Cwiklinski's avatar
Johan Cwiklinski committed
310 311 312
            __("Database selection failed with message \"(%s)\n%s\"."),
            $error[0],
            $error[2]
313 314 315 316 317 318 319 320 321
         );
         $output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
         return self::ERROR_DB_CREATION_FAILED;
      }

      $output->writeln(
         '<comment>' . __('Saving configuration file...') . '</comment>',
         OutputInterface::VERBOSITY_VERBOSE
      );
Johan Cwiklinski's avatar
Johan Cwiklinski committed
322
      if (!DBConnection::createMainConfig('mysql', $db_hostport, $db_user, $db_pass, $db_name)) {
323 324
         $message = sprintf(
            __('Cannot write configuration file "%s".'),
Johan Cwiklinski's avatar
Johan Cwiklinski committed
325
            GLPI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'db.yaml'
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
         );
         $output->writeln(
            '<error>' . $message . '</error>',
            OutputInterface::VERBOSITY_QUIET
         );
         return self::ERROR_DB_CONFIG_FILE_NOT_SAVED;
      }

      $output->writeln(
         '<comment>' . __('Loading default schema...') . '</comment>',
         OutputInterface::VERBOSITY_VERBOSE
      );
      // TODO Get rid of output buffering
      ob_start();
      Toolbox::createSchema($default_language);
      $message = ob_get_clean();
      if (!empty($message)) {
         $output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
         return self::ERROR_SCHEMA_CREATION_FAILED;
      }

347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
      try {
         $localConfigManager = new LocalConfigurationManager(
            GLPI_CONFIG_DIR,
            new PropertyAccessor(),
            new Yaml()
         );
         $localConfigManager->setParameterValue('[cache_uniq_id]', uniqid());
      } catch (\Exception $e) {
         $message = sprintf(
            __('Local configuration file saving failed with message "(%s)\n%s".'),
            $e->getMessage(),
            $e->getTraceAsString()
         );
         $output->writeln('<error>' . $message . '</error>', OutputInterface::VERBOSITY_QUIET);
         return self::ERROR_LOCAL_CONFIG_FILE_NOT_SAVED;
      }

364 365 366 367
      $output->writeln('<info>' . __('Installation done.') . '</info>');

      return 0; // Success
   }
368 369 370 371 372

   public function getNoPluginsOptionValue() {

      return true;
   }
373
}