#!/usr/bin/env php <?php namespace Sabre\VObject; // This sucks.. we have to try to find the composer autoloader. But chances // are, we can't find it this way. So we'll do our bestest $paths = [ __DIR__ . '/../vendor/autoload.php', // In case vobject is cloned directly __DIR__ . '/../../../autoload.php', // In case vobject is a composer dependency. ]; foreach ($paths as $path) { if (file_exists($path)) { include $path; break; } } if (!class_exists('Sabre\\VObject\\Version')) { fwrite(STDERR, "Composer autoloader could not be loaded.\n"); die(1); } echo "sabre/vobject ", Version::VERSION, " duplicate contact merge tool\n"; if ($argc < 3) { echo "\n"; echo "Usage: ", $argv[0], " input.vcf output.vcf [debug.log]\n"; die(1); } $input = fopen($argv[1], 'r'); $output = fopen($argv[2], 'w'); $debug = isset($argv[3]) ? fopen($argv[3], 'w') : null; $splitter = new Splitter\VCard($input); // The following properties are ignored. If they appear in some vcards // but not in others, we don't consider them for the sake of finding // differences. $ignoredProperties = [ "PRODID", "VERSION", "REV", "UID", "X-ABLABEL", ]; $collectedNames = []; $stats = [ "Total vcards" => 0, "No FN property" => 0, "Ignored duplicates" => 0, "Merged values" => 0, "Error" => 0, "Unique cards" => 0, "Total written" => 0, ]; function writeStats() { global $stats; foreach ($stats as $name => $value) { echo str_pad($name, 23, " ", STR_PAD_RIGHT), str_pad($value, 6, " ", STR_PAD_LEFT), "\n"; } // Moving cursor back a few lines. echo "\033[" . count($stats) . "A"; } function write($vcard) { global $stats, $output; $stats["Total written"]++; fwrite($output, $vcard->serialize() . "\n"); } while ($vcard = $splitter->getNext()) { $stats["Total vcards"]++; writeStats(); $fn = isset($vcard->FN) ? (string)$vcard->FN : null; if (empty($fn)) { // Immediately write this vcard, we don't compare it. $stats["No FN property"]++; $stats['Unique cards']++; write($vcard); $vcard->destroy(); continue; } if (!isset($collectedNames[$fn])) { $collectedNames[$fn] = $vcard; $stats['Unique cards']++; continue; } else { // Starting comparison for all properties. We only check if properties // in the current vcard exactly appear in the earlier vcard as well. foreach ($vcard->children() as $newProp) { if (in_array($newProp->name, $ignoredProperties)) { // We don't care about properties such as UID and REV. continue; } $ok = false; foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) { if ($compareProp->serialize() === $newProp->serialize()) { $ok = true; break; } } if (!$ok) { if ($newProp->name === 'EMAIL' || $newProp->name === 'TEL') { // We're going to make another attempt to find this // property, this time just by value. If we find it, we // consider it a success. foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) { if ($compareProp->getValue() === $newProp->getValue()) { $ok = true; break; } } if (!$ok) { // Merging the new value in the old vcard. $collectedNames[$fn]->add(clone $newProp); $ok = true; $stats['Merged values']++; } } } if (!$ok) { // echo $newProp->serialize() . " does not appear in earlier vcard!\n"; $stats['Error']++; if ($debug) fwrite($debug, "Missing '" . $newProp->name . "' property in duplicate. Earlier vcard:\n" . $collectedNames[$fn]->serialize() . "\n\nLater:\n" . $vcard->serialize() . "\n\n"); $vcard->destroy(); continue 2; } } } $vcard->destroy(); $stats['Ignored duplicates']++; } foreach ($collectedNames as $vcard) { // Overwriting any old PRODID $vcard->PRODID = '-//Sabre//Sabre VObject ' . Version::VERSION . '//EN'; write($vcard); writeStats(); } echo str_repeat("\n", count($stats)), "\nDone.\n";