1<?php 2/** 3 * Copyright 2000-2017 Horde LLC (http://www.horde.org/) 4 * 5 * See the enclosed file LICENSE for license information (ASL). If you did 6 * did not receive this file, see http://www.horde.org/licenses/apache. 7 * 8 * @category Horde 9 * @copyright 2000-2017 Horde LLC 10 * @license http://www.horde.org/licenses/apache ASL 11 * @package Turba 12 */ 13 14/** 15 * A base implementation for Turba objects - people, groups, restaurants, etc. 16 * 17 * @author Chuck Hagenbuch <chuck@horde.org> 18 * @author Jon Parise <jon@csh.rit.edu> 19 * @category Horde 20 * @copyright 2000-2017 Horde LLC 21 * @license http://www.horde.org/licenses/apache ASL 22 * @package Turba 23 */ 24class Turba_Object 25{ 26 /** 27 * Underlying driver. 28 * 29 * @var Turba_Driver 30 */ 31 public $driver; 32 33 /** 34 * Hash of attributes for this contact. 35 * 36 * @var array 37 */ 38 public $attributes; 39 40 /** 41 * Keeps the normalized values of sort columns. 42 * 43 * @var array 44 */ 45 public $sortValue = array(); 46 47 /** 48 * Any additional options. 49 * 50 * @var boolean 51 */ 52 protected $_options = array(); 53 54 /** 55 * Reference to this object's VFS instance. 56 * 57 * @var VFS 58 */ 59 protected $_vfs; 60 61 /** 62 * Local cache of available email addresses. Needed to ensure we 63 * populate the email field correctly. See See Bug: 12955 and Bug: 14046. 64 * A hash with turba attribute names as key. 65 * 66 * @var array 67 */ 68 protected $_emailFields = array(); 69 70 /** 71 * Constructs a new Turba_Object object. 72 * 73 * @param Turba_Driver $driver The source that this object came from. 74 * @param array $attributes Hash of attributes for this object. 75 * @param array $options Hash of options for this object. @since 76 * Turba 4.2 77 */ 78 public function __construct(Turba_Driver $driver, 79 array $attributes = array(), 80 array $options = array()) 81 { 82 $this->driver = $driver; 83 $this->attributes = $attributes; 84 $this->attributes['__type'] = 'Object'; 85 $this->_options = $options; 86 } 87 88 /** 89 * Returns a key-value hash containing all properties of this object. 90 * 91 * @return array All properties of this object. 92 */ 93 public function getAttributes() 94 { 95 return $this->attributes; 96 } 97 98 /** 99 * Returns the name of the address book that this object is from. 100 */ 101 public function getSource() 102 { 103 return $this->driver->getName(); 104 } 105 106 /** 107 * Get a fully qualified key for this contact. 108 * 109 * @param string $delimiter Delimiter for the parts of the key, defaults to ':'. 110 * 111 * @return string Fully qualified contact id. 112 */ 113 public function getGuid($delimiter = ':') 114 { 115 return 'turba' . $delimiter . $this->getSource() . $delimiter . $this->getValue('__uid'); 116 } 117 118 /** 119 * Returns the value of the specified attribute. 120 * 121 * @param string $attribute The attribute to retrieve. 122 * 123 * @return mixed The value of $attribute, an array (for photo type) 124 * or the empty string. 125 */ 126 public function getValue($attribute) 127 { 128 global $attributes, $injector; 129 130 if (isset($this->attributes[$attribute]) && 131 ($hooks = $injector->getInstance('Horde_Core_Hooks')) && 132 $hooks->hookExists('decode_attribute', 'turba')) { 133 try { 134 return $hooks->callHook( 135 'decode_attribute', 136 'turba', 137 array($attribute, $this->attributes[$attribute], $this) 138 ); 139 } catch (Turba_Exception $e) {} 140 } elseif (isset($this->driver->map[$attribute]) && 141 is_array($this->driver->map[$attribute])) { 142 $args = array(); 143 foreach ($this->driver->map[$attribute]['fields'] as $field) { 144 $args[] = $this->getValue($field); 145 } 146 return Turba::formatCompositeField($this->driver->map[$attribute]['format'], $args); 147 } elseif (!isset($this->attributes[$attribute])) { 148 if (isset($attributes[$attribute]) && 149 ($attributes[$attribute]['type'] == 'Turba:TurbaTags') && 150 ($uid = $this->getValue('__uid'))) { 151 $this->synchronizeTags($injector->getInstance('Turba_Tagger')->getTags($uid, 'contact')); 152 } else { 153 return null; 154 } 155 } elseif (isset($attributes[$attribute]) && 156 ($attributes[$attribute]['type'] == 'image')) { 157 return empty($this->attributes[$attribute]) 158 ? null 159 : array( 160 'load' => array( 161 'data' => $this->attributes[$attribute], 162 'file' => basename(Horde::getTempFile('horde_form_', false, '', false, true)) 163 ) 164 ); 165 } 166 167 return $this->attributes[$attribute]; 168 } 169 170 /** 171 * Sets the value of the specified attribute. 172 * 173 * @param string $attribute The attribute to set. 174 * @param string $value The value of $attribute. 175 */ 176 public function setValue($attribute, $value) 177 { 178 global $injector, $attributes; 179 180 $hooks = $injector->getInstance('Horde_Core_Hooks'); 181 182 if ($hooks->hookExists('encode_attribute', 'turba')) { 183 try { 184 $value = $hooks->callHook( 185 'encode_attribute', 186 'turba', 187 array( 188 $attribute, 189 $value, 190 isset($this->attributes[$attribute]) ? $this->attributes[$attribute] : null, 191 $this 192 ) 193 ); 194 } catch (Turba_Exception $e) {} 195 } 196 197 // If we don't know the attribute, it's not a private attribute, 198 // and it's an email field, save it in case we need to populate an email 199 // field on save. 200 if (!isset($this->driver->map[$attribute]) && strpos($attribute, '__') === false) { 201 if (isset($attributes[$attribute]) && 202 $attributes[$attribute]['type'] == 'email') { 203 $this->_emailFields[$attribute] = $value; 204 } 205 return; 206 } 207 208 $this->attributes[$attribute] = $value; 209 } 210 211 /** 212 * Determines whether or not the object has a value for the specified 213 * attribute. 214 * 215 * @param string $attribute The attribute to check. 216 * 217 * @return boolean Whether or not there is a value for $attribute. 218 */ 219 public function hasValue($attribute) 220 { 221 if (isset($this->driver->map[$attribute]) && 222 is_array($this->driver->map[$attribute])) { 223 foreach ($this->driver->map[$attribute]['fields'] as $field) { 224 if ($this->hasValue($field)) { 225 return true; 226 } 227 } 228 return false; 229 } else { 230 return !is_null($this->getValue($attribute)); 231 } 232 } 233 234 /** 235 * Syncronizes tags from the tagging backend with the contacts storage 236 * backend, if necessary. 237 * 238 * @param array $tags Tags from the tagging backend. 239 */ 240 public function synchronizeTags(array $tags) 241 { 242 if (!is_null($internaltags = $this->getValue('__internaltags'))) { 243 $internaltags = unserialize($internaltags); 244 usort($tags, 'strcoll'); 245 if (array_diff($internaltags, $tags)) { 246 $GLOBALS['injector']->getInstance('Turba_Tagger')->replaceTags( 247 $this->getValue('__uid'), 248 $internaltags, 249 $this->driver->getContactOwner(), 250 'contact' 251 ); 252 } 253 $this->setValue('__tags', implode(', ', $internaltags)); 254 } else { 255 $this->setValue('__tags', implode(', ', $tags)); 256 } 257 } 258 259 /** 260 * Returns the timestamp of the last modification, whether this was the 261 * creation or editing of the object and stores it as the attribute 262 * __modified. The value is cached for the lifetime of the object. 263 * 264 * @return integer The timestamp of the last modification or zero. 265 */ 266 public function lastModification() 267 { 268 $time = $this->getValue('__modified'); 269 if (!is_null($time)) { 270 return $time; 271 } 272 if (!$this->getValue('__uid')) { 273 $this->setValue('__modified', 0); 274 return 0; 275 } 276 $time = 0; 277 try { 278 $log = $GLOBALS['injector'] 279 ->getInstance('Horde_History') 280 ->getHistory($this->getGuid()); 281 foreach ($log as $entry) { 282 if ($entry['action'] == 'add' || $entry['action'] == 'modify') { 283 284 $time = max($time, $entry['ts']); 285 } 286 } 287 } catch (Exception $e) {} 288 $this->setValue('__modified', $time); 289 290 return $time; 291 } 292 293 /** 294 * Merges another contact into this one by filling empty fields of this 295 * contact with values from the other. 296 * 297 * @param Turba_Object $contact Another contact. 298 */ 299 public function merge(Turba_Object $contact) 300 { 301 foreach (array_keys($contact->attributes) as $attribute) { 302 if (!$this->hasValue($attribute) && $contact->hasValue($attribute)) { 303 $this->setValue($attribute, $contact->getValue($attribute)); 304 } 305 } 306 } 307 308 /** 309 * Returns history information about this contact. 310 * 311 * @return array A hash with the optional entries 'created' and 'modified' 312 * and human readable history information as the values. 313 */ 314 public function getHistory() 315 { 316 if (!$this->getValue('__uid')) { 317 return array(); 318 } 319 $history = array(); 320 try { 321 $log = $GLOBALS['injector'] 322 ->getInstance('Horde_History') 323 ->getHistory($this->getGuid()); 324 foreach ($log as $entry) { 325 if ($entry['action'] == 'add' || $entry['action'] == 'modify') { 326 if ($GLOBALS['registry']->getAuth() != $entry['who']) { 327 $by = sprintf(_("by %s"), Turba::getUserName($entry['who'])); 328 } else { 329 $by = _("by me"); 330 } 331 $history[$entry['action'] == 'add' ? 'created' : 'modified'] 332 = strftime($GLOBALS['prefs']->getValue('date_format'), $entry['ts']) 333 . ' ' 334 . date($GLOBALS['prefs']->getValue('twentyFour') ? 'G:i' : 'g:i a', $entry['ts']) 335 . ' ' 336 . htmlspecialchars($by); 337 } 338 } 339 } catch (Exception $e) { 340 return array(); 341 } 342 343 return $history; 344 } 345 346 /** 347 * Returns true if this object is a group of multiple contacts. 348 * 349 * @return boolean True if this object is a group of multiple contacts. 350 */ 351 public function isGroup() 352 { 353 return false; 354 } 355 356 /** 357 * Returns true if this object is editable by the current user. 358 * 359 * @return boolean Whether or not the current user can edit this object 360 */ 361 public function isEditable() 362 { 363 return $this->driver->hasPermission(Horde_Perms::EDIT); 364 } 365 366 /** 367 * Returns whether or not the current user has the requested permission. 368 * 369 * @param integer $perm The permission to check. 370 * 371 * @return boolean True if user has the permission. 372 */ 373 public function hasPermission($perm) 374 { 375 return $this->driver->hasPermission($perm); 376 } 377 378 /** 379 * Contact url. 380 * 381 * @param string $view The view for the url 382 * @param boolean $full Generate a full url? 383 * 384 * @return string 385 */ 386 public function url($view = null, $full = false) 387 { 388 $url = Horde::url('contact.php', $full)->add(array( 389 'source' => $this->driver->getName(), 390 'key' => $this->getValue('__key') 391 )); 392 393 if (!is_null($view)) { 394 $url->add('view', $view); 395 } 396 397 return $url; 398 } 399 400 /** 401 * Saves a file into the VFS backend associated with this object. 402 * 403 * @param array $info A hash with the file information as returned from a 404 * Horde_Form_Type_file. 405 * @throws Turba_Exception 406 */ 407 public function addFile(array $info) 408 { 409 if (!$this->getValue('__uid')) { 410 throw new Turba_Exception('VFS not supported for this object.'); 411 } 412 413 $vfs = $this->vfsInit(); 414 415 $dir = Turba::VFS_PATH . '/' . $this->getValue('__uid'); 416 $file = $info['name']; 417 while ($vfs->exists($dir, $file)) { 418 if (preg_match('/(.*)\[(\d+)\](\.[^.]*)?$/', $file, $match)) { 419 $file = $match[1] . '[' . ++$match[2] . ']' . $match[3]; 420 } else { 421 $dot = strrpos($file, '.'); 422 if ($dot === false) { 423 $file .= '[1]'; 424 } else { 425 $file = substr($file, 0, $dot) . '[1]' . substr($file, $dot); 426 } 427 } 428 } 429 try { 430 $vfs->write($dir, $file, $info['tmp_name'], true); 431 } catch (Horde_Vfs_Exception $e) { 432 throw new Turba_Exception($e); 433 } 434 } 435 436 /** 437 * Deletes a file from the VFS backend associated with this object. 438 * 439 * @param string $file The file name. 440 * @throws Turba_Exception 441 */ 442 public function deleteFile($file) 443 { 444 if (!$this->getValue('__uid')) { 445 throw new Turba_Exception('VFS not supported for this object.'); 446 } 447 448 try { 449 $this->vfsInit()->deleteFile(Turba::VFS_PATH . '/' . $this->getValue('__uid'), $file); 450 } catch (Horde_Vfs_Exception $e) { 451 throw new Turba_Exception($e); 452 } 453 } 454 455 /** 456 * Deletes all files from the VFS backend associated with this object. 457 * 458 * @throws Turba_Exception 459 */ 460 public function deleteFiles() 461 { 462 if (!$this->getValue('__uid')) { 463 throw new Turba_Exception('VFS not supported for this object.'); 464 } 465 466 $vfs = $this->vfsInit(); 467 468 if ($vfs->exists(Turba::VFS_PATH, $this->getValue('__uid'))) { 469 try { 470 $vfs->deleteFolder(Turba::VFS_PATH, $this->getValue('__uid'), true); 471 } catch (Horde_Vfs_Exception $e) { 472 throw new Turba_Exception($e); 473 } 474 } 475 } 476 477 /** 478 * Returns all files from the VFS backend associated with this object. 479 * 480 * @return array A list of hashes with file informations. 481 */ 482 public function listFiles() 483 { 484 if ($this->getValue('__uid')) { 485 try { 486 $vfs = $this->vfsInit(); 487 if ($vfs->exists(Turba::VFS_PATH, $this->getValue('__uid'))) { 488 return $vfs->listFolder(Turba::VFS_PATH . '/' . $this->getValue('__uid')); 489 } 490 } catch (Turba_Exception $e) {} 491 } 492 493 return array(); 494 } 495 496 /** 497 * Returns a link to display and download a file from the VFS backend 498 * associated with this object. 499 * 500 * @param string $file The file name. 501 * 502 * @return string The HTML code of the generated link. 503 */ 504 public function vfsDisplayUrl($file) 505 { 506 global $registry; 507 508 $mime_part = new Horde_Mime_Part(); 509 $mime_part->setType(Horde_Mime_Magic::extToMime($file['type'])); 510 $viewer = $GLOBALS['injector']->getInstance('Horde_Core_Factory_MimeViewer')->create($mime_part); 511 512 // We can always download files. 513 $url_params = array( 514 'actionID' => 'download_file', 515 'file' => $file['name'], 516 'type' => $file['type'], 517 'source' => $this->driver->getName(), 518 'key' => $this->getValue('__key') 519 ); 520 $dl = Horde::link($registry->downloadUrl($file['name'], $url_params), $file['name']) . Horde_Themes_Image::tag('download.png', array('alt' => _("Download"))) . '</a>'; 521 522 // Let's see if we can view this one, too. 523 if ($viewer && !($viewer instanceof Horde_Mime_Viewer_Default)) { 524 $url = Horde::url('view.php') 525 ->add($url_params) 526 ->add('actionID', 'view_file'); 527 $link = Horde::link($url, $file['name'], null, '_blank') . $file['name'] . '</a>'; 528 } else { 529 $link = $file['name']; 530 } 531 532 return $link . ' ' . $dl; 533 } 534 535 /** 536 * Returns a link to display, download, and delete a file from the VFS 537 * backend associated with this object. 538 * 539 * @param string $file The file name. 540 * 541 * @return string The HTML code of the generated link. 542 */ 543 public function vfsEditUrl($file) 544 { 545 $delform = '<form action="' . 546 Horde::url('deletefile.php') . 547 '" style="display:inline" method="post">' . 548 Horde_Util::formInput() . 549 '<input type="hidden" name="file" value="' . htmlspecialchars($file['name']) . '" />' . 550 '<input type="hidden" name="source" value="' . htmlspecialchars($this->driver->getName()) . '" />' . 551 '<input type="hidden" name="key" value="' . htmlspecialchars($this->getValue('__key')) . '" />' . 552 '<input type="image" class="img" src="' . Horde_Themes::img('delete.png') . '" />' . 553 '</form>'; 554 555 return $this->vfsDisplayUrl($file) . ' ' . $delform; 556 } 557 558 /** 559 * Saves the current state of the object to the storage backend. 560 * 561 * @throws Turba_Exception 562 */ 563 public function store() 564 { 565 $this->_ensureEmail(); 566 return $this->setValue('__key', $this->driver->save($this)); 567 } 568 569 /** 570 * Loads the VFS configuration and initializes the VFS backend. 571 * 572 * @return Horde_Vfs A VFS object. 573 * @throws Turba_Exception 574 */ 575 public function vfsInit() 576 { 577 if (!isset($this->_vfs)) { 578 try { 579 $this->_vfs = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Vfs')->create('documents'); 580 } catch (Horde_Exception $e) { 581 throw new Turba_Exception($e); 582 } 583 } 584 585 return $this->_vfs; 586 } 587 588 /** 589 * Ensure we have an email address set, if available. Needed to cover the 590 * case where a contact might have been imported via vCard with email TYPEs 591 * that do not match the configured attributes for this source. E.g., the 592 * vCard contains a TYPE=HOME but we only have the generic 'email' field 593 * available. 594 * 595 * @return [type] [description] 596 */ 597 protected function _ensureEmail() 598 { 599 global $attributes; 600 601 // If an email type attribute is not known to this object's driver map 602 // then attempt to fill in any email attributes we DO know about that 603 // are currently empty. Not ideal, but if a client is sending unknown 604 // email fields, we have no way of knowing where to put them and this 605 // is better than dropping them. 606 foreach ($this->_emailFields as $attribute => $email) { 607 if (empty($this->driver->map[$attribute]) && $attribute != 'emails') { 608 foreach ($this->driver->map as $driver_att => $driver_value) { 609 if ($attributes[$driver_att]['type'] == 'email' && 610 empty($this->attributes[$driver_att])) { 611 $this->attributes[$driver_att] = $email; 612 break; 613 } 614 } 615 } 616 } 617 } 618 619} 620