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

773 lines
26 KiB
PHP

<?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
*/
/**
* Turba Base Class.
*
* @author Chuck Hagenbuch <chuck@horde.org>
* @author Jon Parise <jon@horde.org>
* @category Horde
* @copyright 2000-2017 Horde LLC
* @license http://www.horde.org/licenses/apache ASL
* @package Turba
*/
class Turba
{
/**
* The virtual path to use for VFS data.
*/
const VFS_PATH = '.horde/turba/documents';
/**
* The current source.
*
* @var string
*/
static public $source;
/**
* Cached data.
*
* @var array
*/
static protected $_cache = array();
/**
* Returns the source entries from config/backends.php that have been
* configured as available sources in the main Turba configuration.
*
* @return array List of available sources.
* @throws Horde_Exception
*/
static public function availableSources()
{
global $registry;
$s = $registry->loadConfigFile('backends.php', 'cfgSources', 'turba')->config['cfgSources'];
$sources = array();
foreach ($s as $key => $source) {
if (empty($source['disabled'])) {
$sources[$key] = $source;
}
}
return $sources;
}
/**
* Get all the address books the user has the requested permissions to and
* return them in the user's preferred order.
*
* @param integer $permission The Horde_Perms::* constant to filter on.
* @param array $options Any additional options.
*
* @return array The filtered, ordered $cfgSources entries.
*/
static public function getAddressBooks($permission = Horde_Perms::READ,
array $options = array())
{
return self::permissionsFilter(
$GLOBALS['cfgSources'],
$permission,
$options
);
}
/**
* Returns the current user's default address book.
*
* @return string The default address book name.
*/
static public function getDefaultAddressbook()
{
/* In case of shares select first user owned address book as default */
if (!empty($_SESSION['turba']['has_share'])) {
try {
$owned_shares = self::listShares(true);
if (count($owned_shares)) {
return key($owned_shares);
}
} catch (Exception $e) {}
}
reset($GLOBALS['cfgSources']);
return key($GLOBALS['cfgSources']);
}
/**
* Returns the sort order selected by the user.
*
* @return array TODO
*/
static public function getPreferredSortOrder()
{
return @unserialize($GLOBALS['prefs']->getValue('sortorder'));
}
/**
* Saves the sort order to the preferences backend.
*
* @param Horde_Variables $vars Variables object.
* @param string $source Source.
*/
static public function setPreferredSortOrder(Horde_Variables $vars,
$source)
{
if (!strlen($sortby = $vars->get('sortby'))) {
return;
}
$sources = self::getColumns();
$columns = isset($sources[$source])
? $sources[$source]
: array();
$column_name = self::getColumnName($sortby, $columns);
$append = true;
$ascending = ($vars->get('sortdir') == 0);
if ($vars->get('sortadd')) {
$sortorder = self::getPreferredSortOrder();
foreach ($sortorder as $i => $elt) {
if ($elt['field'] == $column_name) {
$sortorder[$i]['ascending'] = $ascending;
$append = false;
}
}
} else {
$sortorder = array();
}
if ($append) {
$sortorder[] = array(
'ascending' => $ascending,
'field' => $column_name
);
}
$GLOBALS['prefs']->setValue('sortorder', serialize($sortorder));
}
/**
* Retrieves a column's field name.
*
* @param integer $i TODO
* @param array $columns TODO
*
* @return string TODO
*/
static public function getColumnName($i, $columns)
{
return (($i == 0) || !isset($columns[$i - 1]))
? 'name'
: $columns[$i - 1];
}
/**
* TODO
*/
static public function getColumns()
{
$columns = array();
$lines = explode("\n", $GLOBALS['prefs']->getValue('columns'));
foreach ($lines as $line) {
$line = trim($line);
if ($line) {
$cols = explode("\t", $line);
if (count($cols) > 1) {
$source = array_splice($cols, 0, 1);
$columns[$source[0]] = array();
foreach ($cols as $col) {
if ($col == '__tags' ||
isset($GLOBALS['cfgSources'][$source[0]]['map'][$col])) {
$columns[$source[0]][] = $col;
}
}
}
}
}
return $columns;
}
/**
* Builds and cleans up a composite field.
*
* @param string $format The sprintf field format.
* @param array $fields The fields that compose the composite field.
*
* @return string The formatted composite field.
*/
static public function formatCompositeField($format, $fields)
{
return preg_replace('/ +/', ' ', trim(vsprintf($format, $fields), " \t\n\r\0\x0B,"));
}
/**
* Returns a best guess at the lastname in a string.
*
* @param string $name String contain the full name.
*
* @return string String containing the last name.
*/
static public function guessLastname($name)
{
$name = trim(preg_replace('|\s|', ' ', $name));
if (!empty($name)) {
/* Assume that last names are always before any commas. */
if (is_int(strpos($name, ','))) {
$name = Horde_String::substr($name, 0, strpos($name, ','));
}
/* Take out anything in parentheses. */
$name = trim(preg_replace('|\(.*\)|', '', $name));
$namelist = explode(' ', $name);
$name = $namelist[($nameindex = (count($namelist) - 1))];
while (!empty($name) &&
(($nlength = Horde_String::length($name)) < 5) &&
strspn($name[($nlength - 1)], '.:-') &&
!empty($namelist[($nameindex - 1)])) {
$name = $namelist[--$nameindex];
}
}
return strlen($name)
? $name
: null;
}
/**
* Formats the name according to the user's preference.
*
* If the format is 'none', the full name with all parts is returned. If
* the format is 'last_first' or 'first_last', only the first name and
* last name are returned.
*
* @param Turba_Object $ob The object to get a name from.
* @param string $name_format The formatting. One of 'none', 'last_first'
* or 'first_last'. Defaults to the user
* preference.
*
* @return string The formatted name, either "Firstname Lastname"
* or "Lastname, Firstname" depending on $name_format or
* the user's preference.
*/
static public function formatName(Turba_Object $ob, $name_format = null)
{
if (!$name_format) {
if (!isset(self::$_cache['defaultFormat'])) {
self::$_cache['defaultFormat'] = $GLOBALS['prefs']->getValue('name_format');
}
$name_format = self::$_cache['defaultFormat'];
}
/* If no formatting, return original name. */
if (!in_array($name_format, array('first_last', 'last_first'))) {
return $ob->getValue('name');
}
/* See if we have the name fields split out explicitly. */
if ($ob->hasValue('firstname') && $ob->hasValue('lastname')) {
return ($name_format == 'last_first')
? $ob->getValue('lastname') . ', ' . $ob->getValue('firstname')
: $ob->getValue('firstname') . ' ' . $ob->getValue('lastname');
}
/* One field, we'll have to guess. */
$name = $ob->getValue('name');
$lastname = self::guessLastname($name);
if (($name_format == 'last_first') &&
!is_int(strpos($name, ',')) &&
(Horde_String::length($name) > Horde_String::length($lastname))) {
return $lastname . ', ' . preg_replace('/\s+' . preg_quote($lastname, '/') . '/', '', $name);
}
if (($name_format == 'first_last') &&
is_int(strpos($name, ',')) &&
(Horde_String::length($name) > Horde_String::length($lastname))) {
return preg_replace('/' . preg_quote($lastname, '/') . ',\s*/', '', $name) . ' ' . $lastname;
}
return $name;
}
/**
* TODO
*
* @param mixed $data Either a single email address or an array of email
* addresses to format.
* @param string $name The personal name phrase.
*
* @return mixed Either the formatted address or an array of formatted
* addresses.
*/
static public function formatEmailAddresses($data, $name)
{
if (!isset(self::$_cache['useRegistry'])) {
self::$_cache['useRegistry'] = $GLOBALS['registry']->hasMethod('mail/batchCompose');
}
$out = array();
$rfc822 = $GLOBALS['injector']->getInstance('Horde_Mail_Rfc822');
if (!is_array($data)) {
$data = array($data);
}
foreach ($data as $email_vals) {
foreach ($rfc822->parseAddressList($email_vals) as $ob) {
$addr = strval($ob);
$tmp = null;
if (self::$_cache['useRegistry']) {
try {
$tmp = $GLOBALS['registry']->call('mail/batchCompose', array(array($addr)));
} catch (Horde_Exception $e) {
self::$_cache['useRegistry'] = false;
}
}
$tmp = empty($tmp)
? 'mailto:' . urlencode($addr)
: reset($tmp);
$out[] = Horde::link($tmp) . htmlspecialchars($addr) . '</a>';
}
}
return implode(', ', $out);
}
/**
* Returns the real name, if available, of a user.
*
* @param string $uid The uid of the name to return.
*
* @return string The user's full, real name.
*/
static public function getUserName($uid)
{
if (!isset(self::$_cache['names'])) {
self::$_cache['names'] = array();
}
if (!isset(self::$_cache['names'][$uid])) {
$ident = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Identity')->create($uid);
$ident->setDefault($ident->getDefault());
$name = $ident->getValue('fullname');
self::$_cache['names'][$uid] = empty($name)
? $uid
: $name;
}
return self::$_cache['names'][$uid];
}
/**
* Gets extended permissions on an address book.
*
* @param Turba_Driver $addressBook The address book to get extended
* permissions for.
* @param string $permission What extended permission to get.
*
* @return mixed The requested extended permissions value, or true if it
* doesn't exist.
*/
static public function getExtendedPermission(Turba_Driver $addressBook,
$permission)
{
// We want to check the base source as extended permissions
// are enforced per backend, not per share.
$key = $addressBook->getName() . ':' . $permission;
$perms = $GLOBALS['injector']->getInstance('Horde_Perms');
if (!$perms->exists('turba:sources:' . $key)) {
return true;
}
$allowed = $perms->getPermissions('turba:sources:' . $key, $GLOBALS['registry']->getAuth());
if (is_array($allowed)) {
switch ($permission) {
case 'max_contacts':
$allowed = max($allowed);
break;
}
}
return $allowed;
}
/**
* Filters sources based on permissions.
*
* @param array $in The source list we want filtered.
* @param integer $permission The Horde_Perms::* constant we will filter
* on.
* @param array $options Additional options:
* - require_add: (boolean) Only return sources
* that can be added to.
*
* @return array The filtered data.
*/
static public function permissionsFilter(array $in,
$permission = Horde_Perms::READ,
array $options = array())
{
$factory = $GLOBALS['injector']->getInstance('Turba_Factory_Driver');
$out = array();
foreach ($in as $sourceId => $source) {
try {
$driver = $factory->createFromConfig($source, $sourceId);
if ($driver->hasPermission($permission) &&
(empty($options['require_add']) || $driver->canAdd())) {
$out[$sourceId] = $source;
}
} catch (Turba_Exception $e) {
Horde::log($e, 'ERR');
}
}
return $out;
}
/**
* Replaces all share-enabled sources in a source list with all shares
* from this source that the current user has access to.
*
* This will only sync shares that are unique to Horde (such as a SQL or
* Kolab sources). Any backend that supports ACLs or similar mechanism
* should be configured from within backends.local.php or via Horde's
* share_* hooks.
*
* @param array $sources The default $cfgSources array.
* @param boolean $owner Only return shares that the current user owns?
* @param array $options An array of options:
* - shares: Use this list of provided shares. Default is to get the
* list from the share system using the current user.
* - auth_user: Use this as the authenticated user name.
*
* @return array The $cfgSources array.
*/
public static function getConfigFromShares(array $sources, $owner = false, $options = array())
{
global $notification, $registry, $conf, $injector, $prefs;
if (empty($options['shares'])) {
try {
$shares = self::listShares($owner);
} catch (Horde_Share_Exception $e) {
// Notify the user if we failed, but still return the $cfgSource
// array.
$notification->push($e, 'horde.error');
return $sources;
}
} else {
$shares = $options['shares'];
}
/* See if any of our sources are configured to handle all otherwise
* unassigned Horde_Share objects. */
$all_shares = null;
foreach ($sources as $key => $cfg) {
if (!empty($cfg['all_shares'])) {
// Indicate the source handler that catches unassigned shares.
$all_shares = $key;
}
}
if (empty($options['auth_user'])) {
$auth_user = $registry->getAuth();
} else {
$auth_user = $options['auth_user'];
}
$sortedSources = $vbooks = array();
$personal = false;
foreach ($shares as $name => &$share) {
if (isset($sources[$name])) {
continue;
}
$personal |= ($share->get('owner') == $auth_user);
$params = @unserialize($share->get('params'));
if (empty($params['source']) && !empty($all_shares)) {
$params['source'] = $all_shares;
}
if (isset($params['type']) && $params['type'] == 'vbook') {
// We load vbooks last in case they're based on other shares.
$params['share'] = $share;
$vbooks[$name] = $params;
} elseif (!empty($params['source']) &&
!empty($sources[$params['source']]['use_shares'])) {
if (empty($params['name'])) {
$params['name'] = $name;
$share->set('params', serialize($params));
try {
$share->save();
} catch (Horde_Share_Exception $e) {
Horde::log($e, 'ERR');
}
}
$info = $sources[$params['source']];
$info['params']['config'] = $sources[$params['source']];
$info['params']['config']['params']['share'] = $share;
$info['params']['config']['params']['name'] = $params['name'];
$info['title'] = $share->get('name');
if ($share->get('owner') != $auth_user) {
$info['title'] .= ' [' . $registry->convertUsername($share->get('owner'), false) . ']';
}
$info['type'] = 'share';
$info['use_shares'] = false;
$sortedSources[$params['source']][$name] = $info;
}
}
// Check for the user's default share and built new source list.
$newSources = array();
foreach (array_keys($sources) as $source) {
if (empty($sources[$source]['use_shares'])) {
$newSources[$source] = $sources[$source];
continue;
}
if (isset($sortedSources[$source])) {
$newSources = array_merge($newSources, $sortedSources[$source]);
}
if (!empty($conf['share']['auto_create']) &&
$auth_user &&
!$personal) {
// User's default share is missing.
try {
$driver = $injector
->getInstance('Turba_Factory_Driver')
->create($source);
} catch (Turba_Exception $e) {
$notification->push($e->getMessage(), 'horde.error');
continue;
}
$sourceKey = strval(new Horde_Support_Randomid());
try {
$share = $driver->createShare(
$sourceKey,
array(
'params' => array(
'source' => $source,
'default' => true,
'name' => $auth_user
)
)
);
$source_config = $sources[$source];
$source_config['params']['share'] = $share;
$newSources[$sourceKey] = $source_config;
$personal = true;
$prefs->setValue('default_dir', $share->getName());
} catch (Horde_Share_Exception $e) {
Horde::log($e, 'ERR');
}
}
}
// Add vbooks now that all available address books are loaded.
foreach ($vbooks as $name => $params) {
if (isset($newSources[$params['source']])) {
$newSources[$name] = array(
'title' => $shares[$name]->get('name'),
'type' => 'vbook',
'params' => $params,
'export' => true,
'browse' => true,
'map' => $newSources[$params['source']]['map'],
'search' => $newSources[$params['source']]['search'],
'strict' => $newSources[$params['source']]['strict'],
'use_shares' => false,
);
} else {
$notification->push(sprintf(
_("Removing the virtual address book \"%s\" because the parent source has disappeared."),
$shares[$name]->get('name')), 'horde.message'
);
try {
$injector->getInstance('Turba_Shares')->removeShare($shares[$name]);
} catch (Horde_Share_Exception $e) {
Horde::log($e, 'ERR');
}
}
}
return $newSources;
}
/**
* Retrieve a new source config entry based on a Turba share.
*
* @param Horde_Share_Object object The share to base config on.
*
* @return array The $cfgSource entry for this share source.
*/
public static function getSourceFromShare(Horde_Share_Object $share)
{
// Require a fresh config file.
$cfgSources = self::availableSources();
$params = @unserialize($share->get('params'));
$newConfig = $cfgSources[$params['source']];
$newConfig['params']['config'] = $cfgSources[$params['source']];
$newConfig['params']['config']['params']['share'] = $share;
$newConfig['params']['config']['params']['name'] = $params['name'];
$newConfig['title'] = $share->get('name');
$newConfig['type'] = 'share';
$newConfig['use_shares'] = false;
return $newConfig;
}
/**
* Returns all shares the current user has specified permissions to.
*
* @param boolean $owneronly Only return address books owned by the user?
* Defaults to false.
* @param integer $permission Permissions to filter by.
*
* @return array Shares the user has the requested permissions to.
*/
static public function listShares($owneronly = false,
$permission = Horde_Perms::READ)
{
if (!$GLOBALS['session']->get('turba', 'has_share') ||
($owneronly && !$GLOBALS['registry']->getAuth())) {
return array();
}
try {
return $GLOBALS['injector']->getInstance('Turba_Shares')->listShares(
$GLOBALS['registry']->getAuth(),
array(
'attributes' => $owneronly ? $GLOBALS['registry']->getAuth() : null,
'perm' => $permission
)
);
} catch (Horde_Share_Exception $e) {
Horde::log($e, 'ERR');
return array();
}
}
/**
* Create a new Turba share.
*
* @param string $share_name The id for the new share.
* @param array $params Parameters for the new share.
*
* @return Horde_Share The new share object.
* @throws Turba_Exception
*/
static public function createShare($share_name, $params)
{
if (isset($params['name'])) {
$name = $params['name'];
unset($params['name']);
} else {
/* Sensible default for empty display names */
$name = sprintf(_("Address book of %s"), $GLOBALS['injector']->getInstance('Horde_Core_Factory_Identity')->create()->getName());
}
/* Generate the new share. */
try {
$turba_shares = $GLOBALS['injector']->getInstance('Turba_Shares');
$share = $turba_shares->newShare($GLOBALS['registry']->getAuth(), $share_name, $name);
/* Now any other params. */
foreach ($params as $key => $value) {
if (!is_scalar($value)) {
$value = serialize($value);
}
$share->set($key, $value);
}
$turba_shares->addShare($share);
$share->save();
} catch (Horde_Share_Exception $e) {
Horde::log($e, 'ERR');
throw new Turba_Exception($e);
}
return $share;
}
/**
* Add browse.js javascript to page.
*/
static public function addBrowseJs()
{
global $page_output;
$page_output->addScriptFile('browse.js');
$page_output->addInlineJsVars(array(
'TurbaBrowse.confirmdelete' => _("Are you sure that you want to delete %s?"),
'TurbaBrowse.contact1' => _("You must select at least one contact first."),
'TurbaBrowse.contact2' => _("You must select a target contact list."),
'TurbaBrowse.contact3' => _("Please name the new contact list:"),
'TurbaBrowse.copymove' => _("You must select a target address book."),
'TurbaBrowse.submit' => _("Are you sure that you want to delete the selected contacts?")
));
}
/**
* Return an array of all available attributes of type 'email'. Optionally,
* ensure the field is defined in the specified $source.
*
* @param $source string An optional source identifier.
* @param $searchable boolean If true, and $source is provided, ensure that
* the email field is a configured searchable
* field.
*
* @return array An array of email fields.
* @since 4.2.9
*/
public static function getAvailableEmailFields($source = null, $searchable = true)
{
global $attributes, $injector, $cfgSources;
if (!empty($source)) {
$driver = $injector->getInstance('Turba_Factory_Driver')
->create($source);
}
$emailFields = array();
foreach ($attributes as $field => $data) {
if ($data['type'] == 'email') {
if (empty($source) || (!empty($source) &&
in_array($field, array_keys($driver->map)) &&
(!$searchable || ($searchable && in_array($field, $cfgSources[$source]['search'])))))
$emailFields[] = $field;
}
}
return $emailFields;
}
}