You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
771 lines
21 KiB
771 lines
21 KiB
<?php |
|
|
|
namespace Sabre\VObject; |
|
|
|
use |
|
InvalidArgumentException; |
|
|
|
/** |
|
* This is the CLI interface for sabre-vobject. |
|
* |
|
* @copyright Copyright (C) fruux GmbH (https://fruux.com/) |
|
* @author Evert Pot (http://evertpot.com/) |
|
* @license http://sabre.io/license/ Modified BSD License |
|
*/ |
|
class Cli { |
|
|
|
/** |
|
* No output. |
|
* |
|
* @var bool |
|
*/ |
|
protected $quiet = false; |
|
|
|
/** |
|
* Help display. |
|
* |
|
* @var bool |
|
*/ |
|
protected $showHelp = false; |
|
|
|
/** |
|
* Wether to spit out 'mimedir' or 'json' format. |
|
* |
|
* @var string |
|
*/ |
|
protected $format; |
|
|
|
/** |
|
* JSON pretty print. |
|
* |
|
* @var bool |
|
*/ |
|
protected $pretty; |
|
|
|
/** |
|
* Source file. |
|
* |
|
* @var string |
|
*/ |
|
protected $inputPath; |
|
|
|
/** |
|
* Destination file. |
|
* |
|
* @var string |
|
*/ |
|
protected $outputPath; |
|
|
|
/** |
|
* output stream. |
|
* |
|
* @var resource |
|
*/ |
|
protected $stdout; |
|
|
|
/** |
|
* stdin. |
|
* |
|
* @var resource |
|
*/ |
|
protected $stdin; |
|
|
|
/** |
|
* stderr. |
|
* |
|
* @var resource |
|
*/ |
|
protected $stderr; |
|
|
|
/** |
|
* Input format (one of json or mimedir). |
|
* |
|
* @var string |
|
*/ |
|
protected $inputFormat; |
|
|
|
/** |
|
* Makes the parser less strict. |
|
* |
|
* @var bool |
|
*/ |
|
protected $forgiving = false; |
|
|
|
/** |
|
* Main function. |
|
* |
|
* @return int |
|
*/ |
|
function main(array $argv) { |
|
|
|
// @codeCoverageIgnoreStart |
|
// We cannot easily test this, so we'll skip it. Pretty basic anyway. |
|
|
|
if (!$this->stderr) { |
|
$this->stderr = fopen('php://stderr', 'w'); |
|
} |
|
if (!$this->stdout) { |
|
$this->stdout = fopen('php://stdout', 'w'); |
|
} |
|
if (!$this->stdin) { |
|
$this->stdin = fopen('php://stdin', 'r'); |
|
} |
|
|
|
// @codeCoverageIgnoreEnd |
|
|
|
|
|
try { |
|
|
|
list($options, $positional) = $this->parseArguments($argv); |
|
|
|
if (isset($options['q'])) { |
|
$this->quiet = true; |
|
} |
|
$this->log($this->colorize('green', "sabre/vobject ") . $this->colorize('yellow', Version::VERSION)); |
|
|
|
foreach ($options as $name => $value) { |
|
|
|
switch ($name) { |
|
|
|
case 'q' : |
|
// Already handled earlier. |
|
break; |
|
case 'h' : |
|
case 'help' : |
|
$this->showHelp(); |
|
return 0; |
|
break; |
|
case 'format' : |
|
switch ($value) { |
|
|
|
// jcard/jcal documents |
|
case 'jcard' : |
|
case 'jcal' : |
|
|
|
// specific document versions |
|
case 'vcard21' : |
|
case 'vcard30' : |
|
case 'vcard40' : |
|
case 'icalendar20' : |
|
|
|
// specific formats |
|
case 'json' : |
|
case 'mimedir' : |
|
|
|
// icalendar/vcad |
|
case 'icalendar' : |
|
case 'vcard' : |
|
$this->format = $value; |
|
break; |
|
|
|
default : |
|
throw new InvalidArgumentException('Unknown format: ' . $value); |
|
|
|
} |
|
break; |
|
case 'pretty' : |
|
if (version_compare(PHP_VERSION, '5.4.0') >= 0) { |
|
$this->pretty = true; |
|
} |
|
break; |
|
case 'forgiving' : |
|
$this->forgiving = true; |
|
break; |
|
case 'inputformat' : |
|
switch ($value) { |
|
// json formats |
|
case 'jcard' : |
|
case 'jcal' : |
|
case 'json' : |
|
$this->inputFormat = 'json'; |
|
break; |
|
|
|
// mimedir formats |
|
case 'mimedir' : |
|
case 'icalendar' : |
|
case 'vcard' : |
|
case 'vcard21' : |
|
case 'vcard30' : |
|
case 'vcard40' : |
|
case 'icalendar20' : |
|
|
|
$this->inputFormat = 'mimedir'; |
|
break; |
|
|
|
default : |
|
throw new InvalidArgumentException('Unknown format: ' . $value); |
|
|
|
} |
|
break; |
|
default : |
|
throw new InvalidArgumentException('Unknown option: ' . $name); |
|
|
|
} |
|
|
|
} |
|
|
|
if (count($positional) === 0) { |
|
$this->showHelp(); |
|
return 1; |
|
} |
|
|
|
if (count($positional) === 1) { |
|
throw new InvalidArgumentException('Inputfile is a required argument'); |
|
} |
|
|
|
if (count($positional) > 3) { |
|
throw new InvalidArgumentException('Too many arguments'); |
|
} |
|
|
|
if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) { |
|
throw new InvalidArgumentException('Uknown command: ' . $positional[0]); |
|
} |
|
|
|
} catch (InvalidArgumentException $e) { |
|
$this->showHelp(); |
|
$this->log('Error: ' . $e->getMessage(), 'red'); |
|
return 1; |
|
} |
|
|
|
$command = $positional[0]; |
|
|
|
$this->inputPath = $positional[1]; |
|
$this->outputPath = isset($positional[2]) ? $positional[2] : '-'; |
|
|
|
if ($this->outputPath !== '-') { |
|
$this->stdout = fopen($this->outputPath, 'w'); |
|
} |
|
|
|
if (!$this->inputFormat) { |
|
if (substr($this->inputPath, -5) === '.json') { |
|
$this->inputFormat = 'json'; |
|
} else { |
|
$this->inputFormat = 'mimedir'; |
|
} |
|
} |
|
if (!$this->format) { |
|
if (substr($this->outputPath, -5) === '.json') { |
|
$this->format = 'json'; |
|
} else { |
|
$this->format = 'mimedir'; |
|
} |
|
} |
|
|
|
|
|
$realCode = 0; |
|
|
|
try { |
|
|
|
while ($input = $this->readInput()) { |
|
|
|
$returnCode = $this->$command($input); |
|
if ($returnCode !== 0) $realCode = $returnCode; |
|
|
|
} |
|
|
|
} catch (EofException $e) { |
|
// end of file |
|
} catch (\Exception $e) { |
|
$this->log('Error: ' . $e->getMessage(), 'red'); |
|
return 2; |
|
} |
|
|
|
return $realCode; |
|
|
|
} |
|
|
|
/** |
|
* Shows the help message. |
|
* |
|
* @return void |
|
*/ |
|
protected function showHelp() { |
|
|
|
$this->log('Usage:', 'yellow'); |
|
$this->log(" vobject [options] command [arguments]"); |
|
$this->log(''); |
|
$this->log('Options:', 'yellow'); |
|
$this->log($this->colorize('green', ' -q ') . "Don't output anything."); |
|
$this->log($this->colorize('green', ' -help -h ') . "Display this help message."); |
|
$this->log($this->colorize('green', ' --format ') . "Convert to a specific format. Must be one of: vcard, vcard21,"); |
|
$this->log($this->colorize('green', ' --forgiving ') . "Makes the parser less strict."); |
|
$this->log(" vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir."); |
|
$this->log($this->colorize('green', ' --inputformat ') . "If the input format cannot be guessed from the extension, it"); |
|
$this->log(" must be specified here."); |
|
// Only PHP 5.4 and up |
|
if (version_compare(PHP_VERSION, '5.4.0') >= 0) { |
|
$this->log($this->colorize('green', ' --pretty ') . "json pretty-print."); |
|
} |
|
$this->log(''); |
|
$this->log('Commands:', 'yellow'); |
|
$this->log($this->colorize('green', ' validate') . ' source_file Validates a file for correctness.'); |
|
$this->log($this->colorize('green', ' repair') . ' source_file [output_file] Repairs a file.'); |
|
$this->log($this->colorize('green', ' convert') . ' source_file [output_file] Converts a file.'); |
|
$this->log($this->colorize('green', ' color') . ' source_file Colorize a file, useful for debbugging.'); |
|
$this->log( |
|
<<<HELP |
|
|
|
If source_file is set as '-', STDIN will be used. |
|
If output_file is omitted, STDOUT will be used. |
|
All other output is sent to STDERR. |
|
|
|
HELP |
|
); |
|
|
|
$this->log('Examples:', 'yellow'); |
|
$this->log(' vobject convert contact.vcf contact.json'); |
|
$this->log(' vobject convert --format=vcard40 old.vcf new.vcf'); |
|
$this->log(' vobject convert --inputformat=json --format=mimedir - -'); |
|
$this->log(' vobject color calendar.ics'); |
|
$this->log(''); |
|
$this->log('https://github.com/fruux/sabre-vobject', 'purple'); |
|
|
|
} |
|
|
|
/** |
|
* Validates a VObject file. |
|
* |
|
* @param Component $vObj |
|
* |
|
* @return int |
|
*/ |
|
protected function validate(Component $vObj) { |
|
|
|
$returnCode = 0; |
|
|
|
switch ($vObj->name) { |
|
case 'VCALENDAR' : |
|
$this->log("iCalendar: " . (string)$vObj->VERSION); |
|
break; |
|
case 'VCARD' : |
|
$this->log("vCard: " . (string)$vObj->VERSION); |
|
break; |
|
} |
|
|
|
$warnings = $vObj->validate(); |
|
if (!count($warnings)) { |
|
$this->log(" No warnings!"); |
|
} else { |
|
|
|
$levels = [ |
|
1 => 'REPAIRED', |
|
2 => 'WARNING', |
|
3 => 'ERROR', |
|
]; |
|
$returnCode = 2; |
|
foreach ($warnings as $warn) { |
|
|
|
$extra = ''; |
|
if ($warn['node'] instanceof Property) { |
|
$extra = ' (property: "' . $warn['node']->name . '")'; |
|
} |
|
$this->log(" [" . $levels[$warn['level']] . '] ' . $warn['message'] . $extra); |
|
|
|
} |
|
|
|
} |
|
|
|
return $returnCode; |
|
|
|
} |
|
|
|
/** |
|
* Repairs a VObject file. |
|
* |
|
* @param Component $vObj |
|
* |
|
* @return int |
|
*/ |
|
protected function repair(Component $vObj) { |
|
|
|
$returnCode = 0; |
|
|
|
switch ($vObj->name) { |
|
case 'VCALENDAR' : |
|
$this->log("iCalendar: " . (string)$vObj->VERSION); |
|
break; |
|
case 'VCARD' : |
|
$this->log("vCard: " . (string)$vObj->VERSION); |
|
break; |
|
} |
|
|
|
$warnings = $vObj->validate(Node::REPAIR); |
|
if (!count($warnings)) { |
|
$this->log(" No warnings!"); |
|
} else { |
|
|
|
$levels = [ |
|
1 => 'REPAIRED', |
|
2 => 'WARNING', |
|
3 => 'ERROR', |
|
]; |
|
$returnCode = 2; |
|
foreach ($warnings as $warn) { |
|
|
|
$extra = ''; |
|
if ($warn['node'] instanceof Property) { |
|
$extra = ' (property: "' . $warn['node']->name . '")'; |
|
} |
|
$this->log(" [" . $levels[$warn['level']] . '] ' . $warn['message'] . $extra); |
|
|
|
} |
|
|
|
} |
|
fwrite($this->stdout, $vObj->serialize()); |
|
|
|
return $returnCode; |
|
|
|
} |
|
|
|
/** |
|
* Converts a vObject file to a new format. |
|
* |
|
* @param Component $vObj |
|
* |
|
* @return int |
|
*/ |
|
protected function convert($vObj) { |
|
|
|
$json = false; |
|
$convertVersion = null; |
|
$forceInput = null; |
|
|
|
switch ($this->format) { |
|
case 'json' : |
|
$json = true; |
|
if ($vObj->name === 'VCARD') { |
|
$convertVersion = Document::VCARD40; |
|
} |
|
break; |
|
case 'jcard' : |
|
$json = true; |
|
$forceInput = 'VCARD'; |
|
$convertVersion = Document::VCARD40; |
|
break; |
|
case 'jcal' : |
|
$json = true; |
|
$forceInput = 'VCALENDAR'; |
|
break; |
|
case 'mimedir' : |
|
case 'icalendar' : |
|
case 'icalendar20' : |
|
case 'vcard' : |
|
break; |
|
case 'vcard21' : |
|
$convertVersion = Document::VCARD21; |
|
break; |
|
case 'vcard30' : |
|
$convertVersion = Document::VCARD30; |
|
break; |
|
case 'vcard40' : |
|
$convertVersion = Document::VCARD40; |
|
break; |
|
|
|
} |
|
|
|
if ($forceInput && $vObj->name !== $forceInput) { |
|
throw new \Exception('You cannot convert a ' . strtolower($vObj->name) . ' to ' . $this->format); |
|
} |
|
if ($convertVersion) { |
|
$vObj = $vObj->convert($convertVersion); |
|
} |
|
if ($json) { |
|
$jsonOptions = 0; |
|
if ($this->pretty) { |
|
$jsonOptions = JSON_PRETTY_PRINT; |
|
} |
|
fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions)); |
|
} else { |
|
fwrite($this->stdout, $vObj->serialize()); |
|
} |
|
|
|
return 0; |
|
|
|
} |
|
|
|
/** |
|
* Colorizes a file. |
|
* |
|
* @param Component $vObj |
|
* |
|
* @return int |
|
*/ |
|
protected function color($vObj) { |
|
|
|
fwrite($this->stdout, $this->serializeComponent($vObj)); |
|
|
|
} |
|
|
|
/** |
|
* Returns an ansi color string for a color name. |
|
* |
|
* @param string $color |
|
* |
|
* @return string |
|
*/ |
|
protected function colorize($color, $str, $resetTo = 'default') { |
|
|
|
$colors = [ |
|
'cyan' => '1;36', |
|
'red' => '1;31', |
|
'yellow' => '1;33', |
|
'blue' => '0;34', |
|
'green' => '0;32', |
|
'default' => '0', |
|
'purple' => '0;35', |
|
]; |
|
return "\033[" . $colors[$color] . 'm' . $str . "\033[" . $colors[$resetTo] . "m"; |
|
|
|
} |
|
|
|
/** |
|
* Writes out a string in specific color. |
|
* |
|
* @param string $color |
|
* @param string $str |
|
* |
|
* @return void |
|
*/ |
|
protected function cWrite($color, $str) { |
|
|
|
fwrite($this->stdout, $this->colorize($color, $str)); |
|
|
|
} |
|
|
|
protected function serializeComponent(Component $vObj) { |
|
|
|
$this->cWrite('cyan', 'BEGIN'); |
|
$this->cWrite('red', ':'); |
|
$this->cWrite('yellow', $vObj->name . "\n"); |
|
|
|
/** |
|
* Gives a component a 'score' for sorting purposes. |
|
* |
|
* This is solely used by the childrenSort method. |
|
* |
|
* A higher score means the item will be lower in the list. |
|
* To avoid score collisions, each "score category" has a reasonable |
|
* space to accomodate elements. The $key is added to the $score to |
|
* preserve the original relative order of elements. |
|
* |
|
* @param int $key |
|
* @param array $array |
|
* |
|
* @return int |
|
*/ |
|
$sortScore = function($key, $array) { |
|
|
|
if ($array[$key] instanceof Component) { |
|
|
|
// We want to encode VTIMEZONE first, this is a personal |
|
// preference. |
|
if ($array[$key]->name === 'VTIMEZONE') { |
|
$score = 300000000; |
|
return $score + $key; |
|
} else { |
|
$score = 400000000; |
|
return $score + $key; |
|
} |
|
} else { |
|
// Properties get encoded first |
|
// VCARD version 4.0 wants the VERSION property to appear first |
|
if ($array[$key] instanceof Property) { |
|
if ($array[$key]->name === 'VERSION') { |
|
$score = 100000000; |
|
return $score + $key; |
|
} else { |
|
// All other properties |
|
$score = 200000000; |
|
return $score + $key; |
|
} |
|
} |
|
} |
|
|
|
}; |
|
|
|
$children = $vObj->children(); |
|
$tmp = $children; |
|
uksort( |
|
$children, |
|
function($a, $b) use ($sortScore, $tmp) { |
|
|
|
$sA = $sortScore($a, $tmp); |
|
$sB = $sortScore($b, $tmp); |
|
|
|
return $sA - $sB; |
|
|
|
} |
|
); |
|
|
|
foreach ($children as $child) { |
|
if ($child instanceof Component) { |
|
$this->serializeComponent($child); |
|
} else { |
|
$this->serializeProperty($child); |
|
} |
|
} |
|
|
|
$this->cWrite('cyan', 'END'); |
|
$this->cWrite('red', ':'); |
|
$this->cWrite('yellow', $vObj->name . "\n"); |
|
|
|
} |
|
|
|
/** |
|
* Colorizes a property. |
|
* |
|
* @param Property $property |
|
* |
|
* @return void |
|
*/ |
|
protected function serializeProperty(Property $property) { |
|
|
|
if ($property->group) { |
|
$this->cWrite('default', $property->group); |
|
$this->cWrite('red', '.'); |
|
} |
|
|
|
$this->cWrite('yellow', $property->name); |
|
|
|
foreach ($property->parameters as $param) { |
|
|
|
$this->cWrite('red', ';'); |
|
$this->cWrite('blue', $param->serialize()); |
|
|
|
} |
|
$this->cWrite('red', ':'); |
|
|
|
if ($property instanceof Property\Binary) { |
|
|
|
$this->cWrite('default', 'embedded binary stripped. (' . strlen($property->getValue()) . ' bytes)'); |
|
|
|
} else { |
|
|
|
$parts = $property->getParts(); |
|
$first1 = true; |
|
// Looping through property values |
|
foreach ($parts as $part) { |
|
if ($first1) { |
|
$first1 = false; |
|
} else { |
|
$this->cWrite('red', $property->delimiter); |
|
} |
|
$first2 = true; |
|
// Looping through property sub-values |
|
foreach ((array)$part as $subPart) { |
|
if ($first2) { |
|
$first2 = false; |
|
} else { |
|
// The sub-value delimiter is always comma |
|
$this->cWrite('red', ','); |
|
} |
|
|
|
$subPart = strtr( |
|
$subPart, |
|
[ |
|
'\\' => $this->colorize('purple', '\\\\', 'green'), |
|
';' => $this->colorize('purple', '\;', 'green'), |
|
',' => $this->colorize('purple', '\,', 'green'), |
|
"\n" => $this->colorize('purple', "\\n\n\t", 'green'), |
|
"\r" => "", |
|
] |
|
); |
|
|
|
$this->cWrite('green', $subPart); |
|
} |
|
} |
|
|
|
} |
|
$this->cWrite("default", "\n"); |
|
|
|
} |
|
|
|
/** |
|
* Parses the list of arguments. |
|
* |
|
* @param array $argv |
|
* |
|
* @return void |
|
*/ |
|
protected function parseArguments(array $argv) { |
|
|
|
$positional = []; |
|
$options = []; |
|
|
|
for ($ii = 0; $ii < count($argv); $ii++) { |
|
|
|
// Skipping the first argument. |
|
if ($ii === 0) continue; |
|
|
|
$v = $argv[$ii]; |
|
|
|
if (substr($v, 0, 2) === '--') { |
|
// This is a long-form option. |
|
$optionName = substr($v, 2); |
|
$optionValue = true; |
|
if (strpos($optionName, '=')) { |
|
list($optionName, $optionValue) = explode('=', $optionName); |
|
} |
|
$options[$optionName] = $optionValue; |
|
} elseif (substr($v, 0, 1) === '-' && strlen($v) > 1) { |
|
// This is a short-form option. |
|
foreach (str_split(substr($v, 1)) as $option) { |
|
$options[$option] = true; |
|
} |
|
|
|
} else { |
|
|
|
$positional[] = $v; |
|
|
|
} |
|
|
|
} |
|
|
|
return [$options, $positional]; |
|
|
|
} |
|
|
|
protected $parser; |
|
|
|
/** |
|
* Reads the input file. |
|
* |
|
* @return Component |
|
*/ |
|
protected function readInput() { |
|
|
|
if (!$this->parser) { |
|
if ($this->inputPath !== '-') { |
|
$this->stdin = fopen($this->inputPath, 'r'); |
|
} |
|
|
|
if ($this->inputFormat === 'mimedir') { |
|
$this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); |
|
} else { |
|
$this->parser = new Parser\Json($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); |
|
} |
|
} |
|
|
|
return $this->parser->parse(); |
|
|
|
} |
|
|
|
/** |
|
* Sends a message to STDERR. |
|
* |
|
* @param string $msg |
|
* |
|
* @return void |
|
*/ |
|
protected function log($msg, $color = 'default') { |
|
|
|
if (!$this->quiet) { |
|
if ($color !== 'default') { |
|
$msg = $this->colorize($color, $msg); |
|
} |
|
fwrite($this->stderr, $msg . "\n"); |
|
} |
|
|
|
} |
|
|
|
}
|
|
|