2021-05-28 15:49:42 +00:00
|
|
|
<?php
|
|
|
|
/**
|
2021-05-28 16:01:42 +00:00
|
|
|
* plugins/abook_carddav/abook_class.php -- main class
|
2021-05-28 15:49:42 +00:00
|
|
|
*
|
2021-05-28 16:01:42 +00:00
|
|
|
* SquirrelMail Address Book CardDAV Backend
|
|
|
|
* Copyright (C) 2021 Aleksei Shpakovsky
|
|
|
|
* This program is licensed under GPLv3. See COPYING for details
|
|
|
|
* based on:
|
2021-05-28 15:49:42 +00:00
|
|
|
* SquirrelMail Address Book Backend template
|
|
|
|
* Copyright (C) 2004 Tomas Kuliavas <tokul@users.sourceforge.net>
|
|
|
|
* This program is licensed under GPL. See COPYING for details
|
|
|
|
*/
|
|
|
|
|
2021-05-28 20:49:31 +00:00
|
|
|
require 'vendor/autoload.php';
|
|
|
|
|
|
|
|
use MStilkerich\CardDavClient\{Account, AddressbookCollection, Config};
|
|
|
|
use MStilkerich\CardDavClient\Services\{Discovery, Sync, SyncHandler};
|
|
|
|
|
|
|
|
use Psr\Log\{AbstractLogger, NullLogger, LogLevel};
|
|
|
|
use Sabre\VObject\Component\VCard;
|
|
|
|
|
2021-05-28 21:18:54 +00:00
|
|
|
Config::init();
|
2021-05-28 20:49:31 +00:00
|
|
|
|
2021-05-28 15:49:42 +00:00
|
|
|
/**
|
2021-05-28 16:01:42 +00:00
|
|
|
* address book carddav backend class
|
2021-05-28 15:49:42 +00:00
|
|
|
*/
|
2021-05-28 16:01:42 +00:00
|
|
|
class abook_carddav extends addressbook_backend {
|
2021-05-28 15:49:42 +00:00
|
|
|
var $btype = 'local';
|
2021-05-28 16:01:42 +00:00
|
|
|
var $bname = 'carddav';
|
2021-05-29 22:13:33 +00:00
|
|
|
var $writeable = true;
|
2021-05-28 15:49:42 +00:00
|
|
|
|
|
|
|
/* ========================== Private ======================= */
|
|
|
|
|
|
|
|
/* Constructor */
|
2021-05-28 16:01:42 +00:00
|
|
|
function abook_carddav($param) {
|
2021-05-30 14:18:04 +00:00
|
|
|
// defaults
|
|
|
|
$this->sname = _("CardDAV Address Book");
|
2021-05-28 15:49:42 +00:00
|
|
|
|
|
|
|
if (is_array($param)) {
|
2021-05-30 14:18:04 +00:00
|
|
|
if (!empty($param['name'])) { $this->sname = $param['name']; }
|
|
|
|
if (!empty($param['abook_uri'])) { $this->abook_uri = $param['abook_uri']; }
|
|
|
|
if (!empty($param['base_uri'])) { $this->base_uri = $param['base_uri']; }
|
|
|
|
if (!empty($param['username'])) { $this->username = $param['username']; }
|
|
|
|
if (!empty($param['password'])) { $this->password = $param['password']; }
|
|
|
|
if (isset($param['writeable'])) { $this->writeable = $param['writeable']; }
|
|
|
|
if (isset($param['listing'])) { $this->listing = $param['listing']; }
|
2021-05-30 15:03:30 +00:00
|
|
|
$this->account = new Account($this->base_uri, $this->username, $this->password, $this->base_uri);
|
|
|
|
$this->abook = new AddressbookCollection($this->abook_uri, $this->account);
|
|
|
|
$this->abook_uri_len=strlen($this->abook->getUriPath());
|
2021-05-28 15:49:42 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
return $this->set_error('Invalid argument to constructor');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-05-30 16:27:40 +00:00
|
|
|
* Given a $vcard object, returns squirrelmail contact (array).
|
|
|
|
* $uri is needed to specify nickname if $this->writeable
|
|
|
|
* Optional $email and $tel args overwrite those stored in vcard.
|
2021-05-30 15:03:30 +00:00
|
|
|
* Respects $this->writeable:
|
|
|
|
* for writeable addressbooks, 'nickname' must be unique identifier -
|
2021-05-30 16:27:40 +00:00
|
|
|
* in our case, last part of uid is used
|
2021-05-30 15:03:30 +00:00
|
|
|
* for non-writeable addressbooks, 'nickname' doesn't matter that much -
|
|
|
|
* so we put ORG there
|
2021-05-28 15:49:42 +00:00
|
|
|
*/
|
2021-05-30 16:27:40 +00:00
|
|
|
function vcard2sq($uri, $vcard, $email=null, $tel='def') {
|
|
|
|
if(!$email) { $email = (string)$vcard->EMAIL; }
|
|
|
|
if($tel=='def') { $tel = (string)$vcard->TEL; }
|
2021-05-30 15:03:30 +00:00
|
|
|
if($this->writeable) {
|
|
|
|
$nickname = substr($uri, $this->abook_uri_len);
|
|
|
|
$label = (string)$vcard->ORG;
|
|
|
|
} else {
|
2021-05-30 15:29:07 +00:00
|
|
|
$nickname = (string)$vcard->ORG;
|
2021-05-30 16:27:40 +00:00
|
|
|
$label = $tel;
|
2021-05-30 15:03:30 +00:00
|
|
|
}
|
|
|
|
$names = $vcard->N->getParts();
|
|
|
|
// last,first,additional,prefix,suffix
|
|
|
|
return array(
|
|
|
|
'nickname' => $nickname,
|
|
|
|
'name' => (string)$vcard->FN,
|
|
|
|
'firstname' => (string)$names[1],
|
|
|
|
'lastname' => (string)$names[0],
|
|
|
|
'email' => $email,
|
|
|
|
'label' => $label,
|
|
|
|
'backend' => $this->bnum,
|
|
|
|
'source' => $this->sname);
|
2021-05-28 15:49:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-05-30 14:18:04 +00:00
|
|
|
* Run query against addressbook and return squurrelmail-type address(es)
|
|
|
|
* Params are same as in https://mstilkerich.github.io/carddavclient/classes/MStilkerich-CardDavClient-AddressbookCollection.html#method_query
|
|
|
|
* except the lack of 2nd parameter:
|
|
|
|
* @param array $query
|
|
|
|
* The query filter conditions, for format see https://mstilkerich.github.io/carddavclient/classes/MStilkerich-CardDavClient-XmlElements-Filter.html#method___construct
|
|
|
|
* @param bool $matchAll
|
|
|
|
* Whether all or any of the conditions needs to match.
|
|
|
|
* @param int $limit
|
|
|
|
* Tell the server to return at most $limit results. 0 means no limit.
|
|
|
|
* @return either:
|
|
|
|
* * a single address (array) - if $limit==1
|
|
|
|
* * or array of addresses (arrays)
|
2021-05-28 15:49:42 +00:00
|
|
|
*/
|
2021-05-30 14:18:04 +00:00
|
|
|
function run_query($query, $match_all=false, $limit=0) {
|
2021-05-30 14:40:03 +00:00
|
|
|
$ret = array();
|
2021-05-30 15:49:11 +00:00
|
|
|
$fields = ["FN", "N", "EMAIL", "ORG"];
|
2021-05-30 16:27:40 +00:00
|
|
|
if(!$this->writeable) { $fields[] = "TEL"; }
|
2021-05-30 15:49:11 +00:00
|
|
|
$all=$this->abook->query($query,$fields,$match_all,$limit);
|
2021-05-29 22:11:35 +00:00
|
|
|
/*
|
|
|
|
Returns an array of matched VCards:
|
|
|
|
The keys of the array are the URIs of the vcards
|
|
|
|
The values are associative arrays with keys etag (type: string) and vcard (type: VCard)
|
|
|
|
*/
|
|
|
|
|
|
|
|
foreach($all as $uri => $one) {
|
|
|
|
$vcard = $one['vcard'];
|
|
|
|
if(!isset($vcard->EMAIL)) { continue; }
|
2021-05-30 15:03:30 +00:00
|
|
|
if($this->writeable) {
|
2021-05-30 16:27:40 +00:00
|
|
|
// one line per each vcard
|
2021-05-30 15:14:50 +00:00
|
|
|
$ret[] = $this->vcard2sq($uri, $vcard);
|
2021-05-30 15:03:30 +00:00
|
|
|
} else {
|
2021-05-30 16:27:40 +00:00
|
|
|
// one line per each email
|
|
|
|
foreach($vcard->EMAIL as $i => $email) {
|
|
|
|
// also show one TEL for one EMAIL (extra TELs are ignored)
|
2021-10-02 22:09:43 +00:00
|
|
|
$ret[] = $this->vcard2sq($uri, $vcard, $email, (string)@$vcard->TEL[$i]);
|
2021-05-30 15:03:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if($limit == 1) { return $ret[0]; }
|
2021-05-29 22:11:35 +00:00
|
|
|
}
|
2021-05-30 14:18:04 +00:00
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ========================== Public ======================== */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search addressesbook for entries where any field matches expr.
|
|
|
|
* It's expected to support * and ? wildcards, and search all fields in any position.
|
|
|
|
* Note that currently, it does not.
|
|
|
|
* @param expr string search expression.
|
|
|
|
* @return array of addresses (arrays)
|
|
|
|
*/
|
|
|
|
function search($expr) {
|
2021-05-29 22:11:35 +00:00
|
|
|
|
2021-05-30 14:18:04 +00:00
|
|
|
/* To be replaced by advanded search expression parsing */
|
|
|
|
if(is_array($expr)) { return; }
|
2021-05-28 15:49:42 +00:00
|
|
|
|
2021-05-30 14:18:04 +00:00
|
|
|
if ($expr=='*') { return $this->list_addr(); }
|
|
|
|
|
|
|
|
// list all addresses where any of these fields contains $expr.
|
|
|
|
// wildcards are not supported.
|
|
|
|
// Also note that we don't check for presence of email in the filter,
|
|
|
|
// this will be filtered out inside run_query
|
|
|
|
return $this->run_query(['FN' => "/$expr/", 'EMAIL' => "/$expr/", 'ORG' => "/$expr/", 'NOTE' => "/$expr/"]);
|
2021-05-28 15:49:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-05-29 22:11:35 +00:00
|
|
|
* Lookup by the indicated field
|
|
|
|
*
|
|
|
|
* @param string $value Value to look up
|
|
|
|
* @param integer $field The field to look in, should be one
|
|
|
|
* of the SM_ABOOK_FIELD_* constants
|
|
|
|
* defined in functions/constants.php
|
|
|
|
* (OPTIONAL; defaults to nickname field)
|
|
|
|
* NOTE: uniqueness is only guaranteed
|
|
|
|
* when the nickname field is used here;
|
|
|
|
* otherwise, the first matching address
|
|
|
|
* is returned.
|
|
|
|
* @return a single address (array)
|
2021-05-28 15:49:42 +00:00
|
|
|
*/
|
2021-05-28 20:49:31 +00:00
|
|
|
function lookup($value, $field=SM_ABOOK_FIELD_NICKNAME) {
|
2021-05-28 15:49:42 +00:00
|
|
|
|
2021-06-03 19:14:46 +00:00
|
|
|
if (empty($value)) { return array(); }
|
2021-05-28 15:49:42 +00:00
|
|
|
|
2021-05-29 22:18:36 +00:00
|
|
|
$abook_uri_len=strlen($this->abook->getUriPath());
|
2021-05-29 22:11:35 +00:00
|
|
|
if($field == SM_ABOOK_FIELD_NICKNAME) {
|
|
|
|
// TODO: edit this if we use different nick-naming scheme
|
|
|
|
$uri = $this->abook->getUriPath() . $value;
|
2021-06-03 19:14:46 +00:00
|
|
|
try {
|
|
|
|
$one = $this->abook->getCard($uri);
|
|
|
|
/* returns Associative array with keys:
|
|
|
|
etag(string): Entity tag of the returned card
|
|
|
|
vcf(string): VCard as string
|
|
|
|
vcard(VCard): VCard as Sabre/VObject VCard
|
|
|
|
*/
|
|
|
|
$vcard = $one['vcard'];
|
|
|
|
return $this->vcard2sq($uri, $vcard);
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
// no card was returned
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
2021-05-29 22:11:35 +00:00
|
|
|
}
|
2021-05-30 16:27:40 +00:00
|
|
|
if($field == SM_ABOOK_FIELD_FIRSTNAME) { } // TODO: this will be harder
|
|
|
|
if($field == SM_ABOOK_FIELD_LASTNAME) { $filter=['N' => "/$value;/^", 'EMAIL' => "//"]; }
|
|
|
|
if($field == SM_ABOOK_FIELD_EMAIL) { $filter=['EMAIL' => "/$value/="]; }
|
|
|
|
if($field == SM_ABOOK_FIELD_LABEL) { $filter=['ORG' => "/$value/=", 'EMAIL' => "//"]; }
|
2021-05-29 22:11:35 +00:00
|
|
|
if(!isset($filter)) { return array(); }
|
2021-05-30 14:18:04 +00:00
|
|
|
return $this->run_query($filter,true,1);
|
2021-05-28 15:49:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List all addresses
|
2021-05-29 22:11:35 +00:00
|
|
|
* @return array of addresses (arrays)
|
2021-05-28 15:49:42 +00:00
|
|
|
*/
|
|
|
|
function list_addr() {
|
2021-05-30 15:14:50 +00:00
|
|
|
if(!$this->listing) { return array(); }
|
2021-05-28 20:49:31 +00:00
|
|
|
// list all addresses having an email
|
2021-05-30 21:24:15 +00:00
|
|
|
return array_merge(
|
|
|
|
/*
|
|
|
|
// TODO: a link to switch writeable status
|
|
|
|
array(array(
|
|
|
|
'special_message'=>'The address book is currently in (non)writeable mode. <a href="...">Click here to switch it to (non)writeable mode</a>',
|
|
|
|
'backend' => $this->bnum,
|
|
|
|
'source' => $this->sname,
|
|
|
|
)),
|
|
|
|
*/
|
|
|
|
$this->run_query(['EMAIL' => "//"]));
|
|
|
|
|
2021-05-28 15:49:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add address
|
|
|
|
* @param userdata
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
function add($userdata) {
|
|
|
|
if (!$this->writeable) {
|
|
|
|
return $this->set_error(_("Addressbook is read-only"));
|
|
|
|
}
|
|
|
|
|
|
|
|
/* See if user exist already */
|
2021-05-28 20:49:31 +00:00
|
|
|
/*
|
2021-05-28 15:49:42 +00:00
|
|
|
$ret = $this->lookup($userdata['nickname']);
|
|
|
|
if (!empty($ret)) {
|
|
|
|
return $this->set_error(sprintf(_("User '%s' already exist"),
|
|
|
|
$ret['nickname']));
|
|
|
|
}
|
2021-05-28 20:49:31 +00:00
|
|
|
*/
|
|
|
|
try {
|
|
|
|
$vcard = new VCard([
|
2021-05-29 22:09:43 +00:00
|
|
|
'FN' => $userdata['firstname'] . ' ' . $userdata['lastname'],
|
2021-05-28 20:49:31 +00:00
|
|
|
'N' => [$userdata['lastname'], $userdata['firstname'], '', '', ''],
|
|
|
|
'EMAIL' => $userdata['email'],
|
|
|
|
'ORG' => $userdata['label'],
|
|
|
|
]);
|
|
|
|
|
|
|
|
// insert address function
|
|
|
|
$this->abook->createCard($vcard);
|
|
|
|
|
|
|
|
// return true if operation is succesful.
|
|
|
|
return true;
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
// Return error message if operation fails
|
|
|
|
return $this->set_error(_("Address add operation failed: ") . $e->getMessage());
|
|
|
|
}
|
2021-05-28 15:49:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-05-29 22:23:08 +00:00
|
|
|
* Delete addresses
|
|
|
|
* @param aliases array of nicknames to delete
|
2021-05-28 15:49:42 +00:00
|
|
|
* @return boolean
|
|
|
|
*/
|
2021-05-29 22:23:08 +00:00
|
|
|
function remove($aliases) {
|
2021-05-28 15:49:42 +00:00
|
|
|
if (!$this->writeable) {
|
|
|
|
return $this->set_error(_("Addressbook is read-only"));
|
|
|
|
}
|
|
|
|
|
2021-05-29 22:23:08 +00:00
|
|
|
foreach($aliases as $alias) {
|
|
|
|
// TODO: edit this if we use different nick-naming scheme
|
|
|
|
$uri = $this->abook->getUriPath() . $alias;
|
|
|
|
$this->abook->deleteCard($uri);
|
|
|
|
}
|
2021-05-28 15:49:42 +00:00
|
|
|
|
|
|
|
// FIXME:
|
|
|
|
// return true if operation is succesful.
|
|
|
|
return true;
|
|
|
|
// Return error message if operation fails
|
|
|
|
return $this->set_error(_("Address delete operation failed"));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Modify address
|
|
|
|
* @param alias
|
|
|
|
* @param userdata
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
function modify($alias, $userdata) {
|
|
|
|
if (!$this->writeable) {
|
|
|
|
return $this->set_error(_("Addressbook is read-only"));
|
|
|
|
}
|
|
|
|
|
|
|
|
/* See if user exist */
|
2021-05-29 22:11:35 +00:00
|
|
|
// TODO: edit this if we use different nick-naming scheme
|
2021-05-29 22:18:36 +00:00
|
|
|
$uri = $this->abook->getUriPath() . $alias;
|
2021-05-29 22:11:35 +00:00
|
|
|
$one = $this->abook->getCard($uri);
|
|
|
|
/* returns Associative array with keys:
|
|
|
|
etag(string): Entity tag of the returned card
|
|
|
|
vcf(string): VCard as string
|
|
|
|
vcard(VCard): VCard as Sabre/VObject VCard
|
|
|
|
*/
|
|
|
|
$vcard = $one['vcard'];
|
|
|
|
// TODO: if no vcard
|
|
|
|
$names = $vcard->N->getParts();
|
|
|
|
// last,first,additional,prefix,suffix
|
|
|
|
$names[0]=$userdata['lastname'];
|
|
|
|
$names[1]=$userdata['firstname'];
|
|
|
|
$vcard->N = $names;
|
2021-05-30 15:03:30 +00:00
|
|
|
$vcard->FN = trim($names[3].' '.$names[1].' '.$names[2].' '.$names[0].' '.$names[4]);
|
2021-05-29 22:11:35 +00:00
|
|
|
// [prefix=3] first=1 [additional=2] last=0 [suffix=4]
|
|
|
|
$vcard->EMAIL = $userdata['email'];
|
|
|
|
$vcard->ORG = $userdata['label'];
|
|
|
|
$this->abook->updateCard($uri, $vcard, $one['etag']);
|
2021-05-28 15:49:42 +00:00
|
|
|
|
|
|
|
// FIXME:
|
|
|
|
// return true if operation is succesful.
|
|
|
|
return true;
|
|
|
|
// Return error message if operation fails
|
|
|
|
return $this->set_error(_("Address modify operation failed"));
|
|
|
|
}
|
2021-05-28 16:01:42 +00:00
|
|
|
} /* End of class abook_carddav */
|
|
|
|
?>
|