Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
56.24% |
257 / 457 |
|
21.21% |
7 / 33 |
CRAP | |
0.00% |
0 / 1 |
InstallCommand | |
56.24% |
257 / 457 |
|
21.21% |
7 / 33 |
1807.40 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
configure | |
100.00% |
54 / 54 |
|
100.00% |
1 / 1 |
1 | |||
writeFileToDisk | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getWindowsApacheMessage | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getLinuxApacheMessage | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
90 | |||
getApacheLocation | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
validateBasePath | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
validateSolrPort | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getBasePath | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
4.32 | |||
getSolrPort | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
4.32 | |||
initializeOverrideDir | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
getOverrideDir | |
69.23% |
9 / 13 |
|
0.00% |
0 / 1 |
4.47 | |||
validateModules | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
validateModule | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
4.59 | |||
getModule | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
3.01 | |||
getMultisiteMode | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
20 | |||
validateHost | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getHost | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getInput | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
backUpFile | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
8.74 | |||
buildApacheConfig | |
41.03% |
16 / 39 |
|
0.00% |
0 / 1 |
25.61 | |||
getEnvironmentVariables | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
buildUnixEnvironment | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
buildWindowsConfig | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
buildImportConfig | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
3.01 | |||
buildDirs | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
buildModules | |
28.57% |
2 / 7 |
|
0.00% |
0 / 1 |
14.11 | |||
buildModule | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
42 | |||
failWithError | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
displaySuccessMessage | |
55.17% |
16 / 29 |
|
0.00% |
0 / 1 |
5.44 | |||
collectParameters | |
69.49% |
41 / 59 |
|
0.00% |
0 / 1 |
55.56 | |||
processParameters | |
47.06% |
8 / 17 |
|
0.00% |
0 / 1 |
17.50 | |||
execute | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | /** |
4 | * Console command: VuFind installer. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2020. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Console |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development Wiki |
28 | */ |
29 | |
30 | namespace VuFindConsole\Command\Install; |
31 | |
32 | use Symfony\Component\Console\Attribute\AsCommand; |
33 | use Symfony\Component\Console\Command\Command; |
34 | use Symfony\Component\Console\Input\InputInterface; |
35 | use Symfony\Component\Console\Input\InputOption; |
36 | use Symfony\Component\Console\Output\OutputInterface; |
37 | use Symfony\Component\Console\Question\Question; |
38 | |
39 | use function in_array; |
40 | use function intval; |
41 | |
42 | /** |
43 | * Console command: VuFind installer. |
44 | * |
45 | * @category VuFind |
46 | * @package Console |
47 | * @author Demian Katz <demian.katz@villanova.edu> |
48 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
49 | * @link https://vufind.org/wiki/development Wiki |
50 | */ |
51 | #[AsCommand( |
52 | name: 'install/install', |
53 | description: 'VuFind installer' |
54 | )] |
55 | class InstallCommand extends Command |
56 | { |
57 | public const MULTISITE_NONE = 0; |
58 | public const MULTISITE_DIR_BASED = 1; |
59 | public const MULTISITE_HOST_BASED = 2; |
60 | |
61 | /** |
62 | * Base directory of VuFind installation. |
63 | * |
64 | * @var string |
65 | */ |
66 | protected $baseDir; |
67 | |
68 | /** |
69 | * Local settings directory for VuFind installation. |
70 | * |
71 | * @var string |
72 | */ |
73 | protected $overrideDir; |
74 | |
75 | /** |
76 | * Hostname of VuFind installation (used for host-based multi-site). |
77 | * |
78 | * @var string |
79 | */ |
80 | protected $host = ''; |
81 | |
82 | /** |
83 | * Custom local code module name (if any). |
84 | * |
85 | * @var string |
86 | */ |
87 | protected $module = ''; |
88 | |
89 | /** |
90 | * Active multi-site mode. |
91 | * |
92 | * @var int |
93 | */ |
94 | protected $multisiteMode = self::MULTISITE_NONE; |
95 | |
96 | /** |
97 | * Base path for VuFind URLs. |
98 | * |
99 | * @var string |
100 | */ |
101 | protected $basePath = '/vufind'; |
102 | |
103 | /** |
104 | * Solr port to use. |
105 | * |
106 | * @var string |
107 | */ |
108 | protected $solrPort = '8983'; |
109 | |
110 | /** |
111 | * Should we make backups of existing files? |
112 | * |
113 | * @var bool |
114 | */ |
115 | protected $makeBackups = true; |
116 | |
117 | /** |
118 | * Constructor |
119 | * |
120 | * @param string|null $name The name of the command; passing null means it must |
121 | * be set in configure() |
122 | */ |
123 | public function __construct($name = null) |
124 | { |
125 | $this->baseDir = str_replace( |
126 | '\\', |
127 | '/', |
128 | realpath(__DIR__ . '/../../../../../../') |
129 | ); |
130 | $this->overrideDir = $this->baseDir . '/local'; |
131 | parent::__construct($name); |
132 | } |
133 | |
134 | /** |
135 | * Configure the command. |
136 | * |
137 | * @return void |
138 | */ |
139 | protected function configure() |
140 | { |
141 | $this |
142 | ->setHelp('Set up (or modify) initial VuFind installation.') |
143 | ->addOption( |
144 | 'use-defaults', |
145 | null, |
146 | InputOption::VALUE_NONE, |
147 | 'Use VuFind defaults to configure ' |
148 | . '(ignores any other arguments passed)' |
149 | )->addOption( |
150 | 'overridedir', |
151 | null, |
152 | InputOption::VALUE_REQUIRED, |
153 | 'Where would you like to store your local settings?' |
154 | . " (defaults to {$this->overrideDir} when --non-interactive is set)" |
155 | )->addOption( |
156 | 'module-name', |
157 | null, |
158 | InputOption::VALUE_REQUIRED, |
159 | 'What module name would you like to use? Specify "disabled" to skip' |
160 | )->addOption( |
161 | 'basepath', |
162 | null, |
163 | InputOption::VALUE_REQUIRED, |
164 | 'What base path should be used in VuFind\'s URL?' |
165 | . " (defaults to {$this->baseDir} when --non-interactive is set)" |
166 | )->addOption( |
167 | 'multisite', |
168 | null, |
169 | InputOption::VALUE_OPTIONAL, |
170 | 'Specify we are going to setup a multisite. ' |
171 | . 'Options: directory and host', |
172 | false |
173 | )->addOption( |
174 | 'hostname', |
175 | null, |
176 | InputOption::VALUE_REQUIRED, |
177 | 'Specify the hostname for the VuFind Site, when multisite=host' |
178 | )->addOption( |
179 | 'solr-port', |
180 | null, |
181 | InputOption::VALUE_OPTIONAL, |
182 | 'Port number to use for Solr' |
183 | . " (defaults to {$this->solrPort} when --non-interactive is set)" |
184 | )->addOption( |
185 | 'non-interactive', |
186 | null, |
187 | InputOption::VALUE_NONE, |
188 | 'Use settings if provided via arguments, otherwise use defaults' |
189 | )->addOption( |
190 | 'skip-backups', |
191 | null, |
192 | InputOption::VALUE_NONE, |
193 | 'Overwrite existing files without creating backups' |
194 | ); |
195 | } |
196 | |
197 | /** |
198 | * Write file contents to disk. |
199 | * |
200 | * @param string $filename Filename |
201 | * @param string $content Content |
202 | * |
203 | * @return bool |
204 | */ |
205 | protected function writeFileToDisk($filename, $content) |
206 | { |
207 | return @file_put_contents($filename, $content); |
208 | } |
209 | |
210 | /** |
211 | * Get instructions for editing the Apache configuration under Windows. |
212 | * |
213 | * @return string |
214 | */ |
215 | protected function getWindowsApacheMessage() |
216 | { |
217 | return "Go to Start -> Apache HTTP Server -> Edit the Apache httpd.conf\n" |
218 | . "and add this line to your httpd.conf file: \n" |
219 | . " Include {$this->overrideDir}/httpd-vufind.conf\n\n" |
220 | . "If you are using a bundle like XAMPP and do not have this start\n" |
221 | . "menu option, you should find and edit your httpd.conf file manually\n" |
222 | . "(usually in a location like c:\\xampp\\apache\\conf).\n"; |
223 | } |
224 | |
225 | /** |
226 | * Get instructions for editing the Apache configuration under Linux. |
227 | * |
228 | * @return string |
229 | */ |
230 | protected function getLinuxApacheMessage() |
231 | { |
232 | if (is_dir('/etc/httpd/conf.d')) { // Mandriva / RedHat |
233 | $confD = '/etc/httpd/conf.d'; |
234 | $httpdConf = '/etc/httpd/conf/httpd.conf'; |
235 | } elseif (is_dir('/etc/apache2/2.2/conf.d')) { // Solaris |
236 | $confD = '/etc/apache2/2.2/conf.d'; |
237 | $httpdConf = '/etc/apache2/2.2/httpd.conf'; |
238 | } elseif (is_dir('/etc/apache2/conf-enabled')) { // new Ubuntu / OpenSUSE |
239 | $confD = '/etc/apache2/conf-enabled'; |
240 | $httpdConf = '/etc/apache2/apache2.conf'; |
241 | } elseif (is_dir('/etc/apache2/conf.d')) { // old Ubuntu / OpenSUSE |
242 | $confD = '/etc/apache2/conf.d'; |
243 | $httpdConf = '/etc/apache2/httpd.conf'; |
244 | } elseif (is_dir('/opt/local/apache2/conf/extra')) { // Mac with Mac Ports |
245 | $confD = '/opt/local/apache2/conf/extra'; |
246 | $httpdConf = '/opt/local/apache2/conf/httpd.conf'; |
247 | } else { |
248 | $confD = '/path/to/apache/conf.d'; |
249 | $httpdConf = false; |
250 | } |
251 | |
252 | // Check if httpd.conf really exists before recommending a specific path; |
253 | // if missing, just use the generic name: |
254 | $httpdConf = ($httpdConf && file_exists($httpdConf)) |
255 | ? $httpdConf : 'httpd.conf'; |
256 | |
257 | // Suggest a symlink name based on the local directory, so if running in |
258 | // multisite mode, we don't use the same symlink for multiple instances: |
259 | $symlink = basename($this->overrideDir); |
260 | $symlink = ($symlink == 'local') ? 'vufind' : ('vufind-' . $symlink); |
261 | $symlink .= '.conf'; |
262 | |
263 | return "You can do it in either of two ways:\n\n" |
264 | . " a) Add this line to your {$httpdConf} file:\n" |
265 | . " Include {$this->overrideDir}/httpd-vufind.conf\n\n" |
266 | . " b) Link the configuration to Apache's config directory like this:" |
267 | . "\n ln -s {$this->overrideDir}/httpd-vufind.conf " |
268 | . "{$confD}/{$symlink}\n" |
269 | . "\nOption b is preferable if your platform supports it,\n" |
270 | . "but option a is more certain to be supported.\n"; |
271 | } |
272 | |
273 | /** |
274 | * Display system-specific information for where configuration files are found |
275 | * and/or symbolic links should be created. |
276 | * |
277 | * @param OutputInterface $output Output object |
278 | * |
279 | * @return void |
280 | */ |
281 | protected function getApacheLocation(OutputInterface $output) |
282 | { |
283 | // There is one special case for Windows, and a variety of different |
284 | // Unix-flavored possibilities that all work similarly. |
285 | $msg = PHP_OS_FAMILY === 'Windows' |
286 | ? $this->getWindowsApacheMessage() : $this->getLinuxApacheMessage(); |
287 | $output->writeln($msg); |
288 | } |
289 | |
290 | /** |
291 | * Validate a base path. Returns true on success, message on failure. |
292 | * |
293 | * @param string $basePath String to validate. |
294 | * @param bool $allowEmpty Are empty values acceptable? |
295 | * |
296 | * @return bool|string |
297 | */ |
298 | protected function validateBasePath($basePath, $allowEmpty = false) |
299 | { |
300 | if ($allowEmpty && empty($basePath)) { |
301 | return true; |
302 | } |
303 | return preg_match('/^\/[\w_-]*$/', $basePath) |
304 | ? true |
305 | : 'Error: Base path must start with a slash and contain only' |
306 | . ' alphanumeric characters, dash or underscore.'; |
307 | } |
308 | |
309 | /** |
310 | * Validate a Solr port number. Returns true on success, message on failure. |
311 | * |
312 | * @param string $solrPort Port to validate. |
313 | * |
314 | * @return bool|string |
315 | */ |
316 | protected function validateSolrPort($solrPort) |
317 | { |
318 | if (is_numeric($solrPort)) { |
319 | return true; |
320 | } |
321 | return 'Solr port must be a number.'; |
322 | } |
323 | |
324 | /** |
325 | * Get a base path from the user (or return a default). |
326 | * |
327 | * @param InputInterface $input Input object |
328 | * @param OutputInterface $output Output object |
329 | * |
330 | * @return string |
331 | */ |
332 | protected function getBasePath(InputInterface $input, OutputInterface $output) |
333 | { |
334 | // Get VuFind base path: |
335 | while (true) { |
336 | $basePathInput = $this->getInput( |
337 | $input, |
338 | $output, |
339 | "What base path should be used in VuFind's URL? [{$this->basePath}] " |
340 | ); |
341 | if (empty($basePathInput)) { |
342 | return $this->basePath; |
343 | } elseif (($result = $this->validateBasePath($basePathInput)) === true) { |
344 | return $basePathInput; |
345 | } |
346 | $output->writeln($result); |
347 | } |
348 | } |
349 | |
350 | /** |
351 | * Get a Solr port number from the user (or return a default). |
352 | * |
353 | * @param InputInterface $input Input object |
354 | * @param OutputInterface $output Output object |
355 | * |
356 | * @return string |
357 | */ |
358 | protected function getSolrPort(InputInterface $input, OutputInterface $output) |
359 | { |
360 | // Get VuFind base path: |
361 | while (true) { |
362 | $solrInput = $this->getInput( |
363 | $input, |
364 | $output, |
365 | "What port number should Solr use? [{$this->solrPort}] " |
366 | ); |
367 | if (empty($solrInput)) { |
368 | return $this->solrPort; |
369 | } elseif (($result = $this->validateSolrPort($solrInput)) === true) { |
370 | return $solrInput; |
371 | } |
372 | $output->writeln($result); |
373 | } |
374 | } |
375 | |
376 | /** |
377 | * Initialize the override directory and report success or failure. |
378 | * |
379 | * @param string $dir Path to attempt to initialize |
380 | * |
381 | * @return bool|string |
382 | */ |
383 | protected function initializeOverrideDir($dir) |
384 | { |
385 | return $this->buildDirs( |
386 | [ |
387 | $dir, |
388 | $dir . '/cache', |
389 | $dir . '/config', |
390 | $dir . '/harvest', |
391 | $dir . '/import', |
392 | ] |
393 | ); |
394 | } |
395 | |
396 | /** |
397 | * Get an override directory from the user (or return a default). |
398 | * |
399 | * @param InputInterface $input Input object |
400 | * @param OutputInterface $output Output object |
401 | * |
402 | * @return string |
403 | */ |
404 | protected function getOverrideDir(InputInterface $input, OutputInterface $output) |
405 | { |
406 | // Get override directory path: |
407 | while (true) { |
408 | $overrideDirInput = $this->getInput( |
409 | $input, |
410 | $output, |
411 | 'Where would you like to store your local settings? ' |
412 | . "[{$this->overrideDir}] " |
413 | ); |
414 | if (empty($overrideDirInput)) { |
415 | return $this->overrideDir; |
416 | } elseif (!$this->initializeOverrideDir($overrideDirInput)) { |
417 | $output->writeln( |
418 | "Error: Cannot initialize settings in '$overrideDirInput'.\n" |
419 | ); |
420 | } |
421 | return str_replace('\\', '/', realpath($overrideDirInput)); |
422 | } |
423 | } |
424 | |
425 | /** |
426 | * Validate a comma-separated list of module names. Returns true on success, |
427 | * message on failure. |
428 | * |
429 | * @param string $modules Module names to validate. |
430 | * |
431 | * @return bool|string |
432 | */ |
433 | protected function validateModules($modules) |
434 | { |
435 | foreach (explode(',', $modules) as $module) { |
436 | $result = $this->validateModule(trim($module)); |
437 | if ($result !== true) { |
438 | return $result; |
439 | } |
440 | } |
441 | return true; |
442 | } |
443 | |
444 | /** |
445 | * Validate the custom module name. Returns true on success, message on failure. |
446 | * |
447 | * @param string $module Module name to validate. |
448 | * |
449 | * @return bool|string |
450 | */ |
451 | protected function validateModule($module) |
452 | { |
453 | $regex = '/^[a-zA-Z][0-9a-zA-Z_]*$/'; |
454 | $illegalModules = [ |
455 | 'VuFind', 'VuFindAdmin', 'VuFindConsole', 'VuFindDevTools', |
456 | 'VuFindLocalTemplate', 'VuFindSearch', 'VuFindTest', 'VuFindTheme', |
457 | ]; |
458 | if (in_array($module, $illegalModules)) { |
459 | return "{$module} is a reserved module name; please try another."; |
460 | } elseif (empty($module) || preg_match($regex, $module)) { |
461 | return true; |
462 | } |
463 | return "Illegal name: {$module}; please use alphanumeric text."; |
464 | } |
465 | |
466 | /** |
467 | * Get the custom module name from the user (or blank for none). |
468 | * |
469 | * @param InputInterface $input Input object |
470 | * @param OutputInterface $output Output object |
471 | * |
472 | * @return string |
473 | */ |
474 | protected function getModule(InputInterface $input, OutputInterface $output) |
475 | { |
476 | // Get custom module name: |
477 | $output->writeln( |
478 | "\nVuFind supports use of a custom module for storing local code " |
479 | . "changes.\nIf you do not plan to customize the code, you can " |
480 | . "skip this step.\nIf you decide to use a custom module, the name " |
481 | . "you choose will be used for\nthe module's directory name and its " |
482 | . 'PHP namespace.' |
483 | ); |
484 | while (true) { |
485 | $moduleInput = trim( |
486 | $this->getInput( |
487 | $input, |
488 | $output, |
489 | "\nWhat module name would you like to use? [blank for none] " |
490 | ) |
491 | ); |
492 | if (($result = $this->validateModules($moduleInput)) === true) { |
493 | return $moduleInput; |
494 | } |
495 | $output->writeln("\n$result"); |
496 | } |
497 | } |
498 | |
499 | /** |
500 | * Get the user's preferred multisite mode. |
501 | * |
502 | * @param InputInterface $input Input object |
503 | * @param OutputInterface $output Output object |
504 | * |
505 | * @return int |
506 | */ |
507 | protected function getMultisiteMode( |
508 | InputInterface $input, |
509 | OutputInterface $output |
510 | ) { |
511 | $output->writeln( |
512 | "\nWhen running multiple VuFind sites against a single installation, you" |
513 | . " need\nto decide how to distinguish between instances. Choose an " |
514 | . "option:\n\n" . self::MULTISITE_DIR_BASED . '.) Directory-based ' |
515 | . "(i.e. http://server/vufind1 vs. http://server/vufind2)\n" |
516 | . self::MULTISITE_HOST_BASED |
517 | . '.) Host-based (i.e. http://vufind1.server vs. http://vufind2.server)' |
518 | . "\n\nor enter " . self::MULTISITE_NONE . ' to disable multisite mode.' |
519 | ); |
520 | $legal = [ |
521 | self::MULTISITE_NONE, |
522 | self::MULTISITE_DIR_BASED, |
523 | self::MULTISITE_HOST_BASED, |
524 | ]; |
525 | while (true) { |
526 | $response = $this->getInput( |
527 | $input, |
528 | $output, |
529 | "\nWhich option do you want? " |
530 | ); |
531 | if (is_numeric($response) && in_array(intval($response), $legal)) { |
532 | return intval($response); |
533 | } |
534 | $output->writeln('Invalid selection.'); |
535 | } |
536 | } |
537 | |
538 | /** |
539 | * Validate the user's hostname input. Returns true on success, message on |
540 | * failure. |
541 | * |
542 | * @param string $host String to check |
543 | * |
544 | * @return bool|string |
545 | */ |
546 | protected function validateHost($host) |
547 | { |
548 | // From http://stackoverflow.com/questions/106179/ |
549 | // regular-expression-to-match-hostname-or-ip-address |
550 | $valid = "/^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*" |
551 | . "([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/"; |
552 | return preg_match($valid, $host) |
553 | ? true |
554 | : 'Invalid hostname.'; |
555 | } |
556 | |
557 | /** |
558 | * Get the user's hostname preference. |
559 | * |
560 | * @param InputInterface $input Input object |
561 | * @param OutputInterface $output Output object |
562 | * |
563 | * @return string |
564 | */ |
565 | protected function getHost(InputInterface $input, OutputInterface $output) |
566 | { |
567 | while (true) { |
568 | $response = $this->getInput( |
569 | $input, |
570 | $output, |
571 | "\nPlease enter the hostname for your site: " |
572 | ); |
573 | if (($result = $this->validateHost($response)) === true) { |
574 | return $response; |
575 | } |
576 | $output->writeln($result); |
577 | } |
578 | } |
579 | |
580 | /** |
581 | * Fetch a single line of input from the user. |
582 | * |
583 | * @param InputInterface $input Input object |
584 | * @param OutputInterface $output Output object |
585 | * @param string $prompt Prompt to display to the user. |
586 | * |
587 | * @return string User-entered response. |
588 | */ |
589 | protected function getInput( |
590 | InputInterface $input, |
591 | OutputInterface $output, |
592 | string $prompt |
593 | ): string { |
594 | $question = new Question($prompt, ''); |
595 | return $this->getHelper('question')->ask($input, $output, $question); |
596 | } |
597 | |
598 | /** |
599 | * Back up an existing file and inform the user. Return true on success, |
600 | * error message otherwise. |
601 | * |
602 | * @param OutputInterface $output Output object |
603 | * @param string $filename File to back up (if it exists) |
604 | * @param string $desc Description of file (for output message) |
605 | * |
606 | * @return bool|string |
607 | */ |
608 | protected function backUpFile(OutputInterface $output, string $filename, string $desc) |
609 | { |
610 | if ($this->makeBackups && file_exists($filename)) { |
611 | $bak = $filename . '.bak.' . time(); |
612 | if (!copy($filename, $bak)) { |
613 | return "Problem backing up $filename to $bak"; |
614 | } |
615 | $output->writeln("Backed up existing $desc to $bak."); |
616 | } |
617 | return true; |
618 | } |
619 | |
620 | /** |
621 | * Generate the Apache configuration. Returns true on success, error message |
622 | * otherwise. |
623 | * |
624 | * @param OutputInterface $output Output object |
625 | * |
626 | * @return bool|string |
627 | */ |
628 | protected function buildApacheConfig(OutputInterface $output) |
629 | { |
630 | $baseConfig = $this->baseDir . '/config/vufind/httpd-vufind.conf'; |
631 | $config = @file_get_contents($baseConfig); |
632 | if (empty($config)) { |
633 | return "Problem reading {$baseConfig}."; |
634 | } |
635 | $config = str_replace('/usr/local/vufind/local', '%override-dir%', $config); |
636 | $config = str_replace('/usr/local/vufind', '%base-dir%', $config); |
637 | $config = preg_replace('|([^/])\/vufind|', '$1%base-path%', $config); |
638 | $config = str_replace('%override-dir%', $this->overrideDir, $config); |
639 | $config = str_replace('%base-dir%', $this->baseDir, $config); |
640 | $config = str_replace('%base-path%', $this->basePath, $config); |
641 | // Special cases for root basePath: |
642 | if ('/' == $this->basePath) { |
643 | $config = str_replace('//', '/', $config); |
644 | $config = str_replace('Alias /', '#Alias /', $config); |
645 | } |
646 | if (!empty($this->module)) { |
647 | $config = str_replace( |
648 | '#SetEnv VUFIND_LOCAL_MODULES VuFindLocalTemplate', |
649 | "SetEnv VUFIND_LOCAL_MODULES {$this->module}", |
650 | $config |
651 | ); |
652 | } |
653 | |
654 | // In multisite mode, we need to make environment variables conditional: |
655 | switch ($this->multisiteMode) { |
656 | case self::MULTISITE_DIR_BASED: |
657 | $config = preg_replace( |
658 | '/SetEnv\s+(\w+)\s+(.*)/', |
659 | 'SetEnvIf Request_URI "^' . $this->basePath . '" $1=$2', |
660 | $config |
661 | ); |
662 | break; |
663 | case self::MULTISITE_HOST_BASED: |
664 | if (($result = $this->validateHost($this->host)) !== true) { |
665 | return $result; |
666 | } |
667 | $config = preg_replace( |
668 | '/SetEnv\s+(\w+)\s+(.*)/', |
669 | 'SetEnvIfNoCase Host ' . str_replace('.', '\.', $this->host) |
670 | . ' $1=$2', |
671 | $config |
672 | ); |
673 | break; |
674 | } |
675 | |
676 | $target = $this->overrideDir . '/httpd-vufind.conf'; |
677 | if (($msg = $this->backUpFile($output, $target, 'Apache configuration')) !== true) { |
678 | return $msg; |
679 | } |
680 | return $this->writeFileToDisk($target, $config) |
681 | ? true : "Problem writing {$this->overrideDir}/httpd-vufind.conf."; |
682 | } |
683 | |
684 | /** |
685 | * Get an array of environment variables. |
686 | * |
687 | * @return array |
688 | */ |
689 | protected function getEnvironmentVariables(): array |
690 | { |
691 | $vars = [ |
692 | 'VUFIND_HOME' => $this->baseDir, |
693 | 'VUFIND_LOCAL_DIR' => $this->overrideDir, |
694 | 'VUFIND_LOCAL_MODULES' => $this->module, |
695 | 'SOLR_PORT' => $this->solrPort, |
696 | ]; |
697 | if (empty($vars['VUFIND_LOCAL_MODULES'])) { |
698 | unset($vars['VUFIND_LOCAL_MODULES']); |
699 | } |
700 | return $vars; |
701 | } |
702 | |
703 | /** |
704 | * Build the Unix-specific environment configuration. Returns true on success, |
705 | * error message otherwise. |
706 | * |
707 | * @param OutputInterface $output Output object |
708 | * |
709 | * @return bool|string |
710 | */ |
711 | protected function buildUnixEnvironment($output) |
712 | { |
713 | $filename = $this->baseDir . '/env.sh'; |
714 | if (($msg = $this->backUpFile($output, $filename, 'Unix environment file')) !== true) { |
715 | return $msg; |
716 | } |
717 | $env = ''; |
718 | foreach ($this->getEnvironmentVariables() as $key => $val) { |
719 | $env .= "export $key=$val\n"; |
720 | } |
721 | return $this->writeFileToDisk($filename, $env) |
722 | ? true : "Problem writing {$filename}."; |
723 | } |
724 | |
725 | /** |
726 | * Build the Windows-specific startup configuration. Returns true on success, |
727 | * error message otherwise. |
728 | * |
729 | * @param OutputInterface $output Output object |
730 | * |
731 | * @return bool|string |
732 | */ |
733 | protected function buildWindowsConfig($output) |
734 | { |
735 | $filename = $this->baseDir . '/env.bat'; |
736 | if (($msg = $this->backUpFile($output, $filename, 'Windows environment file')) !== true) { |
737 | return $msg; |
738 | } |
739 | $batch = ''; |
740 | foreach ($this->getEnvironmentVariables() as $key => $val) { |
741 | $batch .= "@set $key=$val\n"; |
742 | } |
743 | return $this->writeFileToDisk($filename, $batch) |
744 | ? true : "Problem writing {$filename}."; |
745 | } |
746 | |
747 | /** |
748 | * Configure a SolrMarc properties file. Returns true on success, error message |
749 | * otherwise. |
750 | * |
751 | * @param OutputInterface $output Output object |
752 | * @param string $filename The properties file to configure |
753 | * |
754 | * @return bool|string |
755 | */ |
756 | protected function buildImportConfig(OutputInterface $output, $filename) |
757 | { |
758 | $target = $this->overrideDir . '/import/' . $filename; |
759 | if (($msg = $this->backUpFile($output, $target, 'import configuration')) !== true) { |
760 | return $msg; |
761 | } |
762 | $import = @file_get_contents($this->baseDir . '/import/' . $filename); |
763 | $import = str_replace( |
764 | ['/usr/local/vufind', ':8983'], |
765 | [$this->baseDir, ':' . $this->solrPort], |
766 | $import |
767 | ); |
768 | $import = preg_replace( |
769 | "/^\s*solrmarc.path\s*=.*$/m", |
770 | "solrmarc.path = {$this->overrideDir}/import|{$this->baseDir}/import", |
771 | $import |
772 | ); |
773 | if (!$this->writeFileToDisk($target, $import)) { |
774 | return "Problem writing {$this->overrideDir}/import/{$filename}."; |
775 | } |
776 | return true; |
777 | } |
778 | |
779 | /** |
780 | * Build a set of directories. |
781 | * |
782 | * @param array $dirs Directories to build |
783 | * |
784 | * @return bool|string True on success, name of problem directory on failure |
785 | */ |
786 | protected function buildDirs($dirs) |
787 | { |
788 | foreach ($dirs as $dir) { |
789 | if (!is_dir($dir) && !@mkdir($dir)) { |
790 | return $dir; |
791 | } |
792 | } |
793 | return true; |
794 | } |
795 | |
796 | /** |
797 | * Make sure all modules exist (and create them if they do not). Returns true |
798 | * on success, error message otherwise. |
799 | * |
800 | * @return bool|string |
801 | */ |
802 | protected function buildModules() |
803 | { |
804 | if (!empty($this->module)) { |
805 | foreach (explode(',', $this->module) as $module) { |
806 | $moduleDir = $this->baseDir . '/module/' . $module; |
807 | // Is module missing? If so, create it from the template: |
808 | if (!file_exists($moduleDir . '/Module.php')) { |
809 | if (($result = $this->buildModule($module)) !== true) { |
810 | return $result; |
811 | } |
812 | } |
813 | } |
814 | } |
815 | return true; |
816 | } |
817 | |
818 | /** |
819 | * Build the module for storing local code changes. Returns true on success, |
820 | * error message otherwise. |
821 | * |
822 | * @param string $module The name of the new module (assumed valid!) |
823 | * |
824 | * @return bool|string |
825 | */ |
826 | protected function buildModule($module) |
827 | { |
828 | // Create directories: |
829 | $moduleDir = $this->baseDir . '/module/' . $module; |
830 | $dirStatus = $this->buildDirs( |
831 | [ |
832 | $moduleDir, |
833 | $moduleDir . '/config', |
834 | $moduleDir . '/src', |
835 | $moduleDir . '/src/' . $module, |
836 | ] |
837 | ); |
838 | if ($dirStatus !== true) { |
839 | return "Problem creating {$dirStatus}."; |
840 | } |
841 | |
842 | // Copy configuration: |
843 | $configFile = $this->baseDir |
844 | . '/module/VuFindLocalTemplate/config/module.config.php'; |
845 | $config = @file_get_contents($configFile); |
846 | if (!$config) { |
847 | return "Problem reading {$configFile}."; |
848 | } |
849 | $success = $this->writeFileToDisk( |
850 | $moduleDir . '/config/module.config.php', |
851 | str_replace('VuFindLocalTemplate', $module, $config) |
852 | ); |
853 | if (!$success) { |
854 | return "Problem writing {$moduleDir}/config/module.config.php."; |
855 | } |
856 | |
857 | // Copy PHP code: |
858 | $moduleFile = $this->baseDir . '/module/VuFindLocalTemplate/Module.php'; |
859 | $contents = @file_get_contents($moduleFile); |
860 | if (!$contents) { |
861 | return "Problem reading {$moduleFile}."; |
862 | } |
863 | $success = $this->writeFileToDisk( |
864 | $moduleDir . '/Module.php', |
865 | str_replace('VuFindLocalTemplate', $module, $contents) |
866 | ); |
867 | return $success ? true : "Problem writing {$moduleDir}/Module.php."; |
868 | } |
869 | |
870 | /** |
871 | * Display an error message and return a failure status. |
872 | * |
873 | * @param OutputInterface $output Output object |
874 | * @param string $msg Error message |
875 | * @param int $status Error status |
876 | * |
877 | * @return int |
878 | */ |
879 | protected function failWithError( |
880 | OutputInterface $output, |
881 | string $msg, |
882 | int $status = 1 |
883 | ): int { |
884 | $output->writeln($msg); |
885 | return $status; |
886 | } |
887 | |
888 | /** |
889 | * Display the final message after successful installation. |
890 | * |
891 | * @param OutputInterface $output Output object |
892 | * |
893 | * @return void |
894 | */ |
895 | protected function displaySuccessMessage(OutputInterface $output) |
896 | { |
897 | $output->writeln( |
898 | "Apache configuration written to {$this->overrideDir}/httpd-vufind.conf." |
899 | . "\n\nYou now need to load this configuration into Apache." |
900 | ); |
901 | $this->getApacheLocation($output); |
902 | if (!empty($this->host)) { |
903 | $output->writeln( |
904 | 'Since you are using a host-based multisite configuration, you will ' |
905 | . "also \nneed to do some virtual host configuration. See\n" |
906 | . " http://httpd.apache.org/docs/2.4/vhosts/\n" |
907 | ); |
908 | } |
909 | if ('/' == $this->basePath) { |
910 | $output->writeln( |
911 | 'Since you are installing VuFind at the root of your domain, you ' |
912 | . "will also\nneed to edit your Apache configuration to change " |
913 | . "DocumentRoot to:\n" . $this->baseDir . "/public\n" |
914 | ); |
915 | } |
916 | $output->writeln( |
917 | 'Once the configuration is linked, restart Apache. You should now be ' |
918 | . "able\nto access VuFind at http://localhost{$this->basePath}\n\nFor " |
919 | . "proper use of command line tools, you should also ensure that your\n" |
920 | ); |
921 | $finalMsg = empty($this->addOptionmodule) |
922 | ? "VUFIND_HOME and VUFIND_LOCAL_DIR environment variables are set to\n" |
923 | . "{$this->baseDir} and {$this->overrideDir} respectively." |
924 | : "VUFIND_HOME, VUFIND_LOCAL_MODULES and VUFIND_LOCAL_DIR environment\n" |
925 | . "variables are set to {$this->baseDir}, {$this->module} and " |
926 | . "{$this->overrideDir} respectively."; |
927 | $output->writeln($finalMsg); |
928 | } |
929 | |
930 | /** |
931 | * Collect input parameters, and return a status (0 = proceed, 1 = fail). |
932 | * |
933 | * @param InputInterface $input Input object |
934 | * @param OutputInterface $output Output object |
935 | * |
936 | * @return int 0 for success |
937 | */ |
938 | protected function collectParameters( |
939 | InputInterface $input, |
940 | OutputInterface $output |
941 | ) { |
942 | // Are we allowing user interaction? |
943 | $interactive = !$input->getOption('non-interactive'); |
944 | $userInputNeeded = []; |
945 | |
946 | // Load user settings if we are not forcing defaults: |
947 | if (!$input->getOption('use-defaults')) { |
948 | $overrideDir = trim($input->getOption('overridedir') ?? ''); |
949 | if (!empty($overrideDir)) { |
950 | $this->overrideDir = $overrideDir; |
951 | } elseif ($interactive) { |
952 | $userInputNeeded['overrideDir'] = true; |
953 | } |
954 | $moduleName = trim($input->getOption('module-name') ?? ''); |
955 | if (!empty($moduleName) && $moduleName !== 'disabled') { |
956 | if (($result = $this->validateModules($moduleName)) !== true) { |
957 | return $this->failWithError($output, $result); |
958 | } |
959 | $this->module = $moduleName; |
960 | } elseif ($interactive) { |
961 | $userInputNeeded['module'] = true; |
962 | } |
963 | |
964 | $basePath = trim($input->getOption('basepath') ?? ''); |
965 | if (!empty($basePath)) { |
966 | if (($result = $this->validateBasePath($basePath, true)) !== true) { |
967 | return $this->failWithError($output, $result); |
968 | } |
969 | $this->basePath = $basePath; |
970 | } elseif ($interactive) { |
971 | $userInputNeeded['basePath'] = true; |
972 | } |
973 | |
974 | $solrPort = trim($input->getOption('solr-port') ?? ''); |
975 | if (!empty($solrPort)) { |
976 | if (($result = $this->validateSolrPort($solrPort)) !== true) { |
977 | return $this->failWithError($output, $result); |
978 | } |
979 | $this->solrPort = $solrPort; |
980 | } elseif ($interactive) { |
981 | $userInputNeeded['solr-port'] = true; |
982 | } |
983 | |
984 | // We assume "single site" mode unless the --multisite option is set; |
985 | // note that $mode will be null if the user provided the option with |
986 | // no value specified, and false if the user did not provide the option. |
987 | $mode = $input->getOption('multisite'); |
988 | if ($mode === 'directory') { |
989 | $this->multisiteMode = self::MULTISITE_DIR_BASED; |
990 | } elseif ($mode === 'host') { |
991 | $this->multisiteMode = self::MULTISITE_HOST_BASED; |
992 | } elseif ($mode !== true && $mode !== null && $mode !== false) { |
993 | return $this->failWithError( |
994 | $output, |
995 | 'Unexpected multisite mode: ' . $mode |
996 | ); |
997 | } elseif ($interactive && $mode !== false) { |
998 | $userInputNeeded['multisiteMode'] = true; |
999 | } |
1000 | |
1001 | // Now that we've validated as many parameters as possible, retrieve |
1002 | // user input where needed. |
1003 | if (isset($userInputNeeded['overrideDir'])) { |
1004 | $this->overrideDir = $this->getOverrideDir($input, $output); |
1005 | } |
1006 | if (isset($userInputNeeded['module'])) { |
1007 | $this->module = $this->getModule($input, $output); |
1008 | } |
1009 | if (isset($userInputNeeded['basePath'])) { |
1010 | $this->basePath = $this->getBasePath($input, $output); |
1011 | } |
1012 | if (isset($userInputNeeded['solr-port'])) { |
1013 | $this->solrPort = $this->getSolrPort($input, $output); |
1014 | } |
1015 | if (isset($userInputNeeded['multisiteMode'])) { |
1016 | $this->multisiteMode = $this->getMultisiteMode($input, $output); |
1017 | } |
1018 | |
1019 | // Load supplemental multisite parameters: |
1020 | if ($this->multisiteMode == self::MULTISITE_HOST_BASED) { |
1021 | $hostOption = trim($input->getOption('hostname')); |
1022 | $this->host = (!empty($hostOption) || !$interactive) |
1023 | ? $hostOption : $this->getHost($input, $output); |
1024 | } |
1025 | } |
1026 | |
1027 | // Normalize the module setting to remove whitespace: |
1028 | $this->module = preg_replace('/\s/', '', $this->module); |
1029 | |
1030 | // Should we make backups of existing files? |
1031 | if ($input->getOption('skip-backups')) { |
1032 | $this->makeBackups = false; |
1033 | } |
1034 | return 0; |
1035 | } |
1036 | |
1037 | /** |
1038 | * Process collected parameters, and return a status (0 = proceed, 1 = fail). |
1039 | * |
1040 | * @param OutputInterface $output Output object |
1041 | * |
1042 | * @return int 0 for success |
1043 | */ |
1044 | protected function processParameters(OutputInterface $output) |
1045 | { |
1046 | // Make sure the override directory is initialized (using defaults or CLI |
1047 | // parameters will not have initialized it yet; attempt to reinitialize it |
1048 | // here is harmless if it was already initialized in interactive mode): |
1049 | if (!$this->initializeOverrideDir($this->overrideDir)) { |
1050 | return $this->failWithError( |
1051 | $output, |
1052 | "Cannot initialize local override directory: {$this->overrideDir}" |
1053 | ); |
1054 | } |
1055 | |
1056 | // Build the Windows start file in case we need it: |
1057 | if (($result = $this->buildWindowsConfig($output)) !== true) { |
1058 | return $this->failWithError($output, $result); |
1059 | } |
1060 | |
1061 | // Build a Unix environment file in case we need it: |
1062 | if (($result = $this->buildUnixEnvironment($output)) !== true) { |
1063 | return $this->failWithError($output, $result); |
1064 | } |
1065 | |
1066 | // Build the import configuration: |
1067 | foreach (['import.properties', 'import_auth.properties'] as $file) { |
1068 | if (($result = $this->buildImportConfig($output, $file)) !== true) { |
1069 | return $this->failWithError($output, $result); |
1070 | } |
1071 | } |
1072 | |
1073 | // Build the custom module(s), if necessary: |
1074 | if (($result = $this->buildModules()) !== true) { |
1075 | return $this->failWithError($output, $result); |
1076 | } |
1077 | |
1078 | // Build the final configuration: |
1079 | if (($result = $this->buildApacheConfig($output)) !== true) { |
1080 | return $this->failWithError($output, $result); |
1081 | } |
1082 | return 0; |
1083 | } |
1084 | |
1085 | /** |
1086 | * Run the command. |
1087 | * |
1088 | * @param InputInterface $input Input object |
1089 | * @param OutputInterface $output Output object |
1090 | * |
1091 | * @return int 0 for success |
1092 | */ |
1093 | protected function execute(InputInterface $input, OutputInterface $output) |
1094 | { |
1095 | $output->writeln("VuFind has been found in {$this->baseDir}."); |
1096 | |
1097 | // Collect and process parameters, and stop if an error is encountered |
1098 | // along the way.... |
1099 | if ( |
1100 | $this->collectParameters($input, $output) !== 0 |
1101 | || $this->processParameters($output) !== 0 |
1102 | ) { |
1103 | return 1; |
1104 | } |
1105 | |
1106 | // Report success: |
1107 | $this->displaySuccessMessage($output); |
1108 | return 0; |
1109 | } |
1110 | } |