<?php
/**
 * Copyright 2000-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file LICENSE for license information (ASL).  If you did
 * did not receive this file, see http://www.horde.org/licenses/apache.
 *
 * @category  Horde
 * @copyright 2000-2017 Horde LLC
 * @license   http://www.horde.org/licenses/apache ASL
 * @package   Turba
 */

/**
 * A base implementation for Turba objects - people, groups, restaurants, etc.
 *
 * @author    Chuck Hagenbuch <chuck@horde.org>
 * @author    Jon Parise <jon@csh.rit.edu>
 * @category  Horde
 * @copyright 2000-2017 Horde LLC
 * @license   http://www.horde.org/licenses/apache ASL
 * @package   Turba
 */
class Turba_Object
{
    /**
     * Underlying driver.
     *
     * @var Turba_Driver
     */
    public $driver;

    /**
     * Hash of attributes for this contact.
     *
     * @var array
     */
    public $attributes;

    /**
     * Keeps the normalized values of sort columns.
     *
     * @var array
     */
    public $sortValue = array();

    /**
     * Any additional options.
     *
     * @var boolean
     */
    protected $_options = array();

    /**
     * Reference to this object's VFS instance.
     *
     * @var VFS
     */
    protected $_vfs;

    /**
     * Local cache of available email addresses. Needed to ensure we
     * populate the email field correctly. See See Bug: 12955 and Bug: 14046.
     * A hash with turba attribute names as key.
     *
     * @var array
     */
    protected $_emailFields = array();

    /**
     * Constructs a new Turba_Object object.
     *
     * @param Turba_Driver $driver  The source that this object came from.
     * @param array $attributes     Hash of attributes for this object.
     * @param array $options        Hash of options for this object. @since
     *                              Turba 4.2
     */
    public function __construct(Turba_Driver $driver,
                                array $attributes = array(),
                                array $options = array())
    {
        $this->driver = $driver;
        $this->attributes = $attributes;
        $this->attributes['__type'] = 'Object';
        $this->_options = $options;
    }

    /**
     * Returns a key-value hash containing all properties of this object.
     *
     * @return array  All properties of this object.
     */
    public function getAttributes()
    {
        return $this->attributes;
    }

    /**
     * Returns the name of the address book that this object is from.
     */
    public function getSource()
    {
        return $this->driver->getName();
    }

    /**
     * Get a fully qualified key for this contact.
     *
     * @param string $delimiter Delimiter for the parts of the key, defaults to ':'.
     *
     * @return string Fully qualified contact id.
     */
    public function getGuid($delimiter = ':')
    {
        return 'turba' . $delimiter . $this->getSource() . $delimiter . $this->getValue('__uid');
    }

    /**
     * Returns the value of the specified attribute.
     *
     * @param string $attribute  The attribute to retrieve.
     *
     * @return mixed  The value of $attribute, an array (for photo type)
     *                or the empty string.
     */
    public function getValue($attribute)
    {
        global $attributes, $injector;

        if (isset($this->attributes[$attribute]) &&
            ($hooks = $injector->getInstance('Horde_Core_Hooks')) &&
            $hooks->hookExists('decode_attribute', 'turba')) {
            try {
                return $hooks->callHook(
                    'decode_attribute',
                    'turba',
                    array($attribute, $this->attributes[$attribute], $this)
                );
            } catch (Turba_Exception $e) {}
        } elseif (isset($this->driver->map[$attribute]) &&
            is_array($this->driver->map[$attribute])) {
            $args = array();
            foreach ($this->driver->map[$attribute]['fields'] as $field) {
                $args[] = $this->getValue($field);
            }
            return Turba::formatCompositeField($this->driver->map[$attribute]['format'], $args);
        } elseif (!isset($this->attributes[$attribute])) {
            if (isset($attributes[$attribute]) &&
                ($attributes[$attribute]['type'] == 'Turba:TurbaTags') &&
                ($uid = $this->getValue('__uid'))) {
                $this->synchronizeTags($injector->getInstance('Turba_Tagger')->getTags($uid, 'contact'));
            } else {
                return null;
            }
        } elseif (isset($attributes[$attribute]) &&
            ($attributes[$attribute]['type'] == 'image')) {
            return empty($this->attributes[$attribute])
                ? null
                : array(
                      'load' => array(
                          'data' => $this->attributes[$attribute],
                          'file' => basename(Horde::getTempFile('horde_form_', false, '', false, true))
                      )
                  );
        }

        return $this->attributes[$attribute];
    }

    /**
     * Sets the value of the specified attribute.
     *
     * @param string $attribute  The attribute to set.
     * @param string $value      The value of $attribute.
     */
    public function setValue($attribute, $value)
    {
        global $injector, $attributes;

        $hooks = $injector->getInstance('Horde_Core_Hooks');

        if ($hooks->hookExists('encode_attribute', 'turba')) {
            try {
                $value = $hooks->callHook(
                    'encode_attribute',
                    'turba',
                    array(
                        $attribute,
                        $value,
                        isset($this->attributes[$attribute]) ? $this->attributes[$attribute] : null,
                        $this
                    )
                );
            } catch (Turba_Exception $e) {}
        }

        // If we don't know the attribute, it's not a private attribute,
        // and it's an email field, save it in case we need to populate an email
        // field on save.
        if (!isset($this->driver->map[$attribute]) && strpos($attribute, '__') === false) {
            if (isset($attributes[$attribute]) &&
                $attributes[$attribute]['type'] == 'email') {
                $this->_emailFields[$attribute] = $value;
            }
            return;
        }

        $this->attributes[$attribute] = $value;
    }

    /**
     * Determines whether or not the object has a value for the specified
     * attribute.
     *
     * @param string $attribute  The attribute to check.
     *
     * @return boolean  Whether or not there is a value for $attribute.
     */
    public function hasValue($attribute)
    {
        if (isset($this->driver->map[$attribute]) &&
            is_array($this->driver->map[$attribute])) {
            foreach ($this->driver->map[$attribute]['fields'] as $field) {
                if ($this->hasValue($field)) {
                    return true;
                }
            }
            return false;
        } else {
            return !is_null($this->getValue($attribute));
        }
    }

    /**
     * Syncronizes tags from the tagging backend with the contacts storage
     * backend, if necessary.
     *
     * @param array $tags  Tags from the tagging backend.
     */
    public function synchronizeTags(array $tags)
    {
        if (!is_null($internaltags = $this->getValue('__internaltags'))) {
            $internaltags = unserialize($internaltags);
            usort($tags, 'strcoll');
            if (array_diff($internaltags, $tags)) {
                $GLOBALS['injector']->getInstance('Turba_Tagger')->replaceTags(
                    $this->getValue('__uid'),
                    $internaltags,
                    $this->driver->getContactOwner(),
                    'contact'
                );
            }
            $this->setValue('__tags', implode(', ', $internaltags));
        } else {
            $this->setValue('__tags', implode(', ', $tags));
        }
    }

    /**
     * Returns the timestamp of the last modification, whether this was the
     * creation or editing of the object and stores it as the attribute
     * __modified. The value is cached for the lifetime of the object.
     *
     * @return integer  The timestamp of the last modification or zero.
     */
    public function lastModification()
    {
        $time = $this->getValue('__modified');
        if (!is_null($time)) {
            return $time;
        }
        if (!$this->getValue('__uid')) {
            $this->setValue('__modified', 0);
            return 0;
        }
        $time = 0;
        try {
            $log = $GLOBALS['injector']
                ->getInstance('Horde_History')
                ->getHistory($this->getGuid());
            foreach ($log as $entry) {
                if ($entry['action'] == 'add' || $entry['action'] == 'modify') {

                    $time = max($time, $entry['ts']);
                }
            }
        } catch (Exception $e) {}
        $this->setValue('__modified', $time);

        return $time;
    }

    /**
     * Merges another contact into this one by filling empty fields of this
     * contact with values from the other.
     *
     * @param Turba_Object $contact  Another contact.
     */
    public function merge(Turba_Object $contact)
    {
        foreach (array_keys($contact->attributes) as $attribute) {
            if (!$this->hasValue($attribute) && $contact->hasValue($attribute)) {
                $this->setValue($attribute, $contact->getValue($attribute));
            }
        }
    }

    /**
     * Returns history information about this contact.
     *
     * @return array  A hash with the optional entries 'created' and 'modified'
     *                and human readable history information as the values.
     */
    public function getHistory()
    {
        if (!$this->getValue('__uid')) {
            return array();
        }
        $history = array();
        try {
            $log = $GLOBALS['injector']
                ->getInstance('Horde_History')
                ->getHistory($this->getGuid());
            foreach ($log as $entry) {
                if ($entry['action'] == 'add' || $entry['action'] == 'modify') {
                    if ($GLOBALS['registry']->getAuth() != $entry['who']) {
                        $by = sprintf(_("by %s"), Turba::getUserName($entry['who']));
                    } else {
                        $by = _("by me");
                    }
                    $history[$entry['action'] == 'add' ? 'created' : 'modified']
                        = strftime($GLOBALS['prefs']->getValue('date_format'), $entry['ts'])
                        . ' '
                        . date($GLOBALS['prefs']->getValue('twentyFour') ? 'G:i' : 'g:i a', $entry['ts'])
                        . ' '
                        . htmlspecialchars($by);
                }
            }
        } catch (Exception $e) {
            return array();
        }

        return $history;
    }

    /**
     * Returns true if this object is a group of multiple contacts.
     *
     * @return boolean  True if this object is a group of multiple contacts.
     */
    public function isGroup()
    {
        return false;
    }

    /**
     * Returns true if this object is editable by the current user.
     *
     * @return boolean  Whether or not the current user can edit this object
     */
    public function isEditable()
    {
        return $this->driver->hasPermission(Horde_Perms::EDIT);
    }

    /**
     * Returns whether or not the current user has the requested permission.
     *
     * @param integer $perm  The permission to check.
     *
     * @return boolean True if user has the permission.
     */
    public function hasPermission($perm)
    {
        return $this->driver->hasPermission($perm);
    }

    /**
     * Contact url.
     *
     * @param string $view   The view for the url
     * @param boolean $full  Generate a full url?
     *
     * @return string
     */
    public function url($view = null, $full = false)
    {
        $url = Horde::url('contact.php', $full)->add(array(
            'source' => $this->driver->getName(),
            'key' => $this->getValue('__key')
        ));

        if (!is_null($view)) {
            $url->add('view', $view);
        }

        return $url;
    }

    /**
     * Saves a file into the VFS backend associated with this object.
     *
     * @param array $info  A hash with the file information as returned from a
     *                     Horde_Form_Type_file.
     * @throws Turba_Exception
     */
    public function addFile(array $info)
    {
        if (!$this->getValue('__uid')) {
            throw new Turba_Exception('VFS not supported for this object.');
        }

        $vfs = $this->vfsInit();

        $dir = Turba::VFS_PATH . '/' . $this->getValue('__uid');
        $file = $info['name'];
        while ($vfs->exists($dir, $file)) {
            if (preg_match('/(.*)\[(\d+)\](\.[^.]*)?$/', $file, $match)) {
                $file = $match[1] . '[' . ++$match[2] . ']' . $match[3];
            } else {
                $dot = strrpos($file, '.');
                if ($dot === false) {
                    $file .= '[1]';
                } else {
                    $file = substr($file, 0, $dot) . '[1]' . substr($file, $dot);
                }
            }
        }
        try {
            $vfs->write($dir, $file, $info['tmp_name'], true);
        } catch (Horde_Vfs_Exception $e) {
            throw new Turba_Exception($e);
        }
    }

    /**
     * Deletes a file from the VFS backend associated with this object.
     *
     * @param string $file  The file name.
     * @throws Turba_Exception
     */
    public function deleteFile($file)
    {
        if (!$this->getValue('__uid')) {
            throw new Turba_Exception('VFS not supported for this object.');
        }

        try {
            $this->vfsInit()->deleteFile(Turba::VFS_PATH . '/' . $this->getValue('__uid'), $file);
        } catch (Horde_Vfs_Exception $e) {
            throw new Turba_Exception($e);
        }
    }

    /**
     * Deletes all files from the VFS backend associated with this object.
     *
     * @throws Turba_Exception
     */
    public function deleteFiles()
    {
        if (!$this->getValue('__uid')) {
            throw new Turba_Exception('VFS not supported for this object.');
        }

        $vfs = $this->vfsInit();

        if ($vfs->exists(Turba::VFS_PATH, $this->getValue('__uid'))) {
            try {
                $vfs->deleteFolder(Turba::VFS_PATH, $this->getValue('__uid'), true);
            } catch (Horde_Vfs_Exception $e) {
                throw new Turba_Exception($e);
            }
        }
    }

    /**
     * Returns all files from the VFS backend associated with this object.
     *
     * @return array  A list of hashes with file informations.
     */
    public function listFiles()
    {
        if ($this->getValue('__uid')) {
            try {
                $vfs = $this->vfsInit();
                if ($vfs->exists(Turba::VFS_PATH, $this->getValue('__uid'))) {
                    return $vfs->listFolder(Turba::VFS_PATH . '/' . $this->getValue('__uid'));
                }
            } catch (Turba_Exception $e) {}
        }

        return array();
    }

    /**
     * Returns a link to display and download a file from the VFS backend
     * associated with this object.
     *
     * @param string $file  The file name.
     *
     * @return string  The HTML code of the generated link.
     */
    public function vfsDisplayUrl($file)
    {
        global $registry;

        $mime_part = new Horde_Mime_Part();
        $mime_part->setType(Horde_Mime_Magic::extToMime($file['type']));
        $viewer = $GLOBALS['injector']->getInstance('Horde_Core_Factory_MimeViewer')->create($mime_part);

        // We can always download files.
        $url_params = array(
            'actionID' => 'download_file',
            'file' => $file['name'],
            'type' => $file['type'],
            'source' => $this->driver->getName(),
            'key' => $this->getValue('__key')
        );
        $dl = Horde::link($registry->downloadUrl($file['name'], $url_params), $file['name']) . Horde_Themes_Image::tag('download.png', array('alt' => _("Download"))) . '</a>';

        // Let's see if we can view this one, too.
        if ($viewer && !($viewer instanceof Horde_Mime_Viewer_Default)) {
            $url = Horde::url('view.php')
                ->add($url_params)
                ->add('actionID', 'view_file');
            $link = Horde::link($url, $file['name'], null, '_blank') . $file['name'] . '</a>';
        } else {
            $link = $file['name'];
        }

        return $link . ' ' . $dl;
    }

    /**
     * Returns a link to display, download, and delete a file from the VFS
     * backend associated with this object.
     *
     * @param string $file  The file name.
     *
     * @return string  The HTML code of the generated link.
     */
    public function vfsEditUrl($file)
    {
        $delform = '<form action="' .
            Horde::url('deletefile.php') .
            '" style="display:inline" method="post">' .
            Horde_Util::formInput() .
            '<input type="hidden" name="file" value="' . htmlspecialchars($file['name']) . '" />' .
            '<input type="hidden" name="source" value="' . htmlspecialchars($this->driver->getName()) . '" />' .
            '<input type="hidden" name="key" value="' . htmlspecialchars($this->getValue('__key')) . '" />' .
            '<input type="image" class="img" src="' . Horde_Themes::img('delete.png') . '" />' .
            '</form>';

        return $this->vfsDisplayUrl($file) . ' ' . $delform;
    }

    /**
     * Saves the current state of the object to the storage backend.
     *
     * @throws Turba_Exception
     */
    public function store()
    {
        $this->_ensureEmail();
        return $this->setValue('__key', $this->driver->save($this));
    }

    /**
     * Loads the VFS configuration and initializes the VFS backend.
     *
     * @return Horde_Vfs  A VFS object.
     * @throws Turba_Exception
     */
    public function vfsInit()
    {
        if (!isset($this->_vfs)) {
            try {
                $this->_vfs = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Vfs')->create('documents');
            } catch (Horde_Exception $e) {
                throw new Turba_Exception($e);
            }
        }

        return $this->_vfs;
    }

    /**
     * Ensure we have an email address set, if available. Needed to cover the
     * case where a contact might have been imported via vCard with email TYPEs
     * that do not match the configured attributes for this source. E.g., the
     * vCard contains a TYPE=HOME but we only have the generic 'email' field
     * available.
     *
     * @return [type] [description]
     */
    protected  function _ensureEmail()
    {
        global $attributes;

        // If an email type attribute is not known to this object's driver map
        // then attempt to fill in any email attributes we DO know about that
        // are currently empty. Not ideal, but if a client is sending unknown
        // email fields, we have no way of knowing where to put them and this
        // is better than dropping them.
        foreach ($this->_emailFields as $attribute => $email) {
            if (empty($this->driver->map[$attribute]) && $attribute != 'emails') {
                foreach ($this->driver->map as $driver_att => $driver_value) {
                    if ($attributes[$driver_att]['type'] == 'email' &&
                        empty($this->attributes[$driver_att])) {
                        $this->attributes[$driver_att] = $email;
                        break;
                    }
                }
            }
        }
    }

}
