InstallCommand.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. <?php
  2. namespace Drupal\Core\Command;
  3. use Drupal\Component\Utility\Crypt;
  4. use Drupal\Core\Database\ConnectionNotDefinedException;
  5. use Drupal\Core\Database\Database;
  6. use Drupal\Core\DrupalKernel;
  7. use Drupal\Core\Extension\ExtensionDiscovery;
  8. use Drupal\Core\Extension\InfoParserDynamic;
  9. use Drupal\Core\Site\Settings;
  10. use Symfony\Component\Console\Command\Command;
  11. use Symfony\Component\Console\Input\InputArgument;
  12. use Symfony\Component\Console\Input\InputInterface;
  13. use Symfony\Component\Console\Input\InputOption;
  14. use Symfony\Component\Console\Output\OutputInterface;
  15. use Symfony\Component\Console\Style\SymfonyStyle;
  16. /**
  17. * Installs a Drupal site for local testing/development.
  18. *
  19. * @internal
  20. * This command makes no guarantee of an API for Drupal extensions.
  21. */
  22. class InstallCommand extends Command {
  23. /**
  24. * The class loader.
  25. *
  26. * @var object
  27. */
  28. protected $classLoader;
  29. /**
  30. * Constructs a new InstallCommand command.
  31. *
  32. * @param object $class_loader
  33. * The class loader.
  34. */
  35. public function __construct($class_loader) {
  36. parent::__construct('install');
  37. $this->classLoader = $class_loader;
  38. }
  39. /**
  40. * {@inheritdoc}
  41. */
  42. protected function configure() {
  43. $this->setName('install')
  44. ->setDescription('Installs a Drupal demo site. This is not meant for production and might be too simple for custom development. It is a quick and easy way to get Drupal running.')
  45. ->addArgument('install-profile', InputArgument::OPTIONAL, 'Install profile to install the site in.')
  46. ->addOption('langcode', NULL, InputOption::VALUE_OPTIONAL, 'The language to install the site in.', 'en')
  47. ->addOption('site-name', NULL, InputOption::VALUE_OPTIONAL, 'Set the site name.', 'Drupal')
  48. ->addUsage('demo_umami --langcode fr')
  49. ->addUsage('standard --site-name QuickInstall');
  50. parent::configure();
  51. }
  52. /**
  53. * {@inheritdoc}
  54. */
  55. protected function execute(InputInterface $input, OutputInterface $output) {
  56. $io = new SymfonyStyle($input, $output);
  57. if (!extension_loaded('pdo_sqlite')) {
  58. $io->getErrorStyle()->error('You must have the pdo_sqlite PHP extension installed. See core/INSTALL.sqlite.txt for instructions.');
  59. return 1;
  60. }
  61. // Change the directory to the Drupal root.
  62. chdir(dirname(dirname(dirname(dirname(dirname(__DIR__))))));
  63. // Check whether there is already an installation.
  64. if ($this->isDrupalInstalled()) {
  65. // Do not fail if the site is already installed so this command can be
  66. // chained with ServerCommand.
  67. $output->writeln('<info>Drupal is already installed.</info> If you want to reinstall, remove sites/default/files and sites/default/settings.php.');
  68. return 0;
  69. }
  70. $install_profile = $input->getArgument('install-profile');
  71. if ($install_profile && !$this->validateProfile($install_profile, $io)) {
  72. return 1;
  73. }
  74. if (!$install_profile) {
  75. $install_profile = $this->selectProfile($io);
  76. }
  77. return $this->install($this->classLoader, $io, $install_profile, $input->getOption('langcode'), $this->getSitePath(), $input->getOption('site-name'));
  78. }
  79. /**
  80. * Returns whether there is already an existing Drupal installation.
  81. *
  82. * @return bool
  83. */
  84. protected function isDrupalInstalled() {
  85. try {
  86. $kernel = new DrupalKernel('prod', $this->classLoader, FALSE);
  87. $kernel::bootEnvironment();
  88. $kernel->setSitePath($this->getSitePath());
  89. Settings::initialize($kernel->getAppRoot(), $kernel->getSitePath(), $this->classLoader);
  90. $kernel->boot();
  91. }
  92. catch (ConnectionNotDefinedException $e) {
  93. return FALSE;
  94. }
  95. return !empty(Database::getConnectionInfo());
  96. }
  97. /**
  98. * Installs Drupal with specified installation profile.
  99. *
  100. * @param object $class_loader
  101. * The class loader.
  102. * @param \Symfony\Component\Console\Style\SymfonyStyle $io
  103. * The Symfony output decorator.
  104. * @param string $profile
  105. * The installation profile to use.
  106. * @param string $langcode
  107. * The language to install the site in.
  108. * @param string $site_path
  109. * The path to install the site to, like 'sites/default'.
  110. * @param string $site_name
  111. * The site name.
  112. *
  113. * @throws \Exception
  114. * Thrown when failing to create the $site_path directory or settings.php.
  115. *
  116. * @return int
  117. * The command exit status.
  118. */
  119. protected function install($class_loader, SymfonyStyle $io, $profile, $langcode, $site_path, $site_name) {
  120. $password = Crypt::randomBytesBase64(12);
  121. $parameters = [
  122. 'interactive' => FALSE,
  123. 'site_path' => $site_path,
  124. 'parameters' => [
  125. 'profile' => $profile,
  126. 'langcode' => $langcode,
  127. ],
  128. 'forms' => [
  129. 'install_settings_form' => [
  130. 'driver' => 'sqlite',
  131. 'sqlite' => [
  132. 'database' => $site_path . '/files/.sqlite',
  133. ],
  134. ],
  135. 'install_configure_form' => [
  136. 'site_name' => $site_name,
  137. 'site_mail' => 'drupal@localhost',
  138. 'account' => [
  139. 'name' => 'admin',
  140. 'mail' => 'admin@localhost',
  141. 'pass' => [
  142. 'pass1' => $password,
  143. 'pass2' => $password,
  144. ],
  145. ],
  146. 'enable_update_status_module' => TRUE,
  147. // form_type_checkboxes_value() requires NULL instead of FALSE values
  148. // for programmatic form submissions to disable a checkbox.
  149. 'enable_update_status_emails' => NULL,
  150. ],
  151. ],
  152. ];
  153. // Create the directory and settings.php if not there so that the installer
  154. // works.
  155. if (!is_dir($site_path)) {
  156. if ($io->isVerbose()) {
  157. $io->writeln("Creating directory: $site_path");
  158. }
  159. if (!mkdir($site_path, 0775)) {
  160. throw new \RuntimeException("Failed to create directory $site_path");
  161. }
  162. }
  163. if (!file_exists("{$site_path}/settings.php")) {
  164. if ($io->isVerbose()) {
  165. $io->writeln("Creating file: {$site_path}/settings.php");
  166. }
  167. if (!copy('sites/default/default.settings.php', "{$site_path}/settings.php")) {
  168. throw new \RuntimeException("Copying sites/default/default.settings.php to {$site_path}/settings.php failed.");
  169. }
  170. }
  171. require_once 'core/includes/install.core.inc';
  172. $progress_bar = $io->createProgressBar();
  173. install_drupal($class_loader, $parameters, function ($install_state) use ($progress_bar) {
  174. static $started = FALSE;
  175. if (!$started) {
  176. $started = TRUE;
  177. // We've already done 1.
  178. $progress_bar->setFormat("%current%/%max% [%bar%]\n%message%\n");
  179. $progress_bar->setMessage(t('Installing @drupal', ['@drupal' => drupal_install_profile_distribution_name()]));
  180. $tasks = install_tasks($install_state);
  181. $progress_bar->start(count($tasks) + 1);
  182. }
  183. $tasks_to_perform = install_tasks_to_perform($install_state);
  184. $task = current($tasks_to_perform);
  185. if (isset($task['display_name'])) {
  186. $progress_bar->setMessage($task['display_name']);
  187. }
  188. $progress_bar->advance();
  189. });
  190. $success_message = t('Congratulations, you installed @drupal!', [
  191. '@drupal' => drupal_install_profile_distribution_name(),
  192. '@name' => 'admin',
  193. '@pass' => $password,
  194. ], ['langcode' => $langcode]);
  195. $progress_bar->setMessage('<info>' . $success_message . '</info>');
  196. $progress_bar->display();
  197. $progress_bar->finish();
  198. $io->writeln('<info>Username:</info> admin');
  199. $io->writeln("<info>Password:</info> $password");
  200. return 0;
  201. }
  202. /**
  203. * Gets the site path.
  204. *
  205. * Defaults to 'sites/default'. For testing purposes this can be overridden
  206. * using the DRUPAL_DEV_SITE_PATH environment variable.
  207. *
  208. * @return string
  209. * The site path to use.
  210. */
  211. protected function getSitePath() {
  212. return getenv('DRUPAL_DEV_SITE_PATH') ?: 'sites/default';
  213. }
  214. /**
  215. * Selects the install profile to use.
  216. *
  217. * @param \Symfony\Component\Console\Style\SymfonyStyle $io
  218. * Symfony style output decorator.
  219. *
  220. * @return string
  221. * The selected install profile.
  222. *
  223. * @see _install_select_profile()
  224. * @see \Drupal\Core\Installer\Form\SelectProfileForm
  225. */
  226. protected function selectProfile(SymfonyStyle $io) {
  227. $profiles = $this->getProfiles();
  228. // If there is a distribution there will be only one profile.
  229. if (count($profiles) == 1) {
  230. return key($profiles);
  231. }
  232. // Display alphabetically by human-readable name, but always put the core
  233. // profiles first (if they are present in the filesystem).
  234. natcasesort($profiles);
  235. if (isset($profiles['minimal'])) {
  236. // If the expert ("Minimal") core profile is present, put it in front of
  237. // any non-core profiles rather than including it with them
  238. // alphabetically, since the other profiles might be intended to group
  239. // together in a particular way.
  240. $profiles = ['minimal' => $profiles['minimal']] + $profiles;
  241. }
  242. if (isset($profiles['standard'])) {
  243. // If the default ("Standard") core profile is present, put it at the very
  244. // top of the list. This profile will have its radio button pre-selected,
  245. // so we want it to always appear at the top.
  246. $profiles = ['standard' => $profiles['standard']] + $profiles;
  247. }
  248. reset($profiles);
  249. return $io->choice('Select an installation profile', $profiles, current($profiles));
  250. }
  251. /**
  252. * Validates a user provided install profile.
  253. *
  254. * @param string $install_profile
  255. * Install profile to validate.
  256. * @param \Symfony\Component\Console\Style\SymfonyStyle $io
  257. * Symfony style output decorator.
  258. *
  259. * @return bool
  260. * TRUE if the profile is valid, FALSE if not.
  261. */
  262. protected function validateProfile($install_profile, SymfonyStyle $io) {
  263. // Allow people to install hidden and non-distribution profiles if they
  264. // supply the argument.
  265. $profiles = $this->getProfiles(TRUE, FALSE);
  266. if (!isset($profiles[$install_profile])) {
  267. $error_msg = sprintf("'%s' is not a valid install profile.", $install_profile);
  268. $alternatives = [];
  269. foreach (array_keys($profiles) as $profile_name) {
  270. $lev = levenshtein($install_profile, $profile_name);
  271. if ($lev <= strlen($profile_name) / 4 || FALSE !== strpos($profile_name, $install_profile)) {
  272. $alternatives[] = $profile_name;
  273. }
  274. }
  275. if (!empty($alternatives)) {
  276. $error_msg .= sprintf(" Did you mean '%s'?", implode("' or '", $alternatives));
  277. }
  278. $io->getErrorStyle()->error($error_msg);
  279. return FALSE;
  280. }
  281. return TRUE;
  282. }
  283. /**
  284. * Gets a list of profiles.
  285. *
  286. * @param bool $include_hidden
  287. * (optional) Whether to include hidden profiles. Defaults to FALSE.
  288. * @param bool $auto_select_distributions
  289. * (optional) Whether to only return the first distribution found.
  290. *
  291. * @return string[]
  292. * An array of profile descriptions keyed by the profile machine name.
  293. */
  294. protected function getProfiles($include_hidden = FALSE, $auto_select_distributions = TRUE) {
  295. // Build a list of all available profiles.
  296. $listing = new ExtensionDiscovery(getcwd(), FALSE);
  297. $listing->setProfileDirectories([]);
  298. $profiles = [];
  299. $info_parser = new InfoParserDynamic(getcwd());
  300. foreach ($listing->scan('profile') as $profile) {
  301. $details = $info_parser->parse($profile->getPathname());
  302. // Don't show hidden profiles.
  303. if (!$include_hidden && !empty($details['hidden'])) {
  304. continue;
  305. }
  306. // Determine the name of the profile; default to the internal name if none
  307. // is specified.
  308. $name = isset($details['name']) ? $details['name'] : $profile->getName();
  309. $description = isset($details['description']) ? $details['description'] : $name;
  310. $profiles[$profile->getName()] = $description;
  311. if ($auto_select_distributions && !empty($details['distribution'])) {
  312. return [$profile->getName() => $description];
  313. }
  314. }
  315. return $profiles;
  316. }
  317. }