Security update for Core, with self-updated composer
[yaffs-website] / vendor / psy / psysh / src / Psy / Configuration.php
1 <?php
2
3 /*
4  * This file is part of Psy Shell.
5  *
6  * (c) 2012-2017 Justin Hileman
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Psy;
13
14 use Psy\Exception\DeprecatedException;
15 use Psy\Exception\RuntimeException;
16 use Psy\ExecutionLoop\ForkingLoop;
17 use Psy\ExecutionLoop\Loop;
18 use Psy\Output\OutputPager;
19 use Psy\Output\ShellOutput;
20 use Psy\Readline\GNUReadline;
21 use Psy\Readline\HoaConsole;
22 use Psy\Readline\Libedit;
23 use Psy\Readline\Readline;
24 use Psy\Readline\Transient;
25 use Psy\TabCompletion\AutoCompleter;
26 use Psy\VarDumper\Presenter;
27 use Psy\VersionUpdater\Checker;
28 use Psy\VersionUpdater\GitHubChecker;
29 use Psy\VersionUpdater\IntervalChecker;
30 use Psy\VersionUpdater\NoopChecker;
31 use XdgBaseDir\Xdg;
32
33 /**
34  * The Psy Shell configuration.
35  */
36 class Configuration
37 {
38     const COLOR_MODE_AUTO     = 'auto';
39     const COLOR_MODE_FORCED   = 'forced';
40     const COLOR_MODE_DISABLED = 'disabled';
41
42     private static $AVAILABLE_OPTIONS = array(
43         'codeCleaner',
44         'colorMode',
45         'configDir',
46         'dataDir',
47         'defaultIncludes',
48         'eraseDuplicates',
49         'errorLoggingLevel',
50         'forceArrayIndexes',
51         'historySize',
52         'loop',
53         'manualDbFile',
54         'pager',
55         'prompt',
56         'requireSemicolons',
57         'runtimeDir',
58         'startupMessage',
59         'tabCompletion',
60         'updateCheck',
61         'useBracketedPaste',
62         'usePcntl',
63         'useReadline',
64         'useUnicode',
65         'warnOnMultipleConfigs',
66     );
67
68     private $defaultIncludes;
69     private $configDir;
70     private $dataDir;
71     private $runtimeDir;
72     private $configFile;
73     /** @var string|false */
74     private $historyFile;
75     private $historySize;
76     private $eraseDuplicates;
77     private $manualDbFile;
78     private $hasReadline;
79     private $useReadline;
80     private $useBracketedPaste;
81     private $hasPcntl;
82     private $usePcntl;
83     private $newCommands       = array();
84     private $requireSemicolons = false;
85     private $useUnicode;
86     private $tabCompletion;
87     private $tabCompletionMatchers = array();
88     private $errorLoggingLevel     = E_ALL;
89     private $warnOnMultipleConfigs = false;
90     private $colorMode;
91     private $updateCheck;
92     private $startupMessage;
93     private $forceArrayIndexes = false;
94
95     // services
96     private $readline;
97     private $output;
98     private $shell;
99     private $cleaner;
100     private $pager;
101     private $loop;
102     private $manualDb;
103     private $presenter;
104     private $completer;
105     private $checker;
106     private $prompt;
107
108     /**
109      * Construct a Configuration instance.
110      *
111      * Optionally, supply an array of configuration values to load.
112      *
113      * @param array $config Optional array of configuration values
114      */
115     public function __construct(array $config = array())
116     {
117         $this->setColorMode(self::COLOR_MODE_AUTO);
118
119         // explicit configFile option
120         if (isset($config['configFile'])) {
121             $this->configFile = $config['configFile'];
122         } elseif ($configFile = getenv('PSYSH_CONFIG')) {
123             $this->configFile = $configFile;
124         }
125
126         // legacy baseDir option
127         if (isset($config['baseDir'])) {
128             $msg = "The 'baseDir' configuration option is deprecated. " .
129                 "Please specify 'configDir' and 'dataDir' options instead.";
130             throw new DeprecatedException($msg);
131         }
132
133         unset($config['configFile'], $config['baseDir']);
134
135         // go go gadget, config!
136         $this->loadConfig($config);
137         $this->init();
138     }
139
140     /**
141      * Initialize the configuration.
142      *
143      * This checks for the presence of Readline and Pcntl extensions.
144      *
145      * If a config file is available, it will be loaded and merged with the current config.
146      *
147      * If no custom config file was specified and a local project config file
148      * is available, it will be loaded and merged with the current config.
149      */
150     public function init()
151     {
152         // feature detection
153         $this->hasReadline = function_exists('readline');
154         $this->hasPcntl    = function_exists('pcntl_signal') && function_exists('posix_getpid');
155
156         if ($configFile = $this->getConfigFile()) {
157             $this->loadConfigFile($configFile);
158         }
159
160         if (!$this->configFile && $localConfig = $this->getLocalConfigFile()) {
161             $this->loadConfigFile($localConfig);
162         }
163     }
164
165     /**
166      * Get the current PsySH config file.
167      *
168      * If a `configFile` option was passed to the Configuration constructor,
169      * this file will be returned. If not, all possible config directories will
170      * be searched, and the first `config.php` or `rc.php` file which exists
171      * will be returned.
172      *
173      * If you're trying to decide where to put your config file, pick
174      *
175      *     ~/.config/psysh/config.php
176      *
177      * @return string
178      */
179     public function getConfigFile()
180     {
181         if (isset($this->configFile)) {
182             return $this->configFile;
183         }
184
185         $files = ConfigPaths::getConfigFiles(array('config.php', 'rc.php'), $this->configDir);
186
187         if (!empty($files)) {
188             if ($this->warnOnMultipleConfigs && count($files) > 1) {
189                 $msg = sprintf('Multiple configuration files found: %s. Using %s', implode($files, ', '), $files[0]);
190                 trigger_error($msg, E_USER_NOTICE);
191             }
192
193             return $files[0];
194         }
195     }
196
197     /**
198      * Get the local PsySH config file.
199      *
200      * Searches for a project specific config file `.psysh.php` in the current
201      * working directory.
202      *
203      * @return string
204      */
205     public function getLocalConfigFile()
206     {
207         $localConfig = getcwd() . '/.psysh.php';
208
209         if (@is_file($localConfig)) {
210             return $localConfig;
211         }
212     }
213
214     /**
215      * Load configuration values from an array of options.
216      *
217      * @param array $options
218      */
219     public function loadConfig(array $options)
220     {
221         foreach (self::$AVAILABLE_OPTIONS as $option) {
222             if (isset($options[$option])) {
223                 $method = 'set' . ucfirst($option);
224                 $this->$method($options[$option]);
225             }
226         }
227
228         foreach (array('commands', 'tabCompletionMatchers', 'casters') as $option) {
229             if (isset($options[$option])) {
230                 $method = 'add' . ucfirst($option);
231                 $this->$method($options[$option]);
232             }
233         }
234     }
235
236     /**
237      * Load a configuration file (default: `$HOME/.config/psysh/config.php`).
238      *
239      * This configuration instance will be available to the config file as $config.
240      * The config file may directly manipulate the configuration, or may return
241      * an array of options which will be merged with the current configuration.
242      *
243      * @throws \InvalidArgumentException if the config file returns a non-array result
244      *
245      * @param string $file
246      */
247     public function loadConfigFile($file)
248     {
249         $__psysh_config_file__ = $file;
250         $load = function ($config) use ($__psysh_config_file__) {
251             $result = require $__psysh_config_file__;
252             if ($result !== 1) {
253                 return $result;
254             }
255         };
256         $result = $load($this);
257
258         if (!empty($result)) {
259             if (is_array($result)) {
260                 $this->loadConfig($result);
261             } else {
262                 throw new \InvalidArgumentException('Psy Shell configuration must return an array of options');
263             }
264         }
265     }
266
267     /**
268      * Set files to be included by default at the start of each shell session.
269      *
270      * @param array $includes
271      */
272     public function setDefaultIncludes(array $includes = array())
273     {
274         $this->defaultIncludes = $includes;
275     }
276
277     /**
278      * Get files to be included by default at the start of each shell session.
279      *
280      * @return array
281      */
282     public function getDefaultIncludes()
283     {
284         return $this->defaultIncludes ?: array();
285     }
286
287     /**
288      * Set the shell's config directory location.
289      *
290      * @param string $dir
291      */
292     public function setConfigDir($dir)
293     {
294         $this->configDir = (string) $dir;
295     }
296
297     /**
298      * Get the current configuration directory, if any is explicitly set.
299      *
300      * @return string
301      */
302     public function getConfigDir()
303     {
304         return $this->configDir;
305     }
306
307     /**
308      * Set the shell's data directory location.
309      *
310      * @param string $dir
311      */
312     public function setDataDir($dir)
313     {
314         $this->dataDir = (string) $dir;
315     }
316
317     /**
318      * Get the current data directory, if any is explicitly set.
319      *
320      * @return string
321      */
322     public function getDataDir()
323     {
324         return $this->dataDir;
325     }
326
327     /**
328      * Set the shell's temporary directory location.
329      *
330      * @param string $dir
331      */
332     public function setRuntimeDir($dir)
333     {
334         $this->runtimeDir = (string) $dir;
335     }
336
337     /**
338      * Get the shell's temporary directory location.
339      *
340      * Defaults to  `/psysh` inside the system's temp dir unless explicitly
341      * overridden.
342      *
343      * @return string
344      */
345     public function getRuntimeDir()
346     {
347         if (!isset($this->runtimeDir)) {
348             $this->runtimeDir = ConfigPaths::getRuntimeDir();
349         }
350
351         if (!is_dir($this->runtimeDir)) {
352             mkdir($this->runtimeDir, 0700, true);
353         }
354
355         return $this->runtimeDir;
356     }
357
358     /**
359      * Set the readline history file path.
360      *
361      * @param string $file
362      */
363     public function setHistoryFile($file)
364     {
365         $this->historyFile = ConfigPaths::touchFileWithMkdir($file);
366     }
367
368     /**
369      * Get the readline history file path.
370      *
371      * Defaults to `/history` inside the shell's base config dir unless
372      * explicitly overridden.
373      *
374      * @return string
375      */
376     public function getHistoryFile()
377     {
378         if (isset($this->historyFile)) {
379             return $this->historyFile;
380         }
381
382         // Deprecation warning for incorrect psysh_history path.
383         // @todo remove this before v0.9.0
384         $xdg = new Xdg();
385         $oldHistory = $xdg->getHomeConfigDir() . '/psysh_history';
386         if (@is_file($oldHistory)) {
387             $dir = $this->configDir ?: ConfigPaths::getCurrentConfigDir();
388             $newHistory = $dir . '/psysh_history';
389
390             $msg = sprintf(
391                 "PsySH history file found at '%s'. Please delete it or move it to '%s'.",
392                 strtr($oldHistory, '\\', '/'),
393                 $newHistory
394             );
395             @trigger_error($msg, E_USER_DEPRECATED);
396             $this->setHistoryFile($oldHistory);
397
398             return $this->historyFile;
399         }
400
401         $files = ConfigPaths::getConfigFiles(array('psysh_history', 'history'), $this->configDir);
402
403         if (!empty($files)) {
404             if ($this->warnOnMultipleConfigs && count($files) > 1) {
405                 $msg = sprintf('Multiple history files found: %s. Using %s', implode($files, ', '), $files[0]);
406                 trigger_error($msg, E_USER_NOTICE);
407             }
408
409             $this->setHistoryFile($files[0]);
410         } else {
411             // fallback: create our own history file
412             $dir = $this->configDir ?: ConfigPaths::getCurrentConfigDir();
413             $this->setHistoryFile($dir . '/psysh_history');
414         }
415
416         return $this->historyFile;
417     }
418
419     /**
420      * Set the readline max history size.
421      *
422      * @param int $value
423      */
424     public function setHistorySize($value)
425     {
426         $this->historySize = (int) $value;
427     }
428
429     /**
430      * Get the readline max history size.
431      *
432      * @return int
433      */
434     public function getHistorySize()
435     {
436         return $this->historySize;
437     }
438
439     /**
440      * Sets whether readline erases old duplicate history entries.
441      *
442      * @param bool $value
443      */
444     public function setEraseDuplicates($value)
445     {
446         $this->eraseDuplicates = (bool) $value;
447     }
448
449     /**
450      * Get whether readline erases old duplicate history entries.
451      *
452      * @return bool
453      */
454     public function getEraseDuplicates()
455     {
456         return $this->eraseDuplicates;
457     }
458
459     /**
460      * Get a temporary file of type $type for process $pid.
461      *
462      * The file will be created inside the current temporary directory.
463      *
464      * @see self::getRuntimeDir
465      *
466      * @param string $type
467      * @param int    $pid
468      *
469      * @return string Temporary file name
470      */
471     public function getTempFile($type, $pid)
472     {
473         return tempnam($this->getRuntimeDir(), $type . '_' . $pid . '_');
474     }
475
476     /**
477      * Get a filename suitable for a FIFO pipe of $type for process $pid.
478      *
479      * The pipe will be created inside the current temporary directory.
480      *
481      * @param string $type
482      * @param int    $pid
483      *
484      * @return string Pipe name
485      */
486     public function getPipe($type, $pid)
487     {
488         return sprintf('%s/%s_%s', $this->getRuntimeDir(), $type, $pid);
489     }
490
491     /**
492      * Check whether this PHP instance has Readline available.
493      *
494      * @return bool True if Readline is available
495      */
496     public function hasReadline()
497     {
498         return $this->hasReadline;
499     }
500
501     /**
502      * Enable or disable Readline usage.
503      *
504      * @param bool $useReadline
505      */
506     public function setUseReadline($useReadline)
507     {
508         $this->useReadline = (bool) $useReadline;
509     }
510
511     /**
512      * Check whether to use Readline.
513      *
514      * If `setUseReadline` as been set to true, but Readline is not actually
515      * available, this will return false.
516      *
517      * @return bool True if the current Shell should use Readline
518      */
519     public function useReadline()
520     {
521         return isset($this->useReadline) ? ($this->hasReadline && $this->useReadline) : $this->hasReadline;
522     }
523
524     /**
525      * Set the Psy Shell readline service.
526      *
527      * @param Readline $readline
528      */
529     public function setReadline(Readline $readline)
530     {
531         $this->readline = $readline;
532     }
533
534     /**
535      * Get the Psy Shell readline service.
536      *
537      * By default, this service uses (in order of preference):
538      *
539      *  * GNU Readline
540      *  * Libedit
541      *  * A transient array-based readline emulation.
542      *
543      * @return Readline
544      */
545     public function getReadline()
546     {
547         if (!isset($this->readline)) {
548             $className = $this->getReadlineClass();
549             $this->readline = new $className(
550                 $this->getHistoryFile(),
551                 $this->getHistorySize(),
552                 $this->getEraseDuplicates()
553             );
554         }
555
556         return $this->readline;
557     }
558
559     /**
560      * Get the appropriate Readline implementation class name.
561      *
562      * @see self::getReadline
563      *
564      * @return string
565      */
566     private function getReadlineClass()
567     {
568         if ($this->useReadline()) {
569             if (GNUReadline::isSupported()) {
570                 return 'Psy\Readline\GNUReadline';
571             } elseif (Libedit::isSupported()) {
572                 return 'Psy\Readline\Libedit';
573             } elseif (HoaConsole::isSupported()) {
574                 return 'Psy\Readline\HoaConsole';
575             }
576         }
577
578         return 'Psy\Readline\Transient';
579     }
580
581     /**
582      * Enable or disable bracketed paste.
583      *
584      * Note that this only works with readline (not libedit) integration for now.
585      *
586      * @param bool $useBracketedPaste
587      */
588     public function setUseBracketedPaste($useBracketedPaste)
589     {
590         $this->useBracketedPaste = (bool) $useBracketedPaste;
591     }
592
593     /**
594      * Check whether to use bracketed paste with readline.
595      *
596      * When this works, it's magical. Tabs in pastes don't try to autcomplete.
597      * Newlines in paste don't execute code until you get to the end. It makes
598      * readline act like you'd expect when pasting.
599      *
600      * But it often (usually?) does not work. And when it doesn't, it just spews
601      * escape codes all over the place and generally makes things ugly :(
602      *
603      * If `useBracketedPaste` has been set to true, but the current readline
604      * implementation is anything besides GNU readline, this will return false.
605      *
606      * @return bool True if the shell should use bracketed paste
607      */
608     public function useBracketedPaste()
609     {
610         // For now, only the GNU readline implementation supports bracketed paste.
611         $supported = ($this->getReadlineClass() === 'Psy\Readline\GNUReadline');
612
613         return $supported && $this->useBracketedPaste;
614
615         // @todo mebbe turn this on by default some day?
616         // return isset($this->useBracketedPaste) ? ($supported && $this->useBracketedPaste) : $supported;
617     }
618
619     /**
620      * Check whether this PHP instance has Pcntl available.
621      *
622      * @return bool True if Pcntl is available
623      */
624     public function hasPcntl()
625     {
626         return $this->hasPcntl;
627     }
628
629     /**
630      * Enable or disable Pcntl usage.
631      *
632      * @param bool $usePcntl
633      */
634     public function setUsePcntl($usePcntl)
635     {
636         $this->usePcntl = (bool) $usePcntl;
637     }
638
639     /**
640      * Check whether to use Pcntl.
641      *
642      * If `setUsePcntl` has been set to true, but Pcntl is not actually
643      * available, this will return false.
644      *
645      * @return bool True if the current Shell should use Pcntl
646      */
647     public function usePcntl()
648     {
649         return isset($this->usePcntl) ? ($this->hasPcntl && $this->usePcntl) : $this->hasPcntl;
650     }
651
652     /**
653      * Enable or disable strict requirement of semicolons.
654      *
655      * @see self::requireSemicolons()
656      *
657      * @param bool $requireSemicolons
658      */
659     public function setRequireSemicolons($requireSemicolons)
660     {
661         $this->requireSemicolons = (bool) $requireSemicolons;
662     }
663
664     /**
665      * Check whether to require semicolons on all statements.
666      *
667      * By default, PsySH will automatically insert semicolons at the end of
668      * statements if they're missing. To strictly require semicolons, set
669      * `requireSemicolons` to true.
670      *
671      * @return bool
672      */
673     public function requireSemicolons()
674     {
675         return $this->requireSemicolons;
676     }
677
678     /**
679      * Enable or disable Unicode in PsySH specific output.
680      *
681      * Note that this does not disable Unicode output in general, it just makes
682      * it so PsySH won't output any itself.
683      *
684      * @param bool $useUnicode
685      */
686     public function setUseUnicode($useUnicode)
687     {
688         $this->useUnicode = (bool) $useUnicode;
689     }
690
691     /**
692      * Check whether to use Unicode in PsySH specific output.
693      *
694      * Note that this does not disable Unicode output in general, it just makes
695      * it so PsySH won't output any itself.
696      *
697      * @return bool
698      */
699     public function useUnicode()
700     {
701         if (isset($this->useUnicode)) {
702             return $this->useUnicode;
703         }
704
705         // @todo detect `chsh` != 65001 on Windows and return false
706         return true;
707     }
708
709     /**
710      * Set the error logging level.
711      *
712      * @see self::errorLoggingLevel
713      *
714      * @param bool $errorLoggingLevel
715      */
716     public function setErrorLoggingLevel($errorLoggingLevel)
717     {
718         $this->errorLoggingLevel = (E_ALL | E_STRICT) & $errorLoggingLevel;
719     }
720
721     /**
722      * Get the current error logging level.
723      *
724      * By default, PsySH will automatically log all errors, regardless of the
725      * current `error_reporting` level. Additionally, if the `error_reporting`
726      * level warrants, an ErrorException will be thrown.
727      *
728      * Set `errorLoggingLevel` to 0 to prevent logging non-thrown errors. Set it
729      * to any valid error_reporting value to log only errors which match that
730      * level.
731      *
732      *     http://php.net/manual/en/function.error-reporting.php
733      *
734      * @return int
735      */
736     public function errorLoggingLevel()
737     {
738         return $this->errorLoggingLevel;
739     }
740
741     /**
742      * Set a CodeCleaner service instance.
743      *
744      * @param CodeCleaner $cleaner
745      */
746     public function setCodeCleaner(CodeCleaner $cleaner)
747     {
748         $this->cleaner = $cleaner;
749     }
750
751     /**
752      * Get a CodeCleaner service instance.
753      *
754      * If none has been explicitly defined, this will create a new instance.
755      *
756      * @return CodeCleaner
757      */
758     public function getCodeCleaner()
759     {
760         if (!isset($this->cleaner)) {
761             $this->cleaner = new CodeCleaner();
762         }
763
764         return $this->cleaner;
765     }
766
767     /**
768      * Enable or disable tab completion.
769      *
770      * @param bool $tabCompletion
771      */
772     public function setTabCompletion($tabCompletion)
773     {
774         $this->tabCompletion = (bool) $tabCompletion;
775     }
776
777     /**
778      * Check whether to use tab completion.
779      *
780      * If `setTabCompletion` has been set to true, but readline is not actually
781      * available, this will return false.
782      *
783      * @return bool True if the current Shell should use tab completion
784      */
785     public function getTabCompletion()
786     {
787         return isset($this->tabCompletion) ? ($this->hasReadline && $this->tabCompletion) : $this->hasReadline;
788     }
789
790     /**
791      * Set the Shell Output service.
792      *
793      * @param ShellOutput $output
794      */
795     public function setOutput(ShellOutput $output)
796     {
797         $this->output = $output;
798     }
799
800     /**
801      * Get a Shell Output service instance.
802      *
803      * If none has been explicitly provided, this will create a new instance
804      * with VERBOSITY_NORMAL and the output page supplied by self::getPager
805      *
806      * @see self::getPager
807      *
808      * @return ShellOutput
809      */
810     public function getOutput()
811     {
812         if (!isset($this->output)) {
813             $this->output = new ShellOutput(
814                 ShellOutput::VERBOSITY_NORMAL,
815                 $this->getOutputDecorated(),
816                 null,
817                 $this->getPager()
818             );
819         }
820
821         return $this->output;
822     }
823
824     /**
825      * Get the decoration (i.e. color) setting for the Shell Output service.
826      *
827      * @return null|bool 3-state boolean corresponding to the current color mode
828      */
829     public function getOutputDecorated()
830     {
831         if ($this->colorMode() === self::COLOR_MODE_AUTO) {
832             return;
833         } elseif ($this->colorMode() === self::COLOR_MODE_FORCED) {
834             return true;
835         } elseif ($this->colorMode() === self::COLOR_MODE_DISABLED) {
836             return false;
837         }
838     }
839
840     /**
841      * Set the OutputPager service.
842      *
843      * If a string is supplied, a ProcOutputPager will be used which shells out
844      * to the specified command.
845      *
846      * @throws \InvalidArgumentException if $pager is not a string or OutputPager instance
847      *
848      * @param string|OutputPager $pager
849      */
850     public function setPager($pager)
851     {
852         if ($pager && !is_string($pager) && !$pager instanceof OutputPager) {
853             throw new \InvalidArgumentException('Unexpected pager instance.');
854         }
855
856         $this->pager = $pager;
857     }
858
859     /**
860      * Get an OutputPager instance or a command for an external Proc pager.
861      *
862      * If no Pager has been explicitly provided, and Pcntl is available, this
863      * will default to `cli.pager` ini value, falling back to `which less`.
864      *
865      * @return string|OutputPager
866      */
867     public function getPager()
868     {
869         if (!isset($this->pager) && $this->usePcntl()) {
870             if ($pager = ini_get('cli.pager')) {
871                 // use the default pager (5.4+)
872                 $this->pager = $pager;
873             } elseif ($less = exec('which less 2>/dev/null')) {
874                 // check for the presence of less...
875                 $this->pager = $less . ' -R -S -F -X';
876             }
877         }
878
879         return $this->pager;
880     }
881
882     /**
883      * Set the Shell evaluation Loop service.
884      *
885      * @param Loop $loop
886      */
887     public function setLoop(Loop $loop)
888     {
889         $this->loop = $loop;
890     }
891
892     /**
893      * Get a Shell evaluation Loop service instance.
894      *
895      * If none has been explicitly defined, this will create a new instance.
896      * If Pcntl is available and enabled, the new instance will be a ForkingLoop.
897      *
898      * @return Loop
899      */
900     public function getLoop()
901     {
902         if (!isset($this->loop)) {
903             if ($this->usePcntl()) {
904                 $this->loop = new ForkingLoop($this);
905             } else {
906                 $this->loop = new Loop($this);
907             }
908         }
909
910         return $this->loop;
911     }
912
913     /**
914      * Set the Shell autocompleter service.
915      *
916      * @param AutoCompleter $completer
917      */
918     public function setAutoCompleter(AutoCompleter $completer)
919     {
920         $this->completer = $completer;
921     }
922
923     /**
924      * Get an AutoCompleter service instance.
925      *
926      * @return AutoCompleter
927      */
928     public function getAutoCompleter()
929     {
930         if (!isset($this->completer)) {
931             $this->completer = new AutoCompleter();
932         }
933
934         return $this->completer;
935     }
936
937     /**
938      * Get user specified tab completion matchers for the AutoCompleter.
939      *
940      * @return array
941      */
942     public function getTabCompletionMatchers()
943     {
944         return $this->tabCompletionMatchers;
945     }
946
947     /**
948      * Add additional tab completion matchers to the AutoCompleter.
949      *
950      * @param array $matchers
951      */
952     public function addTabCompletionMatchers(array $matchers)
953     {
954         $this->tabCompletionMatchers = array_merge($this->tabCompletionMatchers, $matchers);
955         if (isset($this->shell)) {
956             $this->shell->addTabCompletionMatchers($this->tabCompletionMatchers);
957         }
958     }
959
960     /**
961      * Add commands to the Shell.
962      *
963      * This will buffer new commands in the event that the Shell has not yet
964      * been instantiated. This allows the user to specify commands in their
965      * config rc file, despite the fact that their file is needed in the Shell
966      * constructor.
967      *
968      * @param array $commands
969      */
970     public function addCommands(array $commands)
971     {
972         $this->newCommands = array_merge($this->newCommands, $commands);
973         if (isset($this->shell)) {
974             $this->doAddCommands();
975         }
976     }
977
978     /**
979      * Internal method for adding commands. This will set any new commands once
980      * a Shell is available.
981      */
982     private function doAddCommands()
983     {
984         if (!empty($this->newCommands)) {
985             $this->shell->addCommands($this->newCommands);
986             $this->newCommands = array();
987         }
988     }
989
990     /**
991      * Set the Shell backreference and add any new commands to the Shell.
992      *
993      * @param Shell $shell
994      */
995     public function setShell(Shell $shell)
996     {
997         $this->shell = $shell;
998         $this->doAddCommands();
999     }
1000
1001     /**
1002      * Set the PHP manual database file.
1003      *
1004      * This file should be an SQLite database generated from the phpdoc source
1005      * with the `bin/build_manual` script.
1006      *
1007      * @param string $filename
1008      */
1009     public function setManualDbFile($filename)
1010     {
1011         $this->manualDbFile = (string) $filename;
1012     }
1013
1014     /**
1015      * Get the current PHP manual database file.
1016      *
1017      * @return string Default: '~/.local/share/psysh/php_manual.sqlite'
1018      */
1019     public function getManualDbFile()
1020     {
1021         if (isset($this->manualDbFile)) {
1022             return $this->manualDbFile;
1023         }
1024
1025         $files = ConfigPaths::getDataFiles(array('php_manual.sqlite'), $this->dataDir);
1026         if (!empty($files)) {
1027             if ($this->warnOnMultipleConfigs && count($files) > 1) {
1028                 $msg = sprintf('Multiple manual database files found: %s. Using %s', implode($files, ', '), $files[0]);
1029                 trigger_error($msg, E_USER_NOTICE);
1030             }
1031
1032             return $this->manualDbFile = $files[0];
1033         }
1034     }
1035
1036     /**
1037      * Get a PHP manual database connection.
1038      *
1039      * @return \PDO
1040      */
1041     public function getManualDb()
1042     {
1043         if (!isset($this->manualDb)) {
1044             $dbFile = $this->getManualDbFile();
1045             if (is_file($dbFile)) {
1046                 try {
1047                     $this->manualDb = new \PDO('sqlite:' . $dbFile);
1048                 } catch (\PDOException $e) {
1049                     if ($e->getMessage() === 'could not find driver') {
1050                         throw new RuntimeException('SQLite PDO driver not found', 0, $e);
1051                     } else {
1052                         throw $e;
1053                     }
1054                 }
1055             }
1056         }
1057
1058         return $this->manualDb;
1059     }
1060
1061     /**
1062      * Add an array of casters definitions.
1063      *
1064      * @param array $casters
1065      */
1066     public function addCasters(array $casters)
1067     {
1068         $this->getPresenter()->addCasters($casters);
1069     }
1070
1071     /**
1072      * Get the Presenter service.
1073      *
1074      * @return Presenter
1075      */
1076     public function getPresenter()
1077     {
1078         if (!isset($this->presenter)) {
1079             $this->presenter = new Presenter($this->getOutput()->getFormatter(), $this->forceArrayIndexes());
1080         }
1081
1082         return $this->presenter;
1083     }
1084
1085     /**
1086      * Enable or disable warnings on multiple configuration or data files.
1087      *
1088      * @see self::warnOnMultipleConfigs()
1089      *
1090      * @param bool $warnOnMultipleConfigs
1091      */
1092     public function setWarnOnMultipleConfigs($warnOnMultipleConfigs)
1093     {
1094         $this->warnOnMultipleConfigs = (bool) $warnOnMultipleConfigs;
1095     }
1096
1097     /**
1098      * Check whether to warn on multiple configuration or data files.
1099      *
1100      * By default, PsySH will use the file with highest precedence, and will
1101      * silently ignore all others. With this enabled, a warning will be emitted
1102      * (but not an exception thrown) if multiple configuration or data files
1103      * are found.
1104      *
1105      * This will default to true in a future release, but is false for now.
1106      *
1107      * @return bool
1108      */
1109     public function warnOnMultipleConfigs()
1110     {
1111         return $this->warnOnMultipleConfigs;
1112     }
1113
1114     /**
1115      * Set the current color mode.
1116      *
1117      * @param string $colorMode
1118      */
1119     public function setColorMode($colorMode)
1120     {
1121         $validColorModes = array(
1122             self::COLOR_MODE_AUTO,
1123             self::COLOR_MODE_FORCED,
1124             self::COLOR_MODE_DISABLED,
1125         );
1126
1127         if (in_array($colorMode, $validColorModes)) {
1128             $this->colorMode = $colorMode;
1129         } else {
1130             throw new \InvalidArgumentException('invalid color mode: ' . $colorMode);
1131         }
1132     }
1133
1134     /**
1135      * Get the current color mode.
1136      *
1137      * @return string
1138      */
1139     public function colorMode()
1140     {
1141         return $this->colorMode;
1142     }
1143
1144     /**
1145      * Set an update checker service instance.
1146      *
1147      * @param Checker $checker
1148      */
1149     public function setChecker(Checker $checker)
1150     {
1151         $this->checker = $checker;
1152     }
1153
1154     /**
1155      * Get an update checker service instance.
1156      *
1157      * If none has been explicitly defined, this will create a new instance.
1158      *
1159      * @return Checker
1160      */
1161     public function getChecker()
1162     {
1163         if (!isset($this->checker)) {
1164             $interval = $this->getUpdateCheck();
1165             switch ($interval) {
1166                 case Checker::ALWAYS:
1167                     $this->checker = new GitHubChecker();
1168                     break;
1169
1170                 case Checker::DAILY:
1171                 case Checker::WEEKLY:
1172                 case Checker::MONTHLY:
1173                     $checkFile = $this->getUpdateCheckCacheFile();
1174                     if ($checkFile === false) {
1175                         $this->checker = new NoopChecker();
1176                     } else {
1177                         $this->checker = new IntervalChecker($checkFile, $interval);
1178                     }
1179                     break;
1180
1181                 case Checker::NEVER:
1182                     $this->checker = new NoopChecker();
1183                     break;
1184             }
1185         }
1186
1187         return $this->checker;
1188     }
1189
1190     /**
1191      * Get the current update check interval.
1192      *
1193      * One of 'always', 'daily', 'weekly', 'monthly' or 'never'. If none is
1194      * explicitly set, default to 'weekly'.
1195      *
1196      * @return string
1197      */
1198     public function getUpdateCheck()
1199     {
1200         return isset($this->updateCheck) ? $this->updateCheck : Checker::WEEKLY;
1201     }
1202
1203     /**
1204      * Set the update check interval.
1205      *
1206      * @throws \InvalidArgumentDescription if the update check interval is unknown
1207      *
1208      * @param string $interval
1209      */
1210     public function setUpdateCheck($interval)
1211     {
1212         $validIntervals = array(
1213             Checker::ALWAYS,
1214             Checker::DAILY,
1215             Checker::WEEKLY,
1216             Checker::MONTHLY,
1217             Checker::NEVER,
1218         );
1219
1220         if (!in_array($interval, $validIntervals)) {
1221             throw new \InvalidArgumentException('invalid update check interval: ' . $interval);
1222         }
1223
1224         $this->updateCheck = $interval;
1225     }
1226
1227     /**
1228      * Get a cache file path for the update checker.
1229      *
1230      * @return string|false Return false if config file/directory is not writable
1231      */
1232     public function getUpdateCheckCacheFile()
1233     {
1234         $dir = $this->configDir ?: ConfigPaths::getCurrentConfigDir();
1235
1236         return ConfigPaths::touchFileWithMkdir($dir . '/update_check.json');
1237     }
1238
1239     /**
1240      * Set the startup message.
1241      *
1242      * @param string $message
1243      */
1244     public function setStartupMessage($message)
1245     {
1246         $this->startupMessage = $message;
1247     }
1248
1249     /**
1250      * Get the startup message.
1251      *
1252      * @return string|null
1253      */
1254     public function getStartupMessage()
1255     {
1256         return $this->startupMessage;
1257     }
1258
1259     /**
1260      * Set the prompt.
1261      *
1262      * @param string $prompt
1263      */
1264     public function setPrompt($prompt)
1265     {
1266         $this->prompt = $prompt;
1267     }
1268
1269     /**
1270      * Get the prompt.
1271      *
1272      * @return string
1273      */
1274     public function getPrompt()
1275     {
1276         return $this->prompt;
1277     }
1278
1279     /**
1280      * Get the force array indexes.
1281      *
1282      * @return bool
1283      */
1284     public function forceArrayIndexes()
1285     {
1286         return $this->forceArrayIndexes;
1287     }
1288
1289     /**
1290      * Set the force array indexes.
1291      *
1292      * @param bool $forceArrayIndexes
1293      */
1294     public function setForceArrayIndexes($forceArrayIndexes)
1295     {
1296         $this->forceArrayIndexes = $forceArrayIndexes;
1297     }
1298 }