Files
server/usr/share/psa-horde/turba/lib/Api.php
2026-01-07 20:52:11 +01:00

2432 lines
87 KiB
PHP

<?php
/**
* Turba external API interface.
*
* This file defines Turba's external API interface. Other applications can
* interact with Turba through this API.
*
* Copyright 2009-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.
*
* @author Michael Slusarz <slusarz@horde.org>
* @category Horde
* @license http://www.horde.org/licenses/apache ASL
* @package Turba
*/
class Turba_Api extends Horde_Registry_Api
{
/**
* Links.
*
* @var array
*/
protected $_links = array(
'show' => '%application%/contact.php?source=|source|&key=|key|&uid=|uid|',
'smartmobile_browse' => '%application%/smartmobile.php#browse'
);
/**
* The listing of API calls that do not require permissions checking.
*
* @var array
*/
protected $_noPerms = array(
'getClientSource', 'getClient', 'getClients', 'searchClients'
);
/**
* Callback for comment API.
*
* @param integer $id Internal data identifier.
*
* @return mixed Name of object on success, false on failure.
*/
public function commentCallback($id)
{
if (!$GLOBALS['conf']['comments']['allow']) {
return false;
}
@list($source, $key) = explode('.', $id, 2);
if (isset($GLOBALS['cfgSources'][$source]) && $key) {
try {
return $GLOBALS['injector']->getInstance('Turba_Factory_Driver')->create($source)->getObject($key)->getValue('name');
} catch (Horde_Exception $e) {
}
}
return false;
}
/**
* Does this API allow comments?
*
* @return boolean True if API allows comments.
*/
public function hasComments()
{
return !empty($GLOBALS['conf']['comments']['allow']);
}
/**
* Returns a list of available sources.
*
* @param boolean $writeable If true, limits to writeable sources.
* @param boolean $sync_only Only include synchable address books.
*
* @return array An array of the available sources. Keys are source IDs,
* values are source titles.
*/
public function sources($writeable = false, $sync_only = false)
{
$out = array();
foreach (Turba::getAddressBooks($writeable ? Horde_Perms::EDIT : Horde_Perms::READ) as $key => $val) {
$out[$key] = $val['title'];
}
if ($sync_only) {
$syncable = unserialize($GLOBALS['prefs']->getValue('sync_books'));
$out = array_intersect_key($out, array_flip($syncable));
}
return $out;
}
/**
* Returns a list of fields avaiable in a source.
*
* @param string $source The source name.
*
* @return array An array describing the fields. Keys are the field name,
* values are arrays with these keys:
* - name: (string) Field name.
* - label: (string) Field label.
* - search: (boolean) Can this field be searched?
* - type: (string) See turba/config/attributes.php.
*
* @throws Turba_Exception
*/
public function fields($source = null)
{
global $attributes, $cfgSources;
if (is_null($source) || !isset($cfgSources[$source])) {
throw new Turba_Exception(_("Invalid address book."));
}
$fields = array();
foreach (array_keys($cfgSources[$source]['map']) as $name) {
if (substr($name, 0, 2) != '__') {
$fields[$name] = array(
'label' => $attributes[$name]['label'],
'name' => $name,
'search' => in_array($name, $cfgSources[$source]['search']),
'type' => $attributes[$name]['type']
);
}
}
return $fields;
}
/**
* Retrieve the UID for the current user's default Turba share.
*
* @return string UID.
*/
public function getDefaultShare()
{
global $injector, $prefs, $session;
// Bring in a clean copy of sources.
$cfgSources = Turba::availableSources();
if ($session->get('turba', 'has_share')) {
$driver = $injector->getInstance('Turba_Factory_Driver');
foreach (Turba::listShares(true) as $uid => $share) {
$params = @unserialize($share->get('params'));
if (!empty($params['source'])) {
try {
if ($driver->create($uid)->checkDefaultShare($share, $cfgSources[$params['source']])) {
return $uid;
}
} catch (Turba_Exception $e) {}
}
}
}
// Return Turba's default_dir as default.
return $prefs->getValue('default_dir');
}
/**
* Retrieve the UID for the Global Address List source.
*
* @return string|boolean The UID or false if none configured.
*/
public function getGalUid()
{
return empty($GLOBALS['conf']['gal']['addressbook'])
? false
: $GLOBALS['conf']['gal']['addressbook'];
}
/**
* Browses through Turba's object tree.
*
* @param string $path The path of the tree to browse.
* @param array $properties The item properties to return. Defaults to
* 'name', 'icon', and 'browseable'.
*
* @return array Content of the specified path.
* @throws Turba_Exception
* @throws Horde_Exception_NotFound
*/
public function browse($path = '',
$properties = array('name', 'icon', 'browseable'))
{
global $injector, $registry, $session;
// Strip off the application name if present
if (substr($path, 0, 5) == 'turba') {
$path = substr($path, 5);
}
$path = trim($path, '/');
$results = array();
if (empty($path)) {
/* We always provide the "global" folder which contains address
* book sources that are shared among all users. Per-user shares
* are shown in a folder for each respective user. */
$owners = array(
'global' => _("Global Address Books")
);
foreach (Turba::listShares() as $share) {
$owners[$share->get('owner') ? $registry->convertUsername($share->get('owner'), false) : '-system-'] = $share->get('owner') ?: '-system-';
}
$now = time();
foreach ($owners as $externalOwner => $internalOwner) {
if (in_array('name', $properties)) {
$results['turba/' . $externalOwner]['name'] = $injector
->getInstance('Horde_Core_Factory_Identity')
->create($internalOwner)
->getName();
}
if (in_array('icon', $properties)) {
$results['turba/' . $externalOwner]['icon'] = Horde_Themes::img('turba.png');
}
if (in_array('browseable', $properties)) {
$results['turba/' . $externalOwner]['browseable'] = true;
}
if (in_array('read-only', $properties)) {
$results['turba/' . $externalOwner]['read-only'] = true;
}
}
return $results;
}
$parts = explode('/', $path);
if (count($parts) == 1) {
/* We should either have the username that is a valid share owner
* or 'global'. */
if (empty($parts[0])) {
// We need either 'global' or a valid username with shares.
return array();
}
if ($parts[0] == 'global') {
// The client is requesting a list of global address books.
$addressbooks = Turba::getAddressBooks();
foreach ($addressbooks as $addressbook => $info) {
if ($info['type'] == 'share') {
// Ignore address book shares in the 'global' folder
unset($addressbooks[$addressbook]);
}
}
} else {
/* Assume $parts[0] is a valid username and we need to list
* their shared addressbooks. */
if (!$session->get('turba', 'has_share')) {
// No backends are configured to provide shares
return array();
}
$addressbooks = $injector->getInstance('Turba_Shares')
->listShares(
$registry->getAuth(),
array(
'attributes' => $registry->convertUsername($parts[0], true),
'perm' => Horde_Perms::READ
)
);
}
$curpath = 'turba/' . $registry->convertUsername($parts[0], true) . '/';
foreach ($addressbooks as $addressbook => $info) {
$label = ($info instanceof Horde_Share_Object)
? $info->get('name')
: $info['title'];
if (in_array('name', $properties)) {
$results[$curpath . $addressbook]['name'] =
sprintf(_("Contacts from %s"), $label);
}
if (in_array('displayname', $properties)) {
$results[$curpath . $addressbook]['displayname'] = $label;
}
if (in_array('owner', $properties)) {
$results[$curpath . $addressbook]['owner'] = ($info instanceof Horde_Share_Object)
? $registry->convertUsername($info->get('owner'), false)
: '-system-';
}
if (in_array('icon', $properties)) {
$results[$curpath . $addressbook]['icon'] = Horde_Themes::img('turba.png');
}
if (in_array('browseable', $properties)) {
$results[$curpath . $addressbook]['browseable'] = true;
}
if (in_array('read-only', $properties) &&
($info instanceof Horde_Share_Object)) {
$results[$curpath . $addressbook]['read-only'] = !$info->hasPermission($registry->getAuth(), Horde_Perms::EDIT);
}
}
return $results;
}
if (count($parts) == 2) {
/* The client is requesting all contacts from a given
* addressbook. */
if (empty($parts[0]) || empty($parts[1])) {
/* $parts[0] must be either 'global' or a valid user with
* shares; $parts[1] must be an addressbook ID. */
return array();
}
$addressbooks = Turba::getAddressBooks();
if (!isset($addressbooks[$parts[1]])) {
// We must have a valid addressbook to continue.
return array();
}
$addressbook = $injector->getInstance('Turba_Factory_Driver')
->create($parts[1]);
$owner = $registry->convertUsername($addressbook->getContactOwner(), false);
$contacts = $addressbook->search(array());
$contacts->reset();
$curpath = 'turba/' . $registry->convertUsername($parts[0], false) . '/' . $parts[1] . '/';
while ($contact = $contacts->next()) {
$key = $curpath . $contact->getValue('__key');
if (in_array('name', $properties)) {
$results[$key]['name'] = Turba::formatName($contact);
}
if (in_array('owner', $properties)) {
$results[$key]['owner'] = $owner;
}
if (in_array('icon', $properties)) {
$results[$key]['icon'] = Horde_Themes::img('mime/vcard.png');
}
if (in_array('browseable', $properties)) {
$results[$key]['browseable'] = false;
}
if (in_array('read-only', $properties)) {
$results[$key]['read-only'] = !$addressbook->hasPermission(Horde_Perms::EDIT);
}
if (in_array('contenttype', $properties)) {
$results[$key]['contenttype'] = 'text/x-vcard';
}
if (in_array('modified', $properties)) {
$results[$key]['modified'] = $contact->lastModification() ?: null;
}
if (in_array('created', $properties)) {
$results[$key]['created'] = $this->getActionTimestamp($contact->getValue('__uid'), 'add', $parts[1]);
}
}
return $results;
}
if (count($parts) == 3) {
/* The client is requesting an individual contact. */
$addressbooks = Turba::getAddressBooks();
if (!isset($addressbooks[$parts[1]])) {
// We must have a valid addressbook to continue.
return array();
}
// Load the Turba driver.
$driver = $injector->getInstance('Turba_Factory_Driver')->create($parts[1]);
$contact = $driver->getObject($parts[2]);
$result = array(
'data' => $driver->tovCard($contact, '2.1', null, true)->exportVcalendar(),
'mimetype' => 'text/x-vcard'
);
$modified = $this->_modified($contact->getValue('__uid'), $parts[1]);
if (!empty($modified)) {
$result['mtime'] = $modified;
}
return $result;
}
throw new Turba_Exception(_("Malformed request."));
}
/**
* Deletes a file from the Turba tree.
*
* @param string $path The path to the file.
*
* @return string The event's UID.
* @throws Turba_Exception
*/
public function path_delete($path)
{
// Strip off the application name if present
if (substr($path, 0, 5) == 'turba') {
$path = substr($path, 5);
}
$parts = explode('/', trim($path, '/'));
if (count($parts) < 3) {
// Deletes must be on individual contacts.
throw new Turba_Exception(_("Delete denied."));
}
if (!array_key_exists($parts[1], Turba::getAddressBooks())) {
throw new Turba_Exception(_("Address book does not exist"));
}
return $GLOBALS['injector']->getInstance('Turba_Factory_Driver')->create($parts[1])->delete($parts[2]);
}
/**
* Returns an array of UIDs for all contacts that the current user is
* authorized to see.
*
* @param string|array $sources The name(s) of the source(s) to return
* contacts of. If empty, the current user's
* sync sources or default source are used.
*
* @return array An array of UIDs for all contacts the user can access.
* @throws Turba_Exception
*/
public function listUids($sources = null)
{
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver');
$uids = array();
foreach ($this->_getSources($sources) as $source) {
try {
$results = $driver->create($source)->search(array());
} catch (Turba_Exception $e) {
throw new Turba_Exception(sprintf(_("Error searching the address book: %s"), $e->getMessage()));
}
foreach ($results->objects as $o) {
if (!$o->isGroup()) {
$uids[] = $o->getValue('__uid');
}
}
}
return $uids;
}
/**
* Returns an array of UIDs for contacts that have had a given action
* since a certain time.
*
* @param string $action The action to check for - add, modify, or
* delete.
* @param integer $timestamp The time to start the search.
* @param string|array $sources The source(s) for which to retrieve the
* history.
* @param integer $end The optional ending timestamp.
* @param boolean $isModSeq If true, $timestamp and $end are
* modification sequences and not timestamps.
* @since 4.1.1
*
* @return array An array of UIDs matching the action and time criteria.
*
* @throws Turba_Exception
*/
public function listBy($action, $timestamp, $sources = null, $end = null, $isModSeq = false)
{
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver');
$history = $GLOBALS['injector']->getInstance('Horde_History');
$filter = array(
array(
'field' => 'action',
'op' => '=',
'value' => $action
)
);
$uids = array();
if (!empty($end) && !$isModSeq) {
$filter[] = array(
'field' => 'ts',
'op' => '<',
'value' => $end
);
}
foreach ($this->_getSources($sources) as $source) {
$sdriver = $driver->create($source);
if (!$isModSeq) {
$histories = $history->getByTimestamp(
'>', $timestamp, $filter,
'turba:' . $sdriver->getName()
);
} else {
$histories = $history->getByModSeq(
$timestamp, $end, $filter, 'turba:' . $sdriver->getName());
}
// Filter out groups
$nguids = str_replace(
'turba:' . $sdriver->getName() . ':',
'',
array_keys($histories)
);
$include = array();
foreach ($nguids as $uid) {
if ($action != 'delete') {
$list = $sdriver->search(array('__uid' => $uid));
if ($list->count()) {
$object = $list->next();
if ($object->isGroup()) {
continue;
}
}
}
$include[] = $uid;
}
// Strip leading turba:addressbook:.
$uids = array_merge($uids, $include);
}
return $uids;
}
/**
* Method for obtaining all server changes between two timestamps.
* Essentially a wrapper around listBy(), but returns an array containing
* all adds, edits, and deletions.
*
* @param integer $start The starting timestamp
* @param integer $end The ending timestamp.
* @param boolean $isModSeq If true, $start and $end are modification
* sequences and not timestamps. @since 4.1.1
* @param string|array $sources The sources to check. @since 4.2.0
*
* @return array A hash with 'add', 'modify' and 'delete' arrays.
*/
public function getChanges($start, $end, $isModSeq = false, $sources = null)
{
return array(
'add' => $this->listBy('add', $start, $sources, $end, $isModSeq),
'modify' => $this->listBy('modify', $start, $sources, $end, $isModSeq),
'delete' => $this->listBy('delete', $start, $sources, $end, $isModSeq)
);
}
/**
* Return all changes occuring between the specified modification
* sequences.
*
* @param integer $start The starting modseq.
* @param integer $end The ending modseq.
* @param string|array $sources The sources to check. @since 4.2.0
*
* @return array The changes @see getChanges()
* @since 4.1.1
*/
public function getChangesByModSeq($start, $end, $sources = null)
{
return $this->getChanges($start, $end, true, $sources);
}
/**
* Returns the timestamp of an operation for a given UID and action.
*
* @param string $uid The UID to look for.
* @param string $action The action to check for - add, modify, or
* delete.
* @param string|array $sources The source(s) for which to retrieve the
* history.
* @param boolean $modSeq Request a modification sequence instead of
* timestamp. @since 4.1.1
*
* @return integer The timestamp for this action.
*
* @throws Turba_Exception
*/
public function getActionTimestamp($uid, $action, $sources = null, $modSeq = false)
{
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver');
$history = $GLOBALS['injector']->getInstance('Horde_History');
$last = 0;
foreach ($this->_getSources($sources) as $source) {
if (!$modSeq) {
$ts = $history->getActionTimestamp(
'turba:' . $driver->create($source)->getName() . ':' . $uid,
$action);
} else {
$ts = $history->getActionModSeq(
'turba:' . $driver->create($source)->getName() . ':' . $uid,
$action);
}
if (!empty($ts) && $ts > $last) {
$last = $ts;
}
}
return $last;
}
/**
* Return the largest modification sequence from the history backend.
*
* @param string $id Addressbook id to return highest MODSEQ for. If
* null, the highest MODSEQ across all addressbooks is
* returned. @since 4.2.0
*
* @return integer The modseq.
* @since 4.1.1
*/
public function getHighestModSeq($id = null)
{
$parent = 'turba';
if (!empty($id)) {
$parent .= ':' . $id;
}
return $GLOBALS['injector']->getInstance('Horde_History')->getHighestModSeq($parent);
}
/**
* Import a contact represented in the specified contentType.
*
* @param string $content The content of the contact.
* @param string $contentType What format is the data in? Currently
* supports array, text/directory, text/vcard,
* text/x-vcard, and activesync.
* @param string $source The source into which the contact will be
* imported.
* @param array $options Additional options:
* - match_on_email: (boolean) If true, will detect entry as duplicate
* if ANY email field matches. Useful for
* automatically adding contacts from an
* email application, such as IMP.
* @since 4.2.9
*
* @return string The new UID.
*
* @throws Turba_Exception
* @throws Turba_Exception_ObjectExists
*/
public function import($content, $contentType = 'array', $source = null, array $options = array())
{
global $cfgSources, $injector, $prefs;
/* Get default address book from user preferences. */
if (empty($source) &&
!($source = $prefs->getValue('default_dir'))) {
// On new installations default_dir is not set. Try default
// addressbook if it's editable. Otherwise use first editable
// addressbook.
$edit_sources = Turba::getAddressBooks(Horde_Perms::EDIT);
$default_source = Turba::getDefaultAddressbook();
if (isset($edit_sources[$default_source])) {
// use default addressbook
$source = $default_source;
} else {
// Use first writable source
$source = reset($edit_sources);
}
}
// Check existence of and permissions on the specified source.
if (!isset($cfgSources[$source])) {
throw new Turba_Exception(sprintf(_("Invalid address book: %s"), $source));
}
$driver = $injector
->getInstance('Turba_Factory_Driver')
->create($source);
if (!$driver->hasPermission(Horde_Perms::EDIT)) {
throw new Turba_Exception(_("Permission denied"));
}
if (!($content instanceof Horde_Icalendar_Vcard)) {
switch ($contentType) {
case 'activesync':
$content = $driver->fromASContact($content);
break;
case 'array':
if (!isset($content['emails']) && isset($content['email'])) {
$content['emails'] = $content['email'];
}
break;
case 'text/x-vcard':
case 'text/vcard':
case 'text/directory':
$iCal = new Horde_Icalendar();
if (!$iCal->parsevCalendar($content)) {
throw new Turba_Exception(_("There was an error importing the iCalendar data."));
}
switch ($iCal->getComponentCount()) {
case 0:
throw new Turba_Exception(_("No vCard data was found."));
case 1:
$content = $driver->toHash($iCal->getComponent(0));
break;
default:
$ids = array();
foreach ($iCal->getComponents() as $c) {
if ($c instanceof Horde_Icalendar_Vcard) {
$content = $driver->toHash($c);
$result = $driver->search($content);
if (count($result)) {
continue;
}
$ids[] = $driver->add($content);
}
}
return $ids;
}
break;
default:
throw new Turba_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
}
}
if ($content instanceof Horde_Icalendar_Vcard) {
$content = $driver->toHash($content);
}
if (!empty($options['match_on_email'])) {
$content_copy = array();
foreach (Turba::getAvailableEmailFields() as $field) {
if (!empty($content[$field])) {
$rfc = new Horde_Mail_Rfc822();
$email = $rfc->parseAddressList($content[$field]);
$content_copy[$field] = (string)$email;
}
}
} else {
$content_copy = $content;
}
// Check if the entry already exists in the data source.
$result = $driver->search(
$content_copy, null, !empty($options['match_on_email']) ? 'OR' : 'AND');
if (count($result)) {
throw new Turba_Exception_ObjectExists(_("Already Exists"));
}
// We can't use $object->setValue() here since that cannot be used
// with composite fields.
$hooks = $injector->getInstance('Horde_Core_Hooks');
if ($hooks->hookExists('encode_attribute', 'turba')) {
foreach ($content as $attribute => &$value) {
try {
$value = $hooks->callHook(
'encode_attribute',
'turba',
array($attribute, $value, null, null)
);
} catch (Turba_Exception $e) {}
}
}
$result = $driver->add($content);
return $driver->getObject($result)->getValue('__uid');
}
/**
* Export a contact, identified by UID, in the requested contentType.
*
* @param string $uid Identify the contact to export.
* @param mixed $contentType What format should the data be in?
* - text/directory: Returns RFC2426 vcard3.0
* - text/vcard: Returns RFC2426 vcard3.0
* - text/x-vcard: Returns imc.org vcard 2.1 format.
* - array: Returns a raw array
* - activesync: Returns a Horde_ActiveSync_Message_Contact:: object
* @param string|array $sources The source(s) from which the contact will
* be exported.
* @param array $fields Hash of field names and
* Horde_SyncMl_Property properties with the
* requested fields.
* @param array $options Any additional options to be passed to the
* exporter. Currently supported:
* - skip_empty: (boolean) {text/vcard or text/x-vcard} Set to
* true to not output empty properties.
* DEFAULT: false.
* - protocolversion: (float) {activesync} The EAS version to support
* DEFAULT: 2.5
* - bodyprefs: (array) {activesync} A BODYPREFERENCE array.
* DEFAULT: none (No body prefs enforced).
* - truncation: (integer) {activesync} Truncate event body to this
* length.
* DEFAULT: none (No truncation).
*
*
* @return mixed The requested data.
* @throws Turba_Exception
*/
public function export($uid, $contentType, $sources = null, $fields = null, array $options = array())
{
if (empty($uid)) {
throw new Turba_Exception(_("Invalid ID"));
}
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver');
foreach ($this->_getSources($sources) as $source) {
$sdriver = $driver->create($source);
if (!$sdriver->hasPermission(Horde_Perms::READ)) {
continue;
}
$result = $sdriver->search(array('__uid' => $uid));
if (count($result) == 0) {
continue;
} elseif (count($result) > 1) {
throw new Turba_Exception(sprintf("Internal Horde Error: multiple Turba objects with same objectId %s.", $uid));
}
$version = '3.0';
list($contentType,) = explode(';', $contentType);
switch ($contentType) {
case 'text/x-vcard':
$version = '2.1';
// Fall-through
case 'text/vcard':
case 'text/directory':
$export = '';
foreach ($result->objects as $obj) {
$vcard = $sdriver->tovCard($obj, $version, $fields, !empty($options['skip_empty']));
/* vCards are not enclosed in
* BEGIN:VCALENDAR..END:VCALENDAR. Export the individual
* cards instead. */
$export .= $vcard->exportvCalendar();
}
return $export;
case 'array':
$attributes = array();
foreach ($result->objects as $object) {
foreach (array_keys($GLOBALS['cfgSources'][$source]['map']) as $field) {
$attributes[$field] = $object->getValue($field);
}
}
return $attributes;
case 'activesync':
foreach ($result->objects as $object) {
$return = $object;
}
return $sdriver->toASContact($return, $options);
default:
throw new Turba_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
}
}
throw new Turba_Exception(sprintf(_("Object %s not found."), $uid));
}
/**
* Exports the user's own contact as a vCard string.
*
* @return string The requested vCard data.
* @throws Turba_Exception
*/
public function ownVCard()
{
$contact = $this->getOwnContactObject();
$vcard = $GLOBALS['injector']->getInstance('Turba_Factory_Driver')->create($contact['source'])->tovCard($contact['contact'], '3.0', null, true);
$vcard->setAttribute('VERSION', '3.0');
return $vcard->exportvCalendar();
}
/**
* Export the user's own contact as a hash.
*
* @return array The contact hash.
* @throws Turba_Exception
*/
public function ownContact()
{
$contact = $this->getOwnContactObject();
return $contact['contact']->getAttributes();
}
/**
* Helper function to return the user's own contact object
*
* @return array An array containing the following keys:
* - contact: (Turba_Object) Object representing the user's own contact.
* - source: (string) The source of the user's own contact.
* @throws Turba_Exception
*/
public function getOwnContactObject()
{
$own_contact = $GLOBALS['prefs']->getValue('own_contact');
if (empty($own_contact)) {
throw new Turba_Exception(_("You didn't mark a contact as your own yet."));
}
@list($source, $id) = explode(';', $own_contact);
if (!isset($GLOBALS['cfgSources'][$source])) {
throw new Turba_Exception(_("The address book with your own contact doesn't exist anymore."));
}
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver')->create($source);
if (!$driver->hasPermission(Horde_Perms::READ)) {
throw new Turba_Exception(_("You don't have sufficient permissions to read the address book that contains your own contact."));
}
try {
$contact = $driver->getObject($id);
} catch (Horde_Exception_NotFound $e) {
throw new Turba_Exception(_("Your own contact cannot be found in the address book."));
}
return array(
'contact' => $contact,
'source'=> $source
);
}
/**
* Deletes a contact identified by UID.
*
* @param string|array $uid Identify the contact to delete, either a
* single UID or an array.
* @param string|array $sources The source(s) from which the contact will
* be deleted.
*
* @return boolean Success or failure.
* @throws Turba_Exception
*/
public function delete($uid, $sources = null)
{
if (empty($uid)) {
throw new Turba_Exception(_("Invalid ID"));
}
// Handle an array of UIDs for convenience of deleting multiple
// contacts at once.
if (is_array($uid)) {
foreach ($uid as $g) {
if (!$this->delete($g, $sources)) {
return false;
}
}
return true;
}
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver');
foreach ($this->_getSources($sources) as $source) {
$sdriver = $driver->create($source);
if (!$GLOBALS['registry']->isAdmin() &&
!$sdriver->hasPermission(Horde_Perms::DELETE)) {
continue;
}
// If the objectId isn't in $source in the first place, just
// return true. Otherwise, try to delete it and return success or
// failure.
$result = $sdriver->search(array('__uid' => $uid));
if (count($result) != 0) {
$r = $result->objects[0];
try {
$sdriver->delete($r->getValue('__key'));
} catch (Turba_Exception $e) {
return false;
}
}
}
return true;
}
/**
* Replaces the contact identified by UID with the content represented in
* the specified contentType.
*
* @param string $uid Idenfity the contact to replace.
* @param mixed $content The content of the contact.
* @param string $contentType What format is the data in? Currently
* supports array, text/directory,
* text/vcard, text/x-vcard and activesync.
* @param string|array $sources The source(s) where the contact will be
* replaced.
*
* @return boolean Success or failure.
* @throws Turba_Exception
*/
public function replace($uid, $content, $contentType, $sources = null)
{
if (empty($uid)) {
throw new Turba_Exception(_("Invalid contact unique ID"));
}
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver');
foreach ($this->_getSources($sources) as $source) {
$sdriver = $driver->create($source);
// Check permissions.
if (!$sdriver->hasPermission(Horde_Perms::EDIT)) {
continue;
}
$result = $sdriver->search(array('__uid' => $uid));
if (!count($result)) {
continue;
} elseif (count($result) > 1) {
throw new Turba_Exception(sprintf(_("Multiple contacts found with same unique ID %s."), $uid));
}
$object = $result->objects[0];
switch ($contentType) {
case 'activesync':
$content = $sdriver->fromASContact($content);
foreach ($content as $attribute => $value) {
if ($attribute != '__key') {
$object->setValue($attribute, $value);
}
}
return $object->store();
case 'array':
break;
case 'text/x-vcard':
case 'text/vcard':
case 'text/directory':
$iCal = new Horde_Icalendar();
if (!$iCal->parsevCalendar($content)) {
throw new Turba_Exception(_("There was an error importing the iCalendar data."));
}
switch ($iCal->getComponentCount()) {
case 0:
throw new Turba_Exception(_("No vCard data was found."));
case 1:
$content = $sdriver->toHash($iCal->getComponent(0));
break;
default:
throw new Turba_Exception(_("Only one vcard supported."));
}
break;
default:
throw new Turba_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
}
foreach ($content as $attribute => $value) {
if ($attribute != '__key') {
$object->setValue($attribute, $value);
}
}
return $object->store();
}
throw new Turba_Exception(sprintf(_("Object %s not found."), $uid));
}
/**
* Returns a contact search result.
*
* @param mixed $names The search filter values.
* @param array $opts Optional parameters:
* - customStrict: (array) An array of fields that must match exactly.
* DEFAULT: None
* - fields: (array) The fields to search on.
* DEFAULT: All fields
* - forceSource: (boolean) Whether to use the specified sources, even
* if they have been disabled in the preferences?
* DEFAULT: false
* - matchBegin: (boolean) Match word boundaries only?
* DEFAULT: false
* - returnFields: Only return these fields. Note that the __key field
* will always be returned.
* DEFAULT: Return all fields.
* - rfc822Return: Return a Horde_Mail_Rfc822_List object.
* DEFAULT: Returns an array of search results.
* - sources: (array) The sources to search in.
* DEFAULT: Search the user's default address book
* - count_only: (boolean) If true, only return the count of matching
* results.
* DEFAULT: false (Return the full data set).
* - emailSearch: (boolean) If true, indicates this is an email search
* like e.g., for an autocompleter. Ensures that ALL email
* fields are both included in 'fields' and returned in the
* results, regardless of the requested returnFields value.
* @todo Make this a separate API method in H6 (or separate
* API "module", e.g., an 'email search' API).
* @since 4.3.0
*
* @return mixed Either a hash containing the search results or a
* Rfc822 List object (if 'rfc822Return' is true).
* @throws Turba_Exception
*/
public function search($names = null, array $opts = array())
{
global $attributes, $cfgSources, $injector;
$opts = array_merge(array(
'fields' => array(),
'forceSource' => false,
'matchBegin' => false,
'returnFields' => array(),
'rfc822Return' => false,
'sources' => array(),
'customStrict' => array(),
'count_only' => false,
), $opts);
$results = !empty($opts['count_only'])
? 0
: (empty($opts['rfc822Return'])
? array()
: new Horde_Mail_Rfc822_List());
if (!isset($cfgSources) ||
!is_array($cfgSources) ||
!count($cfgSources) ||
is_null($names)) {
return $results;
}
if (!is_array($names)) {
$names = array($names);
}
if (!$opts['forceSource']) {
// Make sure the selected source is activated in Turba.
$addressbooks = array_keys(Turba::getAddressBooks());
foreach (array_keys($opts['sources']) as $id) {
if (!in_array($opts['sources'][$id], $addressbooks)) {
unset($opts['sources'][$id]);
}
}
}
// ...and ensure the default source is used as a default.
if (!count($opts['sources'])) {
$opts['sources'] = array(Turba::getDefaultAddressbook());
if (!empty($opts['fields']) && empty($opts['fields'][$opts['sources'][0]])) {
$opts['fields'][$opts['sources'][0]] = $opts['fields'];
}
}
$driver = $injector->getInstance('Turba_Factory_Driver');
foreach ($opts['sources'] as $source) {
// Skip invalid sources -or-
// skip sources that aren't browseable if the search is empty.
if (!isset($cfgSources[$source]) ||
(empty($cfgSources[$source]['browse']) &&
(!count($names) ||
((count($names) == 1) && empty($names[0]))))) {
continue;
}
// Ensure we have search fields for each source.
if (empty($opts['fields'][$source])) {
$opts['fields'][$source] = $cfgSources[$source]['search'];
}
// If this is an email search, ensure we are searching for and
// returning all email-type fields.
if (!empty($opts['emailSearch'])) {
$opts['returnFields'] = array_merge(
$opts['returnFields'],
Turba::getAvailableEmailFields($source, false)
);
$opts['fields'][$source] = array_merge(
$opts['fields'][$source],
Turba::getAvailableEmailFields($source)
);
}
$sdriver = $driver->create($source);
foreach ($names as $name) {
$trimname = trim($name);
$out = $criteria = array();
unset($tname);
if (strlen($trimname)) {
if (isset($opts['fields'][$source])) {
foreach ($opts['fields'][$source] as $field) {
$criteria[$field] = $trimname;
}
}
}
try {
$search = $sdriver->search(
$criteria,
Turba::getPreferredSortOrder(),
'OR',
$opts['returnFields'],
$opts['customStrict'],
$opts['matchBegin'],
$opts['count_only']
);
} catch (Exception $e) {
continue;
}
if ($opts['count_only']) {
$results += $search;
continue;
} elseif (!($search instanceof Turba_List)) {
continue;
}
$rfc822 = new Horde_Mail_Rfc822();
while ($ob = $search->next()) {
$emails = $seen = array();
if ($ob->isGroup()) {
/* Is a distribution list. */
$members = $ob->listMembers();
if (!($members instanceof Turba_List) ||
!count($members)) {
continue;
}
$listatt = $ob->getAttributes();
$listName = $ob->getValue('name');
while ($ob = $members->next()) {
foreach (array_keys($ob->getAttributes()) as $key) {
$value = $ob->getValue($key);
if (empty($value)) {
continue;
}
$seen_key = trim(Horde_String::lower($ob->getValue('name'))) . trim(Horde_String::lower(is_array($value) ? $value['load']['file'] : $value));
if (isset($attributes[$key]) &&
($attributes[$key]['type'] == 'email') &&
empty($seen[$seen_key])) {
$emails[] = $value;
$seen[$seen_key] = true;
}
}
}
if (empty($opts['rfc822Return'])) {
$out[] = array(
'email' => implode(', ', $emails),
'id' => $listatt['__key'],
'name' => $listName,
'source' => $source,
'__key' => $listatt['__key'],
'__type' => $listatt['__type']
);
} else {
$results->add(new Horde_Mail_Rfc822_Group($listName, $emails));
}
} else {
/* Not a group. */
$att = array(
'__key' => $ob->getValue('__key')
);
foreach (array_keys($ob->driver->getCriteria()) as $key) {
if (empty($opts['returnFields']) ||
(!empty($opts['returnFields']) && in_array($key, $opts['returnFields']))) {
$att[$key] = $ob->getValue($key);
}
}
$email = new Horde_Mail_Rfc822_List();
$display_name = ($ob->hasValue('name') || !isset($ob->driver->alternativeName))
? Turba::formatName($ob)
: $ob->getValue($ob->driver->alternativeName);
unset($tdisplay_name);
$email_fields = array();
foreach (array_keys($att) as $key) {
// Only concerned about keys that we want returned.
if (!empty($opts['returnFields']) &&
!in_array($key, $opts['returnFields'])) {
continue;
}
if ($ob->getValue($key) &&
isset($attributes[$key]) &&
($attributes[$key]['type'] == 'email')) {
$e_val = $ob->getValue($key);
$email_fields[$key] = $e_val;
if (strlen($trimname)) {
/* Ticket #12480: Don't return email if it
* doesn't contain the search string, since
* an entry can contain multiple e-mail
* fields. Return all e-mails if it
* occurs in the name. */
if (!isset($tname)) {
$tname = Horde_String_Transliterate::toAscii($name);
}
if (!isset($tdisplay_name)) {
$tdisplay_name = Horde_String_Transliterate::toAscii($display_name);
}
$add = ((Horde_String::ipos(Horde_String_Transliterate::toAscii($e_val), $tname) !== false) ||
(Horde_String::ipos($tdisplay_name, $tname) !== false));
} else {
$add = true;
}
if ($add) {
// Multiple addresses support
$email->add($rfc822->parseAddressList($e_val, array(
'limit' => (isset($attributes[$key]['params']) && is_array($attributes[$key]['params']) && !empty($attributes[$key]['params']['allow_multi'])) ? 0 : 1
)));
}
}
}
// If we haven't added any results yet, add any available
// addresses since the search term might not have matched
// the display_name OR any of the email addresses.
// See Bug: 13945
if (!count($email)) {
foreach ($email_fields as $e_field => $e_value) {
$email->add($rfc822->parseAddressList($e_value, array(
'limit' => (isset($attributes[$e_field]['params']) && is_array($attributes[$e_field]['params']) && !empty($attributes[$e_field]['params']['allow_multi'])) ? 0 : 1
)));
}
}
if (count($email)) {
foreach ($email as $val) {
$seen_key = trim(Horde_String::lower($display_name)) . '/' . Horde_String::lower($val->bare_address);
if (empty($seen[$seen_key])) {
$seen[$seen_key] = true;
if (empty($opts['rfc822Return'])) {
$emails[] = $val->bare_address;
} else {
$val->personal = $display_name;
$results->add($val);
}
}
}
} elseif (empty($opts['rfc822Return'])) {
$emails[] = null;
}
if (empty($opts['rfc822Return'])) {
foreach ($emails as $val) {
$atts = array(
'__type' => 'Object',
'id' => $att['__key'],
'source' => $source
);
if (empty($opts['returnFields'])) {
$atts = array(
'__type' => 'Object',
'id' => $att['__key'],
'source' => $source,
'email' => $val,
'name' => $display_name
);
} else {
$atts = array();
$fields = array(
'__type' => 'Object',
'id' => $att['__key'],
'source' => $source,
'email' => $val,
'name' => $display_name
);
foreach($fields as $field => $value) {
if (in_array($field, $opts['returnFields'])) {
$atts[$field] = $value;
}
}
}
$out[] = array_merge($att, $atts);
}
}
}
}
if (!empty($out)) {
$results[$name] = $out;
}
}
}
return $results;
}
/**
* Retrieves a contact.
*
* @param string $source The source name where the contact is stored
* @param string $objectId The unique id of the contact to retrieve
*
* @return array The retrieved contact.
* @throws Turba_Exception
* @throws Horde_Exception_NotFound
*/
public function getContact($source = null, $objectId = '')
{
global $cfgSources;
if (!isset($cfgSources) ||
!is_array($cfgSources) ||
!isset($cfgSources[$source])) {
return array();
}
$attributes = array();
$object = $GLOBALS['injector']->getInstance('Turba_Factory_Driver')->create($source)->getObject($objectId);
foreach (array_keys($cfgSources[$source]['map']) as $field) {
$attributes[$field] = $object->getValue($field);
}
return $attributes;
}
/**
* Retrieves a set of contacts from a single source.
*
* @param string $source The source name where the contact is stored
* @param array $objectIds The unique ids of the contact to retrieve.
*
* @return array The retrieved contact.
* @throws Turba_Exception
* @throws Horde_Exception_NotFound
*/
public function getContacts($source = '', array $objectIds = array())
{
global $cfgSources;
if (!isset($cfgSources) ||
!is_array($cfgSources) ||
!isset($cfgSources[$source])) {
return array();
}
if (!is_array($objectIds)) {
$objectIds = array($objectIds);
}
$objects = $GLOBALS['injector']->getInstance('Turba_Factory_Driver')->create($source)->getObjects($objectIds);
$results = array();
foreach ($objects as $object) {
$attributes = array();
foreach (array_keys($cfgSources[$source]['map']) as $field) {
$attributes[$field] = $object->getValue($field);
}
$results[] = $attributes;
}
return $results;
}
/**
* Retrieves a list of all possible values of a field in specified
* source(s).
*
* @param string $field Field name to check.
* @param array $sources Array containing the sources to look in.
*
* @return array An array of fields and possible values.
* @throws Turba_Exception
*/
public function getAllAttributeValues($field = '',
array $sources = array())
{
global $cfgSources;
if (!isset($cfgSources) || !is_array($cfgSources)) {
return array();
}
if (!count($sources)) {
$sources = array(Turba::getDefaultAddressbook());
}
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver');
$results = array();
foreach ($sources as $source) {
if (isset($cfgSources[$source])) {
$res = $driver->create($source)->search(array());
if (!($res instanceof Turba_List)) {
throw new Turba_Exception(_("Search failed"));
}
while ($ob = $res->next()) {
if ($ob->hasValue($field)) {
$results[$source . ':' . $ob->getValue('__key')] = array(
'email' => $ob->getValue('email'),
'name' => $ob->getValue('name'),
$field => $ob->getValue($field)
);
}
}
}
}
return $results;
}
/**
* Retrieves a list of available time objects categories.
*
* @return array An array of all configured time object categories.
*/
public function listTimeObjectCategories()
{
$categories = array();
foreach ($GLOBALS['attributes'] as $key => $attribute) {
if (($attribute['type'] == 'monthdayyear') &&
!empty($attribute['time_object_label'])) {
foreach ($GLOBALS['cfgSources'] as $srcKey => $source) {
if (!empty($source['map'][$key])) {
$categories[$key . '/'. $srcKey] =array(
'title' => sprintf(_("%s in %s"), $attribute['time_object_label'], $source['title']),
'type' => 'share');
}
}
}
}
return $categories;
}
/**
* Lists birthdays and/or anniversaries as time objects.
*
* @param array $time_categories The time categories (from
* listTimeObjectCategories) to list.
* @param mixed $start The start date of the period.
* @param mixed $end The end date of the period.
*
* @return array An array of timeObject results.
* @throws Turba_Exception
*/
public function listTimeObjects($time_categories, $start, $end)
{
$start = new Horde_Date($start);
$end = new Horde_Date($end);
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver');
$objects = array();
foreach ($time_categories as $category) {
list($category, $source) = explode('/', $category, 2);
$objects = array_merge($objects, $driver->create($source)->listTimeObjects($start, $end, $category));
}
return $objects;
}
/**
* Returns the client source name.
*
* @return string The name of the source to use with the clients api.
*/
public function getClientSource()
{
return empty($GLOBALS['conf']['client']['addressbook'])
? false
: $GLOBALS['conf']['client']['addressbook'];
}
/**
* Returns the available client fields.
*
* @return array An array describing the fields.
*/
public function clientFields()
{
return $this->fields($GLOBALS['conf']['client']['addressbook']);
}
/**
* Returns a contact from the client source.
*
* @param string $objectId Client unique ID.
*
* @return array Array of client data.
* @throws Turba_Exception
*/
public function getClient($objectId = '')
{
return $this->getContact($GLOBALS['conf']['client']['addressbook'], $objectId);
}
/**
* Returns mulitple contacts from the client source.
*
* @param array $objectIds client unique ids.
*
* @return array An array of clients data.
* @throws Turba_Exception
*/
public function getClients($objectIds = array())
{
return $this->getContacts($GLOBALS['conf']['client']['addressbook'], $objectIds);
}
/**
* Adds a client to the client source.
*
* @param array $attributes Array containing the client attributes.
*
* @return boolean
*/
public function addClient(array $attributes = array())
{
return $this->import($attributes, 'array', $this->getClientSource());
}
/**
* Updates client data.
*
* @param string $objectId The unique id of the client.
* @param array $attributes An array of client attributes.
*
* @return boolean
*/
public function updateClient($objectId = '', array $attributes = array())
{
return $this->replace($this->getClientSource() . ':' . $objectId, $attributes, 'array');
}
/**
* Deletes a client
*
* @param string $objectId The unique id of the client
*
* @return boolean
*/
public function deleteClient($objectId = '')
{
return $this->delete($this->getClientSource() . ':' . $objectId);
}
/**
* Search for clients.
*
* @param array $names The search filter values.
* @param array $fields The fields to search in.
* @param boolean $matchBegin Match word boundaries only.
*
* @return array A hash containing the search results.
* @throws Turba_Exception
*/
public function searchClients(array $names = array(),
array $fields = array(),
$matchBegin = false)
{
$abook = $this->getClientSource();
return $this->search(
$names,
array('sources' => array($abook),
'fields' => array($abook => $fields),
'matchBegin' => $matchBegin,
'forceSource' => true)
);
}
/**
* Sets the value of the specified attribute of a contact
*
* @param string|array $address Contact email address(es).
* @param string $name Contact name.
* @param string $field Field to update.
* @param string $value Field value to set.
* @param string $source Contact source.
*
* @throws Turba_Exception
*/
public function addField($address = '', $name = '', $field = '',
$value = '', $source = '')
{
if (is_array($address)) {
$e = null;
$success = 0;
foreach ($address as $tmp) {
try {
$this->addField($tmp, $name, $field, $value, $source);
++$success;
} catch (Exception $e) {}
}
if ($e) {
if ($success) {
throw new Turba_Exception(sprintf(ngettext("Added or updated %d contact, but at least one contact failed:", "Added or updated %d contacts, but at least one contact failed:", $success), $success) . ' ' . $e->getMessage());
} else {
throw $e;
}
}
}
global $cfgSources;
if (empty($source) || !isset($cfgSources[$source])) {
throw new Turba_Exception(sprintf(_("Invalid address book: %s"), $source));
}
if (empty($address)) {
throw new Turba_Exception(_("Invalid email"));
}
if (empty($name)) {
throw new Turba_Exception(_("Invalid name"));
}
if (empty($value)) {
throw new Turba_Exception(_("Invalid entry"));
}
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver')->create($source);
if (!$driver->hasPermission(Horde_Perms::EDIT)) {
throw new Turba_Exception(_("Permission denied"));
}
try {
$res = $driver->search(array('email' => trim($address)), null, 'AND');
} catch (Turba_Exception $e) {
throw new Turba_Exception(sprintf(_("Search failed: %s"), $res->getMessage()));
}
if (count($res) > 1) {
try {
$res2 = $driver->search(array('email' => trim($address), 'name' => trim($name)), null, 'AND');
} catch (Turba_Exception $e) {
throw new Turba_Exception(sprintf(_("Search failed: %s"), $e->getMessage()));
}
if (!count($res2)) {
throw new Turba_Exception(sprintf(_("Multiple persons with address [%s], but none with name [%s] already exist"), trim($address), trim($name)));
}
try {
$res3 = $driver->search(array('email' => $address, 'name' => $name, $field => $value));
} catch (Turba_Exception $e) {
throw new Turba_Exception(sprintf(_("Search failed: %s"), $e->getMessage()));
}
if (count($res3)) {
throw new Turba_Exception(sprintf(_("This person already has a %s entry in the address book"), $field));
}
$ob = $res2->next();
$ob->setValue($field, $value);
$ob->store();
} elseif (count($res) == 1) {
try {
$res4 = $driver->search(array('email' => $address, $field => $value));
} catch (Turba_Exception $e) {
throw new Turba_Exception(sprintf(_("Search failed: %s"), $e->getMessage()));
}
if (count($res4)) {
throw new Turba_Exception(sprintf(_("This person already has a %s entry in the address book"), $field));
}
$ob = $res->next();
$ob->setValue($field, $value);
$ob->store();
} else {
$driver->add(array('email' => $address, 'name' => $name, $field => $value, '__owner' => $GLOBALS['registry']->getAuth()));
}
}
/**
* Returns a field value.
*
* @param string $address Contact email address.
* @param string $field Field to get.
* @param array $sources Sources to check.
* @param boolean $strict Match the email address strictly.
* @param boolean $multiple Return more than one entry if found and true,
* return an error if this is false.
*
* @return array An array of field value(s).
* @throws Turba_Exception
*/
public function getField($address = '', $field = '', $sources = array(),
$strict = false, $multiple = false)
{
global $cfgSources, $attributes, $injector;
if (empty($address)) {
throw new Turba_Exception(_("Invalid email"));
}
if (!isset($cfgSources) || !is_array($cfgSources)) {
return array();
}
if (!count($sources)) {
$sources = array(Turba::getDefaultAddressbook());
}
$result = array();
foreach ($sources as $source) {
if (!isset($cfgSources[$source])) {
continue;
}
$criterium = array_fill_keys(
Turba::getAvailableEmailFields($source),
$address
);
$driver = $injector->getInstance('Turba_Factory_Driver')->create($source);
try {
$list = $driver->search($criterium, null, 'OR', array(), $strict ? array_keys($criterium) : array());
} catch (Turba_Exception $e) {
Horde::log($e, 'ERR');
continue;
}
if ($list instanceof Turba_List) {
while ($ob = $list->next()) {
if ($ob->hasValue($field)) {
$result[] = $ob->getValue($field);
}
}
}
}
if (count($result) > 1) {
if ($multiple) {
return $result;
} else {
throw new Turba_Exception(_("More than 1 entry found"));
}
} elseif (empty($result)) {
throw new Turba_Exception(sprintf(_("No %s entry found for %s"), $field, $address));
}
return reset($result);
}
/**
* Deletes a field value.
*
* @param string $address Contact email address.
* @param string $field Field to delete value for.
* @param array $sources Sources to delete value from.
*
* @throws Turba_Exception
*/
public function deleteField($address = '', $field = '', $sources = array())
{
global $cfgSources;
if (!strlen($address)) {
throw new Turba_Exception(_("Invalid email"));
}
if (!isset($cfgSources) || !is_array($cfgSources)) {
return;
}
if (count($sources) == 0) {
$sources = array(Turba::getDefaultAddressbook());
}
$driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver');
$success = false;
foreach ($sources as $source) {
if (isset($cfgSources[$source])) {
$sdriver = $driver->create($source);
if (!$sdriver->hasPermission(Horde_Perms::EDIT)) {
continue;
}
$res = $sdriver->search(array('email' => $address));
if ($res instanceof Turba_List) {
if (count($res) == 1) {
$ob = $res->next();
if (is_object($ob) && $ob->hasValue($field)) {
$ob->setValue($field, '');
$ob->store();
$success = true;
}
}
}
}
}
if (!$success) {
throw new Turba_Exception(sprintf(_("No %s entry found for %s"), $field, $address));
}
}
/**
* Obtain an array of $cfgSource entries matching the filter criteria.
*
* @param type $filter A single key -> value hash to filter the sources.
*
* @return array
*/
public function getSourcesConfig($filter = array())
{
$results = array();
if (!empty($filter)) {
foreach (Turba::availableSources() as $key => $source) {
$curr = current(array_keys($filter));
if (!empty($source[$curr]) &&
($source[$curr] == current($filter))) {
$results[$key] = $source;
}
}
}
return $results;
}
/**
* Lists all shares the current user has access to.
*
* @param integer $perms
*
* @return array of Turba_Share objects.
*/
public function listShares($perms = Horde_Perms::READ)
{
return Turba::listShares(true, $perms);
}
/**
* GroupObject API - Lists all turba lists for the current user that can
* be treated as Horde_Group objects.
*
* @return array A hash of all visible groups in the form of
* group_id => group_name
* @throws Horde_Exception
*/
public function listUserGroupObjects()
{
$groups = $owners = array();
// Only turba's SQL based sources can act as Horde_Groups
$sources = $this->getSourcesConfig(array('type' => 'sql'));
foreach ($sources as $key => $source) {
// Each source could have a different database connection
$db[$key] = empty($source['params']['sql'])
? $GLOBALS['injector']->getInstance('Horde_Db_Adapter')
: $GLOBALS['injector']->getInstance('Horde_Core_Factory_Db')->create('turba', $source['params']['sql']);
if ($source['use_shares']) {
if (empty($contact_shares)) {
$contact_shares = $this->listShares(Horde_Perms::SHOW);
}
foreach ($contact_shares as $id => $share) {
$params = @unserialize($share->get('params'));
if ($params['source'] == $key) {
$owners[] = $params['name'];
}
}
if (!$owners) {
return array();
}
} else {
$owners = array($GLOBALS['registry']->getAuth());
}
$owner_ids = array();
foreach ($owners as $owner) {
$owner_ids[] = $db[$key]->quoteString($owner);
}
$sql = 'SELECT ' . $source['map']['__key'] . ', ' . $source['map'][$source['list_name_field']]
. ' FROM ' . $source['params']['table'] . ' WHERE '
. $source['map']['__type'] . ' = \'Group\' AND '
. $source['map']['__owner'] . ' IN (' . implode(',', $owner_ids ) . ')';
try {
$results = $db[$key]->selectAssoc($sql);
} catch (Horde_Db_Exception $e) {
throw new Horde_Exception(_("Server error when performing search."));
}
foreach ($results as $id => $name) {
$groups[$key . ':' . $id] = $name;
}
}
return $groups;
}
/**
* Returns all contact groups.
*
* @return array A list of group hashes.
* @throws Turba_Exception
*/
public function getGroupObjects()
{
$ret = array();
foreach ($this->getSourcesConfig(array('type' => 'sql')) as $key => $source) {
if (empty($source['map']['__type'])) {
continue;
}
list($db, $sql) = $this->_getGroupObject($source, 'Group');
try {
$results = $db->selectAll($sql);
} catch (Horde_Db_Exception $e) {
throw new Horde_Exception(_("Server error when performing search."));
}
foreach ($results as $row) {
/* name is a reserved word in Postgresql (at a minimum). */
$row['name'] = $row['lname'];
unset($row['lname']);
$ret[$key . ':' . $row['id']] = $row;
}
}
return $ret;
}
/**
* Returns all contact groups that the specified user is a member of.
*
* @param string $user The user.
* @param boolean $parentGroups Include user as a member of the any
* parent group as well.
*
* @return array An array of group identifiers that the specified user is
* a member of.
* @throws Horde_Exception
*/
public function getGroupMemberships($user, $parentGroups = false)
{
$memberships = array();
foreach ($this->getGroupObjects() as $id => $list) {
if (in_array($user, $this->getGroupMembers($id, $parentGroups))) {
$memberships[$id] = $list['name'];
}
}
return $memberships;
}
/**
* Returns a contact group hash.
*
* @param string $gid The group identifier.
*
* @return array A hash defining the group.
* @throws Turba_Exception
*/
public function getGroupObject($gid)
{
if (empty($gid) || (strpos($gid, ':') === false)) {
throw new Turba_Exception(sprintf('Unsupported group id: %s', $gid));
}
$sources = $this->getSourcesConfig(array('type' => 'sql'));
list($source, $id) = explode(':', $gid);
if (empty($sources[$source])) {
return array();
}
list($db, $sql) = $this->_getGroupObject($sources[$source], $id);
try {
$ret = $db->selectOne($sql);
$ret['name'] = $ret['lname'];
unset($ret['lname']);
return $ret;
} catch (Horde_Db_Exception $e) {
throw new Horde_Exception(_("Server error when performing search."));
}
}
/**
*/
protected function _getGroupObject($source, $key)
{
$db = empty($source['params']['sql'])
? $GLOBALS['injector']->getInstance('Horde_Db_Adapter')
: $GLOBALS['injector']->getInstance('Horde_Core_Factory_Db')->create('turba', $source['params']['sql']);
$sql = 'SELECT ' . $source['map']['__members'] . ' members,'
. $source['map']['email'] . ' email,'
. $source['map'][$source['list_name_field']]
. ' lname FROM ' . $source['params']['table'] . ' WHERE '
. $source['map']['__key'] . ' = ' . $db->quoteString($key);
return array($db, $sql);
}
/**
* Returns a list of all members belonging to a contact group.
*
* @param string $gid The group identifier
* @param boolean $subGroups Also include members of any subgroups?
*
* @return array An array of group members (identified by email address).
* @throws Horde_Exception
*/
public function getGroupMembers($gid, $subGroups = false)
{
$contact_shares = $this->listShares(Horde_Perms::SHOW);
$sources = $this->getSourcesConfig(array('type' => 'sql'));
$entry = $this->getGroupObject($gid);
if (!$entry) {
return array();
}
list($source,) = explode(':', $gid);
$members = @unserialize($entry['members']);
if (!is_array($members)) {
return array();
}
$db[$source] = empty($sources[$source]['params']['sql'])
? $GLOBALS['injector']->getInstance('Horde_Db_Adapter')
: $GLOBALS['injector']->getInstance('Horde_Core_Factory_Db')->create('turba', $sources[$source]['params']['sql']);
$users = array();
foreach ($members as $member) {
// Is this member from the same source or a different one?
if (strpos($member, ':') !== false) {
list($newSource, $uid) = explode(':', $member);
if (!empty($contact_shares[$newSource])) {
$params = @unserialize($contact_shares[$newSource]->get('params'));
$newSource = $params['source'];
$member = $uid;
$db[$newSource] = empty($sources[$newSource]['params']['sql'])
? $GLOBALS['injector']->getInstance('Horde_Db_Adapter')
: $GLOBALS['injector']->getInstance('Horde_Core_Factory_Db')->create('turba', $sources[$newSource]['params']['sql']);
} elseif (empty($sources[$newSource])) {
// Last chance, it's not in one of our non-share sources
continue;
}
} else {
// Same source
$newSource = $source;
}
$type = $sources[$newSource]['map']['__type'];
$email = $sources[$newSource]['map']['email'];
$sql = 'SELECT ' . $email . ', ' . $type
. ' FROM ' . $sources[$newSource]['params']['table']
. ' WHERE ' . $sources[$newSource]['map']['__key']
. ' = ' . $db[$newSource]->quoteString($member);
try {
$results = $db[$newSource]->selectOne($sql);
} catch (Horde_Db_Exception $e) {
throw new Horde_Exception(_("Server error when performing search."));
}
// Sub-Lists are treated as sub groups the best that we can...
if ($subGroups && $results[$type] == 'Group') {
$users = array_merge($users, $this->getGroupMembers($newSource . ':' . $member));
}
if (strlen($results[$email])) {
// use a key to dump dups
$users[$results[$email]] = true;
}
}
ksort($users);
return array_keys($users);
}
/**
* Creates a new addressbook.
*
* @param string $name The display name for the addressbook.
* @param array $params Any addtional parameters needed.
* - synchronize: (boolean) If true, add address book to the list of
* address books to syncronize.
* DEFAULT: false (do not add to the list).
* @since 4.2.1
*
* @return string The new addressbook's id (share name).
* @since 4.2.0
*/
public function addAddressbook($name, array $params = array())
{
global $conf, $injector, $prefs;
$cfgSources = Turba::availableSources();
$driver = $injector->getInstance('Turba_Factory_Driver')
->createFromConfig($cfgSources[$conf['shares']['source']]);
$share = $driver->createShare(
strval(new Horde_Support_Randomid()),
array(
'params' => array('source' => $conf['shares']['source']),
'name' => $name
)
);
$shareName = $share->getName();
if (!empty($params['synchronize'])) {
$sync = @unserialize($prefs->getValue('sync_books'));
$sync[] = $shareName;
$prefs->setValue('sync_books', serialize($sync));
}
return $shareName;
}
/**
* Delete the specified addressbook.
*
* @param string $id The addressbook id.
* @since 4.2.0
*/
public function deleteAddressbook($id)
{
$share = $GLOBALS['injector']
->getInstance('Turba_Factory_Driver')
->create($id);
$GLOBALS['injector']
->getInstance('Turba_Shares')
->removeShare($share);
}
/**
* Update an existing addressbook's name or description.
*
* @param string $id The addressbook id.
* @param array $info The info to change:
* - name: The addressbook's display name.
* - desc: The addressbook's description.
*
* @since 4.2.0
*/
public function updateAddressbook($id, array $info)
{
$share = $GLOBALS['injector']->getInstance('Turba_Factory_Driver')->create($id);
if (!empty($info['name'])) {
$share->set('name', $info['name']);
}
if (!empty($info['desc'])) {
$share->set('desc', $info['desc']);
}
$share->save();
}
/* Helper methods. */
/**
*/
private function _modified($uid, $sources)
{
$modified = $this->getActionTimestamp($uid, 'modify', $sources);
if (empty($modified)) {
$modified = $this->getActionTimestamp($uid, 'add', $sources);
}
return $modified;
}
/**
* @throws Turba_Exception
*/
private function _getSources($sources)
{
/* Get default address book from user preferences. */
if (empty($sources)) {
$sources = @unserialize($GLOBALS['prefs']->getValue('sync_books'));
} elseif (!is_array($sources)) {
$sources = array($sources);
}
if (empty($sources)) {
$sources = array(Turba::getDefaultAddressbook());
if (empty($sources)) {
throw new Turba_Exception(_("No address book specified"));
}
}
foreach ($sources as $val) {
if (!strlen($val) || !isset($GLOBALS['cfgSources'][$val])) {
throw new Turba_Exception(sprintf(_("Invalid address book: %s"), $val));
}
}
return $sources;
}
/**
* Retrieve the list of used tag_names, tag_ids and the total number
* of resources that are linked to that tag.
*
* @param array $tags An optional array of tag_ids. If omitted, all tags
* will be included.
*
* @return array An array containing tag_name, and total
*/
public function listTagInfo($tags = null, $user = null)
{
return $GLOBALS['injector']->getInstance('Turba_Tagger')
->getTagInfo($tags, 500, null, $user);
}
/**
* SearchTags API:
* Returns an application-agnostic array (useful for when doing a tag search
* across multiple applications)
*
* The 'raw' results array can be returned instead by setting $raw = true.
*
* @param array $names An array of tag_names to search for.
* @param integer $max The maximum number of resources to return.
* @param integer $from The number of the resource to start with.
* @param string $resource_type The resource type [bookmark, '']
* @param string $user Restrict results to resources owned by $user.
* @param boolean $raw Return the raw data?
*
* @return array An array of results:
* <pre>
* 'title' - The title for this resource.
* 'desc' - A terse description of this resource.
* 'view_url' - The URL to view this resource.
* 'app' - The Horde application this resource belongs to.
* 'icon' - URL to an image.
* </pre>
*/
public function searchTags($names, $max = 10, $from = 0,
$resource_type = '', $user = null, $raw = false)
{
global $injector, $registry;
$results = $injector
->getInstance('Turba_Tagger')
->search(
$names,
array('user' => $user));
// Check for error or if we requested the raw data array.
if ($raw) {
return $results;
}
$return = array();
foreach ($results as $contact_uid) {
try {
$driver = $injector->getInstance('Turba_Factory_Driver');
foreach ($this->_getSources($sources) as $source) {
$sdriver = $driver->create($source);
if (!$sdriver->hasPermission(Horde_Perms::READ)) {
continue;
}
$result = $sdriver->search(array('__uid' => $contact_uid));
if (count($result) == 0) {
continue;
} elseif (count($result) > 1) {
throw new Turba_Exception(sprintf("Internal Horde Error: multiple Turba objects with same objectId %s.", $uid));
}
foreach ($result->objects as $obj) {
$return[] = array(
'title' => $obj->getValue('name'),
'desc' => $obj->getValue('name'),
'view_url' => $obj->url,
'app' => 'turba',
'icon' => $this->_getContactImageUrl($obj)
);
}
}
} catch (Exception $e) {
}
}
return $return;
}
protected function _getContactImageUrl($obj)
{
if ($photo = $obj->getValue('photo')) {
try {
$img = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Image')->create();
$img->loadString($photo['load']['data']);
$img->resize(50, 50, true);
$data = $img->raw(true);
$type = $img->getContentType();
} catch (Horde_Image_Exception $e) {
$data = $photo['load']['data'];
$type = $obj->getValue('phototype');
}
return Horde_Url_Data::create($type, $data);
}
}
}