VCard   C
last analyzed

Complexity

Total Complexity 66

Size/Duplication

Total Lines 379
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 5

Test Coverage

Coverage 23.62%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 66
lcom 2
cbo 5
dl 0
loc 379
ccs 47
cts 199
cp 0.2362
rs 5.7474
c 1
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
B paramName() 0 27 4
C decodeProperty() 0 26 7
A fixPropertyParameters() 0 15 4
F validate() 0 163 39
B propertyGroups() 0 11 5
A inGroup() 0 7 2
A addToGroup() 0 7 2
A removeFromGroup() 0 19 3

How to fix   Complexity   

Complex Class

Complex classes like VCard often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use VCard, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * ownCloud - VCard component
4
 *
5
 * This component represents the BEGIN:VCARD and END:VCARD found in every
6
 * vcard.
7
 *
8
 * @author Thomas Tanghus
9
 * @author Evert Pot (http://www.rooftopsolutions.nl/)
10
 * @copyright 2013-2014 Thomas Tanghus ([email protected])
11
 *
12
 * This library is free software; you can redistribute it and/or
13
 * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
14
 * License as published by the Free Software Foundation; either
15
 * version 3 of the License, or any later version.
16
 *
17
 * This library is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
 * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public
23
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
24
 *
25
 */
26
27
namespace OCA\Contacts\VObject;
28
29
use OCA\Contacts\Utils;
30
use Sabre\VObject;
31
32
/**
33
 * This class overrides \Sabre\VObject\Component\VCard::validate() to be add
34
 * to import partially invalid vCards by ignoring invalid lines and to
35
 * validate and upgrade using ....
36
 *
37
 * Satisfy PHP Analyzer:
38
 * @property \OC\VObject\CompoundProperty N
39
 * @property \OC\VObject\CompoundProperty ORG
40
 * @property \OC\VObject\CompoundProperty ADR
41
 * @property \OCA\Contacts\VObject\GroupProperty CATEGORIES
42
 * @property \Sabre\VObject\Property\Text FN
43
 * @property \Sabre\VObject\Property\Text EMAIL
44
 * @property \Sabre\VObject\Property\Text VERSION
45
 * @property \Sabre\VObject\Property\Text BDAY
46
 * @property \Sabre\VObject\Property\Text UID
47
 * @property \Sabre\VObject\Property\Text REV
48
 * @property \Sabre\VObject\Property\Binary PHOTO
49
 * @property \Sabre\VObject\Property\Binary LOGO
50
 * @property \Sabre\VObject\Property\FlatText PRODID
51
*/
52
class VCard extends VObject\Component\VCard {
53
54
	/**
55
	* The following constants are used by the validate() method.
56
	*/
57
    const REPAIR = 1;
0 ignored issues
show
Coding Style introduced by
Tabs must be used to indent lines; spaces are not allowed
Loading history...
58
	const UPGRADE = 2;
59
60
	/**
61
	 * The groups in the contained properties
62
	 *
63
	 * @var array
64
	 */
65
	protected $groups = array();
66
67
	/**
68
	* VCards with version 2.1, 3.0 and 4.0 are found.
69
	*
70
	* If the VCARD doesn't know its version, 3.0 is assumed and if
71
	* option UPGRADE is given it will be upgraded to version 3.0.
72
	*/
73
	const DEFAULT_VERSION = '3.0';
74
75
	/**
76
	* The vCard 2.1 specification allows parameter values without a name.
77
	* The parameter name is then determined from the unique parameter value.
78
	* In version 2.1 e.g. a phone can be formatted like: TEL;HOME;CELL:123456789
79
	* This has to be changed to either TEL;TYPE=HOME,CELL:123456789 or TEL;TYPE=HOME;TYPE=CELL:123456789 - both are valid.
80
	*
81
	* From: https://github.com/barnabywalters/vcard/blob/master/barnabywalters/VCard/VCard.php
82
	*
83
	* @param string value
84
	* @return string
85
	*/
86
	protected function paramName($value) {
87
		static $types = array (
88
				'DOM', 'INTL', 'POSTAL', 'PARCEL','HOME', 'WORK',
89
				'PREF', 'VOICE', 'FAX', 'MSG', 'CELL', 'PAGER',
90
				'BBS', 'MODEM', 'CAR', 'ISDN', 'VIDEO',
91
				'AOL', 'APPLELINK', 'ATTMAIL', 'CIS', 'EWORLD',
92
				'INTERNET', 'IBMMAIL', 'MCIMAIL',
93
				'POWERSHARE', 'PRODIGY', 'TLX', 'X400',
94
				'GIF', 'CGM', 'WMF', 'BMP', 'MET', 'PMB', 'DIB',
95
				'PICT', 'TIFF', 'PDF', 'PS', 'JPEG', 'QTIME',
96
				'MPEG', 'MPEG2', 'AVI',
97
				'WAVE', 'AIFF', 'PCM',
98
				'X509', 'PGP');
99
		static $values = array (
100
				'INLINE', 'URL', 'CID');
101
		static $encodings = array (
102
				'7BIT', 'QUOTED-PRINTABLE', 'BASE64');
103
		$name = 'UNKNOWN';
104
		if (in_array($value, $types)) {
105
			$name = 'TYPE';
106
		} elseif (in_array($value, $values)) {
107
			$name = 'VALUE';
108
		} elseif (in_array($value, $encodings)) {
109
			$name = 'ENCODING';
110
		}
111
		return $name;
112
	}
113
114
	/**
115
	* Decode properties for upgrading from v. 2.1
116
	*
117
	* @param \Sabre\VObject\Property $property Reference to a \Sabre\VObject\Property.
118
	* The only encoding allowed in version 3.0 is 'b' for binary. All encoded strings
119
	* must therefore be decoded and the parameters removed.
120
	*/
121
	protected function decodeProperty(&$property) {
122
		foreach($property->parameters as $key=>&$parameter) {
123
			// Check for values without names which Sabre interprets
124
			// as names without values.
125
			if(trim($parameter->getValue()) === '') {
126
				$parameter->setValue($parameter->name);
127
				$parameter->name = $this->paramName($parameter->name);
128
			}
129
			// Check out for encoded string and decode them :-[
130
			if(strtoupper($parameter->name) == 'ENCODING') {
131
				if(strtoupper($parameter->getValue()) == 'QUOTED-PRINTABLE') {
132
					$property->setValue(str_replace(
133
						"\r\n", "\n",
134
						VObject\StringUtil::convertToUTF8(
135
							quoted_printable_decode($property->getValue())
0 ignored issues
show
Bug introduced by
The method getValue() does not seem to exist on object<Sabre\VObject\Property>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
136
						)
137
					));
138
					unset($property->parameters[$key]);
139
				} elseif(strtoupper($parameter->getValue()) == 'BASE64') {
140
					$parameter->setValue('b');
141
				}
142
			} elseif(strtoupper($parameter->name) == 'CHARSET') {
143
				unset($property->parameters[$key]);
144
			}
145
		}
146
	}
147
148
	/**
149
	* Work around issue in older VObject sersions
150
	* https://github.com/fruux/sabre-vobject/issues/24
151
	*
152
	* @param \Sabre\VObject\Property $property Reference to a \Sabre\VObject\Property.
153
	*/
154
	public function fixPropertyParameters(&$property) {
155
		// Work around issue in older VObject sersions
156
		// https://github.com/fruux/sabre-vobject/issues/24
157
		foreach($property->parameters as $key=>$parameter) {
158
			if(strpos($parameter->getValue(), ',') === false) {
159
				continue;
160
			}
161
			$values = explode(',', $parameter->getValue());
162
			$values = array_map('trim', $values);
163
			$parameter->setValue(array_shift($values));
164
			foreach($values as $value) {
165
				$property->add($parameter->name, $value);
166
			}
167
		}
168
	}
169
170
	/**
171
	* Validates the node for correctness.
172
	*
173
	* The following options are supported:
174
	*   - VCard::REPAIR - If something is broken, and automatic repair may
175
	*                    be attempted.
176
	*   - VCard::UPGRADE - If needed the vCard will be upgraded to version 3.0.
177
	*
178
	* An array is returned with warnings.
179
	*
180
	* Every item in the array has the following properties:
181
	*    * level - (number between 1 and 3 with severity information)
182
	*    * message - (human readable message)
183
	*    * node - (reference to the offending node)
184
	*
185
	* @param int $options
186
	* @return array
187
	*/
188 4
	public function validate($options = 0) {
189
190 4
		$warnings = array();
191 4
		$repaired = false;
192
193 4
		$version = $this->select('VERSION');
194 4
		if (count($version) !== 1) {
195
			$warnings[] = array(
196
				'level' => 1,
197
				'message' => 'The VERSION property must appear in the VCARD component exactly 1 time',
198
				'node' => $this,
199
			);
200
			if ($options & self::REPAIR) {
201
				if (count($version) > 1) {
202
					$version = (string) current($version);
203
					$this->remove('VERSION');
0 ignored issues
show
Bug introduced by
The method remove() does not exist on OCA\Contacts\VObject\VCard. Did you maybe mean removeFromGroup()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
204
					$this->VERSION = $version;
205
				} else {
206
					$this->VERSION = self::DEFAULT_VERSION;
207
				}
208
				$repaired = true;
209
			}
210
		} 
211
212 4
		$version = (string)$this->VERSION;
213 4
		if ($version!=='2.1' && $version!=='3.0' && $version!=='4.0') {
214
			$warnings[] = array(
215
				'level' => 1,
216 1
				'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.',
217
				'node' => $this,
218
			);
219 4
			if ($options & self::REPAIR) {
220
				$this->VERSION = self::DEFAULT_VERSION;
221
				$repaired = true;
222
			}
223
		}
224
225
		# upgrade 2.1 cards to 3.0 cards.
226 4
		if ($options & self::UPGRADE && $version === '2.1') {
227
			$this->VERSION = self::DEFAULT_VERSION;
228
			$repaired = true;
229
			foreach($this->children as $idx => &$property) {
230
231
				$this->decodeProperty($property);
232
				$this->fixPropertyParameters($property);
233
				/* What exactly was I thinking here?
234
				switch((string)$property->name) {
235
					case 'LOGO':
236
					case 'SOUND':
237
					case 'PHOTO':
238
						if(isset($property['TYPE']) && strpos((string)$property['TYPE'], '/') === false) {
239
							$property['TYPE'] = 'image/' . strtolower($property['TYPE']);
240
						}
241
				}*/
242
			}
243
		}
244
245 4
		$fn = $this->select('FN');
246 4
		if (count($fn) !== 1 || trim((string)$this->FN) === '') {
247
			$warnings[] = array(
248
				'level' => 1,
249
				'message' => 'The FN property must appear in the VCARD component exactly 1 time',
250
				'node' => $this,
251
			);
252
			if ($options & self::REPAIR) {
253
				// We're going to try to see if we can use the contents of the
254
				// N property.
255
				$repaired = true;
256
				if (isset($this->N)
257
					&& substr((string)$this->N, 2) !== ';;'
258
					&& (string)$this->N !== ''
259
				) {
260
					$value = explode(';', (string)$this->N);
261
					if (isset($value[1]) && $value[1]) {
262
						$this->FN = $value[1] . ' ' . $value[0];
263
					} else {
264
						$this->FN = $value[0];
265
					}
266
				// Otherwise, the ORG property may work
267
				} elseif (isset($this->ORG)) {
268
					$this->FN = (string)$this->ORG;
269
				} elseif (isset($this->EMAIL)) {
270
					$this->FN = (string)$this->EMAIL;
271
				}
272
273
			}
274
		}
275
276 4
		if(isset($this->BDAY)) {
277
			if ($options & self::REPAIR) {
278
				// If the BDAY has a format of e.g. 19960401
279
				$bday = (string)$this->BDAY;
280
				if(strlen($bday) >= 8
281
					&& is_int(substr($bday, 0, 4))
282
					&& is_int(substr($bday, 4, 2))
283
					&& is_int(substr($bday, 6, 2))) {
284
					$this->BDAY = substr($bday, 0, 4).'-'.substr($bday, 4, 2).'-'.substr($bday, 6, 2);
285
					$this->BDAY->VALUE = 'DATE';
286
					$repaired = true;
287
				} else if($bday[5] !== '-' || $bday[7] !== '-') {
288
					try {
289
						// Skype exports as e.g. Jan 14, 1996
290
						$date = new \DateTime($bday);
291
						$this->BDAY = $date->format('Y-m-d');
292
						$this->BDAY->VALUE = 'DATE';
293
						$repaired = true;
294
					} catch(\Exception $e) {
295
						\OCP\Util::writeLog('contacts', __METHOD__.' Removing invalid BDAY: ' . $bday, \OCP\Util::DEBUG);
296
						unset($this->BDAY);
297
					}
298
				}
299
			}
300
		}
301
302 4
		$n = $this->select('N');
303 4
		if (count($n) !== 1) {
304 4
			$warnings[] = array(
305 4
				'level' => 1,
306 4
				'message' => 'The N property must appear in the VCARD component exactly 1 time',
307 4
				'node' => $this,
308
			);
309
			// TODO: Make a better effort parsing FN.
310 4
			if (($options & self::REPAIR) && count($n) === 0) {
311
				// Take 2 first name parts of 'FN' and reverse.
312 4
				$slice = array_reverse(array_slice(explode(' ', (string)$this->FN), 0, 2));
313 4
				if(count($slice) < 2) { // If not enought, add one more...
314
					$slice[] = "";
315
				}
316 4
				$this->N = $slice;
317 4
				$repaired = true;
318 4
			}
319 4
		}
320
321 4
		if (!isset($this->UID) || trim((string)$this->UID) === '') {
322 1
			$warnings[] = array(
323 1
				'level' => 1,
324 1
				'message' => 'Every vCard must have a UID',
325 1
				'node' => $this,
326
			);
327 1
			if ($options & self::REPAIR) {
328 1
				$this->UID = Utils\Properties::generateUID();
329 1
				$repaired = true;
330 1
			}
331 1
		}
332
333 4
		if ($repaired) {
334 4
			$now = new \DateTime;
335 4
			$this->REV = $now->format(\DateTime::W3C);
336
337 4
			if (count($this->select('PRODID')) > 1) {
338
				$this->remove('PRODID');
0 ignored issues
show
Bug introduced by
The method remove() does not exist on OCA\Contacts\VObject\VCard. Did you maybe mean removeFromGroup()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
339
			}
340 4
			$appInfo = \OCP\App::getAppInfo('contacts');
341 4
			$appVersion = \OCP\App::getAppVersion('contacts');
342 4
			$this->PRODID = '-//ownCloud//NONSGML '.$appInfo['name'].' '.$appVersion.'//EN';
343 4
		}
344
345 4
		return array_merge(
346 4
			parent::validate($options),
347
			$warnings
348 4
		);
349
350
	}
351
352
	/**
353
	 * Get all group names in the vCards properties
354
	 *
355
	 * NOTE: Not to confuse with CATEGORIES groups
356
	 *
357
	 * @return array
358
	 */
359
	public function propertyGroups() {
360
		foreach($this->children as $property) {
361
			if($property->group && !isset($this->groups[$property->group])) {
362
				$this->groups[] = $property->group;
363
			}
364
		}
365
		if(count($this->groups) > 1) {
366
			sort($this->groups);
367
		}
368
		return $this->groups;
369
	}
370
371
	/**
372
	* Test if vcard has group (CATEGORIES) $name
373
	*
374
	* @param string $name
375
	* @return bool
376
	*/
377
	public function inGroup($name) {
378
		if(!isset($this->CATEGORIES)) {
379
			return false;
380
		}
381
382
		return $this->CATEGORIES->hasGroup($name);
383
	}
384
385
	/**
386
	* Add group (CATEGORIES) $name to vcard
387
	*
388
	* Return true if contact wasn't already in group
389
	*
390
	* @param string $name
391
	* @return bool
392
	*/
393
	public function addToGroup($name) {
394
		if(!isset($this->CATEGORIES)) {
395
			$this->add('CATEGORIES');
396
		}
397
398
		return $this->CATEGORIES->addGroup($name);
399
	}
400
401
	/**
402
	* Remove group (CATEGORIES) $name from vcard
403
	*
404
	* Return true if vcard has been updated.
405
	*
406
	* @param string $name
407
	* @return bool
408
	*/
409
	public function removeFromGroup($name) {
410
411
		if(!isset($this->CATEGORIES)) {
412
			return false;
413
		}
414
415
		$updated = $this->CATEGORIES->removeGroup($name);
416
		// getParts() returns an array with an empty element if
417
		// CATEGORIES is empty
418
		$groups = $this->CATEGORIES->getParts();
419
		// Remove empty elements
420
		$groups = array_filter($groups, 'strlen');
421
		if(count($groups) === 0) {
422
			unset($this->{'CATEGORIES'});
423
			$updated = true;
424
		}
425
426
		return $updated;
427
	}
428
429
430
}
431