comparison vendor/sabre/vobject/lib/Cli.php @ 7:430dbd5346f7

vendor sabre as distributed
author Charlie Root
date Sat, 13 Jan 2018 09:06:10 -0500
parents
children
comparison
equal deleted inserted replaced
6:cec75ba50afc 7:430dbd5346f7
1 <?php
2
3 namespace Sabre\VObject;
4
5 use
6 InvalidArgumentException;
7
8 /**
9 * This is the CLI interface for sabre-vobject.
10 *
11 * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/).
12 * @author Evert Pot (http://evertpot.com/)
13 * @license http://sabre.io/license/ Modified BSD License
14 */
15 class 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
308 If source_file is set as '-', STDIN will be used.
309 If output_file is omitted, STDOUT will be used.
310 All other output is sent to STDERR.
311
312 HELP
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 }