1#!/usr/bin/env php
2<?php
3
4namespace Sabre\VObject;
5
6// This sucks.. we have to try to find the composer autoloader. But chances
7// are, we can't find it this way. So we'll do our bestest
8$paths = [
9    __DIR__ . '/../vendor/autoload.php',  // In case vobject is cloned directly
10    __DIR__ . '/../../../autoload.php',   // In case vobject is a composer dependency.
11];
12
13foreach ($paths as $path) {
14    if (file_exists($path)) {
15        include $path;
16        break;
17    }
18}
19
20if (!class_exists('Sabre\\VObject\\Version')) {
21    fwrite(STDERR, "Composer autoloader could not be loaded.\n");
22    die(1);
23}
24
25echo "sabre/vobject ", Version::VERSION, " duplicate contact merge tool\n";
26
27if ($argc < 3) {
28
29    echo "\n";
30    echo "Usage: ", $argv[0], " input.vcf output.vcf [debug.log]\n";
31    die(1);
32
33}
34
35$input = fopen($argv[1], 'r');
36$output = fopen($argv[2], 'w');
37$debug = isset($argv[3]) ? fopen($argv[3], 'w') : null;
38
39$splitter = new Splitter\VCard($input);
40
41// The following properties are ignored. If they appear in some vcards
42// but not in others, we don't consider them for the sake of finding
43// differences.
44$ignoredProperties = [
45    "PRODID",
46    "VERSION",
47    "REV",
48    "UID",
49    "X-ABLABEL",
50];
51
52
53$collectedNames = [];
54
55$stats = [
56    "Total vcards"       => 0,
57    "No FN property"     => 0,
58    "Ignored duplicates" => 0,
59    "Merged values"      => 0,
60    "Error"              => 0,
61    "Unique cards"       => 0,
62    "Total written"      => 0,
63];
64
65function writeStats() {
66
67    global $stats;
68    foreach ($stats as $name => $value) {
69        echo str_pad($name, 23, " ", STR_PAD_RIGHT), str_pad($value, 6, " ", STR_PAD_LEFT), "\n";
70    }
71    // Moving cursor back a few lines.
72    echo "\033[" . count($stats) . "A";
73
74}
75
76function write($vcard) {
77
78    global $stats, $output;
79
80    $stats["Total written"]++;
81    fwrite($output, $vcard->serialize() . "\n");
82
83}
84
85while ($vcard = $splitter->getNext()) {
86
87    $stats["Total vcards"]++;
88    writeStats();
89
90    $fn = isset($vcard->FN) ? (string)$vcard->FN : null;
91
92    if (empty($fn)) {
93
94        // Immediately write this vcard, we don't compare it.
95        $stats["No FN property"]++;
96        $stats['Unique cards']++;
97        write($vcard);
98        $vcard->destroy();
99        continue;
100
101    }
102
103    if (!isset($collectedNames[$fn])) {
104
105        $collectedNames[$fn] = $vcard;
106        $stats['Unique cards']++;
107        continue;
108
109    } else {
110
111        // Starting comparison for all properties. We only check if properties
112        // in the current vcard exactly appear in the earlier vcard as well.
113        foreach ($vcard->children() as $newProp) {
114
115            if (in_array($newProp->name, $ignoredProperties)) {
116                // We don't care about properties such as UID and REV.
117                continue;
118            }
119            $ok = false;
120            foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) {
121
122                if ($compareProp->serialize() === $newProp->serialize()) {
123                    $ok = true;
124                    break;
125                }
126            }
127
128            if (!$ok) {
129
130                if ($newProp->name === 'EMAIL' || $newProp->name === 'TEL') {
131
132                    // We're going to make another attempt to find this
133                    // property, this time just by value. If we find it, we
134                    // consider it a success.
135                    foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) {
136
137                        if ($compareProp->getValue() === $newProp->getValue()) {
138                            $ok = true;
139                            break;
140                        }
141                    }
142
143                    if (!$ok) {
144
145                        // Merging the new value in the old vcard.
146                        $collectedNames[$fn]->add(clone $newProp);
147                        $ok = true;
148                        $stats['Merged values']++;
149
150                    }
151
152                }
153
154            }
155
156            if (!$ok) {
157
158                // echo $newProp->serialize() . " does not appear in earlier vcard!\n";
159                $stats['Error']++;
160                if ($debug) fwrite($debug, "Missing '" . $newProp->name . "' property in duplicate. Earlier vcard:\n" . $collectedNames[$fn]->serialize() . "\n\nLater:\n" . $vcard->serialize() . "\n\n");
161
162                $vcard->destroy();
163                continue 2;
164            }
165
166        }
167
168    }
169
170    $vcard->destroy();
171    $stats['Ignored duplicates']++;
172
173}
174
175foreach ($collectedNames as $vcard) {
176
177    // Overwriting any old PRODID
178    $vcard->PRODID = '-//Sabre//Sabre VObject ' . Version::VERSION . '//EN';
179    write($vcard);
180    writeStats();
181
182}
183
184echo str_repeat("\n", count($stats)), "\nDone.\n";
185