1<?php 2 3namespace Sabre\VObject; 4 5use 6 InvalidArgumentException; 7 8/** 9 * This is the CLI interface for sabre-vobject. 10 * 11 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 12 * @author Evert Pot (http://evertpot.com/) 13 * @license http://sabre.io/license/ Modified BSD License 14 */ 15class Cli { 16 17 /** 18 * No output 19 * 20 * @var bool 21 */ 22 protected $quiet = false; 23 24 /** 25 * Help display 26 * 27 * @var bool 28 */ 29 protected $showHelp = false; 30 31 /** 32 * Wether to spit out 'mimedir' or 'json' format. 33 * 34 * @var string 35 */ 36 protected $format; 37 38 /** 39 * JSON pretty print 40 * 41 * @var bool 42 */ 43 protected $pretty; 44 45 /** 46 * Source file 47 * 48 * @var string 49 */ 50 protected $inputPath; 51 52 /** 53 * Destination file 54 * 55 * @var string 56 */ 57 protected $outputPath; 58 59 /** 60 * output stream 61 * 62 * @var resource 63 */ 64 protected $stdout; 65 66 /** 67 * stdin 68 * 69 * @var resource 70 */ 71 protected $stdin; 72 73 /** 74 * stderr 75 * 76 * @var resource 77 */ 78 protected $stderr; 79 80 /** 81 * Input format (one of json or mimedir) 82 * 83 * @var string 84 */ 85 protected $inputFormat; 86 87 /** 88 * Makes the parser less strict. 89 * 90 * @var bool 91 */ 92 protected $forgiving = false; 93 94 /** 95 * Main function 96 * 97 * @return int 98 */ 99 public function main(array $argv) { 100 101 // @codeCoverageIgnoreStart 102 // We cannot easily test this, so we'll skip it. Pretty basic anyway. 103 104 if (!$this->stderr) { 105 $this->stderr = fopen('php://stderr', 'w'); 106 } 107 if (!$this->stdout) { 108 $this->stdout = fopen('php://stdout', 'w'); 109 } 110 if (!$this->stdin) { 111 $this->stdin = fopen('php://stdin', 'r'); 112 } 113 114 // @codeCoverageIgnoreEnd 115 116 117 try { 118 119 list($options, $positional) = $this->parseArguments($argv); 120 121 if (isset($options['q'])) { 122 $this->quiet = true; 123 } 124 $this->log($this->colorize('green', "sabre/vobject ") . $this->colorize('yellow', Version::VERSION)); 125 126 foreach($options as $name=>$value) { 127 128 switch($name) { 129 130 case 'q' : 131 // Already handled earlier. 132 break; 133 case 'h' : 134 case 'help' : 135 $this->showHelp(); 136 return 0; 137 break; 138 case 'format' : 139 switch($value) { 140 141 // jcard/jcal documents 142 case 'jcard' : 143 case 'jcal' : 144 145 // specific document versions 146 case 'vcard21' : 147 case 'vcard30' : 148 case 'vcard40' : 149 case 'icalendar20' : 150 151 // specific formats 152 case 'json' : 153 case 'mimedir' : 154 155 // icalendar/vcad 156 case 'icalendar' : 157 case 'vcard' : 158 $this->format = $value; 159 break; 160 161 default : 162 throw new InvalidArgumentException('Unknown format: ' . $value); 163 164 } 165 break; 166 case 'pretty' : 167 if (version_compare(PHP_VERSION, '5.4.0') >= 0) { 168 $this->pretty = true; 169 } 170 break; 171 case 'forgiving' : 172 $this->forgiving = true; 173 break; 174 case 'inputformat' : 175 switch($value) { 176 // json formats 177 case 'jcard' : 178 case 'jcal' : 179 case 'json' : 180 $this->inputFormat = 'json'; 181 break; 182 183 // mimedir formats 184 case 'mimedir' : 185 case 'icalendar' : 186 case 'vcard' : 187 case 'vcard21' : 188 case 'vcard30' : 189 case 'vcard40' : 190 case 'icalendar20' : 191 192 $this->inputFormat = 'mimedir'; 193 break; 194 195 default : 196 throw new InvalidArgumentException('Unknown format: ' . $value); 197 198 } 199 break; 200 default : 201 throw new InvalidArgumentException('Unknown option: ' . $name); 202 203 } 204 205 } 206 207 if (count($positional) === 0) { 208 $this->showHelp(); 209 return 1; 210 } 211 212 if (count($positional) === 1) { 213 throw new InvalidArgumentException('Inputfile is a required argument'); 214 } 215 216 if (count($positional) > 3) { 217 throw new InvalidArgumentException('Too many arguments'); 218 } 219 220 if (!in_array($positional[0], array('validate','repair','convert','color'))) { 221 throw new InvalidArgumentException('Uknown command: ' . $positional[0]); 222 } 223 224 } catch (InvalidArgumentException $e) { 225 $this->showHelp(); 226 $this->log('Error: ' . $e->getMessage(), 'red'); 227 return 1; 228 } 229 230 $command = $positional[0]; 231 232 $this->inputPath = $positional[1]; 233 $this->outputPath = isset($positional[2])?$positional[2]:'-'; 234 235 if ($this->outputPath !== '-') { 236 $this->stdout = fopen($this->outputPath, 'w'); 237 } 238 239 if (!$this->inputFormat) { 240 if (substr($this->inputPath, -5)==='.json') { 241 $this->inputFormat = 'json'; 242 } else { 243 $this->inputFormat = 'mimedir'; 244 } 245 } 246 if (!$this->format) { 247 if (substr($this->outputPath,-5)==='.json') { 248 $this->format = 'json'; 249 } else { 250 $this->format = 'mimedir'; 251 } 252 } 253 254 255 $realCode = 0; 256 257 try { 258 259 while($input = $this->readInput()) { 260 261 $returnCode = $this->$command($input); 262 if ($returnCode!==0) $realCode = $returnCode; 263 264 } 265 266 } catch (EofException $e) { 267 // end of file 268 } catch (\Exception $e) { 269 $this->log('Error: ' . $e->getMessage(),'red'); 270 return 2; 271 } 272 273 return $realCode; 274 275 } 276 277 /** 278 * Shows the help message. 279 * 280 * @return void 281 */ 282 protected function showHelp() { 283 284 $this->log('Usage:', 'yellow'); 285 $this->log(" vobject [options] command [arguments]"); 286 $this->log(''); 287 $this->log('Options:', 'yellow'); 288 $this->log($this->colorize('green', ' -q ') . "Don't output anything."); 289 $this->log($this->colorize('green', ' -help -h ') . "Display this help message."); 290 $this->log($this->colorize('green', ' --format ') . "Convert to a specific format. Must be one of: vcard, vcard21,"); 291 $this->log($this->colorize('green', ' --forgiving ') . "Makes the parser less strict."); 292 $this->log(" vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir."); 293 $this->log($this->colorize('green', ' --inputformat ') . "If the input format cannot be guessed from the extension, it"); 294 $this->log(" must be specified here."); 295 // Only PHP 5.4 and up 296 if (version_compare(PHP_VERSION, '5.4.0') >= 0) { 297 $this->log($this->colorize('green', ' --pretty ') . "json pretty-print."); 298 } 299 $this->log(''); 300 $this->log('Commands:', 'yellow'); 301 $this->log($this->colorize('green', ' validate') . ' source_file Validates a file for correctness.'); 302 $this->log($this->colorize('green', ' repair') . ' source_file [output_file] Repairs a file.'); 303 $this->log($this->colorize('green', ' convert') . ' source_file [output_file] Converts a file.'); 304 $this->log($this->colorize('green', ' color') . ' source_file Colorize a file, useful for debbugging.'); 305 $this->log( 306 <<<HELP 307 308If source_file is set as '-', STDIN will be used. 309If output_file is omitted, STDOUT will be used. 310All other output is sent to STDERR. 311 312HELP 313 ); 314 315 $this->log('Examples:', 'yellow'); 316 $this->log(' vobject convert contact.vcf contact.json'); 317 $this->log(' vobject convert --format=vcard40 old.vcf new.vcf'); 318 $this->log(' vobject convert --inputformat=json --format=mimedir - -'); 319 $this->log(' vobject color calendar.ics'); 320 $this->log(''); 321 $this->log('https://github.com/fruux/sabre-vobject','purple'); 322 323 } 324 325 /** 326 * Validates a VObject file 327 * 328 * @param Component $vObj 329 * @return int 330 */ 331 protected function validate($vObj) { 332 333 $returnCode = 0; 334 335 switch($vObj->name) { 336 case 'VCALENDAR' : 337 $this->log("iCalendar: " . (string)$vObj->VERSION); 338 break; 339 case 'VCARD' : 340 $this->log("vCard: " . (string)$vObj->VERSION); 341 break; 342 } 343 344 $warnings = $vObj->validate(); 345 if (!count($warnings)) { 346 $this->log(" No warnings!"); 347 } else { 348 349 $levels = array( 350 1 => 'REPAIRED', 351 2 => 'WARNING', 352 3 => 'ERROR', 353 ); 354 $returnCode = 2; 355 foreach($warnings as $warn) { 356 357 $extra = ''; 358 if ($warn['node'] instanceof Property) { 359 $extra = ' (property: "' . $warn['node']->name . '")'; 360 } 361 $this->log(" [" . $levels[$warn['level']] . '] ' . $warn['message'] . $extra); 362 363 } 364 365 } 366 367 return $returnCode; 368 369 } 370 371 /** 372 * Repairs a VObject file 373 * 374 * @param Component $vObj 375 * @return int 376 */ 377 protected function repair($vObj) { 378 379 $returnCode = 0; 380 381 switch($vObj->name) { 382 case 'VCALENDAR' : 383 $this->log("iCalendar: " . (string)$vObj->VERSION); 384 break; 385 case 'VCARD' : 386 $this->log("vCard: " . (string)$vObj->VERSION); 387 break; 388 } 389 390 $warnings = $vObj->validate(Node::REPAIR); 391 if (!count($warnings)) { 392 $this->log(" No warnings!"); 393 } else { 394 395 $levels = array( 396 1 => 'REPAIRED', 397 2 => 'WARNING', 398 3 => 'ERROR', 399 ); 400 $returnCode = 2; 401 foreach($warnings as $warn) { 402 403 $extra = ''; 404 if ($warn['node'] instanceof Property) { 405 $extra = ' (property: "' . $warn['node']->name . '")'; 406 } 407 $this->log(" [" . $levels[$warn['level']] . '] ' . $warn['message'] . $extra); 408 409 } 410 411 } 412 fwrite($this->stdout, $vObj->serialize()); 413 414 return $returnCode; 415 416 } 417 418 /** 419 * Converts a vObject file to a new format. 420 * 421 * @param Component $vObj 422 * @return int 423 */ 424 protected function convert($vObj) { 425 426 $json = false; 427 $convertVersion = null; 428 $forceInput = null; 429 430 switch($this->format) { 431 case 'json' : 432 $json = true; 433 if ($vObj->name === 'VCARD') { 434 $convertVersion = Document::VCARD40; 435 } 436 break; 437 case 'jcard' : 438 $json = true; 439 $forceInput = 'VCARD'; 440 $convertVersion = Document::VCARD40; 441 break; 442 case 'jcal' : 443 $json = true; 444 $forceInput = 'VCALENDAR'; 445 break; 446 case 'mimedir' : 447 case 'icalendar' : 448 case 'icalendar20' : 449 case 'vcard' : 450 break; 451 case 'vcard21' : 452 $convertVersion = Document::VCARD21; 453 break; 454 case 'vcard30' : 455 $convertVersion = Document::VCARD30; 456 break; 457 case 'vcard40' : 458 $convertVersion = Document::VCARD40; 459 break; 460 461 } 462 463 if ($forceInput && $vObj->name !== $forceInput) { 464 throw new \Exception('You cannot convert a ' . strtolower($vObj->name) . ' to ' . $this->format); 465 } 466 if ($convertVersion) { 467 $vObj = $vObj->convert($convertVersion); 468 } 469 if ($json) { 470 $jsonOptions = 0; 471 if ($this->pretty) { 472 $jsonOptions = JSON_PRETTY_PRINT; 473 } 474 fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions)); 475 } else { 476 fwrite($this->stdout, $vObj->serialize()); 477 } 478 479 return 0; 480 481 } 482 483 /** 484 * Colorizes a file 485 * 486 * @param Component $vObj 487 * @return int 488 */ 489 protected function color($vObj) { 490 491 fwrite($this->stdout, $this->serializeComponent($vObj)); 492 493 } 494 495 /** 496 * Returns an ansi color string for a color name. 497 * 498 * @param string $color 499 * @return string 500 */ 501 protected function colorize($color, $str, $resetTo = 'default') { 502 503 $colors = array( 504 'cyan' => '1;36', 505 'red' => '1;31', 506 'yellow' => '1;33', 507 'blue' => '0;34', 508 'green' => '0;32', 509 'default' => '0', 510 'purple' => '0;35', 511 ); 512 return "\033[" . $colors[$color] . 'm' . $str . "\033[".$colors[$resetTo]."m"; 513 514 } 515 516 /** 517 * Writes out a string in specific color. 518 * 519 * @param string $color 520 * @param string $str 521 * @return void 522 */ 523 protected function cWrite($color, $str) { 524 525 fwrite($this->stdout, $this->colorize($color, $str)); 526 527 } 528 529 protected function serializeComponent(Component $vObj) { 530 531 $this->cWrite('cyan', 'BEGIN'); 532 $this->cWrite('red', ':'); 533 $this->cWrite('yellow', $vObj->name . "\n"); 534 535 /** 536 * Gives a component a 'score' for sorting purposes. 537 * 538 * This is solely used by the childrenSort method. 539 * 540 * A higher score means the item will be lower in the list. 541 * To avoid score collisions, each "score category" has a reasonable 542 * space to accomodate elements. The $key is added to the $score to 543 * preserve the original relative order of elements. 544 * 545 * @param int $key 546 * @param array $array 547 * @return int 548 */ 549 $sortScore = function($key, $array) { 550 551 if ($array[$key] instanceof Component) { 552 553 // We want to encode VTIMEZONE first, this is a personal 554 // preference. 555 if ($array[$key]->name === 'VTIMEZONE') { 556 $score=300000000; 557 return $score+$key; 558 } else { 559 $score=400000000; 560 return $score+$key; 561 } 562 } else { 563 // Properties get encoded first 564 // VCARD version 4.0 wants the VERSION property to appear first 565 if ($array[$key] instanceof Property) { 566 if ($array[$key]->name === 'VERSION') { 567 $score=100000000; 568 return $score+$key; 569 } else { 570 // All other properties 571 $score=200000000; 572 return $score+$key; 573 } 574 } 575 } 576 577 }; 578 579 $tmp = $vObj->children; 580 uksort( 581 $vObj->children, 582 function($a, $b) use ($sortScore, $tmp) { 583 584 $sA = $sortScore($a, $tmp); 585 $sB = $sortScore($b, $tmp); 586 587 return $sA - $sB; 588 589 } 590 ); 591 592 foreach($vObj->children as $child) { 593 if ($child instanceof Component) { 594 $this->serializeComponent($child); 595 } else { 596 $this->serializeProperty($child); 597 } 598 } 599 600 $this->cWrite('cyan', 'END'); 601 $this->cWrite('red', ':'); 602 $this->cWrite('yellow', $vObj->name . "\n"); 603 604 } 605 606 /** 607 * Colorizes a property. 608 * 609 * @param Property $property 610 * @return void 611 */ 612 protected function serializeProperty(Property $property) { 613 614 if ($property->group) { 615 $this->cWrite('default', $property->group); 616 $this->cWrite('red', '.'); 617 } 618 619 $str = ''; 620 $this->cWrite('yellow', $property->name); 621 622 foreach($property->parameters as $param) { 623 624 $this->cWrite('red',';'); 625 $this->cWrite('blue', $param->serialize()); 626 627 } 628 $this->cWrite('red',':'); 629 630 if ($property instanceof Property\Binary) { 631 632 $this->cWrite('default', 'embedded binary stripped. (' . strlen($property->getValue()) . ' bytes)'); 633 634 } else { 635 636 $parts = $property->getParts(); 637 $first1 = true; 638 // Looping through property values 639 foreach($parts as $part) { 640 if ($first1) { 641 $first1 = false; 642 } else { 643 $this->cWrite('red', $property->delimiter); 644 } 645 $first2 = true; 646 // Looping through property sub-values 647 foreach((array)$part as $subPart) { 648 if ($first2) { 649 $first2 = false; 650 } else { 651 // The sub-value delimiter is always comma 652 $this->cWrite('red', ','); 653 } 654 655 $subPart = strtr( 656 $subPart, 657 array( 658 '\\' => $this->colorize('purple', '\\\\', 'green'), 659 ';' => $this->colorize('purple', '\;', 'green'), 660 ',' => $this->colorize('purple', '\,', 'green'), 661 "\n" => $this->colorize('purple', "\\n\n\t", 'green'), 662 "\r" => "", 663 ) 664 ); 665 666 $this->cWrite('green', $subPart); 667 } 668 } 669 670 } 671 $this->cWrite("default", "\n"); 672 673 } 674 675 /** 676 * Parses the list of arguments. 677 * 678 * @param array $argv 679 * @return void 680 */ 681 protected function parseArguments(array $argv) { 682 683 $positional = array(); 684 $options = array(); 685 686 for($ii=0; $ii < count($argv); $ii++) { 687 688 // Skipping the first argument. 689 if ($ii===0) continue; 690 691 $v = $argv[$ii]; 692 693 if (substr($v,0,2)==='--') { 694 // This is a long-form option. 695 $optionName = substr($v,2); 696 $optionValue = true; 697 if (strpos($optionName,'=')) { 698 list($optionName, $optionValue) = explode('=', $optionName); 699 } 700 $options[$optionName] = $optionValue; 701 } elseif (substr($v,0,1) === '-' && strlen($v)>1) { 702 // This is a short-form option. 703 foreach(str_split(substr($v,1)) as $option) { 704 $options[$option] = true; 705 } 706 707 } else { 708 709 $positional[] = $v; 710 711 } 712 713 } 714 715 return array($options, $positional); 716 717 } 718 719 protected $parser; 720 721 /** 722 * Reads the input file 723 * 724 * @return Component 725 */ 726 protected function readInput() { 727 728 if (!$this->parser) { 729 if ($this->inputPath!=='-') { 730 $this->stdin = fopen($this->inputPath,'r'); 731 } 732 733 if ($this->inputFormat === 'mimedir') { 734 $this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving?Reader::OPTION_FORGIVING:0)); 735 } else { 736 $this->parser = new Parser\Json($this->stdin, ($this->forgiving?Reader::OPTION_FORGIVING:0)); 737 } 738 } 739 740 return $this->parser->parse(); 741 742 } 743 744 /** 745 * Sends a message to STDERR. 746 * 747 * @param string $msg 748 * @return void 749 */ 750 protected function log($msg, $color = 'default') { 751 752 if (!$this->quiet) { 753 if ($color!=='default') { 754 $msg = $this->colorize($color, $msg); 755 } 756 fwrite($this->stderr, $msg . "\n"); 757 } 758 759 } 760 761} 762