1<?php
2
3/**
4 * Pure-PHP ANSI Decoder
5 *
6 * PHP version 5
7 *
8 * If you call read() in \phpseclib3\Net\SSH2 you may get {@link http://en.wikipedia.org/wiki/ANSI_escape_code ANSI escape codes} back.
9 * They'd look like chr(0x1B) . '[00m' or whatever (0x1B = ESC).  They tell a
10 * {@link http://en.wikipedia.org/wiki/Terminal_emulator terminal emulator} how to format the characters, what
11 * color to display them in, etc. \phpseclib3\File\ANSI is a {@link http://en.wikipedia.org/wiki/VT100 VT100} terminal emulator.
12 *
13 * @author    Jim Wigginton <terrafrost@php.net>
14 * @copyright 2012 Jim Wigginton
15 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
16 * @link      http://phpseclib.sourceforge.net
17 */
18
19namespace phpseclib3\File;
20
21/**
22 * Pure-PHP ANSI Decoder
23 *
24 * @author  Jim Wigginton <terrafrost@php.net>
25 */
26class ANSI
27{
28    /**
29     * Max Width
30     *
31     * @var int
32     */
33    private $max_x;
34
35    /**
36     * Max Height
37     *
38     * @var int
39     */
40    private $max_y;
41
42    /**
43     * Max History
44     *
45     * @var int
46     */
47    private $max_history;
48
49    /**
50     * History
51     *
52     * @var array
53     */
54    private $history;
55
56    /**
57     * History Attributes
58     *
59     * @var array
60     */
61    private $history_attrs;
62
63    /**
64     * Current Column
65     *
66     * @var int
67     */
68    private $x;
69
70    /**
71     * Current Row
72     *
73     * @var int
74     */
75    private $y;
76
77    /**
78     * Old Column
79     *
80     * @var int
81     */
82    private $old_x;
83
84    /**
85     * Old Row
86     *
87     * @var int
88     */
89    private $old_y;
90
91    /**
92     * An empty attribute cell
93     *
94     * @var object
95     */
96    private $base_attr_cell;
97
98    /**
99     * The current attribute cell
100     *
101     * @var object
102     */
103    private $attr_cell;
104
105    /**
106     * An empty attribute row
107     *
108     * @var array
109     */
110    private $attr_row;
111
112    /**
113     * The current screen text
114     *
115     * @var list<string>
116     */
117    private $screen;
118
119    /**
120     * The current screen attributes
121     *
122     * @var array
123     */
124    private $attrs;
125
126    /**
127     * Current ANSI code
128     *
129     * @var string
130     */
131    private $ansi;
132
133    /**
134     * Tokenization
135     *
136     * @var array
137     */
138    private $tokenization;
139
140    /**
141     * Default Constructor.
142     *
143     * @return \phpseclib3\File\ANSI
144     */
145    public function __construct()
146    {
147        $attr_cell = new \stdClass();
148        $attr_cell->bold = false;
149        $attr_cell->underline = false;
150        $attr_cell->blink = false;
151        $attr_cell->background = 'black';
152        $attr_cell->foreground = 'white';
153        $attr_cell->reverse = false;
154        $this->base_attr_cell = clone $attr_cell;
155        $this->attr_cell = clone $attr_cell;
156
157        $this->setHistory(200);
158        $this->setDimensions(80, 24);
159    }
160
161    /**
162     * Set terminal width and height
163     *
164     * Resets the screen as well
165     *
166     * @param int $x
167     * @param int $y
168     */
169    public function setDimensions($x, $y)
170    {
171        $this->max_x = $x - 1;
172        $this->max_y = $y - 1;
173        $this->x = $this->y = 0;
174        $this->history = $this->history_attrs = [];
175        $this->attr_row = array_fill(0, $this->max_x + 2, $this->base_attr_cell);
176        $this->screen = array_fill(0, $this->max_y + 1, '');
177        $this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row);
178        $this->ansi = '';
179    }
180
181    /**
182     * Set the number of lines that should be logged past the terminal height
183     *
184     * @param int $history
185     */
186    public function setHistory($history)
187    {
188        $this->max_history = $history;
189    }
190
191    /**
192     * Load a string
193     *
194     * @param string $source
195     */
196    public function loadString($source)
197    {
198        $this->setDimensions($this->max_x + 1, $this->max_y + 1);
199        $this->appendString($source);
200    }
201
202    /**
203     * Appdend a string
204     *
205     * @param string $source
206     */
207    public function appendString($source)
208    {
209        $this->tokenization = [''];
210        for ($i = 0; $i < strlen($source); $i++) {
211            if (strlen($this->ansi)) {
212                $this->ansi .= $source[$i];
213                $chr = ord($source[$i]);
214                // http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements
215                // single character CSI's not currently supported
216                switch (true) {
217                    case $this->ansi == "\x1B=":
218                        $this->ansi = '';
219                        continue 2;
220                    case strlen($this->ansi) == 2 && $chr >= 64 && $chr <= 95 && $chr != ord('['):
221                    case strlen($this->ansi) > 2 && $chr >= 64 && $chr <= 126:
222                        break;
223                    default:
224                        continue 2;
225                }
226                $this->tokenization[] = $this->ansi;
227                $this->tokenization[] = '';
228                // http://ascii-table.com/ansi-escape-sequences-vt-100.php
229                switch ($this->ansi) {
230                    case "\x1B[H": // Move cursor to upper left corner
231                        $this->old_x = $this->x;
232                        $this->old_y = $this->y;
233                        $this->x = $this->y = 0;
234                        break;
235                    case "\x1B[J": // Clear screen from cursor down
236                        $this->history = array_merge($this->history, array_slice(array_splice($this->screen, $this->y + 1), 0, $this->old_y));
237                        $this->screen = array_merge($this->screen, array_fill($this->y, $this->max_y, ''));
238
239                        $this->history_attrs = array_merge($this->history_attrs, array_slice(array_splice($this->attrs, $this->y + 1), 0, $this->old_y));
240                        $this->attrs = array_merge($this->attrs, array_fill($this->y, $this->max_y, $this->attr_row));
241
242                        if (count($this->history) == $this->max_history) {
243                            array_shift($this->history);
244                            array_shift($this->history_attrs);
245                        }
246                        // fall-through
247                    case "\x1B[K": // Clear screen from cursor right
248                        $this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x);
249
250                        array_splice($this->attrs[$this->y], $this->x + 1, $this->max_x - $this->x, array_fill($this->x, $this->max_x - ($this->x - 1), $this->base_attr_cell));
251                        break;
252                    case "\x1B[2K": // Clear entire line
253                        $this->screen[$this->y] = str_repeat(' ', $this->x);
254                        $this->attrs[$this->y] = $this->attr_row;
255                        break;
256                    case "\x1B[?1h": // set cursor key to application
257                    case "\x1B[?25h": // show the cursor
258                    case "\x1B(B": // set united states g0 character set
259                        break;
260                    case "\x1BE": // Move to next line
261                        $this->newLine();
262                        $this->x = 0;
263                        break;
264                    default:
265                        switch (true) {
266                            case preg_match('#\x1B\[(\d+)B#', $this->ansi, $match): // Move cursor down n lines
267                                $this->old_y = $this->y;
268                                $this->y += (int) $match[1];
269                                break;
270                            case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h
271                                $this->old_x = $this->x;
272                                $this->old_y = $this->y;
273                                $this->x = $match[2] - 1;
274                                $this->y = (int) $match[1] - 1;
275                                break;
276                            case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines
277                                $this->old_x = $this->x;
278                                $this->x += $match[1];
279                                break;
280                            case preg_match('#\x1B\[(\d+)D#', $this->ansi, $match): // Move cursor left n lines
281                                $this->old_x = $this->x;
282                                $this->x -= $match[1];
283                                if ($this->x < 0) {
284                                    $this->x = 0;
285                                }
286                                break;
287                            case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window
288                                break;
289                            case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes
290                                $attr_cell = &$this->attr_cell;
291                                $mods = explode(';', $match[1]);
292                                foreach ($mods as $mod) {
293                                    switch ($mod) {
294                                        case '':
295                                        case '0': // Turn off character attributes
296                                            $attr_cell = clone $this->base_attr_cell;
297                                            break;
298                                        case '1': // Turn bold mode on
299                                            $attr_cell->bold = true;
300                                            break;
301                                        case '4': // Turn underline mode on
302                                            $attr_cell->underline = true;
303                                            break;
304                                        case '5': // Turn blinking mode on
305                                            $attr_cell->blink = true;
306                                            break;
307                                        case '7': // Turn reverse video on
308                                            $attr_cell->reverse = !$attr_cell->reverse;
309                                            $temp = $attr_cell->background;
310                                            $attr_cell->background = $attr_cell->foreground;
311                                            $attr_cell->foreground = $temp;
312                                            break;
313                                        default: // set colors
314                                            //$front = $attr_cell->reverse ? &$attr_cell->background : &$attr_cell->foreground;
315                                            $front = &$attr_cell->{ $attr_cell->reverse ? 'background' : 'foreground' };
316                                            //$back = $attr_cell->reverse ? &$attr_cell->foreground : &$attr_cell->background;
317                                            $back = &$attr_cell->{ $attr_cell->reverse ? 'foreground' : 'background' };
318                                            switch ($mod) {
319                                                // @codingStandardsIgnoreStart
320                                                case '30': $front = 'black'; break;
321                                                case '31': $front = 'red'; break;
322                                                case '32': $front = 'green'; break;
323                                                case '33': $front = 'yellow'; break;
324                                                case '34': $front = 'blue'; break;
325                                                case '35': $front = 'magenta'; break;
326                                                case '36': $front = 'cyan'; break;
327                                                case '37': $front = 'white'; break;
328
329                                                case '40': $back = 'black'; break;
330                                                case '41': $back = 'red'; break;
331                                                case '42': $back = 'green'; break;
332                                                case '43': $back = 'yellow'; break;
333                                                case '44': $back = 'blue'; break;
334                                                case '45': $back = 'magenta'; break;
335                                                case '46': $back = 'cyan'; break;
336                                                case '47': $back = 'white'; break;
337                                                // @codingStandardsIgnoreEnd
338
339                                                default:
340                                                    //user_error('Unsupported attribute: ' . $mod);
341                                                    $this->ansi = '';
342                                                    break 2;
343                                            }
344                                    }
345                                }
346                                break;
347                            default:
348                                //user_error("{$this->ansi} is unsupported\r\n");
349                        }
350                }
351                $this->ansi = '';
352                continue;
353            }
354
355            $this->tokenization[count($this->tokenization) - 1] .= $source[$i];
356            switch ($source[$i]) {
357                case "\r":
358                    $this->x = 0;
359                    break;
360                case "\n":
361                    $this->newLine();
362                    break;
363                case "\x08": // backspace
364                    if ($this->x) {
365                        $this->x--;
366                        $this->attrs[$this->y][$this->x] = clone $this->base_attr_cell;
367                        $this->screen[$this->y] = substr_replace(
368                            $this->screen[$this->y],
369                            $source[$i],
370                            $this->x,
371                            1
372                        );
373                    }
374                    break;
375                case "\x0F": // shift
376                    break;
377                case "\x1B": // start ANSI escape code
378                    $this->tokenization[count($this->tokenization) - 1] = substr($this->tokenization[count($this->tokenization) - 1], 0, -1);
379                    //if (!strlen($this->tokenization[count($this->tokenization) - 1])) {
380                    //    array_pop($this->tokenization);
381                    //}
382                    $this->ansi .= "\x1B";
383                    break;
384                default:
385                    $this->attrs[$this->y][$this->x] = clone $this->attr_cell;
386                    if ($this->x > strlen($this->screen[$this->y])) {
387                        $this->screen[$this->y] = str_repeat(' ', $this->x);
388                    }
389                    $this->screen[$this->y] = substr_replace(
390                        $this->screen[$this->y],
391                        $source[$i],
392                        $this->x,
393                        1
394                    );
395
396                    if ($this->x > $this->max_x) {
397                        $this->x = 0;
398                        $this->newLine();
399                    } else {
400                        $this->x++;
401                    }
402            }
403        }
404    }
405
406    /**
407     * Add a new line
408     *
409     * Also update the $this->screen and $this->history buffers
410     *
411     */
412    private function newLine()
413    {
414        //if ($this->y < $this->max_y) {
415        //    $this->y++;
416        //}
417
418        while ($this->y >= $this->max_y) {
419            $this->history = array_merge($this->history, [array_shift($this->screen)]);
420            $this->screen[] = '';
421
422            $this->history_attrs = array_merge($this->history_attrs, [array_shift($this->attrs)]);
423            $this->attrs[] = $this->attr_row;
424
425            if (count($this->history) >= $this->max_history) {
426                array_shift($this->history);
427                array_shift($this->history_attrs);
428            }
429
430            $this->y--;
431        }
432        $this->y++;
433    }
434
435    /**
436     * Returns the current coordinate without preformating
437     *
438     * @param \stdClass $last_attr
439     * @param \stdClass $cur_attr
440     * @param string $char
441     * @return string
442     */
443    private function processCoordinate(\stdClass $last_attr, \stdClass $cur_attr, $char)
444    {
445        $output = '';
446
447        if ($last_attr != $cur_attr) {
448            $close = $open = '';
449            if ($last_attr->foreground != $cur_attr->foreground) {
450                if ($cur_attr->foreground != 'white') {
451                    $open .= '<span style="color: ' . $cur_attr->foreground . '">';
452                }
453                if ($last_attr->foreground != 'white') {
454                    $close = '</span>' . $close;
455                }
456            }
457            if ($last_attr->background != $cur_attr->background) {
458                if ($cur_attr->background != 'black') {
459                    $open .= '<span style="background: ' . $cur_attr->background . '">';
460                }
461                if ($last_attr->background != 'black') {
462                    $close = '</span>' . $close;
463                }
464            }
465            if ($last_attr->bold != $cur_attr->bold) {
466                if ($cur_attr->bold) {
467                    $open .= '<b>';
468                } else {
469                    $close = '</b>' . $close;
470                }
471            }
472            if ($last_attr->underline != $cur_attr->underline) {
473                if ($cur_attr->underline) {
474                    $open .= '<u>';
475                } else {
476                    $close = '</u>' . $close;
477                }
478            }
479            if ($last_attr->blink != $cur_attr->blink) {
480                if ($cur_attr->blink) {
481                    $open .= '<blink>';
482                } else {
483                    $close = '</blink>' . $close;
484                }
485            }
486            $output .= $close . $open;
487        }
488
489        $output .= htmlspecialchars($char);
490
491        return $output;
492    }
493
494    /**
495     * Returns the current screen without preformating
496     *
497     * @return string
498     */
499    private function getScreenHelper()
500    {
501        $output = '';
502        $last_attr = $this->base_attr_cell;
503        for ($i = 0; $i <= $this->max_y; $i++) {
504            for ($j = 0; $j <= $this->max_x; $j++) {
505                $cur_attr = $this->attrs[$i][$j];
506                $output .= $this->processCoordinate($last_attr, $cur_attr, isset($this->screen[$i][$j]) ? $this->screen[$i][$j] : '');
507                $last_attr = $this->attrs[$i][$j];
508            }
509            $output .= "\r\n";
510        }
511        $output = substr($output, 0, -2);
512        // close any remaining open tags
513        $output .= $this->processCoordinate($last_attr, $this->base_attr_cell, '');
514        return rtrim($output);
515    }
516
517    /**
518     * Returns the current screen
519     *
520     * @return string
521     */
522    public function getScreen()
523    {
524        return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $this->getScreenHelper() . '</pre>';
525    }
526
527    /**
528     * Returns the current screen and the x previous lines
529     *
530     * @return string
531     */
532    public function getHistory()
533    {
534        $scrollback = '';
535        $last_attr = $this->base_attr_cell;
536        for ($i = 0; $i < count($this->history); $i++) {
537            for ($j = 0; $j <= $this->max_x + 1; $j++) {
538                $cur_attr = $this->history_attrs[$i][$j];
539                $scrollback .= $this->processCoordinate($last_attr, $cur_attr, isset($this->history[$i][$j]) ? $this->history[$i][$j] : '');
540                $last_attr = $this->history_attrs[$i][$j];
541            }
542            $scrollback .= "\r\n";
543        }
544        $base_attr_cell = $this->base_attr_cell;
545        $this->base_attr_cell = $last_attr;
546        $scrollback .= $this->getScreen();
547        $this->base_attr_cell = $base_attr_cell;
548
549        return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $scrollback . '</span></pre>';
550    }
551}
552