1<?php
2
3/*
4 * This file is part of Psy Shell.
5 *
6 * (c) 2012-2020 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
12namespace Psy\Readline;
13
14use Psy\Util\Str;
15
16/**
17 * A Libedit-based Readline implementation.
18 *
19 * This is largely the same as the Readline implementation, but it emulates
20 * support for `readline_list_history` since PHP decided it was a good idea to
21 * ship a fake Readline implementation that is missing history support.
22 *
23 * NOTE: As of PHP 7.4, PHP sometimes has history support in the Libedit
24 * wrapper, so it will use the GNUReadline implementation rather than this one.
25 */
26class Libedit extends GNUReadline
27{
28    private $hasWarnedOwnership = false;
29
30    /**
31     * Let's emulate GNU Readline by manually reading and parsing the history file!
32     *
33     * @return bool
34     */
35    public static function isSupported()
36    {
37        return \function_exists('readline') && !\function_exists('readline_list_history');
38    }
39
40    /**
41     * {@inheritdoc}
42     */
43    public function listHistory()
44    {
45        $history = \file_get_contents($this->historyFile);
46        if (!$history) {
47            return [];
48        }
49
50        // libedit doesn't seem to support non-unix line separators.
51        $history = \explode("\n", $history);
52
53        // remove history signature if it exists
54        if ($history[0] === '_HiStOrY_V2_') {
55            \array_shift($history);
56        }
57
58        // decode the line
59        $history = \array_map([$this, 'parseHistoryLine'], $history);
60        // filter empty lines & comments
61        return \array_values(\array_filter($history));
62    }
63
64    /**
65     * {@inheritdoc}
66     */
67    public function writeHistory()
68    {
69        $res = parent::writeHistory();
70
71        // Libedit apparently refuses to save history if the history file is not
72        // owned by the user, even if it is writable. Warn when this happens.
73        //
74        // See https://github.com/bobthecow/psysh/issues/552
75        if ($res === false && !$this->hasWarnedOwnership) {
76            if (\is_file($this->historyFile) && \is_writable($this->historyFile)) {
77                $this->hasWarnedOwnership = true;
78                $msg = \sprintf('Error writing history file, check file ownership: %s', $this->historyFile);
79                \trigger_error($msg, \E_USER_NOTICE);
80            }
81        }
82
83        return $res;
84    }
85
86    /**
87     * From GNUReadline (readline/histfile.c & readline/histexpand.c):
88     * lines starting with "\0" are comments or timestamps;
89     * if "\0" is found in an entry,
90     * everything from it until the next line is a comment.
91     *
92     * @param string $line The history line to parse
93     *
94     * @return string | null
95     */
96    protected function parseHistoryLine($line)
97    {
98        // empty line, comment or timestamp
99        if (!$line || $line[0] === "\0") {
100            return;
101        }
102        // if "\0" is found in an entry, then
103        // everything from it until the end of line is a comment.
104        if (($pos = \strpos($line, "\0")) !== false) {
105            $line = \substr($line, 0, $pos);
106        }
107
108        return ($line !== '') ? Str::unvis($line) : null;
109    }
110}
111