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 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