Completed
Pull Request — master (#1054)
by
unknown
73:47
created

Contact   D

Complexity

Total Complexity 156

Size/Duplication

Total Lines 812
Duplicated Lines 6.9 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 35.5%

Importance

Changes 11
Bugs 3 Features 1
Metric Value
wmc 156
lcom 1
cbo 6
dl 56
loc 812
ccs 169
cts 476
cp 0.355
rs 4.4444
c 11
b 3
f 1

34 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 7 57 17
A getMetaData() 0 20 4
A combinedKey() 0 3 1
A getOwner() 0 5 2
A getId() 0 3 2
A getDisplayName() 0 8 4
A getURI() 0 3 2
A getETag() 0 4 1
A getParent() 0 3 1
A getBackend() 0 3 1
A getPermissions() 0 5 2
A hasPermission() 0 3 1
A delete() 0 9 2
D save() 0 41 9
D retrieve() 14 91 23
C getPhoto() 0 30 8
A setPhoto() 6 18 3
A getPropertyIndexByChecksum() 6 11 3
A getPropertyByChecksum() 5 9 3
A unsetPropertyByChecksum() 0 5 1
C setPropertyByChecksum() 4 44 11
C setPropertyByName() 11 52 11
C setParameters() 0 35 14
A lastModified() 0 8 4
C mergeFromArray() 3 27 8
A __get() 0 7 2
A __isset() 0 7 2
A __set() 0 10 3
A __unset() 0 17 3
A setRetrieved() 0 3 1
A isRetrieved() 0 3 1
A setSaved() 0 3 1
A isSaved() 0 3 1
B getBirthdayEvent() 0 45 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Contact 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 Contact, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * ownCloud - Contact object
4
 *
5
 * @author Thomas Tanghus
6
 * @copyright 2012-2014 Thomas Tanghus ([email protected])
7
 *
8
 * This library is free software; you can redistribute it and/or
9
 * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10
 * License as published by the Free Software Foundation; either
11
 * version 3 of the License, or any later version.
12
 *
13
 * This library is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public
19
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
20
 *
21
 */
22
23
namespace OCA\Contacts;
24
25
use Exception;
26
use Sabre\VObject\Property;
27
use OCA\Contacts\Utils\Properties;
28
29
/**
30
 * Subclass this class or implement IPIMObject interface for PIM objects
31
 */
32
33
class Contact extends VObject\VCard implements IPIMObject {
34
35
	/**
36
	 * The name of the object type in this case VCARD.
37
	 *
38
	 * This is used when serializing the object.
39
	 *
40
	 * @var string
41
	 */
42
	public $name = 'VCARD';
43
44
	/**
45
	 * @brief language object
46
	 *
47
	 * @var \OCP\IL10N
48
	 */
49
	public static $l10n;
50
51
	protected $props = array();
52
53
	/**
54
	 * Create a new Contact object
55
	 *
56
	 * @param AddressBook $parent
57
	 * @param Backend\AbstractBackend $backend
58
	 * @param mixed $data
59
	 */
60 7
	public function __construct($parent, $backend, $data = null) {
61 7
		parent::__construct('VCARD');
62
63 7
		self::$l10n = $parent::$l10n;
0 ignored issues
show
Documentation Bug introduced by
It seems like $parent::$l10n of type object<OC_L10N> is incompatible with the declared type object<OCP\IL10N> of property $l10n.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
64
		//\OCP\Util::writeLog('contacts', __METHOD__ . ' , data: ' . print_r($data, true), \OCP\Util::DEBUG);
65 7
		$this->props['parent'] = $parent;
66 7
		$this->props['backend'] = $backend;
67 7
		$this->props['retrieved'] = false;
68 7
		$this->props['saved'] = false;
69
70 7
		if (!is_null($data)) {
71 7
			if ($data instanceof VObject\VCard) {
72 3 View Code Duplication
				foreach ($data->children as $child) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
73 3
					if($child->name === 'VERSION' || $child->name === 'PRODID') {
74 3
						parent::__set($child->name, $child);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (__set() instead of __construct()). Are you sure this is correct? If so, you might want to change this to $this->__set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
75 3
					} else {
76 3
						$this->add($child);
77
					}
78 3
				}
79 3
				$this->setRetrieved(true);
80 7
			} elseif (is_array($data)) {
81 6
				foreach ($data as $key => $value) {
82
					switch ($key) {
83 6
						case 'id':
84 6
							$this->props['id'] = $value;
85 6
							break;
86 6
						case 'permissions':
87 6
							$this->props['permissions'] = $value;
88 6
							break;
89 6
						case 'lastmodified':
90 2
							$this->props['lastmodified'] = $value;
91 2
							break;
92 6
						case 'uri':
93 2
							$this->props['uri'] = $value;
94 2
							break;
95 6
						case 'carddata':
96 6
							$this->props['carddata'] = $value;
97 6
							$this->retrieve();
98 6
							break;
99 6
						case 'vcard':
100
							$this->props['vcard'] = $value;
101
							$this->retrieve();
102
							break;
103 6
						case 'displayname':
104 6
						case 'fullname':
105 6
							if(is_string($value)) {
106 6
								$this->props['displayname'] = $value;
107 6
								$this->FN = $value;
108
								// Set it to saved again as we're not actually changing anything
109 6
								$this->setSaved();
110 6
							}
111 6
							break;
112
					}
113 6
				}
114 6
			}
115 7
		}
116 7
	}
117
118
	/**
119
	 * @return array|null
120
	 */
121 2
	public function getMetaData() {
122 2
		if (!$this->hasPermission(\OCP\Constants::PERMISSION_READ)) {
123
			throw new Exception(self::$l10n->t('You do not have permissions to see this contact'), 403);
124
		}
125 2
		if (!isset($this->props['displayname'])) {
126
			if (!$this->retrieve()) {
127
				\OCP\Util::writeLog('contacts', __METHOD__.' error reading: '.print_r($this->props, true), \OCP\Util::ERROR);
128
				return null;
129
			}
130
		}
131
		return array(
132 2
			'id' => $this->getId(),
133 2
			'displayname' => $this->getDisplayName(),
134 2
			'permissions' => $this->getPermissions(),
135 2
			'lastmodified' => $this->lastModified(),
136 2
			'owner' => $this->getOwner(),
137 2
			'parent' => $this->getParent()->getId(),
138 2
			'backend' => $this->getBackend()->name,
139 2
		);
140
	}
141
142
	/**
143
	 * Get a unique key combined of backend name, address book id and contact id.
144
	 *
145
	 * @return string
146
	 */
147
	public function combinedKey() {
148
		return $this->getBackend()->name . '::' . $this->getParent()->getId() . '::' . $this->getId();
149
	}
150
151
	/**
152
	 * @return string|null
153
	 */
154 2
	public function getOwner() {
155 2
		return isset($this->props['owner'])
156 2
			? $this->props['owner']
157 2
			: $this->getParent()->getOwner();
158
	}
159
160
	/**
161
	 * @return string|null
162
	 */
163 8
	public function getId() {
164 8
		return isset($this->props['id']) ? $this->props['id'] : null;
165
	}
166
167
	/**
168
	 * @return string|null
169
	 */
170 5
	public function getDisplayName() {
171 5
		if (!$this->hasPermission(\OCP\Constants::PERMISSION_READ)) {
172
			throw new Exception(self::$l10n->t('You do not have permissions to see this contact'), 403);
173
		}
174 5
		return isset($this->props['displayname'])
175 5
			? $this->props['displayname']
176 5
			: (isset($this->FN) ? $this->FN : null);
177
	}
178
179
	/**
180
	 * @return string|null
181
	 */
182
	public function getURI() {
183
		return isset($this->props['uri']) ? $this->props['uri'] : null;
184
	}
185
186
	/**
187
	 * @return string
188
	 * TODO: Cache result.
189
	 */
190
	public function getETag() {
191
		$this->retrieve();
192
		return md5($this->serialize());
193
	}
194
195
	/**
196
	 * If this object is part of a collection return a reference
197
	 * to the parent object, otherwise return null.
198
	 * @return IPIMObject|null
199
	 */
200 7
	public function getParent() {
201 7
		return $this->props['parent'];
202
	}
203
204 3
	public function getBackend() {
205 3
		return $this->props['backend'];
206
	}
207
208
	/** CRUDS permissions (Create, Read, Update, Delete, Share)
209
	 *
210
	 * @return integer
211
	 */
212 6
	public function getPermissions() {
213 6
		return isset($this->props['permissions'])
214 6
			? $this->props['permissions']
215 6
			: $this->getParent()->getPermissions();
216
	}
217
218
	/**
219
	 * @param integer $permission
220
	 * @return integer
221
	 */
222 6
	public function hasPermission($permission) {
223 6
		return $this->getPermissions() & $permission;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getPermissions() & $permission; (integer) is incompatible with the return type declared by the interface OCA\Contacts\IPIMObject::hasPermission of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
224
	}
225
226
	/**
227
	 * Save the address book data to backend
228
	 * FIXME
229
	 *
230
	 * @param array $data
231
	 * @return bool
232
	 */
233
/*	public function update(array $data) {
234
235
		foreach($data as $key => $value) {
236
			switch($key) {
237
				case 'displayname':
238
					$this->addressBookInfo['displayname'] = $value;
239
					break;
240
				case 'description':
241
					$this->addressBookInfo['description'] = $value;
242
					break;
243
			}
244
		}
245
		return $this->props['backend']->updateContact(
246
			$this->getParent()->getId(),
247
			$this->getId(),
248
			$this
249
		);
250
	}
251
*/
252
	/**
253
	 * Delete the data from backend
254
	 *
255
	 * FIXME: Should be removed as it could leave the parent with a dataless object.
256
	 *
257
	 * @return bool
258
	 */
259
	public function delete() {
260
		if (!$this->hasPermission(\OCP\Constants::PERMISSION_DELETE)) {
261
			throw new Exception(self::$l10n->t('You do not have permissions to delete this contact'), 403);
262
		}
263
		return $this->props['backend']->deleteContact(
264
			$this->getParent()->getId(),
265
			$this->getId()
266
		);
267
	}
268
269
	/**
270
	 * Save the contact data to backend
271
	 *
272
	 * @return bool
273
	 */
274 3
	public function save($force = false) {
275 3
		if (!$this->hasPermission(\OCP\Constants::PERMISSION_UPDATE)) {
276
			throw new Exception(self::$l10n->t('You do not have permissions to update this contact'), 403);
277
		}
278 3
		if ($this->isSaved() && !$force) {
279
			\OCP\Util::writeLog('contacts', __METHOD__.' Already saved: ' . print_r($this->props, true), \OCP\Util::DEBUG);
280
			return true;
281
		}
282
283 3
		if (isset($this->FN)) {
284 3
			$this->props['displayname'] = (string)$this->FN;
285 3
		}
286
287 3
		if ($this->getId()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getId() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
288
			if (!$this->getBackend()->hasContactMethodFor(\OCP\Constants::PERMISSION_UPDATE)) {
289
				throw new Exception(self::$l10n->t('The backend for this contact does not support updating it'), 501);
290
			}
291
			if ($this->getBackend()
292
				->updateContact(
293
					$this->getParent()->getId(),
294
					$this->getId(),
295
					$this
296
				)
297
			) {
298
				$this->props['lastmodified'] = time();
299
				$this->setSaved(true);
300
				return true;
301
			} else {
302
				return false;
303
			}
304
		} else {
305 3
			if (!$this->getBackend()->hasContactMethodFor(\OCP\Constants::PERMISSION_CREATE)) {
306
				throw new Exception(self::$l10n->t('This backend does not support adding contacts'), 501);
307
			}
308 3
			$this->props['id'] = $this->getBackend()->createContact(
309 3
				$this->getParent()->getId(), $this
310 3
			);
311 3
			$this->setSaved(true);
312 3
			return $this->getId() !== false;
313
		}
314
	}
315
316
	/**
317
	 * Get the data from the backend
318
	 * FIXME: Clean this up and make sure the logic is OK.
319
	 *
320
	 * @return bool
321
	 */
322 7
	public function retrieve() {
323 6
		if ($this->isRetrieved()) {
324
			//\OCP\Util::writeLog('contacts', __METHOD__. ' children', \OCP\Util::DEBUG);
325 6
			return true;
326
		} else {
327 6
			$data = null;
328 6
			if(isset($this->props['vcard'])
329 6
				&& $this->props['vcard'] instanceof VObject\VCard) {
330
				foreach($this->props['vcard']->children() as $child) {
331
					if($child->name === 'VERSION' || $child->name === 'PRODID') {
332
						parent::__set($child->name, $child);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (__set() instead of retrieve()). Are you sure this is correct? If so, you might want to change this to $this->__set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
333
					} else {
334
						$this->add($child);
335
					}
336
					if($child->name === 'FN') {
337
						$this->props['displayname']
338
							= strtr($child->getValue(), array('\,' => ',', '\;' => ';', '\\\\' => '\\'));
339
					}
340
				}
341
				$this->setRetrieved(true);
342
				$this->setSaved(true);
343
				//$this->children = $this->props['vcard']->children();
344
				unset($this->props['vcard']);
345
				return true;
346 6
			} elseif (!isset($this->props['carddata'])) {
347 4
				$result = $this->props['backend']->getContact(
348 4
					$this->getParent()->getId(),
349 4
					$this->getId()
350 4
				);
351 4
				if ($result) {
352 4
					if (isset($result['vcard'])
353 4
						&& $result['vcard'] instanceof VObject\VCard) {
354 View Code Duplication
						foreach ($result['vcard']->children() as $child) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
355
							if($child->name === 'VERSION' || $child->name === 'PRODID') {
356
								parent::__set($child->name, $child);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (__set() instead of retrieve()). Are you sure this is correct? If so, you might want to change this to $this->__set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
357
							} else {
358
								$this->add($child);
359
							}
360
						}
361
						$this->setRetrieved(true);
362
						return true;
363 4
					} elseif (isset($result['carddata'])) {
364
						// Save internal values
365 4
						$data = $result['carddata'];
366 4
						$this->props['carddata'] = $result['carddata'];
367 4
						$this->props['lastmodified'] = isset($result['lastmodified'])
368 4
							? $result['lastmodified']
369 4
							: null;
370 4
						$this->props['displayname'] = $result['displayname'];
371 4
						$this->props['permissions'] = $result['permissions'];
372 4
					} else {
373
						\OCP\Util::writeLog('contacts', __METHOD__
374
							. ' Could not get vcard or carddata: '
375
							. $this->getId()
376
							. print_r($result, true), \OCP\Util::DEBUG);
377
						return false;
378
					}
379 4
				} else {
380
					\OCP\Util::writeLog('contacts', __METHOD__.' Error getting contact: ' . $this->getId(), \OCP\Util::DEBUG);
381
				}
382 7
			} elseif (isset($this->props['carddata'])) {
383 2
				$data = $this->props['carddata'];
384 2
			}
385
			try {
386 6
				$obj = \Sabre\VObject\Reader::read(
387 6
					$data,
388
					\Sabre\VObject\Reader::OPTION_IGNORE_INVALID_LINES
389 6
				);
390 6
				if ($obj) {
391 6 View Code Duplication
					foreach ($obj->children as $child) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
392 6
						if($child->name === 'VERSION' || $child->name === 'PRODID') {
393 6
							parent::__set($child->name, $child);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (__set() instead of retrieve()). Are you sure this is correct? If so, you might want to change this to $this->__set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
394 6
						} else {
395 6
							$this->add($child);
396
						}
397 6
					}
398 6
					$this->setRetrieved(true);
399 6
					$this->setSaved(true);
400 6
				} else {
401
					\OCP\Util::writeLog('contacts', __METHOD__.' Error reading: ' . print_r($data, true), \OCP\Util::DEBUG);
402
					return false;
403
				}
404 6
			} catch (Exception $e) {
405
				\OCP\Util::writeLog('contacts', __METHOD__ .
406
					' Error parsing carddata  for: ' . $this->getId() . ' ' . $e->getMessage(),
407
						\OCP\Util::ERROR);
408
				return false;
409
			}
410
		}
411 6
		return true;
412
	}
413
414
	/**
415
	 * Get the PHOTO or LOGO
416
	 *
417
	 * @return \OCP\Image|null
418
	 */
419
	public function getPhoto() {
420
		$image = new \OCP\Image();
421
422
		if (isset($this->PHOTO)) {
423
			$photo = $this->PHOTO;
424
		} elseif (isset($this->LOGO)) {
425
			$photo = $this->LOGO;
426
		} else {
427
			return null;
428
		}
429
430
		$photovalue = $photo->getValue();
431
432
		if ( $photo instanceof \Sabre\VObject\Property\Uri && substr($photovalue, 0, 5) === 'data:' ) {
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\Property\Uri does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
433
			$mimeType = substr($photovalue, 5, strpos($photovalue, ',')-5);
434
			if (strpos($mimeType, ';')) {
435
			    $mimeType = substr($mimeType,0,strpos($mimeType, ';'));
0 ignored issues
show
Unused Code introduced by
$mimeType is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
436
			}
437
438
			$photovalue = substr($photovalue, strpos($photovalue,',')+1);
439
440
                        if ($image->loadFromBase64($photovalue)) {
0 ignored issues
show
Coding Style introduced by
Tabs must be used to indent lines; spaces are not allowed
Loading history...
441
                                return $image;
0 ignored issues
show
Coding Style introduced by
Tabs must be used to indent lines; spaces are not allowed
Loading history...
442
			}
443
		} elseif ($image->loadFromData($photovalue)) {
444
			return $image;
445
		}
446
447
		return null;
448
	}
449
450
	/**
451
	 * Set the contact photo.
452
	 *
453
	 * @param \OCP\Image $photo
454
	 */
455
	public function setPhoto(\OCP\Image $photo) {
456
		// For vCard 3.0 the type must be e.g. JPEG or PNG
457
		// For version 4.0 the full mimetype should be used.
458
		// https://tools.ietf.org/html/rfc2426#section-3.1.4
459 View Code Duplication
		if (strval($this->VERSION) === '4.0') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
460
			$type = $photo->mimeType();
461
		} else {
462
			$type = explode('/', $photo->mimeType());
463
			$type = strtoupper(array_pop($type));
464
		}
465
		if (isset($this->PHOTO)) {
466
			$this->remove('PHOTO');
0 ignored issues
show
Bug introduced by
The method remove() does not exist on OCA\Contacts\Contact. 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...
467
		}
468
		$this->add('PHOTO', $photo->data(), ['ENCODING' => 'b', 'TYPE' => $type]);
469
		$this->setSaved(false);
470
471
		return true;
472
	}
473
474
	/**
475
	* Get a property index in the contact by the checksum of its serialized value
476
	*
477
	* @param string $checksum An 8 char m5d checksum.
478
	* @return integer Property by reference
479
	* @throws Exception with error code 404 if the property is not found.
480
	*/
481
	public function getPropertyIndexByChecksum($checksum) {
482
		$this->retrieve();
483
		$idx = 0;
484 View Code Duplication
		foreach ($this->children as $i => &$property) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
485
			if (substr(md5($property->serialize()), 0, 8) == $checksum ) {
486
				return $idx;
487
			}
488
			$idx += 1;
489
		}
490
		throw new Exception(self::$l10n->t('Property not found'), 404);
491
	}
492
493
	/**
494
	* Get a property by the checksum of its serialized value
495
	*
496
	* @param string $checksum An 8 char m5d checksum.
497
	* @return \Sabre\VObject\Property Property by reference
498
	* @throws Exception with error code 404 if the property is not found.
499
	*/
500
	public function getPropertyByChecksum($checksum) {
501
		$this->retrieve();
502 View Code Duplication
		foreach ($this->children as $i => &$property) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
503
			if (substr(md5($property->serialize()), 0, 8) == $checksum ) {
504
				return $property;
505
			}
506
		}
507
		throw new Exception(self::$l10n->t('Property not found'), 404);
508
	}
509
510
	/**
511
	* Delete a property by the checksum of its serialized value
512
	* It is up to the caller to call ->save()
513
	*
514
	* @param string $checksum An 8 char m5d checksum.
515
	* @throws @see getPropertyByChecksum
516
	*/
517
	public function unsetPropertyByChecksum($checksum) {
518
		$idx = $this->getPropertyIndexByChecksum($checksum);
519
		unset($this->children[$idx]);
520
		$this->setSaved(false);
521
	}
522
523
	/**
524
	* Set a property by the checksum of its serialized value
525
	* It is up to the caller to call ->save()
526
	*
527
	* @param string $checksum An 8 char m5d checksum.
528
	* @param string $name Property name
529
	* @param mixed $value
530
	* @param array $parameters
531
	* @throws @see getPropertyByChecksum
532
	* @return string new checksum
533
	*/
534
	public function setPropertyByChecksum($checksum, $name, $value, $parameters=array()) {
535
		if ($checksum === 'new') {
536
			$property = $this->createProperty($name);
0 ignored issues
show
Bug introduced by
The method createProperty() does not exist on OCA\Contacts\Contact. Did you maybe mean create()?

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...
537
			$this->add($property);
538
		} else {
539
			$property = $this->getPropertyByChecksum($checksum);
540
		}
541
		switch ($name) {
542
			case 'EMAIL':
543
			case 'CLOUD':
544
				$value = strtolower($value);
545
				$property->setValue($value);
546
				break;
547
			case 'ADR':
548
				if(is_array($value)) {
549
					$property->setParts($value);
550
				} else {
551
					$property->setValue($value);
552
				}
553
				break;
554
			case 'IMPP':
555
				if (is_null($parameters) || !isset($parameters['X-SERVICE-TYPE'])) {
556
					throw new \InvalidArgumentException(self::$l10n->t(' Missing IM parameter for: ') . $name. ' ' . $value, 412);
557
				}
558
				$serviceType = $parameters['X-SERVICE-TYPE'];
559
				if (is_array($serviceType)) {
560
					$serviceType = $serviceType[0];
561
				}
562
				$impp = Utils\Properties::getIMOptions($serviceType);
563
				if (is_null($impp)) {
564
					throw new \UnexpectedValueException(self::$l10n->t('Unknown IM: ') . $serviceType, 415);
565
				}
566
				$value = $impp['protocol'] . ':' . $value;
567
				$property->setValue($value);
568
				break;
569 View Code Duplication
			default:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
570
				\OCP\Util::writeLog('contacts', __METHOD__.' adding: '.$name. ' ' . $value, \OCP\Util::DEBUG);
571
				$property->setValue($value);
572
				break;
573
		}
574
		$this->setParameters($property, $parameters, true);
575
		$this->setSaved(false);
576
		return substr(md5($property->serialize()), 0, 8);
577
	}
578
579
	/**
580
	* Set a property by the property name.
581
	* It is up to the caller to call ->save()
582
	*
583
	* @param string $name Property name
584
	* @param mixed $value
585
	* @param array $parameters
586
	* @return bool
587
	*/
588 2
	public function setPropertyByName($name, $value, $parameters=array()) {
0 ignored issues
show
Unused Code introduced by
The parameter $parameters is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
589
		// TODO: parameters are ignored for now.
590
		switch ($name) {
591
			case 'BDAY':
592
				try {
593
					$date = New \DateTime($value);
594
				} catch(Exception $e) {
595
					\OCP\Util::writeLog('contacts',
596
						__METHOD__.' DateTime exception: ' . $e->getMessage(),
597
						\OCP\Util::ERROR
598
					);
599
					return false;
600
				}
601
				$value = $date->format('Y-m-d');
602
				$this->BDAY = $value;
603
				$this->BDAY->add('VALUE', 'DATE');
0 ignored issues
show
Bug introduced by
The method add cannot be called on $this->BDAY (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
604
				//\OCP\Util::writeLog('contacts', __METHOD__.' BDAY: '.$this->BDAY->serialize(), \OCP\Util::DEBUG);
605
				break;
606 2
			case 'CATEGORIES':
607
			case 'N':
608
			case 'ORG':
609
				$property = $this->select($name);
610
				if (count($property) === 0) {
611 2
					$property = $this->createProperty($name);
0 ignored issues
show
Bug introduced by
The method createProperty() does not exist on OCA\Contacts\Contact. Did you maybe mean create()?

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...
612
					$this->add($property);
613 View Code Duplication
				} elseif (count($property) > 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
614
					\OCP\Util::writeLog('contacts',
615
						__METHOD__.' more than one property for ' . $name,
616
						\OCP\Util::ERROR
617
					);
618 2
					return false;
619
				} else {
620
					// select returns an array...
621
					$property = array_shift($property);
622
				}
623
				if (($name === 'N') && !is_array($value)) {
624
					$value = explode(';', (string)$value);
625
				}
626
				if (is_array($value)) {
627
					$property->setParts($value);
628
				} else {
629
					$property->setValue($value);
630
				}
631
				break;
632 View Code Duplication
			default:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
633
				\OCP\Util::writeLog('contacts', __METHOD__.' adding: '.$name. ' ' . $value, \OCP\Util::DEBUG);
634
				$this->{$name} = $value;
635
				break;
636
		}
637
		$this->setSaved(false);
638
		return true;
639
	}
640
641
	protected function setParameters($property, $parameters, $reset = false) {
642
		if (!$parameters) {
643
			return;
644
		}
645
646
		if ($reset) {
647
			$property->parameters = array();
648
		}
649
		//debug('Setting parameters: ' . print_r($parameters, true));
650
		foreach ($parameters as $key => $parameter) {
651
			//debug('Adding parameter: ' . $key);
652
			if (is_array($parameter)) {
653
				foreach ($parameter as $val) {
654
					if (is_array($val)) {
655
						foreach ($val as $val2) {
656
							if (trim($key) && trim($val2)) {
657
								//debug('Adding parameter: '.$key.'=>'.print_r($val2, true));
658
								$property->add($key, strip_tags($val2));
659
							}
660
						}
661
					} else {
662
						if (trim($key) && trim($val)) {
663
							//debug('Adding parameter: '.$key.'=>'.print_r($val, true));
664
							$property->add($key, strip_tags($val));
665
						}
666
					}
667
				}
668
			} else {
669
				if (trim($key) && trim($parameter)) {
670
					//debug('Adding parameter: '.$key.'=>'.print_r($parameter, true));
671
					$property->add($key, strip_tags($parameter));
672
				}
673
			}
674
		}
675
	}
676
677 2
	public function lastModified() {
678 2
		if (!isset($this->props['lastmodified']) && !$this->isRetrieved()) {
679
			$this->retrieve();
680
		}
681 2
		return isset($this->props['lastmodified'])
682 2
			? $this->props['lastmodified']
683 2
			: null;
684
	}
685
686
	/**
687
	 * Merge in data from a multi-dimentional array
688
	 *
689
	 * NOTE: The data has actually already been merged client side!
690
	 * NOTE: The only properties coming from the web client are the ones
691
	 * defined in \OCA\Contacts\Utils\Properties::$indexProperties and
692
	 * UID is skipped for obvious reasons, and PHOTO is currently not updated.
693
	 * The data array has this structure:
694
	 *
695
	 * array(
696
	 * 	'EMAIL' => array(array('value' => '[email protected]', 'parameters' = array('TYPE' => array('HOME','VOICE'))))
697
	 * );
698
	 * @param array $data
699
	 * @return bool
700
	 */
701
	public function mergeFromArray(array $data) {
702
		foreach ($data as $name => $properties) {
703
			if (in_array($name, array('PHOTO', 'UID'))) {
704
				continue;
705
			}
706 View Code Duplication
			if (!is_array($properties)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
707
				\OCP\Util::writeLog('contacts', __METHOD__.' not an array?: ' .$name. ' '.print_r($properties, true), \OCP\Util::DEBUG);
708
			}
709
			if (in_array($name, Utils\Properties::$multiProperties)) {
710
				unset($this->{$name});
711
			}
712
			foreach ($properties as $parray) {
713
				\OCP\Util::writeLog('contacts', __METHOD__.' adding: ' .$name. ' '.print_r($parray['value'], true) . ' ' . print_r($parray['parameters'], true), \OCP\Util::DEBUG);
714
				if (in_array($name, Utils\Properties::$multiProperties)) {
715
					// TODO: wrap in try/catch, check return value
716
					$this->setPropertyByChecksum('new', $name, $parray['value'], $parray['parameters']);
717
				} else {
718
					// TODO: Check return value
719
					if (!isset($this->{$name})) {
720
						$this->setPropertyByName($name, $parray['value'], $parray['parameters']);
721
					}
722
				}
723
			}
724
		}
725
		$this->setSaved(false);
726
		return true;
727
	}
728
729 3
	public function __get($key) {
730 3
		if (!$this->isRetrieved()) {
731
			$this->retrieve();
732
		}
733
734 3
		return parent::__get($key);
735
	}
736
737 3
	public function __isset($key) {
738 3
		if (!$this->isRetrieved()) {
739
			$this->retrieve();
740
		}
741
742 3
		return parent::__isset($key);
743
	}
744
745 6
	public function __set($key, $value) {
746 6
		if (!$this->isRetrieved()) {
747 4
			$this->retrieve();
748 4
		}
749 6
		parent::__set($key, $value);
750 6
		if ($key === 'FN') {
751 6
			$this->props['displayname'] = $value;
752 6
		}
753 6
		$this->setSaved(false);
754 6
	}
755
756
	public function __unset($key) {
757
		if (!$this->isRetrieved()) {
758
			$this->retrieve();
759
		}
760
		parent::__unset($key);
761
		if ($key === 'PHOTO') {
762
			Properties::cacheThumbnail(
763
				$this->getBackend()->name,
764
				$this->getParent()->getId(),
765
				$this->getId(),
766
				null,
767
				$this,
768
				array('remove' => true)
769
			);
770
		}
771
		$this->setSaved(false);
772
	}
773
774
	/**
775
	 * @param boolean $state
776
	 */
777 7
	public function setRetrieved($state) {
778 7
		$this->props['retrieved'] = $state;
779 7
	}
780
781 7
	public function isRetrieved() {
782 7
		return $this->props['retrieved'];
783
	}
784
785 7
	public function setSaved($state = true) {
786 7
		$this->props['saved'] = $state;
787 7
	}
788
789 3
	public function isSaved() {
790 3
		return $this->props['saved'];
791
	}
792
793
	/**
794
	 * Generate an event to show in the calendar
795
	 *
796
	 * @return \Sabre\VObject\Component\VCalendar|null
797
	 */
798
	public function getBirthdayEvent() {
799
		if (!isset($this->BDAY)) {
800
			return null;
801
		}
802
		$birthday = $this->BDAY;
803
		if ((string)$birthday) {
804
			$title = str_replace('{name}',
805
				strtr((string)$this->FN, array('\,' => ',', '\;' => ';')),
806
				App::$l10n->t('{name}\'s Birthday')
807
			);
808
			try {
809
				$date = new \DateTime($birthday);
810
			} catch(Exception $e) {
811
				return null;
812
			}
813
			$vCal = new \Sabre\VObject\Component\VCalendar();
0 ignored issues
show
Bug introduced by
The call to VCalendar::__construct() misses a required argument $name.

This check looks for function calls that miss required arguments.

Loading history...
814
			$vCal->VERSION = '2.0';
815
			$vEvent = $vCal->createComponent('VEVENT');
0 ignored issues
show
Bug introduced by
The method createComponent() does not exist on Sabre\VObject\Component\VCalendar. Did you maybe mean create()?

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...
816
			$vEvent->add('DTSTART');
817
			$vEvent->DTSTART->setDateTime(
818
				$date
819
			);
820
			$vEvent->DTSTART['VALUE'] = 'DATE';
821
			$vEvent->add('DTEND');
822
			$date->add(new \DateInterval('P1D'));
823
			$vEvent->DTEND->setDateTime(
824
				$date
825
			);
826
			$vEvent->DTEND['VALUE'] = 'DATE';
827
			$lm = new \DateTime('@' . $this->lastModified());
828
			$lm->setTimeZone(new \DateTimeZone('UTC'));
829
			$vEvent->DTSTAMP->setDateTime($lm);
830
			$vEvent->{'UID'} = $this->UID;
831
			$vEvent->{'RRULE'} = 'FREQ=YEARLY';
832
			$vEvent->{'SUMMARY'} = $title . ' (' . $date->format('Y') . ')';
833
			$vEvent->{'TRANSP'} = 'TRANSPARENT';
834
			$appInfo = \OCP\App::getAppInfo('contacts');
835
			$appVersion = \OCP\App::getAppVersion('contacts');
836
			$vCal->PRODID = '-//ownCloud//NONSGML '.$appInfo['name'].' '.$appVersion.'//EN';
837
			$vCal->add($vEvent);
838
			return $vCal;
839
		}
840
841
		return null;
842
	}
843
844
}
845