Mercurial > hg > rc1
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 } |