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( <<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"); } } }