Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F1842394
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
473 KB
Referenced Files
None
Subscribers
None
View Options
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php b/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php
index 1b02733c..d7538b8a 100644
--- a/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php
+++ b/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php
@@ -1,1400 +1,1402 @@
<?php
/**
* Backend class for a custom address book
*
* This part of the Roundcube+Kolab integration and connects the
* rcube_addressbook interface with the kolab_storage wrapper from libkolab
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @see rcube_addressbook
*/
class kolab_contacts extends rcube_addressbook
{
public $primary_key = 'ID';
public $rights = 'lrs';
public $readonly = true;
public $undelete = true;
public $groups = true;
public $coltypes = array(
'name' => array('limit' => 1),
'firstname' => array('limit' => 1),
'surname' => array('limit' => 1),
'middlename' => array('limit' => 1),
'prefix' => array('limit' => 1),
'suffix' => array('limit' => 1),
'nickname' => array('limit' => 1),
'jobtitle' => array('limit' => 1),
'organization' => array('limit' => 1),
'department' => array('limit' => 1),
'email' => array('subtypes' => array('home','work','other')),
'phone' => array(),
'address' => array('subtypes' => array('home','work','office')),
'website' => array('subtypes' => array('homepage','blog')),
'im' => array('subtypes' => null),
'gender' => array('limit' => 1),
'birthday' => array('limit' => 1),
'anniversary' => array('limit' => 1),
'profession' => array(
'type' => 'text',
'size' => 40,
'maxlength' => 80,
'limit' => 1,
'label' => 'kolab_addressbook.profession',
'category' => 'personal'
),
'manager' => array('limit' => null),
'assistant' => array('limit' => null),
'spouse' => array('limit' => 1),
'children' => array(
'type' => 'text',
'size' => 40,
'maxlength' => 80,
'limit' => null,
'label' => 'kolab_addressbook.children',
'category' => 'personal'
),
'freebusyurl' => array(
'type' => 'text',
'size' => 40,
'limit' => 1,
'label' => 'kolab_addressbook.freebusyurl'
),
'pgppublickey' => array(
'type' => 'textarea',
'size' => 70,
'rows' => 10,
'limit' => 1,
'label' => 'kolab_addressbook.pgppublickey'
),
'pkcs7publickey' => array(
'type' => 'textarea',
'size' => 70,
'rows' => 10,
'limit' => 1,
'label' => 'kolab_addressbook.pkcs7publickey'
),
'notes' => array('limit' => 1),
'photo' => array('limit' => 1),
// TODO: define more Kolab-specific fields such as: language, latitude, longitude, crypto settings
);
/**
* vCard additional fields mapping
*/
public $vcard_map = array(
'profession' => 'X-PROFESSION',
'officelocation' => 'X-OFFICE-LOCATION',
'initials' => 'X-INITIALS',
'children' => 'X-CHILDREN',
'freebusyurl' => 'X-FREEBUSY-URL',
'pgppublickey' => 'KEY',
);
/**
* List of date type fields
*/
public $date_cols = array('birthday', 'anniversary');
private $gid;
private $storagefolder;
private $dataset;
private $sortindex;
private $contacts;
private $distlists;
private $groupmembers;
private $filter;
private $result;
private $namespace;
private $imap_folder = 'INBOX/Contacts';
private $action;
// list of fields used for searching in "All fields" mode
private $search_fields = array(
'name',
'firstname',
'surname',
'middlename',
'prefix',
'suffix',
'nickname',
'jobtitle',
'organization',
'department',
'email',
'phone',
'address',
'profession',
'manager',
'assistant',
'spouse',
'children',
'notes',
);
public function __construct($imap_folder = null)
{
if ($imap_folder) {
$this->imap_folder = $imap_folder;
}
// extend coltypes configuration
$format = kolab_format::factory('contact');
$this->coltypes['phone']['subtypes'] = array_keys($format->phonetypes);
$this->coltypes['address']['subtypes'] = array_keys($format->addresstypes);
$rcube = rcube::get_instance();
// set localized labels for proprietary cols
foreach ($this->coltypes as $col => $prop) {
if (!empty($prop['label']) && is_string($prop['label'])) {
$this->coltypes[$col]['label'] = $rcube->gettext($prop['label']);
}
}
// fetch objects from the given IMAP folder
$this->storagefolder = kolab_storage::get_folder($this->imap_folder);
$this->ready = $this->storagefolder && !PEAR::isError($this->storagefolder);
// Set readonly and rights flags according to folder permissions
if ($this->ready) {
if ($this->storagefolder->get_owner() == $_SESSION['username']) {
$this->readonly = false;
$this->rights = 'lrswikxtea';
}
else {
$rights = $this->storagefolder->get_myrights();
if ($rights && !PEAR::isError($rights)) {
$this->rights = $rights;
if (strpos($rights, 'i') !== false && strpos($rights, 't') !== false) {
$this->readonly = false;
}
}
}
}
$this->action = rcube::get_instance()->action;
}
/**
* Getter for the address book name to be displayed
*
* @return string Name of this address book
*/
public function get_name()
{
return $this->storagefolder->get_name();
}
/**
* Wrapper for kolab_storage_folder::get_foldername()
*/
public function get_foldername()
{
return $this->storagefolder->get_foldername();
}
/**
* Getter for the IMAP folder name
*
* @return string Name of the IMAP folder
*/
public function get_realname()
{
return $this->imap_folder;
}
/**
* Getter for the name of the namespace to which the IMAP folder belongs
*
* @return string Name of the namespace (personal, other, shared)
*/
public function get_namespace()
{
if ($this->namespace === null && $this->ready) {
$this->namespace = $this->storagefolder->get_namespace();
}
return $this->namespace;
}
/**
* Getter for parent folder path
*
* @return string Full path to parent folder
*/
public function get_parent()
{
return $this->storagefolder->get_parent();
}
/**
* Check subscription status of this folder
*
* @return boolean True if subscribed, false if not
*/
public function is_subscribed()
{
return kolab_storage::folder_is_subscribed($this->imap_folder);
}
/**
* Compose an URL for CardDAV access to this address book (if configured)
*/
public function get_carddav_url()
{
$rcmail = rcmail::get_instance();
if ($template = $rcmail->config->get('kolab_addressbook_carddav_url', null)) {
return strtr($template, array(
'%h' => $_SERVER['HTTP_HOST'],
'%u' => urlencode($rcmail->get_user_name()),
'%i' => urlencode($this->storagefolder->get_uid()),
'%n' => urlencode($this->imap_folder),
));
}
return false;
}
/**
* Setter for the current group
*/
public function set_group($gid)
{
$this->gid = $gid;
}
/**
* Save a search string for future listings
*
* @param mixed Search params to use in listing method, obtained by get_search_set()
*/
public function set_search_set($filter)
{
$this->filter = $filter;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
public function get_search_set()
{
return $this->filter;
}
/**
* Reset saved results and search parameters
*/
public function reset()
{
$this->result = null;
$this->filter = null;
}
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @param int Search mode. Sum of self::SEARCH_*
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
$this->_fetch_groups();
$groups = array();
foreach ((array)$this->distlists as $group) {
if (!$search || strstr(mb_strtolower($group['name']), mb_strtolower($search))) {
$groups[$group['ID']] = array('ID' => $group['ID'], 'name' => $group['name']);
}
}
// sort groups by name
uasort($groups, function($a, $b) { return strcoll($a['name'], $b['name']); });
return array_values($groups);
}
/**
* List the current set of contact records
*
* @param array List of cols to show
* @param int Only return this number of records, use negative values for tail
* @param bool True to skip the count query (select only)
*
* @return array Indexed list of contact records, each a hash array
*/
public function list_records($cols = null, $subset = 0, $nocount = false)
{
$this->result = new rcube_result_set(0, ($this->list_page-1) * $this->page_size);
$fetch_all = false;
$fast_mode = !empty($cols) && is_array($cols);
// list member of the selected group
if ($this->gid) {
$this->_fetch_groups();
$this->sortindex = array();
$this->contacts = array();
$local_sortindex = array();
$uids = array();
// get members with email specified
foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
// skip member that don't match the search filter
if (!empty($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false) {
continue;
}
if (!empty($member['uid'])) {
$uids[] = $member['uid'];
}
else if (!empty($member['email'])) {
$this->contacts[$member['ID']] = $member;
$local_sortindex[$member['ID']] = $this->_sort_string($member);
$fetch_all = true;
}
}
// get members by UID
if (!empty($uids)) {
$this->_fetch_contacts($query = array(array('uid', '=', $uids)), $fetch_all ? false : count($uids), $fast_mode);
$this->sortindex = array_merge($this->sortindex, $local_sortindex);
}
}
- else if (is_array($this->filter['ids'])) {
+ else if ($this->filter && is_array($this->filter['ids'])) {
$ids = $this->filter['ids'];
if (count($ids)) {
$uids = array_map(array($this, 'id2uid'), $this->filter['ids']);
$this->_fetch_contacts($query = array(array('uid', '=', $uids)), count($ids), $fast_mode);
}
}
else {
$this->_fetch_contacts($query = 'contact', true, $fast_mode);
}
if ($fetch_all) {
// sort results (index only)
asort($this->sortindex, SORT_LOCALE_STRING);
$ids = array_keys($this->sortindex);
// fill contact data into the current result set
$this->result->count = count($ids);
$start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
$last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, $this->result->count);
for ($i = $start_row; $i < $last_row; $i++) {
if (array_key_exists($i, $ids)) {
$idx = $ids[$i];
$this->result->add($this->contacts[$idx] ?: $this->_to_rcube_contact($this->dataset[$idx]));
}
}
}
else if (!empty($this->dataset)) {
// get all records count, skip the query if possible
if (!isset($query) || count($this->dataset) < $this->page_size) {
$this->result->count = count($this->dataset) + $this->page_size * ($this->list_page - 1);
}
else {
$this->result->count = $this->storagefolder->count($query);
}
$start_row = $subset < 0 ? $this->page_size + $subset : 0;
$last_row = min($subset != 0 ? $start_row + abs($subset) : $this->page_size, $this->result->count);
for ($i = $start_row; $i < $last_row; $i++) {
$this->result->add($this->_to_rcube_contact($this->dataset[$i]));
}
}
return $this->result;
}
/**
* Search records
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* 4 - include groups (if supported)
* @param bool $select True if results are requested, False if count only
* @param bool $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return rcube_result_set List of contact records and 'count' value
*/
public function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
{
// search by ID
if ($fields == $this->primary_key) {
$ids = !is_array($value) ? explode(',', $value) : $value;
$result = new rcube_result_set();
foreach ($ids as $id) {
if ($rec = $this->get_record($id, true)) {
$result->add($rec);
$result->count++;
}
}
return $result;
}
else if ($fields == '*') {
$fields = $this->search_fields;
}
if (!is_array($fields)) {
$fields = array($fields);
}
if (!is_array($required) && !empty($required)) {
$required = array($required);
}
// advanced search
if (is_array($value)) {
$advanced = true;
$value = array_map('mb_strtolower', $value);
}
else {
$value = mb_strtolower($value);
}
$scount = count($fields);
// build key name regexp
$regexp = '/^(' . implode('|', $fields) . ')(?:.*)$/';
// pass query to storage if only indexed cols are involved
// NOTE: this is only some rough pre-filtering but probably includes false positives
$squery = $this->_search_query($fields, $value, $mode);
// add magic selector to select contacts with birthday dates only
if (in_array('birthday', $required)) {
$squery[] = array('tags', '=', 'x-has-birthday');
}
$squery[] = array('type', '=', 'contact');
// get all/matching records
$this->_fetch_contacts($squery);
// save searching conditions
$this->filter = array('fields' => $fields, 'value' => $value, 'mode' => $mode, 'ids' => array());
// search by iterating over all records in dataset
foreach ($this->dataset as $record) {
$contact = $this->_to_rcube_contact($record);
$id = $contact['ID'];
// check if current contact has required values, otherwise skip it
if ($required) {
foreach ($required as $f) {
// required field might be 'email', but contact might contain 'email:home'
if (!($v = rcube_addressbook::get_col_values($f, $contact, true)) || empty($v)) {
continue 2;
}
}
}
$found = array();
$contents = '';
foreach (preg_grep($regexp, array_keys($contact)) as $col) {
$pos = strpos($col, ':');
$colname = $pos ? substr($col, 0, $pos) : $col;
foreach ((array)$contact[$col] as $val) {
if ($advanced) {
$found[$colname] = $this->compare_search_value($colname, $val, $value[array_search($colname, $fields)], $mode);
}
else {
$contents .= ' ' . join(' ', (array)$val);
}
}
}
// compare matches
if (($advanced && count($found) >= $scount) ||
(!$advanced && rcube_utils::words_match(mb_strtolower($contents), $value))) {
$this->filter['ids'][] = $id;
}
}
// dummy result with contacts count
if (!$select) {
return new rcube_result_set(count($this->filter['ids']), ($this->list_page-1) * $this->page_size);
}
// list records (now limited by $this->filter)
return $this->list_records();
}
/**
* Refresh saved search results after data has changed
*/
public function refresh_search()
{
if ($this->filter) {
$this->search($this->filter['fields'], $this->filter['value'], $this->filter['mode']);
}
return $this->get_search_set();
}
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result set with values for 'count' and 'first'
*/
public function count()
{
if ($this->gid) {
$this->_fetch_groups();
$count = count($this->distlists[$this->gid]['member']);
}
else if (is_array($this->filter['ids'])) {
$count = count($this->filter['ids']);
}
else {
$count = $this->storagefolder->count('contact');
}
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Return the last result set
*
* @return rcube_result_set Current result set or NULL if nothing selected yet
*/
public function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed Record identifier(s)
* @param bool True to return record as associative array, otherwise a result set is returned
*
* @return mixed Result object with all record fields or False if not found
*/
public function get_record($id, $assoc = false)
{
$rec = null;
$uid = $this->id2uid($id);
$rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
if (strpos($uid, 'mailto:') === 0) {
$this->_fetch_groups(true);
$rec = $this->contacts[$id];
$this->readonly = true; // set source to read-only
}
else if (!empty($rev)) {
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->get_plugin('kolab_addressbook');
if ($plugin && ($object = $plugin->get_revision($id, kolab_storage::id_encode($this->imap_folder), $rev))) {
$rec = $this->_to_rcube_contact($object);
$rec['rev'] = $rev;
}
$this->readonly = true; // set source to read-only
}
else if ($object = $this->storagefolder->get_object($uid)) {
$rec = $this->_to_rcube_contact($object);
}
if ($rec) {
$this->result = new rcube_result_set(1);
$this->result->add($rec);
return $assoc ? $rec : $this->result;
}
return false;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
*
* @return array List of assigned groups as ID=>Name pairs
*/
public function get_record_groups($id)
{
$out = array();
$this->_fetch_groups();
if (!empty($this->groupmembers[$id])) {
foreach ((array) $this->groupmembers[$id] as $gid) {
if (!empty($this->distlists[$gid])) {
$group = $this->distlists[$gid];
$out[$gid] = $group['name'];
}
}
}
return $out;
}
/**
* Create a new contact record
*
* @param array Associative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @param bool True to check for duplicates first
*
* @return mixed The created record ID on success, False on error
*/
public function insert($save_data, $check=false)
{
if (!is_array($save_data)) {
return false;
}
$insert_id = $existing = false;
// check for existing records by e-mail comparison
if ($check) {
foreach ($this->get_col_values('email', $save_data, true) as $email) {
if (($res = $this->search('email', $email, true, false)) && $res->count) {
$existing = true;
break;
}
}
}
if (!$existing) {
// remove existing id attributes (#1101)
unset($save_data['ID'], $save_data['uid']);
// generate new Kolab contact item
$object = $this->_from_rcube_contact($save_data);
$saved = $this->storagefolder->save($object, 'contact');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server"),
true, false);
}
else {
$insert_id = $this->uid2id($object['uid']);
}
}
return $insert_id;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Associative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
*
* @return bool True on success, False on error
*/
public function update($id, $save_data)
{
$updated = false;
if ($old = $this->storagefolder->get_object($this->id2uid($id))) {
$object = $this->_from_rcube_contact($save_data, $old);
if (!$this->storagefolder->save($object, 'contact', $old['uid'])) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server"
),
true, false
);
}
else {
$updated = true;
// TODO: update data in groups this contact is member of
}
}
return $updated;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @param bool Remove record(s) irreversible (mark as deleted otherwise)
*
* @return int Number of records deleted
*/
public function delete($ids, $force=true)
{
$this->_fetch_groups();
if (!is_array($ids)) {
$ids = explode(',', $ids);
}
$count = 0;
foreach ($ids as $id) {
if ($uid = $this->id2uid($id)) {
$is_mailto = strpos($uid, 'mailto:') === 0;
$deleted = $is_mailto || $this->storagefolder->delete($uid, $force);
if (!$deleted) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting a contact object $uid from the Kolab server"
),
true, false
);
}
else {
// remove from distribution lists
foreach ((array) $this->groupmembers[$id] as $gid) {
if (!$is_mailto || $gid == $this->gid) {
$this->remove_from_group($gid, $id);
}
}
// clear internal cache
unset($this->groupmembers[$id]);
$count++;
}
}
}
return $count;
}
/**
* Undelete one or more contact records.
* Only possible just after delete (see 2nd argument of delete() method).
*
* @param array Record identifiers
*
* @return int Number of records restored
*/
public function undelete($ids)
{
if (!is_array($ids)) {
$ids = explode(',', $ids);
}
$count = 0;
foreach ($ids as $id) {
$uid = $this->id2uid($id);
if ($this->storagefolder->undelete($uid)) {
$count++;
}
else {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error undeleting a contact object $uid from the Kolab server"
),
true, false
);
}
}
return $count;
}
/**
* Remove all records from the database
*
* @param bool $with_groups Remove also groups
*/
public function delete_all($with_groups = false)
{
if ($this->storagefolder->delete_all()) {
$this->contacts = array();
$this->sortindex = array();
$this->dataset = null;
$this->result = null;
}
}
/**
* Close connection to source
* Called on script shutdown
*/
public function close()
{
}
/**
* Create a contact group with the given name
*
* @param string The group name
*
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
$this->_fetch_groups();
$result = false;
$list = array(
'name' => $name,
'member' => array(),
);
$saved = $this->storagefolder->save($list, 'distribution-list');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server"
),
true, false
);
return false;
}
else {
$id = $this->uid2id($list['uid']);
$this->distlists[$id] = $list;
$result = array('id' => $id, 'name' => $name);
}
return $result;
}
/**
* Delete the given group and all linked group members
*
* @param string Group identifier
*
* @return bool True on success, false if no data was changed
*/
function delete_group($gid)
{
$this->_fetch_groups();
$result = false;
if ($list = $this->distlists[$gid]) {
$deleted = $this->storagefolder->delete($list['uid']);
}
if (!$deleted) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting distribution-list object from the Kolab server"
),
true, false
);
}
else {
$result = true;
}
return $result;
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @param string New group identifier (if changed, otherwise don't set)
*
* @return bool New name on success, false if no data was changed
*/
function rename_group($gid, $newname, &$newid)
{
$this->_fetch_groups();
$list = $this->distlists[$gid];
if ($newname != $list['name']) {
$list['name'] = $newname;
$saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
}
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server"
),
true, false
);
return false;
}
return $newname;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be added
* @return int Number of contacts added
*/
function add_to_group($gid, $ids)
{
if (!is_array($ids)) {
$ids = explode(',', $ids);
}
$this->_fetch_groups(true);
$list = $this->distlists[$gid];
$added = 0;
$uids = array();
$exists = array();
foreach ((array)$list['member'] as $member) {
$exists[] = $member['ID'];
}
// substract existing assignments from list
$ids = array_unique(array_diff($ids, $exists));
// add mailto: members
foreach ($ids as $contact_id) {
$uid = $this->id2uid($contact_id);
if (strpos($uid, 'mailto:') === 0 && ($contact = $this->contacts[$contact_id])) {
$list['member'][] = array(
'email' => $contact['email'],
'name' => $contact['name'],
);
$this->groupmembers[$contact_id][] = $gid;
$added++;
}
else {
$uids[$uid] = $contact_id;
}
}
// add members with UID
if (!empty($uids)) {
foreach ($uids as $uid => $contact_id) {
$list['member'][] = array('uid' => $uid);
$this->groupmembers[$contact_id][] = $gid;
$added++;
}
}
if ($added) {
$saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
}
else {
$saved = true;
}
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list to Kolab server"
),
true, false
);
$added = false;
$this->set_error(self::ERROR_SAVING, 'errorsaving');
}
else {
$this->distlists[$gid] = $list;
}
return $added;
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be removed
* @return int Number of deleted group members
*/
function remove_from_group($gid, $ids)
{
if (!is_array($ids)) {
$ids = explode(',', $ids);
}
$this->_fetch_groups();
if (!($list = $this->distlists[$gid])) {
return false;
}
$new_member = array();
foreach ((array)$list['member'] as $member) {
if (!in_array($member['ID'], $ids)) {
$new_member[] = $member;
}
}
// write distribution list back to server
$list['member'] = $new_member;
$saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server"
),
true, false
);
}
else {
// remove group assigments in local cache
foreach ($ids as $id) {
$j = array_search($gid, $this->groupmembers[$id]);
unset($this->groupmembers[$id][$j]);
}
$this->distlists[$gid] = $list;
return true;
}
return false;
}
/**
* Check the given data before saving.
* If input not valid, the message to display can be fetched using get_error()
*
* @param array Associative array with contact data to save
* @param bool Attempt to fix/complete data automatically
*
* @return bool True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
// validate e-mail addresses
$valid = parent::validate($save_data);
// require at least one e-mail address if there's no name
// (syntax check is already done)
if ($valid) {
if (!strlen($save_data['name'])
&& !strlen($save_data['organization'])
&& !array_filter($this->get_col_values('email', $save_data, true))
) {
$this->set_error('warning', 'kolab_addressbook.noemailnamewarning');
$valid = false;
}
}
return $valid;
}
/**
* Query storage layer and store records in private member var
*/
private function _fetch_contacts($query = array(), $limit = false, $fast_mode = false)
{
if (!isset($this->dataset) || !empty($query)) {
if ($limit) {
$size = is_int($limit) && $limit < $this->page_size ? $limit : $this->page_size;
$this->storagefolder->set_order_and_limit($this->_sort_columns(), $size, ($this->list_page-1) * $this->page_size);
}
$this->sortindex = array();
$this->dataset = $this->storagefolder->select($query, $fast_mode);
foreach ($this->dataset as $idx => $record) {
$contact = $this->_to_rcube_contact($record);
$this->sortindex[$idx] = $this->_sort_string($contact);
}
}
}
/**
* Extract a string for sorting from the given contact record
*/
private function _sort_string($rec)
{
$str = '';
switch ($this->sort_col) {
case 'name':
- $str = $rec['name'] . $rec['prefix'];
+ $str = ($rec['name'] ?? null) . ($rec['prefix'] ?? null);
case 'firstname':
- $str .= $rec['firstname'] . $rec['middlename'] . $rec['surname'];
+ $str .= ($rec['firstname'] ?? null) . ($rec['middlename'] ?? null) . ($rec['surname'] ?? null);
break;
case 'surname':
- $str = $rec['surname'] . $rec['firstname'] . $rec['middlename'];
+ $str = ($rec['surname'] ?? null) . ($rec['firstname'] ?? null) . ($rec['middlename'] ?? null);
break;
default:
$str = $rec[$this->sort_col];
break;
}
- $str .= is_array($rec['email']) ? $rec['email'][0] : $rec['email'];
+ if ($rec['email'] ?? null) {
+ $str .= is_array($rec['email']) ? $rec['email'][0] : $rec['email'];
+ }
return mb_strtolower($str);
}
/**
* Return the cache table columns to order by
*/
private function _sort_columns()
{
$sortcols = array();
switch ($this->sort_col) {
case 'name':
$sortcols[] = 'name';
case 'firstname':
$sortcols[] = 'firstname';
break;
case 'surname':
$sortcols[] = 'surname';
break;
}
$sortcols[] = 'email';
return $sortcols;
}
/**
* Read distribution-lists AKA groups from server
*/
private function _fetch_groups($with_contacts = false)
{
if (!isset($this->distlists)) {
$this->distlists = $this->groupmembers = array();
foreach ($this->storagefolder->select('distribution-list', true) as $record) {
$record['ID'] = $this->uid2id($record['uid']);
foreach ((array)$record['member'] as $i => $member) {
$mid = $this->uid2id($member['uid'] ? $member['uid'] : 'mailto:' . $member['email']);
$record['member'][$i]['ID'] = $mid;
$record['member'][$i]['readonly'] = empty($member['uid']);
$this->groupmembers[$mid][] = $record['ID'];
if ($with_contacts && empty($member['uid'])) {
$this->contacts[$mid] = $record['member'][$i];
}
}
$this->distlists[$record['ID']] = $record;
}
}
}
/**
* Encode object UID into a safe identifier
*/
public function uid2id($uid)
{
return rtrim(strtr(base64_encode($uid), '+/', '-_'), '=');
}
/**
* Convert Roundcube object identifier back into the original UID
*/
public function id2uid($id)
{
return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
}
/**
* Build SQL query for fulltext matches
*/
private function _search_query($fields, $value, $mode)
{
$query = array();
$cols = array();
// $fulltext_cols might contain composite field names e.g. 'email:address' while $fields not
foreach (kolab_format_contact::$fulltext_cols as $col) {
if ($pos = strpos($col, ':')) {
$col = substr($col, 0, $pos);
}
if (in_array($col, $fields)) {
$cols[] = $col;
}
}
if (count($cols) == count($fields)) {
if ($mode & rcube_addressbook::SEARCH_STRICT) {
$prefix = '^'; $suffix = '$';
}
else if ($mode & rcube_addressbook::SEARCH_PREFIX) {
$prefix = '^'; $suffix = '';
}
else {
$prefix = ''; $suffix = '';
}
$search_string = is_array($value) ? join(' ', $value) : $value;
foreach (rcube_utils::normalize_string($search_string, true) as $word) {
$query[] = array('words', 'LIKE', $prefix . $word . $suffix);
}
}
return $query;
}
/**
* Map fields from internal Kolab_Format to Roundcube contact format
*/
private function _to_rcube_contact($record)
{
$record['ID'] = $this->uid2id($record['uid']);
// convert email, website, phone values
foreach (array('email'=>'address', 'website'=>'url', 'phone'=>'number') as $col => $propname) {
- if (is_array($record[$col])) {
+ if (is_array($record[$col] ?? null)) {
$values = $record[$col];
unset($record[$col]);
foreach ((array)$values as $i => $val) {
$key = $col . ($val['type'] ? ':' . $val['type'] : '');
$record[$key][] = $val[$propname];
}
}
}
- if (is_array($record['address'])) {
+ if (is_array($record['address'] ?? null)) {
$addresses = $record['address'];
unset($record['address']);
foreach ($addresses as $i => $adr) {
$key = 'address' . ($adr['type'] ? ':' . $adr['type'] : '');
$record[$key][] = array(
'street' => $adr['street'],
'locality' => $adr['locality'],
'zipcode' => $adr['code'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
}
// photo is stored as separate attachment
- if ($record['photo'] && strlen($record['photo']) < 255 && !empty($record['_attachments'][$record['photo']])) {
+ if (($record['photo'] ?? null) && strlen($record['photo']) < 255 && !empty($record['_attachments'][$record['photo']])) {
$att = $record['_attachments'][$record['photo']];
// only fetch photo content if requested
if ($this->action == 'photo') {
if (!empty($att['content'])) {
$record['photo'] = $att['content'];
}
else {
$record['photo'] = $this->storagefolder->get_attachment($record['uid'], $att['id']);
}
}
}
// truncate publickey value for display
if (!empty($record['pgppublickey']) && $this->action == 'show') {
$record['pgppublickey'] = substr($record['pgppublickey'], 0, 140) . '...';
}
// remove empty fields
$record = array_filter($record);
// remove kolab_storage internal data
unset($record['_msguid'], $record['_formatobj'], $record['_mailbox'], $record['_type'], $record['_size']);
return $record;
}
/**
* Map fields from Roundcube format to internal kolab_format_contact properties
*/
private function _from_rcube_contact($contact, $old = array())
{
if (empty($contact['uid']) && !empty($contact['ID'])) {
$contact['uid'] = $this->id2uid($contact['ID']);
}
else if (empty($contact['uid']) && !empty($old['uid'])) {
$contact['uid'] = $old['uid'];
}
$contact['im'] = array_filter($this->get_col_values('im', $contact, true));
// convert email, website, phone values
foreach (array('email'=>'address', 'website'=>'url', 'phone'=>'number') as $col => $propname) {
$col_values = $this->get_col_values($col, $contact);
$contact[$col] = array();
foreach ($col_values as $type => $values) {
foreach ((array)$values as $val) {
if (!empty($val)) {
$contact[$col][] = array($propname => $val, 'type' => $type);
}
}
unset($contact[$col.':'.$type]);
}
}
$addresses = array();
foreach ($this->get_col_values('address', $contact) as $type => $values) {
foreach ((array)$values as $adr) {
// skip empty address
$adr = array_filter($adr);
if (empty($adr)) {
continue;
}
$addresses[] = array(
'type' => $type,
'street' => $adr['street'],
'locality' => $adr['locality'],
'code' => $adr['zipcode'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
unset($contact['address:'.$type]);
}
$contact['address'] = $addresses;
// categories are not supported in the web client but should be preserved (#2608)
$contact['categories'] = $old['categories'] ?? null;
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($contact[$key]) && $key[0] == '_') {
$contact[$key] = $val;
}
}
// convert one-item-array elements into string element
// this is needed e.g. to properly import birthday field
foreach ($this->coltypes as $type => $col_def) {
if (($col_def['limit'] ?? 0) == 1 && !empty($contact[$type]) && is_array($contact[$type])) {
$contact[$type] = array_shift(array_filter($contact[$type]));
}
}
// When importing contacts 'vcard' data is added, we don't need it (Bug #1711)
unset($contact['vcard']);
// add empty values for some fields which can be removed in the UI
return array_filter($contact) + array(
'nickname' => '',
'birthday' => '',
'anniversary' => '',
'freebusyurl' => '',
'photo' => $contact['photo']
);
}
}
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index 6e34b973..d5d7bac6 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -1,1142 +1,1142 @@
<?php
/**
* Kolab address book
*
* Sample plugin to add a new address book source with data from Kolab storage
* It provides also a possibilities to manage contact folders
* (create/rename/delete/acl) directly in Addressbook UI.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_addressbook extends rcube_plugin
{
public $task = '?(?!logout).*';
public $driver;
public $bonnie_api = false;
private $sources;
private $rc;
private $ui;
private $recurrent = false;
const GLOBAL_FIRST = 0;
const PERSONAL_FIRST = 1;
const GLOBAL_ONLY = 2;
const PERSONAL_ONLY = 3;
/**
* Startup method of a Roundcube plugin
*/
public function init()
{
$this->rc = rcube::get_instance();
// load required plugin
$this->require_plugin('libkolab');
$this->load_config();
$driver = $this->rc->config->get('kolab_addressbook_driver') ?: 'kolab';
$driver_class = "{$driver}_contacts_driver";
require_once dirname(__FILE__) . "/drivers/{$driver}/{$driver}_contacts_driver.php";
require_once dirname(__FILE__) . "/drivers/{$driver}/{$driver}_contacts.php";
$this->driver = new $driver_class($this);
// register hooks
$this->add_hook('addressbooks_list', array($this, 'address_sources'));
$this->add_hook('addressbook_get', array($this, 'get_address_book'));
$this->add_hook('config_get', array($this, 'config_get'));
if ($this->rc->task == 'addressbook') {
$this->add_texts('localization');
if ($this->driver instanceof kolab_contacts_driver) {
$this->add_hook('contact_form', array($this, 'contact_form'));
$this->add_hook('contact_photo', array($this, 'contact_photo'));
}
$this->add_hook('template_object_directorylist', array($this, 'directorylist_html'));
// Plugin actions
$this->register_action('plugin.book', array($this, 'book_actions'));
$this->register_action('plugin.book-save', array($this, 'book_save'));
$this->register_action('plugin.book-search', array($this, 'book_search'));
$this->register_action('plugin.book-subscribe', array($this, 'book_subscribe'));
$this->register_action('plugin.contact-changelog', array($this, 'contact_changelog'));
$this->register_action('plugin.contact-diff', array($this, 'contact_diff'));
$this->register_action('plugin.contact-restore', array($this, 'contact_restore'));
// get configuration for the Bonnie API
$this->bonnie_api = libkolab::get_bonnie_api();
// Load UI elements
if ($this->api->output->type == 'html') {
require_once $this->home . '/lib/kolab_addressbook_ui.php';
$this->ui = new kolab_addressbook_ui($this);
if ($this->bonnie_api) {
$this->add_button(array(
'command' => 'contact-history-dialog',
'class' => 'history contact-history disabled',
'classact' => 'history contact-history active',
'innerclass' => 'icon inner',
'label' => 'kolab_addressbook.showhistory',
'type' => 'link-menuitem'
), 'contactmenu');
}
}
}
else if ($this->rc->task == 'settings') {
$this->add_texts('localization');
$this->add_hook('preferences_list', array($this, 'prefs_list'));
$this->add_hook('preferences_save', array($this, 'prefs_save'));
}
if ($this->driver instanceof kolab_contacts_driver) {
$this->add_hook('folder_delete', array($this, 'prefs_folder_delete'));
$this->add_hook('folder_rename', array($this, 'prefs_folder_rename'));
$this->add_hook('folder_update', array($this, 'prefs_folder_update'));
}
}
/**
* Handler for the addressbooks_list hook.
*
* This will add all instances of available Kolab-based address books
* to the list of address sources of Roundcube.
* This will also hide some addressbooks according to kolab_addressbook_prio setting.
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function address_sources($p)
{
$abook_prio = $this->addressbook_prio();
// Disable all global address books
// Assumes that all non-kolab_addressbook sources are global
if ($abook_prio == self::PERSONAL_ONLY) {
$p['sources'] = array();
}
$sources = array();
foreach ($this->_list_sources() as $abook_id => $abook) {
// register this address source
$sources[$abook_id] = $this->driver->abook_prop($abook_id, $abook);
// flag folders with 'i' right as writeable
if ($this->rc->action == 'add' && strpos($abook->rights, 'i') !== false) {
$sources[$abook_id]['readonly'] = false;
}
}
// Add personal address sources to the list
if ($abook_prio == self::PERSONAL_FIRST) {
// $p['sources'] = array_merge($sources, $p['sources']);
// Don't use array_merge(), because if you have folders name
// that resolve to numeric identifier it will break output array keys
foreach ($p['sources'] as $idx => $value) {
$sources[$idx] = $value;
}
$p['sources'] = $sources;
}
else {
// $p['sources'] = array_merge($p['sources'], $sources);
foreach ($sources as $idx => $value) {
$p['sources'][$idx] = $value;
}
}
return $p;
}
/**
*
*/
public function directorylist_html($args)
{
$out = '';
$spec = '';
$kolab = '';
$jsdata = [];
$sources = (array) $this->rc->get_address_sources();
// list all non-kolab sources first (also exclude hidden sources), special folders will go last
foreach ($sources as $j => $source) {
$id = strval(strlen($source['id']) ? $source['id'] : $j);
if (!empty($source['kolab']) || !empty($source['hidden'])) {
continue;
}
// Roundcube >= 1.5, Collected Recipients and Trusted Senders sources will be listed at the end
if ((defined('rcube_addressbook::TYPE_RECIPIENT') && $source['id'] == (string) rcube_addressbook::TYPE_RECIPIENT)
|| (defined('rcube_addressbook::TYPE_TRUSTED_SENDER') && $source['id'] == (string) rcube_addressbook::TYPE_TRUSTED_SENDER)
) {
$spec .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
}
else {
$out .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
}
}
// render a hierarchical list of kolab contact folders
// TODO: Move this to the drivers
if ($this->driver instanceof kolab_contacts_driver) {
$folders = kolab_storage::sort_folders(kolab_storage::get_folders('contact'));
kolab_storage::folder_hierarchy($folders, $tree);
if ($tree && !empty($tree->children)) {
$kolab .= $this->folder_tree_html($tree, $sources, $jsdata);
}
}
else {
$filter = function($source) { return !empty($source['kolab']) && empty($source['hidden']); };
foreach (array_filter($sources, $filter) as $j => $source) {
$id = strval(strlen($source['id']) ? $source['id'] : $j);
$kolab .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
}
}
$out .= $kolab . $spec;
$this->rc->output->set_env('contactgroups', array_filter($jsdata, function($src){ return isset($src['type']) && $src['type'] == 'group'; }));
$this->rc->output->set_env('address_sources', array_filter($jsdata, function($src){ return !isset($src['type']) || $src['type'] != 'group'; }));
$args['content'] = html::tag('ul', $args, $out, html::$common_attrib);
return $args;
}
/**
* Return html for a structured list <ul> for the folder tree
*/
protected function folder_tree_html($node, $data, &$jsdata)
{
$out = '';
foreach ($node->children as $folder) {
$id = $folder->id;
$source = $data[$id];
$is_collapsed = strpos($this->rc->config->get('collapsed_abooks',''), '&'.rawurlencode($id).'&') !== false;
- if ($folder->virtual) {
+ if (!empty($folder->virtual)) {
$source = $this->driver->abook_prop($folder->id, $folder);
}
else if (empty($source)) {
$this->sources[$id] = new kolab_contacts($folder->name);
$source = $this->driver->abook_prop($id, $this->sources[$id]);
}
$content = $this->addressbook_list_item($id, $source, $jsdata);
if (!empty($folder->children)) {
$child_html = $this->folder_tree_html($folder, $data, $jsdata);
// copy group items...
if (preg_match('!<ul[^>]*>(.*)</ul>\n*$!Ums', $content, $m)) {
$child_html = $m[1] . $child_html;
$content = substr($content, 0, -strlen($m[0]) - 1);
}
// ... and re-create the subtree
if (!empty($child_html)) {
$content .= html::tag('ul', array('class' => 'groups', 'style' => ($is_collapsed ? "display:none;" : null)), $child_html);
}
}
$out .= $content . '</li>';
}
return $out;
}
/**
*
*/
protected function addressbook_list_item($id, $source, &$jsdata, $search_mode = false)
{
$current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
if (empty($source['virtual'])) {
$jsdata[$id] = $source;
$jsdata[$id]['name'] = html_entity_decode($source['name'], ENT_NOQUOTES, RCUBE_CHARSET);
}
// set class name(s)
$classes = array('addressbook');
if (!empty($source['group']))
$classes[] = $source['group'];
if ($current === $id)
$classes[] = 'selected';
if (!empty($source['readonly']))
$classes[] = 'readonly';
if (!empty($source['virtual']))
$classes[] = 'virtual';
if (!empty($source['class_name']))
$classes[] = $source['class_name'];
$name = !empty($source['listname']) ? $source['listname'] : (!empty($source['name']) ? $source['name'] : $id);
$label_id = 'kabt:' . $id;
$inner = (!empty($source['virtual']) ?
html::a(array('tabindex' => '0'), $name) :
html::a(array(
'href' => $this->rc->url(array('_source' => $id)),
'rel' => $source['id'],
'id' => $label_id,
'onclick' => "return " . rcmail_output::JS_OBJECT_NAME.".command('list','" . rcube::JQ($id) . "',this)",
), $name)
);
if ($this->driver instanceof kolab_contacts_driver && isset($source['subscribed'])) {
$inner .= html::span(array(
'class' => 'subscribed',
'title' => $this->gettext('foldersubscribe'),
'role' => 'checkbox',
'aria-checked' => $source['subscribed'] ? 'true' : 'false',
), '');
}
// don't wrap in <li> but add a checkbox for search results listing
if ($search_mode) {
$jsdata[$id]['group'] = join(' ', $classes);
if (!$source['virtual']) {
$inner .= html::tag('input', array(
'type' => 'checkbox',
'name' => '_source[]',
'value' => $id,
'checked' => false,
'aria-labelledby' => $label_id,
));
}
return html::div(null, $inner);
}
$out = html::tag('li', array(
'id' => 'rcmli' . rcube_utils::html_identifier($id, true),
'class' => join(' ', $classes),
'noclose' => true,
),
html::div(!empty($source['subscribed']) ? 'subscribed' : null, $inner)
);
$groupdata = array('out' => '', 'jsdata' => $jsdata, 'source' => $id);
if ($source['groups']) {
if (function_exists('rcmail_contact_groups')) {
$groupdata = rcmail_contact_groups($groupdata);
}
else {
// Roundcube >= 1.5
$groupdata = rcmail_action_contacts_index::contact_groups($groupdata);
}
}
$jsdata = $groupdata['jsdata'];
$out .= $groupdata['out'];
return $out;
}
/**
* Sets autocomplete_addressbooks option according to
* kolab_addressbook_prio setting extending list of address sources
* to be used for autocompletion.
*/
public function config_get($args)
{
if ($args['name'] != 'autocomplete_addressbooks' || $this->recurrent) {
return $args;
}
$abook_prio = $this->addressbook_prio();
// Get the original setting, use temp flag to prevent from an infinite recursion
$this->recurrent = true;
$sources = $this->rc->config->get('autocomplete_addressbooks');
$this->recurrent = false;
// Disable all global address books
// Assumes that all non-kolab_addressbook sources are global
if ($abook_prio == self::PERSONAL_ONLY) {
$sources = array();
}
if (!is_array($sources)) {
$sources = array();
}
$kolab_sources = array();
foreach (array_keys($this->_list_sources()) as $abook_id) {
if (!in_array($abook_id, $sources))
$kolab_sources[] = $abook_id;
}
// Add personal address sources to the list
if (!empty($kolab_sources)) {
if ($abook_prio == self::PERSONAL_FIRST) {
$sources = array_merge($kolab_sources, $sources);
}
else {
$sources = array_merge($sources, $kolab_sources);
}
}
$args['result'] = $sources;
return $args;
}
/**
* Getter for the rcube_addressbook instance
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function get_address_book($p)
{
if ($p['id']) {
if ($source = $this->driver->get_address_book($p['id'])) {
$p['instance'] = $source;
// flag source as writeable if 'i' right is given
if ($p['writeable'] && $this->rc->action == 'save' && strpos($p['instance']->rights, 'i') !== false) {
$p['instance']->readonly = false;
}
else if ($this->rc->action == 'delete' && strpos($p['instance']->rights, 't') !== false) {
$p['instance']->readonly = false;
}
}
}
return $p;
}
/**
* List addressbook sources list
*/
private function _list_sources()
{
// already read sources
if (isset($this->sources)) {
return $this->sources;
}
$this->sources = [];
$abook_prio = $this->addressbook_prio();
// Personal address source(s) disabled?
if ($abook_prio == kolab_addressbook::GLOBAL_ONLY) {
return $this->sources;
}
$folders = $this->driver->list_folders();
// get all folders that have "contact" type
foreach ($folders as $id => $source) {
$this->sources[$id] = $source;
}
return $this->sources;
}
/**
* Plugin hook called before rendering the contact form or detail view
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function contact_form($p)
{
// none of our business
if (empty($GLOBALS['CONTACTS']) || !($GLOBALS['CONTACTS'] instanceof kolab_contacts)) {
return $p;
}
// extend the list of contact fields to be displayed in the 'personal' section
if (is_array($p['form']['personal'])) {
$p['form']['personal']['content']['profession'] = array('size' => 40);
$p['form']['personal']['content']['children'] = array('size' => 40);
$p['form']['personal']['content']['freebusyurl'] = array('size' => 40);
$p['form']['personal']['content']['pgppublickey'] = array('size' => 70);
$p['form']['personal']['content']['pkcs7publickey'] = array('size' => 70);
// re-order fields according to the coltypes list
$p['form']['contact']['content'] = $this->_sort_form_fields($p['form']['contact']['content'], $GLOBALS['CONTACTS']);
$p['form']['personal']['content'] = $this->_sort_form_fields($p['form']['personal']['content'], $GLOBALS['CONTACTS']);
/* define a separate section 'settings'
$p['form']['settings'] = array(
'name' => $this->gettext('settings'),
'content' => array(
'freebusyurl' => array('size' => 40, 'visible' => true),
'pgppublickey' => array('size' => 70, 'visible' => true),
'pkcs7publickey' => array('size' => 70, 'visible' => false),
)
);
*/
}
if ($this->bonnie_api && $this->rc->action == 'show' && empty($p['record']['rev'])) {
$this->rc->output->set_env('kolab_audit_trail', true);
}
return $p;
}
/**
* Plugin hook for the contact photo image
*/
public function contact_photo($p)
{
// add photo data from old revision inline as data url
if (!empty($p['record']['rev']) && !empty($p['data'])) {
$p['url'] = 'data:image/gif;base64,' . base64_encode($p['data']);
}
return $p;
}
/**
* Handler for contact audit trail changelog requests
*/
public function contact_changelog()
{
if (empty($this->bonnie_api)) {
return false;
}
$contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source);
$result = $uid && $mailbox ? $this->bonnie_api->changelog('contact', $uid, $mailbox, $msguid) : null;
if (is_array($result) && $result['uid'] == $uid) {
if (is_array($result['changes'])) {
$rcmail = $this->rc;
$dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
array_walk($result['changes'], function(&$change) use ($rcmail, $dtformat) {
if ($change['date']) {
$dt = rcube_utils::anytodatetime($change['date']);
if ($dt instanceof DateTime) {
$change['date'] = $rcmail->format_date($dt, $dtformat);
}
}
});
}
$this->rc->output->command('contact_render_changelog', $result['changes']);
}
else {
$this->rc->output->command('contact_render_changelog', false);
}
$this->rc->output->send();
}
/**
* Handler for audit trail diff view requests
*/
public function contact_diff()
{
if (empty($this->bonnie_api)) {
return false;
}
$contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
$rev1 = rcube_utils::get_input_value('rev1', rcube_utils::INPUT_POST);
$rev2 = rcube_utils::get_input_value('rev2', rcube_utils::INPUT_POST);
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source);
$result = $this->bonnie_api->diff('contact', $uid, $rev1, $rev2, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid) {
$result['rev1'] = $rev1;
$result['rev2'] = $rev2;
$result['cid'] = $contact;
// convert some properties, similar to kolab_contacts::_to_rcube_contact()
$keymap = array(
'lastmodified-date' => 'changed',
'additional' => 'middlename',
'fn' => 'name',
'tel' => 'phone',
'url' => 'website',
'bday' => 'birthday',
'note' => 'notes',
'role' => 'profession',
'title' => 'jobtitle',
);
$propmap = array('email' => 'address', 'website' => 'url', 'phone' => 'number');
$date_format = $this->rc->config->get('date_format', 'Y-m-d');
// map kolab object properties to keys and values the client expects
array_walk($result['changes'], function(&$change, $i) use ($keymap, $propmap, $date_format) {
if (array_key_exists($change['property'], $keymap)) {
$change['property'] = $keymap[$change['property']];
}
// format date-time values
if ($change['property'] == 'created' || $change['property'] == 'changed') {
if ($old_ = rcube_utils::anytodatetime($change['old'])) {
$change['old_'] = $this->rc->format_date($old_);
}
if ($new_ = rcube_utils::anytodatetime($change['new'])) {
$change['new_'] = $this->rc->format_date($new_);
}
}
// format dates
else if ($change['property'] == 'birthday' || $change['property'] == 'anniversary') {
if ($old_ = rcube_utils::anytodatetime($change['old'])) {
$change['old_'] = $this->rc->format_date($old_, $date_format);
}
if ($new_ = rcube_utils::anytodatetime($change['new'])) {
$change['new_'] = $this->rc->format_date($new_, $date_format);
}
}
// convert email, website, phone values
else if (array_key_exists($change['property'], $propmap)) {
$propname = $propmap[$change['property']];
foreach (array('old','new') as $k) {
$k_ = $k . '_';
if (!empty($change[$k])) {
$change[$k_] = html::quote($change[$k][$propname] ?: '--');
if ($change[$k]['type']) {
$change[$k_] .= ' ' . html::span('subtype', $this->get_type_label($change[$k]['type']));
}
$change['ishtml'] = true;
}
}
}
// serialize address structs
if ($change['property'] == 'address') {
foreach (array('old','new') as $k) {
$k_ = $k . '_';
$change[$k]['zipcode'] = $change[$k]['code'];
$template = $this->rc->config->get('address_template', '{'.join('} {', array_keys($change[$k])).'}');
$composite = array();
foreach ($change[$k] as $p => $val) {
if (strlen($val))
$composite['{'.$p.'}'] = $val;
}
$change[$k_] = preg_replace('/\{\w+\}/', '', strtr($template, $composite));
if ($change[$k]['type']) {
$change[$k_] .= html::div('subtype', $this->get_type_label($change[$k]['type']));
}
$change['ishtml'] = true;
}
$change['diff_'] = libkolab::html_diff($change['old_'], $change['new_'], true);
}
// localize gender values
else if ($change['property'] == 'gender') {
if ($change['old']) $change['old_'] = $this->rc->gettext($change['old']);
if ($change['new']) $change['new_'] = $this->rc->gettext($change['new']);
}
// translate 'key' entries in individual properties
else if ($change['property'] == 'key') {
$p = $change['old'] ?: $change['new'];
$t = $p['type'];
$change['property'] = $t . 'publickey';
$change['old'] = $change['old'] ? $change['old']['key'] : '';
$change['new'] = $change['new'] ? $change['new']['key'] : '';
}
// compute a nice diff of notes
else if ($change['property'] == 'notes') {
$change['diff_'] = libkolab::html_diff($change['old'], $change['new'], false);
}
});
$this->rc->output->command('contact_show_diff', $result);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
}
$this->rc->output->send();
}
/**
* Handler for audit trail revision restore requests
*/
public function contact_restore()
{
if (empty($this->bonnie_api)) {
return false;
}
$success = false;
$contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
$rev = rcube_utils::get_input_value('rev', rcube_utils::INPUT_POST);
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source, $folder);
if ($folder && ($raw_msg = $this->bonnie_api->rawdata('contact', $uid, $rev, $mailbox))) {
$imap = $this->rc->get_storage();
// insert $raw_msg as new message
if ($imap->save_message($folder->name, $raw_msg, null, false)) {
$success = true;
// delete old revision from imap and cache
$imap->delete_message($msguid, $folder->name);
$folder->cache->set($msguid, false);
$this->cache = array();
}
}
if ($success) {
$this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rev))), 'confirmation');
$this->rc->output->command('close_contact_history_dialog', $contact);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
}
$this->rc->output->send();
}
/**
* Get a previous revision of the given contact record from the Bonnie API
*/
public function get_revision($cid, $source, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($cid, $source);
// call Bonnie API
$result = $this->bonnie_api->get('contact', $uid, $rev, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
$format = kolab_format::factory('contact');
$format->load($result['xml']);
$rec = $format->to_array();
if ($format->is_valid()) {
$rec['rev'] = $result['rev'];
return $rec;
}
}
return false;
}
/**
* Helper method to resolved the given contact identifier into uid and mailbox
*
* @return array (uid,mailbox,msguid) tuple
*/
private function _resolve_contact_identity($id, $abook, &$folder = null)
{
$mailbox = $msguid = null;
$source = $this->get_address_book(array('id' => $abook));
if ($source['instance']) {
$uid = $source['instance']->id2uid($id);
$list = kolab_storage::id_decode($abook);
}
else {
return array(null, $mailbox, $msguid);
}
// get resolve message UID and mailbox identifier
if ($folder = kolab_storage::get_folder($list)) {
$mailbox = $folder->get_mailbox_id();
$msguid = $folder->cache->uid2msguid($uid);
}
return array($uid, $mailbox, $msguid);
}
/**
*
*/
private function _sort_form_fields($contents, $source)
{
$block = [];
foreach (array_keys($source->coltypes) as $col) {
if (isset($contents[$col])) {
$block[$col] = $contents[$col];
}
}
return $block;
}
/**
* Handler for user preferences form (preferences_list hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_list($args)
{
if ($args['section'] != 'addressbook') {
return $args;
}
$ldap_public = $this->rc->config->get('ldap_public');
// Hide option if there's no global addressbook
if (empty($ldap_public)) {
return $args;
}
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
$prio = $this->addressbook_prio();
if (!in_array('kolab_addressbook_prio', $dont_override)) {
// Load localization
$this->add_texts('localization');
$field_id = '_kolab_addressbook_prio';
$select = new html_select(array('name' => $field_id, 'id' => $field_id));
$select->add($this->gettext('globalfirst'), self::GLOBAL_FIRST);
$select->add($this->gettext('personalfirst'), self::PERSONAL_FIRST);
$select->add($this->gettext('globalonly'), self::GLOBAL_ONLY);
$select->add($this->gettext('personalonly'), self::PERSONAL_ONLY);
$args['blocks']['main']['options']['kolab_addressbook_prio'] = array(
'title' => html::label($field_id, rcube::Q($this->gettext('addressbookprio'))),
'content' => $select->show($prio),
);
}
return $args;
}
/**
* Handler for user preferences save (preferences_save hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_save($args)
{
if ($args['section'] != 'addressbook') {
return $args;
}
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
$key = 'kolab_addressbook_prio';
if (!in_array('kolab_addressbook_prio', $dont_override) || !isset($_POST['_'.$key])) {
$args['prefs'][$key] = (int) rcube_utils::get_input_value('_'.$key, rcube_utils::INPUT_POST);
}
return $args;
}
/**
* Handler for plugin actions
*/
public function book_actions()
{
$action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC));
if ($action == 'create') {
$this->ui->book_edit();
}
else if ($action == 'edit') {
$this->ui->book_edit();
}
else if ($action == 'delete') {
$this->book_delete();
}
}
/**
* Handler for address book create/edit form submit
*/
public function book_save()
{
$this->driver->folder_save();
$this->rc->output->send('iframe');
}
/**
*
*/
public function book_search()
{
$results = [];
$query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
kolab_storage::$encode_ids = true;
$search_more_results = false;
$this->sources = array();
$this->folders = array();
// find unsubscribed IMAP folders that have "event" type
if ($source == 'folders') {
foreach ((array)kolab_storage::search_folders('contact', $query, array('other')) as $folder) {
$this->folders[$folder->id] = $folder;
$this->sources[$folder->id] = new kolab_contacts($folder->name);
}
}
// search other user's namespace via LDAP
else if ($source == 'users') {
$limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
$folders = array();
// search for contact folders shared by this user
foreach (kolab_storage::list_user_folders($user, 'contact', false) as $foldername) {
$folders[] = new kolab_storage_folder($foldername, 'contact');
}
if (count($folders)) {
$userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
$this->folders[$userfolder->id] = $userfolder;
$this->sources[$userfolder->id] = $userfolder;
foreach ($folders as $folder) {
$this->folders[$folder->id] = $folder;
$this->sources[$folder->id] = new kolab_contacts($folder->name);;
$count++;
}
}
if ($count >= $limit) {
$search_more_results = true;
break;
}
}
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
// build results list
foreach ($this->sources as $id => $source) {
$folder = $this->folders[$id];
$imap_path = explode($delim, $folder->name);
// find parent
do {
array_pop($imap_path);
$parent_id = kolab_storage::folder_id(join($delim, $imap_path));
}
while (count($imap_path) > 1 && !$this->folders[$parent_id]);
// restore "real" parent ID
if ($parent_id && !$this->folders[$parent_id]) {
$parent_id = kolab_storage::folder_id($folder->get_parent());
}
$prop = $this->driver->abook_prop($id, $source);
$prop['parent'] = $parent_id;
$html = $this->addressbook_list_item($id, $prop, $jsdata, true);
unset($prop['group']);
$prop += (array)$jsdata[$id];
$prop['html'] = $html;
$results[] = $prop;
}
// report more results available
if ($search_more_results) {
$this->rc->output->show_message('autocompletemore', 'notice');
}
$this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
}
/**
*
*/
public function book_subscribe()
{
$success = false;
$id = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
if ($id && ($folder = kolab_storage::get_folder(kolab_storage::id_decode($id)))) {
if (isset($_POST['_permanent']))
$success |= $folder->subscribe(intval($_POST['_permanent']));
if (isset($_POST['_active']))
$success |= $folder->activate(intval($_POST['_active']));
// list groups for this address book
if (!empty($_POST['_groups'])) {
$abook = new kolab_contacts($folder->name);
foreach ((array)$abook->list_groups() as $prop) {
$prop['source'] = $id;
$prop['id'] = $prop['ID'];
unset($prop['ID']);
$this->rc->output->command('insert_contact_group', $prop);
}
}
}
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
}
else {
$this->rc->output->show_message($this->gettext('errorsaving'), 'error');
}
$this->rc->output->send();
}
/**
* Handler for address book delete action (AJAX)
*/
private function book_delete()
{
$source = trim(rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC, true));
if ($source && ($result = $this->driver->folder_delete($source))) {
$storage = $this->rc->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$this->rc->output->show_message('kolab_addressbook.bookdeleted', 'confirmation');
$this->rc->output->set_env('pagecount', 0);
$this->rc->output->command('set_rowcount', $this->rc->gettext('nocontactsfound'));
$this->rc->output->command('set_env', 'delimiter', $delimiter);
$this->rc->output->command('list_contacts_clear');
$this->rc->output->command('book_delete_done', $source);
}
else {
$this->rc->output->show_message('kolab_addressbook.bookdeleteerror', 'error');
}
$this->rc->output->send();
}
/**
* Returns value of kolab_addressbook_prio setting
*/
private function addressbook_prio()
{
$abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio');
// Make sure any global addressbooks are defined
if ($abook_prio == 0 || $abook_prio == 2) {
$ldap_public = $this->rc->config->get('ldap_public');
if (empty($ldap_public)) {
$abook_prio = 1;
}
}
return $abook_prio;
}
/**
* Hook for (contact) folder deletion
*/
function prefs_folder_delete($args)
{
// ignore...
if ($args['abort'] && !$args['result']) {
return $args;
}
$this->_contact_folder_rename($args['name'], false);
}
/**
* Hook for (contact) folder renaming
*/
function prefs_folder_rename($args)
{
// ignore...
if ($args['abort'] && !$args['result']) {
return $args;
}
$this->_contact_folder_rename($args['oldname'], $args['newname']);
}
/**
* Hook for (contact) folder updates. Forward to folder_rename handler if name was changed
*/
function prefs_folder_update($args)
{
// ignore...
if ($args['abort'] && !$args['result']) {
return $args;
}
if ($args['record']['name'] != $args['record']['oldname']) {
$this->_contact_folder_rename($args['record']['oldname'], $args['record']['name']);
}
}
/**
* Apply folder renaming or deletion to the registered birthday calendar address books
*/
private function _contact_folder_rename($oldname, $newname = false)
{
$update = false;
$delimiter = $this->rc->get_storage()->get_hierarchy_delimiter();
$bday_addressbooks = (array) $this->rc->config->get('calendar_birthday_adressbooks', []);
foreach ($bday_addressbooks as $i => $id) {
$folder_name = kolab_storage::id_decode($id);
if ($oldname === $folder_name || strpos($folder_name, $oldname.$delimiter) === 0) {
if ($newname) { // rename
$new_folder = $newname . substr($folder_name, strlen($oldname));
$bday_addressbooks[$i] = kolab_storage::id_encode($new_folder);
}
else { // delete
unset($bday_addressbooks[$i]);
}
$update = true;
}
}
if ($update) {
$this->rc->user->save_prefs(['calendar_birthday_adressbooks' => $bday_addressbooks]);
}
}
/**
* Get a localization label for specified field type
*/
private function get_type_label($type)
{
// Roundcube < 1.5
if (function_exists('rcmail_get_type_label')) {
return rcmail_get_type_label($type);
}
// Roundcube >= 1.5
return rcmail_action_contacts_index::get_type_label($type);
}
}
diff --git a/plugins/kolab_auth/kolab_auth.php b/plugins/kolab_auth/kolab_auth.php
index 0a7ea953..bac892ea 100644
--- a/plugins/kolab_auth/kolab_auth.php
+++ b/plugins/kolab_auth/kolab_auth.php
@@ -1,890 +1,890 @@
<?php
/**
* Kolab Authentication (based on ldap_authentication plugin)
*
* Authenticates on LDAP server, finds canonized authentication ID for IMAP
* and for new users creates identity based on LDAP information.
*
* Supports impersonate feature (login as another user). To use this feature
* imap_auth_type/smtp_auth_type must be set to DIGEST-MD5 or PLAIN.
*
* @version @package_version@
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011-2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_auth extends rcube_plugin
{
static $ldap;
private $username;
private $data = array();
public function init()
{
$rcmail = rcube::get_instance();
$this->load_config();
$this->require_plugin('libkolab');
$this->add_hook('authenticate', array($this, 'authenticate'));
$this->add_hook('startup', array($this, 'startup'));
$this->add_hook('ready', array($this, 'ready'));
$this->add_hook('user_create', array($this, 'user_create'));
// Hook for password change
$this->add_hook('password_ldap_bind', array($this, 'password_ldap_bind'));
// Hooks related to "Login As" feature
$this->add_hook('template_object_loginform', array($this, 'login_form'));
$this->add_hook('storage_connect', array($this, 'imap_connect'));
$this->add_hook('managesieve_connect', array($this, 'imap_connect'));
$this->add_hook('smtp_connect', array($this, 'smtp_connect'));
$this->add_hook('identity_form', array($this, 'identity_form'));
// Hook to modify some configuration, e.g. ldap
$this->add_hook('config_get', array($this, 'config_get'));
// Hook to modify logging directory
$this->add_hook('write_log', array($this, 'write_log'));
$this->username = $_SESSION['username'] ?? null;
// Enable debug logs (per-user), when logged as another user
if (!empty($_SESSION['kolab_auth_admin']) && $rcmail->config->get('kolab_auth_auditlog')) {
$rcmail->config->set('debug_level', 1);
$rcmail->config->set('smtp_log', true);
$rcmail->config->set('log_logins', true);
$rcmail->config->set('log_session', true);
$rcmail->config->set('memcache_debug', true);
$rcmail->config->set('imap_debug', true);
$rcmail->config->set('ldap_debug', true);
$rcmail->config->set('smtp_debug', true);
$rcmail->config->set('sql_debug', true);
// SQL debug need to be set directly on DB object
// setting config variable will not work here because
// the object is already initialized/configured
if ($db = $rcmail->get_dbh()) {
$db->set_debug(true);
}
}
}
/**
* Ready hook handler
*/
public function ready($args)
{
$rcmail = rcube::get_instance();
// Store user unique identifier for freebusy_session_auth feature
if (!($uniqueid = $rcmail->config->get('kolab_uniqueid'))) {
$uniqueid = $_SESSION['kolab_auth_uniqueid'];
if (!$uniqueid) {
// Find user record in LDAP
if (($ldap = self::ldap()) && $ldap->ready) {
if ($record = $ldap->get_user_record($rcmail->get_user_name(), $_SESSION['kolab_host'])) {
$uniqueid = $record['uniqueid'];
}
}
}
if ($uniqueid) {
$uniqueid = md5($uniqueid);
$rcmail->user->save_prefs(array('kolab_uniqueid' => $uniqueid));
}
}
// Set/update freebusy_session_auth entry
if ($uniqueid && empty($_SESSION['kolab_auth_admin'])
&& ($ttl = $rcmail->config->get('freebusy_session_auth'))
) {
if ($ttl === true) {
$ttl = $rcmail->config->get('session_lifetime', 0) * 60;
if (!$ttl) {
$ttl = 10 * 60;
}
}
$rcmail->config->set('freebusy_auth_cache', 'db');
$rcmail->config->set('freebusy_auth_cache_ttl', $ttl);
if ($cache = $rcmail->get_cache_shared('freebusy_auth', false)) {
$key = md5($uniqueid . ':' . rcube_utils::remote_addr() . ':' . $rcmail->get_user_name());
$value = $cache->get($key);
$deadline = new DateTime('now', new DateTimeZone('UTC'));
// We don't want to do the cache update on every request
// do it once in a 1/10 of the ttl
if ($value) {
$value = new DateTime($value);
$value->sub(new DateInterval('PT' . intval($ttl * 9/10) . 'S'));
if ($value > $deadline) {
return;
}
}
$deadline->add(new DateInterval('PT' . $ttl . 'S'));
$cache->set($key, $deadline->format(DateTime::ISO8601));
}
}
}
/**
* Startup hook handler
*/
public function startup($args)
{
// Check access rights when logged in as another user
if (!empty($_SESSION['kolab_auth_admin']) && $args['task'] != 'login' && $args['task'] != 'logout') {
// access to specified task is forbidden,
// redirect to the first task on the list
if (!empty($_SESSION['kolab_auth_allowed_tasks'])) {
$tasks = (array)$_SESSION['kolab_auth_allowed_tasks'];
if (!in_array($args['task'], $tasks) && !in_array('*', $tasks)) {
header('Location: ?_task=' . array_shift($tasks));
die;
}
// add script that will remove disabled taskbar buttons
if (!in_array('*', $tasks)) {
$this->add_hook('render_page', array($this, 'render_page'));
}
}
}
// load per-user settings
$this->load_user_role_plugins_and_settings();
return $args;
}
/**
* Modify some configuration according to LDAP user record
*/
public function config_get($args)
{
// Replaces ldap_vars (%dc, etc) in public kolab ldap addressbooks
// config based on the users base_dn. (for multi domain support)
if ($args['name'] == 'ldap_public' && !empty($args['result'])) {
$rcmail = rcube::get_instance();
$kolab_books = (array) $rcmail->config->get('kolab_auth_ldap_addressbooks');
foreach ($args['result'] as $name => $config) {
if (in_array($name, $kolab_books) || in_array('*', $kolab_books)) {
$args['result'][$name] = $this->patch_ldap_config($config);
}
}
}
else if ($args['name'] == 'kolab_users_directory' && !empty($args['result'])) {
$args['result'] = $this->patch_ldap_config($args['result']);
}
return $args;
}
/**
* Helper method to patch the given LDAP directory config with user-specific values
*/
protected function patch_ldap_config($config)
{
if (is_array($config)) {
$config['base_dn'] = self::parse_ldap_vars($config['base_dn']);
$config['search_base_dn'] = self::parse_ldap_vars($config['search_base_dn']);
$config['bind_dn'] = str_replace('%dn', $_SESSION['kolab_dn'], $config['bind_dn']);
if (!empty($config['groups'])) {
$config['groups']['base_dn'] = self::parse_ldap_vars($config['groups']['base_dn']);
}
}
return $config;
}
/**
* Modifies list of plugins and settings according to
* specified LDAP roles
*/
public function load_user_role_plugins_and_settings($startup = false)
{
if (empty($_SESSION['user_roledns'])) {
return;
}
$rcmail = rcube::get_instance();
// Example 'kolab_auth_role_plugins' =
//
// Array(
// '<role_dn>' => Array('plugin1', 'plugin2'),
// );
//
// NOTE that <role_dn> may in fact be something like: 'cn=role,%dc'
$role_plugins = $rcmail->config->get('kolab_auth_role_plugins');
// Example $rcmail_config['kolab_auth_role_settings'] =
//
// Array(
// '<role_dn>' => Array(
// '$setting' => Array(
// 'mode' => '(override|merge)', (default: override)
// 'value' => <>,
// 'allow_override' => (true|false) (default: false)
// ),
// ),
// );
//
// NOTE that <role_dn> may in fact be something like: 'cn=role,%dc'
$role_settings = $rcmail->config->get('kolab_auth_role_settings');
if (!empty($role_plugins)) {
foreach ($role_plugins as $role_dn => $plugins) {
$role_dn = self::parse_ldap_vars($role_dn);
if (!empty($role_plugins[$role_dn])) {
$role_plugins[$role_dn] = array_unique(array_merge((array)$role_plugins[$role_dn], $plugins));
} else {
$role_plugins[$role_dn] = $plugins;
}
}
}
if (!empty($role_settings)) {
foreach ($role_settings as $role_dn => $settings) {
$role_dn = self::parse_ldap_vars($role_dn);
if (!empty($role_settings[$role_dn])) {
$role_settings[$role_dn] = array_merge((array)$role_settings[$role_dn], $settings);
} else {
$role_settings[$role_dn] = $settings;
}
}
}
foreach ($_SESSION['user_roledns'] as $role_dn) {
if (!empty($role_settings[$role_dn]) && is_array($role_settings[$role_dn])) {
foreach ($role_settings[$role_dn] as $setting_name => $setting) {
if (!isset($setting['mode'])) {
$setting['mode'] = 'override';
}
if ($setting['mode'] == "override") {
$rcmail->config->set($setting_name, $setting['value']);
} elseif ($setting['mode'] == "merge") {
$orig_setting = $rcmail->config->get($setting_name);
if (!empty($orig_setting)) {
if (is_array($orig_setting)) {
$rcmail->config->set($setting_name, array_merge($orig_setting, $setting['value']));
}
} else {
$rcmail->config->set($setting_name, $setting['value']);
}
}
$dont_override = (array) $rcmail->config->get('dont_override');
if (empty($setting['allow_override'])) {
$rcmail->config->set('dont_override', array_merge($dont_override, array($setting_name)));
}
else {
if (in_array($setting_name, $dont_override)) {
$_dont_override = array();
foreach ($dont_override as $_setting) {
if ($_setting != $setting_name) {
$_dont_override[] = $_setting;
}
}
$rcmail->config->set('dont_override', $_dont_override);
}
}
if ($setting_name == 'skin') {
if ($rcmail->output->type == 'html') {
$rcmail->output->set_skin($setting['value']);
$rcmail->output->set_env('skin', $setting['value']);
}
}
}
}
if (!empty($role_plugins[$role_dn])) {
foreach ((array)$role_plugins[$role_dn] as $plugin) {
$loaded = $this->api->load_plugin($plugin);
// Some plugins e.g. kolab_2fa use 'startup' hook to
// register other hooks, but when called on 'authenticate' hook
// we're already after 'startup', so we'll call it directly
if ($loaded && $startup && $plugin == 'kolab_2fa'
&& ($plugin = $this->api->get_plugin($plugin))
) {
$plugin->startup(array('task' => $rcmail->task, 'action' => $rcmail->action));
}
}
}
}
}
/**
* Logging method replacement to print debug/errors into
* a separate (sub)folder for each user
*/
public function write_log($args)
{
$rcmail = rcube::get_instance();
if ($rcmail->config->get('log_driver') == 'syslog') {
return $args;
}
// log_driver == 'file' is assumed here
$log_dir = $rcmail->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
// Append original username + target username for audit-logging
if ($rcmail->config->get('kolab_auth_auditlog') && !empty($_SESSION['kolab_auth_admin'])) {
$args['dir'] = $log_dir . '/' . strtolower($_SESSION['kolab_auth_admin']) . '/' . strtolower($this->username);
// Attempt to create the directory
if (!is_dir($args['dir'])) {
@mkdir($args['dir'], 0750, true);
}
}
// Define the user log directory if a username is provided
else if ($rcmail->config->get('per_user_logging') && !empty($this->username)
&& !stripos($log_dir, '/' . $this->username) // maybe already set by syncroton, skip
) {
$user_log_dir = $log_dir . '/' . strtolower($this->username);
if (is_writable($user_log_dir)) {
$args['dir'] = $user_log_dir;
}
else if (!in_array($args['name'], array('errors', 'userlogins', 'sendmail'))) {
$args['abort'] = true; // don't log if unauthenticed or no per-user log dir
}
}
return $args;
}
/**
* Sets defaults for new user.
*/
public function user_create($args)
{
if (!empty($this->data['user_email'])) {
// addresses list is supported
if (array_key_exists('email_list', $args)) {
$email_list = array_unique($this->data['user_email']);
// add organization to the list
if (!empty($this->data['user_organization'])) {
foreach ($email_list as $idx => $email) {
$email_list[$idx] = array(
'organization' => $this->data['user_organization'],
'email' => $email,
);
}
}
$args['email_list'] = $email_list;
}
else {
$args['user_email'] = $this->data['user_email'][0];
}
}
if (!empty($this->data['user_name'])) {
$args['user_name'] = $this->data['user_name'];
}
return $args;
}
/**
* Modifies login form adding additional "Login As" field
*/
public function login_form($args)
{
$this->add_texts('localization/');
$rcmail = rcube::get_instance();
$admin_login = $rcmail->config->get('kolab_auth_admin_login');
$group = $rcmail->config->get('kolab_auth_group');
$role_attr = $rcmail->config->get('kolab_auth_role');
// Show "Login As" input
if (empty($admin_login) || (empty($group) && empty($role_attr))) {
return $args;
}
// Don't add the extra field on 2FA form
if (strpos($args['content'], 'plugin.kolab-2fa-login')) {
return $args;
}
$input = new html_inputfield(array('name' => '_loginas', 'id' => 'rcmloginas',
'type' => 'text', 'autocomplete' => 'off'));
$row = html::tag('tr', null,
html::tag('td', 'title', html::label('rcmloginas', rcube::Q($this->gettext('loginas'))))
. html::tag('td', 'input', $input->show(trim(rcube_utils::get_input_value('_loginas', rcube_utils::INPUT_POST))))
);
// add icon style for Elastic
$style = html::tag('style', [], '#login-form .input-group .icon.loginas::before { content: "\f508"; } ');
$args['content'] = preg_replace('/<\/tbody>/i', $row . '</tbody>' . $style, $args['content']);
return $args;
}
/**
* Find user credentials In LDAP.
*/
public function authenticate($args)
{
// get username and host
$host = $args['host'];
$user = $args['user'];
$pass = $args['pass'];
$loginas = trim(rcube_utils::get_input_value('_loginas', rcube_utils::INPUT_POST));
if (empty($user) || (empty($pass) && empty($_SERVER['REMOTE_USER']))) {
$args['abort'] = true;
return $args;
}
// temporarily set the current username to the one submitted
$this->username = $user;
$ldap = self::ldap();
if (!$ldap || !$ldap->ready) {
self::log_login_error($user, "LDAP not ready");
$args['abort'] = true;
$args['kolab_ldap_error'] = true;
return $args;
}
// Find user record in LDAP
$record = $ldap->get_user_record($user, $host);
if (empty($record)) {
self::log_login_error($user, "No user record found");
$args['abort'] = true;
return $args;
}
$rcmail = rcube::get_instance();
$admin_login = $rcmail->config->get('kolab_auth_admin_login');
$admin_pass = $rcmail->config->get('kolab_auth_admin_password');
$login_attr = $rcmail->config->get('kolab_auth_login');
$name_attr = $rcmail->config->get('kolab_auth_name');
$email_attr = $rcmail->config->get('kolab_auth_email');
$org_attr = $rcmail->config->get('kolab_auth_organization');
$role_attr = $rcmail->config->get('kolab_auth_role');
$imap_attr = $rcmail->config->get('kolab_auth_mailhost');
if (!empty($role_attr) && !empty($record[$role_attr])) {
$_SESSION['user_roledns'] = (array)($record[$role_attr]);
}
if (!empty($imap_attr) && !empty($record[$imap_attr])) {
$default_host = $rcmail->config->get('default_host');
if (!empty($default_host)) {
rcube::write_log("errors", "Both default host and kolab_auth_mailhost set. Incompatible.");
} else {
$args['host'] = "tls://" . $record[$imap_attr];
}
}
// Login As...
if (!empty($loginas) && $admin_login) {
// Authenticate to LDAP
$result = $ldap->bind($record['dn'], $pass);
if (!$result) {
self::log_login_error($user, "Unable to bind with '" . $record['dn'] . "'");
$args['abort'] = true;
return $args;
}
$isadmin = false;
$admin_rights = $rcmail->config->get('kolab_auth_admin_rights', array());
// @deprecated: fall-back to the old check if the original user has/belongs to administrative role/group
if (empty($admin_rights)) {
$group = $rcmail->config->get('kolab_auth_group');
$role_dn = $rcmail->config->get('kolab_auth_role_value');
// check role attribute
if (!empty($role_attr) && !empty($role_dn) && !empty($record[$role_attr])) {
$role_dn = $ldap->parse_vars($role_dn, $user, $host);
if (in_array($role_dn, (array)$record[$role_attr])) {
$isadmin = true;
}
}
// check group
if (!$isadmin && !empty($group)) {
$groups = $ldap->get_user_groups($record['dn'], $user, $host);
if (in_array($group, $groups)) {
$isadmin = true;
}
}
if ($isadmin) {
// user has admin privileges privilage, get "login as" user credentials
$target_entry = $ldap->get_user_record($loginas, $host);
$allowed_tasks = $rcmail->config->get('kolab_auth_allowed_tasks');
}
}
else {
// get "login as" user credentials
$target_entry = $ldap->get_user_record($loginas, $host);
if (!empty($target_entry)) {
// get effective rights to determine login-as permissions
$effective_rights = (array)$ldap->effective_rights($target_entry['dn']);
if (!empty($effective_rights)) {
// compat with out of date Net_LDAP3
$effective_rights = array_change_key_case($effective_rights, CASE_LOWER);
$effective_rights['attrib'] = $effective_rights['attributelevelrights'];
$effective_rights['entry'] = $effective_rights['entrylevelrights'];
// compare the rights with the permissions mapping
$allowed_tasks = array();
foreach ($admin_rights as $task => $perms) {
$perms_ = explode(':', $perms);
$type = array_shift($perms_);
$req = array_pop($perms_);
$attrib = array_pop($perms_);
if (array_key_exists($type, $effective_rights)) {
if ($type == 'entry' && in_array($req, $effective_rights[$type])) {
$allowed_tasks[] = $task;
}
else if ($type == 'attrib' && array_key_exists($attrib, $effective_rights[$type]) &&
in_array($req, $effective_rights[$type][$attrib])) {
$allowed_tasks[] = $task;
}
}
}
$isadmin = !empty($allowed_tasks);
}
}
}
// Save original user login for log (see below)
if ($login_attr) {
$origname = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
}
else {
$origname = $user;
}
if (!$isadmin || empty($target_entry)) {
$this->add_texts('localization/');
$args['abort'] = true;
$args['error'] = $this->gettext(array(
'name' => 'loginasnotallowed',
'vars' => array('user' => rcube::Q($loginas)),
));
self::log_login_error($user, "No privileges to login as '" . $loginas . "'", $loginas);
return $args;
}
// replace $record with target entry
$record = $target_entry;
$args['user'] = $this->username = $loginas;
// Mark session to use SASL proxy for IMAP authentication
$_SESSION['kolab_auth_admin'] = strtolower($origname);
$_SESSION['kolab_auth_login'] = $rcmail->encrypt($admin_login);
$_SESSION['kolab_auth_password'] = $rcmail->encrypt($admin_pass);
$_SESSION['kolab_auth_allowed_tasks'] = $allowed_tasks;
}
// Store UID and DN of logged user in session for use by other plugins
$_SESSION['kolab_uid'] = is_array($record['uid']) ? $record['uid'][0] : $record['uid'];
$_SESSION['kolab_dn'] = $record['dn'];
// Store LDAP replacement variables used for current user
// This improves performance of load_user_role_plugins_and_settings()
// which is executed on every request (via startup hook) and where
// we don't like to use LDAP (connection + bind + search)
$_SESSION['kolab_auth_vars'] = $ldap->get_parse_vars();
// Store user unique identifier for freebusy_session_auth feature
$_SESSION['kolab_auth_uniqueid'] = is_array($record['uniqueid']) ? $record['uniqueid'][0] : $record['uniqueid'];
// Store also host as we need it for get_user_reacod() in 'ready' hook handler
$_SESSION['kolab_host'] = $host;
// Set user login
if ($login_attr) {
$this->data['user_login'] = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
}
if ($this->data['user_login']) {
$args['user'] = $this->username = $this->data['user_login'];
}
// User name for identity (first log in)
foreach ((array)$name_attr as $field) {
- $name = is_array($record[$field]) ? $record[$field][0] : $record[$field];
+ $name = is_array($record[$field] ?? null) ? $record[$field][0] : ($record[$field] ?? null);
if (!empty($name)) {
$this->data['user_name'] = $name;
break;
}
}
// User email(s) for identity (first log in)
foreach ((array)$email_attr as $field) {
$email = is_array($record[$field]) ? array_filter($record[$field]) : $record[$field];
if (!empty($email)) {
$this->data['user_email'] = array_merge((array)($this->data['user_email'] ?? null), (array)$email);
}
}
// Organization name for identity (first log in)
foreach ((array)$org_attr as $field) {
$organization = is_array($record[$field]) ? $record[$field][0] : $record[$field];
if (!empty($organization)) {
$this->data['user_organization'] = $organization;
break;
}
}
// Log "Login As" usage
if (!empty($origname)) {
rcube::write_log('userlogins', sprintf('Admin login for %s by %s from %s',
$args['user'], $origname, rcube_utils::remote_ip()));
}
// load per-user settings/plugins
$this->load_user_role_plugins_and_settings(true);
return $args;
}
/**
* Set user DN for password change (password plugin with ldap_simple driver)
*/
public function password_ldap_bind($args)
{
$args['user_dn'] = $_SESSION['kolab_dn'];
$rcmail = rcube::get_instance();
$rcmail->config->set('password_ldap_method', 'user');
return $args;
}
/**
* Sets SASL Proxy login/password for IMAP and Managesieve auth
*/
public function imap_connect($args)
{
if (!empty($_SESSION['kolab_auth_admin'])) {
$rcmail = rcube::get_instance();
$admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
$admin_pass = $rcmail->decrypt($_SESSION['kolab_auth_password']);
$args['auth_cid'] = $admin_login;
$args['auth_pw'] = $admin_pass;
}
return $args;
}
/**
* Sets SASL Proxy login/password for SMTP auth
*/
public function smtp_connect($args)
{
if (!empty($_SESSION['kolab_auth_admin'])) {
$rcmail = rcube::get_instance();
$admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
$admin_pass = $rcmail->decrypt($_SESSION['kolab_auth_password']);
$args['smtp_auth_cid'] = $admin_login;
$args['smtp_auth_pw'] = $admin_pass;
}
return $args;
}
/**
* Hook to replace the plain text input field for email address by a drop-down list
* with all email addresses (including aliases) from this user's LDAP record.
*/
public function identity_form($args)
{
$rcmail = rcube::get_instance();
$ident_level = intval($rcmail->config->get('identities_level', 0));
// do nothing if email address modification is disabled
if ($ident_level == 1 || $ident_level == 3) {
return $args;
}
$ldap = self::ldap();
if (!$ldap || !$ldap->ready || empty($_SESSION['kolab_dn'])) {
return $args;
}
$emails = array();
$user_record = $ldap->get_record($_SESSION['kolab_dn']);
foreach ((array)$rcmail->config->get('kolab_auth_email', array()) as $col) {
$values = rcube_addressbook::get_col_values($col, $user_record, true);
if (!empty($values))
$emails = array_merge($emails, array_filter($values));
}
// kolab_delegation might want to modify this addresses list
$plugin = $rcmail->plugins->exec_hook('kolab_auth_emails', array('emails' => $emails));
$emails = $plugin['emails'];
if (!empty($emails)) {
$args['form']['addressing']['content']['email'] = array(
'type' => 'select',
'options' => array_combine($emails, $emails),
);
}
return $args;
}
/**
* Action executed before the page is rendered to add an onload script
* that will remove all taskbar buttons for disabled tasks
*/
public function render_page($args)
{
$rcmail = rcube::get_instance();
$tasks = (array)$_SESSION['kolab_auth_allowed_tasks'];
$tasks[] = 'logout';
// disable buttons in taskbar
$script = "
\$('a').filter(function() {
var ev = \$(this).attr('onclick');
return ev && ev.match(/'switch-task','([a-z]+)'/)
&& \$.inArray(RegExp.\$1, " . json_encode($tasks) . ") < 0;
}).remove();
";
$rcmail->output->add_script($script, 'docready');
}
/**
* Initializes LDAP object and connects to LDAP server
*/
public static function ldap()
{
self::$ldap = kolab_storage::ldap('kolab_auth_addressbook');
if (self::$ldap) {
self::$ldap->extend_fieldmap(array('uniqueid' => 'nsuniqueid'));
}
return self::$ldap;
}
/**
* Close LDAP connection
*/
public static function ldap_close()
{
if (self::$ldap) {
self::$ldap->close();
self::$ldap = null;
}
}
/**
* Parses LDAP DN string with replacing supported variables.
* See kolab_ldap::parse_vars()
*
* @param string $str LDAP DN string
*
* @return string Parsed DN string
*/
public static function parse_ldap_vars($str)
{
if (!empty($_SESSION['kolab_auth_vars'])) {
$str = strtr($str, $_SESSION['kolab_auth_vars']);
}
return $str;
}
/**
* Log failed logins
*
* @param string $username Username/Login
* @param string $message Error message (failure reason)
* @param string $login_as Username/Login of "login as" user
*/
public static function log_login_error($username, $message = null, $login_as = null)
{
$config = rcube::get_instance()->config;
if ($config->get('log_logins')) {
// don't fill the log with complete input, which could
// have been prepared by a hacker
if (strlen($username) > 256) {
$username = substr($username, 0, 256) . '...';
}
if (strlen($login_as) > 256) {
$login_as = substr($login_as, 0, 256) . '...';
}
if ($login_as) {
$username = sprintf('%s (as user %s)', $username, $login_as);
}
// Don't log full session id for better security
$session_id = session_id();
$session_id = $session_id ? substr($session_id, 0, 16) : 'no-session';
$message = sprintf(
"Failed login for %s from %s in session %s %s",
$username,
rcube_utils::remote_ip(),
$session_id,
$message ? "($message)" : ''
);
rcube::write_log('userlogins', $message);
// disable log_logins to prevent from duplicate log entries
$config->set('log_logins', false);
}
}
}
diff --git a/plugins/kolab_files/lib/kolab_files_engine.php b/plugins/kolab_files/lib/kolab_files_engine.php
index 311a8695..7e1b79ee 100644
--- a/plugins/kolab_files/lib/kolab_files_engine.php
+++ b/plugins/kolab_files/lib/kolab_files_engine.php
@@ -1,1810 +1,1814 @@
<?php
/**
* Kolab files storage engine
*
* @version @package_version@
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2013-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_files_engine
{
private $plugin;
private $rc;
private $url;
private $url_srv;
+ private $filetypes_style;
private $timeout = 600;
private $files_sort_cols = array('name', 'mtime', 'size');
private $sessions_sort_cols = array('name');
private $mimetypes = null;
const API_VERSION = 4;
/**
* Class constructor
*/
public function __construct($plugin, $client_url, $server_url = null)
{
$this->url = rtrim(rcube_utils::resolve_url($client_url), '/ ');
$this->url_srv = $server_url ? rtrim(rcube_utils::resolve_url($server_url), '/ ') : $this->url;
$this->plugin = $plugin;
$this->rc = $plugin->rc;
$this->timeout = $this->rc->config->get('session_lifetime') * 60;
}
/**
* User interface initialization
*/
public function ui()
{
$this->plugin->add_texts('localization/');
$templates = array();
$list_widget = false;
// set templates of Files UI and widgets
if ($this->rc->task == 'mail') {
if (in_array($this->rc->action, array('', 'show', 'compose'))) {
$templates[] = 'compose_plugin';
}
if (in_array($this->rc->action, array('show', 'preview', 'get'))) {
$templates[] = 'message_plugin';
if ($this->rc->action == 'get') {
// add "Save as" button into attachment toolbar
$this->plugin->add_button(array(
'id' => 'saveas',
'name' => 'saveas',
'type' => 'link',
'onclick' => 'kolab_directory_selector_dialog()',
'class' => 'button buttonPas saveas',
'classact' => 'button saveas',
'label' => 'kolab_files.save',
'title' => 'kolab_files.saveto',
), 'toolbar');
}
else {
// add "Save as" button into attachment menu
$this->plugin->add_button(array(
'id' => 'attachmenusaveas',
'name' => 'attachmenusaveas',
'type' => 'link',
'wrapper' => 'li',
'onclick' => 'return false',
'class' => 'icon active saveas',
'classact' => 'icon active saveas',
'innerclass' => 'icon active saveas',
'label' => 'kolab_files.saveto',
), 'attachmentmenu');
}
}
$list_widget = true;
}
else if (!$this->rc->action && in_array($this->rc->task, array('calendar', 'tasks'))) {
$list_widget = true;
$templates[] = 'compose_plugin';
}
else if ($this->rc->task == 'files') {
$templates[] = 'files';
// get list of external sources
$this->get_external_storage_drivers();
// these labels may be needed even if fetching ext sources failed
$this->plugin->add_label('folderauthtitle', 'authenticating', 'foldershare', 'saving');
}
if ($list_widget) {
$this->folder_list_env();
$this->plugin->add_label('save', 'cancel', 'saveto',
'saveall', 'fromcloud', 'attachsel', 'selectfiles', 'attaching',
'collection_audio', 'collection_video', 'collection_image', 'collection_document',
'folderauthtitle', 'authenticating'
);
}
// add taskbar button
if (empty($_REQUEST['framed'])) {
$this->plugin->add_button(array(
'command' => 'files',
'class' => 'button-files',
'classsel' => 'button-files button-selected',
'innerclass' => 'button-inner',
'label' => 'kolab_files.files',
'type' => 'link'
), 'taskbar');
}
$caps = $this->capabilities();
$this->plugin->include_stylesheet($this->plugin->local_skin_path().'/style.css');
$this->plugin->include_script($this->url . '/js/files_api.js');
$this->plugin->include_script('kolab_files.js');
$this->rc->output->set_env('files_url', $this->url . '/api/');
$this->rc->output->set_env('files_token', $this->get_api_token());
$this->rc->output->set_env('files_caps', $caps);
$this->rc->output->set_env('files_api_version', $caps['VERSION'] ?? 3);
$this->rc->output->set_env('files_user', $this->rc->get_user_name());
- if ($caps['DOCEDIT']) {
+ if ($caps['DOCEDIT'] ?? false) {
$this->plugin->add_label('declinednotice', 'invitednotice', 'acceptedownernotice',
'declinedownernotice', 'requestednotice', 'acceptednotice', 'declinednotice',
'more', 'accept', 'decline', 'join', 'status', 'when', 'file', 'comment',
'statusaccepted', 'statusinvited', 'statusdeclined', 'statusrequested',
'invitationaccepting', 'invitationdeclining', 'invitationrequesting',
'close', 'invitationtitle', 'sessions', 'saving');
}
if (!empty($templates)) {
$collapsed_folders = (string) $this->rc->config->get('kolab_files_collapsed_folders');
$this->rc->output->include_script('treelist.js');
$this->rc->output->set_env('kolab_files_collapsed_folders', $collapsed_folders);
// register template objects for dialogs (and main interface)
$this->rc->output->add_handlers(array(
'folder-create-form' => array($this, 'folder_create_form'),
'folder-edit-form' => array($this, 'folder_edit_form'),
'folder-mount-form' => array($this, 'folder_mount_form'),
'folder-auth-options'=> array($this, 'folder_auth_options'),
'file-search-form' => array($this, 'file_search_form'),
'file-rename-form' => array($this, 'file_rename_form'),
'file-create-form' => array($this, 'file_create_form'),
'file-edit-dialog' => array($this, 'file_edit_dialog'),
'file-session-dialog' => array($this, 'file_session_dialog'),
'filelist' => array($this, 'file_list'),
'sessionslist' => array($this, 'sessions_list'),
'filequotadisplay' => array($this, 'quota_display'),
'document-editors-dialog' => array($this, 'document_editors_dialog'),
));
if ($this->rc->task != 'files') {
// add dialog(s) content at the end of page body
foreach ($templates as $template) {
$this->rc->output->add_footer(
$this->rc->output->parse('kolab_files.' . $template, false, false));
}
}
}
}
/**
* Engine actions handler
*/
public function actions()
{
if ($this->rc->task == 'files' && $this->rc->action) {
$action = $this->rc->action;
}
else if ($this->rc->task != 'files' && $_POST['act']) {
$action = $_POST['act'];
}
else {
$action = 'index';
}
$method = 'action_' . str_replace('-', '_', $action);
if (method_exists($this, $method)) {
$this->plugin->add_texts('localization/');
$this->{$method}();
}
}
/**
* Template object for folder creation form
*/
public function folder_create_form($attrib)
{
$attrib['name'] = 'folder-create-form';
if (empty($attrib['id'])) {
$attrib['id'] = 'folder-create-form';
}
$input_name = new html_inputfield(array('id' => 'folder-name', 'name' => 'name', 'size' => 30));
$select_parent = new html_select(array('id' => 'folder-parent', 'name' => 'parent'));
$table = new html_table(array('cols' => 2, 'class' => 'propform'));
$table->add('title', html::label('folder-name', rcube::Q($this->plugin->gettext('foldername'))));
$table->add(null, $input_name->show());
$table->add('title', html::label('folder-parent', rcube::Q($this->plugin->gettext('folderinside'))));
$table->add(null, $select_parent->show());
$out = $table->show();
// add form tag around text field
if (empty($attrib['form'])) {
$out = $this->rc->output->form_tag($attrib, $out);
}
$this->plugin->add_label('foldercreating', 'foldercreatenotice', 'create', 'foldercreate', 'cancel', 'addfolder');
$this->rc->output->add_gui_object('folder-create-form', $attrib['id']);
return $out;
}
/**
* Template object for folder editing form
*/
public function folder_edit_form($attrib)
{
$attrib['name'] = 'folder-edit-form';
if (empty($attrib['id'])) {
$attrib['id'] = 'folder-edit-form';
}
$input_name = new html_inputfield(array('id' => 'folder-edit-name', 'name' => 'name', 'size' => 30));
$select_parent = new html_select(array('id' => 'folder-edit-parent', 'name' => 'parent'));
$table = new html_table(array('cols' => 2, 'class' => 'propform'));
$table->add('title', html::label('folder-edit-name', rcube::Q($this->plugin->gettext('foldername'))));
$table->add(null, $input_name->show());
$table->add('title', html::label('folder-edit-parent', rcube::Q($this->plugin->gettext('folderinside'))));
$table->add(null, $select_parent->show());
$out = $table->show();
// add form tag around text field
if (empty($attrib['form'])) {
$out = $this->rc->output->form_tag($attrib, $out);
}
$this->plugin->add_label('folderupdating', 'folderupdatenotice', 'save', 'folderedit', 'cancel');
$this->rc->output->add_gui_object('folder-edit-form', $attrib['id']);
return $out;
}
/**
* Template object for folder mounting form
*/
public function folder_mount_form($attrib)
{
$sources = $this->rc->output->get_env('external_sources');
if (empty($sources) || !is_array($sources)) {
return '';
}
$attrib['name'] = 'folder-mount-form';
if (empty($attrib['id'])) {
$attrib['id'] = 'folder-mount-form';
}
// build form content
$table = new html_table(array('cols' => 2, 'class' => 'propform'));
$input_name = new html_inputfield(array('id' => 'folder-mount-name', 'name' => 'name', 'size' => 30));
$input_driver = new html_radiobutton(array('name' => 'driver', 'size' => 30));
$table->add('title', html::label('folder-mount-name', rcube::Q($this->plugin->gettext('name'))));
$table->add(null, $input_name->show());
foreach ($sources as $key => $source) {
$id = 'source-' . $key;
$form = new html_table(array('cols' => 2, 'class' => 'propform driverform'));
foreach ((array) $source['form'] as $idx => $label) {
$iid = $id . '-' . $idx;
$type = stripos($idx, 'pass') !== false ? 'html_passwordfield' : 'html_inputfield';
$input = new $type(array('size' => 30));
$form->add('title', html::label($iid, rcube::Q($label)));
$form->add(null, $input->show('', array(
'id' => $iid,
'name' => $key . '[' . $idx . ']'
)));
}
$row = $input_driver->show(null, array('value' => $key))
. html::img(array('src' => $source['image'], 'alt' => $key, 'title' => $source['name']))
. html::div(null, html::span('name', rcube::Q($source['name']))
. html::br()
. html::span('description hint', rcube::Q($source['description']))
. $form->show()
);
$table->add(array('id' => $id, 'colspan' => 2, 'class' => 'source'), $row);
}
$out = $table->show() . $this->folder_auth_options(array('suffix' => '-form'));
// add form tag around text field
if (empty($attrib['form'])) {
$out = $this->rc->output->form_tag($attrib, $out);
}
$this->plugin->add_label('foldermounting', 'foldermountnotice', 'foldermount',
'save', 'cancel', 'folderauthtitle', 'authenticating'
);
$this->rc->output->add_gui_object('folder-mount-form', $attrib['id']);
return $out;
}
/**
* Template object for folder authentication options
*/
public function folder_auth_options($attrib)
{
$checkbox = new html_checkbox(array(
'name' => 'store_passwords',
'value' => '1',
'class' => 'pretty-checkbox',
));
return html::div('auth-options',
html::label(null, $checkbox->show() . ' ' . $this->plugin->gettext('storepasswords'))
. html::p('description hint', $this->plugin->gettext('storepasswordsdesc'))
);
}
/**
* Template object for sharing form
*/
public function folder_share_form($attrib)
{
$folder = rcube_utils::get_input_value('_folder', rcube_utils::INPUT_GET, true);
$info = $this->get_share_info($folder);
if (empty($info) || empty($info['form'])) {
$msg = $this->plugin->gettext($info === false ? 'sharepermissionerror' : 'sharestorageerror');
return html::div(array('class' => 'boxerror', 'id' => 'share-notice'), rcube::Q($msg));
}
if (empty($attrib['id'])) {
$attrib['id'] = 'foldershareform';
}
$out = '';
foreach ($info['form'] as $mode => $tab) {
$table = new html_table(array(
'cols' => ($tab['list_column'] ? 1 : count($tab['form'])) + 1,
'data-mode' => $mode,
'data-single' => $tab['single'] ? 1 : 0,
));
$submit = new html_button(array('class' => 'btn btn-secondary submit'));
$delete = new html_button(array('class' => 'btn btn-secondary btn-danger delete'));
$fields = array();
// Table header
if (!empty($tab['list_column'])) {
$table->add_header(null, rcube::Q($tab['list_column_label']));
}
else {
foreach ($tab['form'] as $field) {
$table->add_header(null, rcube::Q($field['title']));
}
}
$table->add_header(null, '');
// Submit form
$record = '';
foreach ($tab['form'] as $index => $field) {
$add = '';
if ($field['type'] == 'select') {
$ff = new html_select(array('name' => $index));
foreach ($field['options'] as $opt_idx => $opt) {
$ff->add($opt, $opt_idx);
}
}
else if ($field['type'] == 'password') {
$ff = new html_passwordfield(array(
'name' => $index,
'placeholder' => $this->rc->gettext('password'),
));
$add = new html_passwordfield(array(
'name' => $index . 'confirm',
'placeholder' => $this->plugin->gettext('confirmpassword'),
));
$add = $add->show();
}
else {
$ff = new html_inputfield(array(
'name' => $index,
'data-autocomplete' => $field['autocomplete'],
'placeholder' => $field['placeholder'],
));
}
if (!empty($tab['list_column'])) {
$record .= $ff->show() . $add;
}
else {
$table->add(null, $ff->show() . $add);
}
$fields[$index] = $ff;
}
if (!empty($tab['list_column'])) {
$table->add('form', $record);
}
$hidden = '';
foreach ((array) $tab['extra_fields'] as $key => $default) {
$h = new html_hiddenfield(array('name' => $key, 'value' => $default));
$hidden .= $h->show();
}
$table->add(null, $hidden . $submit->show(rcube::Q($tab['label'] ?: $this->plugin->gettext('submit'))));
// Existing entries
foreach ((array) $info['rights'] as $entry) {
if ($entry['mode'] == $mode) {
if (!empty($tab['list_column'])) {
$table->add(null, html::span(array('title' => $entry['title'], 'class' => 'name'), rcube::Q($entry[$tab['list_column']])));
}
else {
foreach ($tab['form'] as $index => $field) {
if ($fields[$index] instanceof html_select) {
$table->add(null, $fields[$index]->show($entry[$index]));
}
else if ($fields[$index] instanceof html_inputfield) {
$table->add(null, html::span(array('title' => $entry['title'], 'class' => 'name'), rcube::Q($entry[$index])));
}
}
}
$hidden = '';
foreach ((array) $tab['extra_fields'] as $key => $default) {
if (isset($entry[$key])) {
$h = new html_hiddenfield(array('name' => $key, 'value' => $entry[$key]));
$hidden .= $h->show();
}
}
$table->add(null, $hidden . $delete->show(rcube::Q($this->rc->gettext('delete'))));
}
}
$this->rc->output->add_label('kolab_files.updatingfolder' . $mode);
$out .= html::tag('fieldset', $mode, html::tag('legend', null, rcube::Q($tab['title'])) . $table->show()) . "\n";
}
$this->rc->autocomplete_init();
$this->rc->output->set_env('folder', $folder);
$this->rc->output->set_env('form_info', $info['form']);
$this->rc->output->add_gui_object('shareform', $attrib['id']);
$this->rc->output->add_label('kolab_files.submit', 'kolab_files.passwordconflict', 'delete');
return html::div($attrib, $out);
}
/**
* Template object for file edit dialog/warnings
*/
public function file_edit_dialog($attrib)
{
$this->plugin->add_label('select', 'create', 'cancel', 'editfiledialog', 'editfilesessions',
'newsession', 'ownedsession', 'invitedsession', 'joinsession', 'editfilero', 'editfilerotitle',
'newsessionro'
);
return '<div></div>';
}
/**
* Template object for file session dialog
*/
public function file_session_dialog($attrib)
{
$this->plugin->add_label('join', 'open', 'close', 'request', 'cancel',
'sessiondialog', 'sessiondialogcontent');
return '<div></div>';
}
/**
* Template object for dcument editors dialog
*/
public function document_editors_dialog($attrib)
{
$table = new html_table($attrib + array('cols' => 3, 'border' => 0, 'cellpadding' => 0));
$table->add_header('username', $this->plugin->gettext('participant'));
$table->add_header('status', $this->plugin->gettext('status'));
$table->add_header('options', null);
$input = new html_inputfield(array('name' => 'participant', 'id' => 'invitation-editor-name', 'size' => 30, 'class' => 'form-control'));
$textarea = new html_textarea(array('name' => 'comment', 'id' => 'invitation-comment',
'rows' => 4, 'cols' => 55, 'class' => 'form-control', 'title' => $this->plugin->gettext('invitationtexttitle')));
$button = new html_inputfield(array('type' => 'button', 'class' => 'button', 'id' => 'invitation-editor-add',
'value' => $this->plugin->gettext('addparticipant')));
$this->plugin->add_label('manageeditors', 'statusorganizer', 'addparticipant');
// initialize attendees autocompletion
$this->rc->autocomplete_init();
return html::div(null, $table->show() . html::div(null,
html::div('form-searchbar', $input->show() . " " . $button->show())
. html::p('attendees-commentbox', html::label(null,
$this->plugin->gettext('invitationtextlabel') . $textarea->show())
)
));
}
/**
* Template object for file_rename form
*/
public function file_rename_form($attrib)
{
$attrib['name'] = 'file-rename-form';
if (empty($attrib['id'])) {
$attrib['id'] = 'file-rename-form';
}
$input_name = new html_inputfield(array('id' => 'file-rename-name', 'name' => 'name', 'size' => 50));
$table = new html_table(array('cols' => 2, 'class' => 'propform'));
$table->add('title', html::label('file-rename-name', rcube::Q($this->plugin->gettext('filename'))));
$table->add(null, $input_name->show());
$out = $table->show();
// add form tag around text field
if (empty($attrib['form'])) {
$out = $this->rc->output->form_tag($attrib, $out);
}
$this->plugin->add_label('save', 'cancel', 'fileupdating', 'renamefile');
$this->rc->output->add_gui_object('file-rename-form', $attrib['id']);
return $out;
}
/**
* Template object for file_create form
*/
public function file_create_form($attrib)
{
$attrib['name'] = 'file-create-form';
if (empty($attrib['id'])) {
$attrib['id'] = 'file-create-form';
}
$input_name = new html_inputfield(array('id' => 'file-create-name', 'name' => 'name', 'size' => 30));
$select_parent = new html_select(array('id' => 'file-create-parent', 'name' => 'parent'));
$select_type = new html_select(array('id' => 'file-create-type', 'name' => 'type'));
$table = new html_table(array('cols' => 2, 'class' => 'propform'));
$types = array();
foreach ($this->get_mimetypes('edit') as $type => $mimetype) {
$types[$type] = $mimetype['ext'];
$select_type->add($mimetype['label'], $type);
}
$table->add('title', html::label('file-create-name', rcube::Q($this->plugin->gettext('filename'))));
$table->add(null, $input_name->show());
$table->add('title', html::label('file-create-type', rcube::Q($this->plugin->gettext('type'))));
$table->add(null, $select_type->show());
$table->add('title', html::label('file-create-parent', rcube::Q($this->plugin->gettext('folderinside'))));
$table->add(null, $select_parent->show());
$out = $table->show();
// add form tag around text field
if (empty($attrib['form'])) {
$out = $this->rc->output->form_tag($attrib, $out);
}
$this->plugin->add_label('create', 'cancel', 'filecreating', 'createfile', 'createandedit',
'copyfile', 'copyandedit');
$this->rc->output->add_gui_object('file-create-form', $attrib['id']);
$this->rc->output->set_env('file_extensions', $types);
return $out;
}
/**
* Template object for file search form in "From cloud" dialog
*/
public function file_search_form($attrib)
{
$attrib += array(
'name' => '_q',
'gui-object' => 'filesearchbox',
'form-name' => 'filesearchform',
'command' => 'files-search',
'reset-command' => 'files-search-reset',
);
// add form tag around text field
return $this->rc->output->search_form($attrib);
}
/**
* Template object for files list
*/
public function file_list($attrib)
{
return $this->list_handler($attrib, 'files');
}
/**
* Template object for sessions list
*/
public function sessions_list($attrib)
{
return $this->list_handler($attrib, 'sessions');
}
/**
* Creates unified template object for files|sessions list
*/
protected function list_handler($attrib, $type = 'files')
{
$prefix = 'kolab_' . $type . '_';
$c_prefix = 'kolab_files' . ($type != 'files' ? '_' . $type : '') . '_';
// define list of cols to be displayed based on parameter or config
if (empty($attrib['columns'])) {
$list_cols = $this->rc->config->get($c_prefix . 'list_cols');
$dont_override = $this->rc->config->get('dont_override');
$a_show_cols = is_array($list_cols) ? $list_cols : array('name');
$this->rc->output->set_env($type . '_col_movable', !in_array($c_prefix . 'list_cols', (array)$dont_override));
}
else {
$columns = str_replace(array("'", '"'), '', $attrib['columns']);
$a_show_cols = preg_split('/[\s,;]+/', $columns);
}
// make sure 'name' and 'options' column is present
if (!in_array('name', $a_show_cols)) {
array_unshift($a_show_cols, 'name');
}
if (!in_array('options', $a_show_cols)) {
array_unshift($a_show_cols, 'options');
}
$attrib['columns'] = $a_show_cols;
// save some variables for use in ajax list
$_SESSION[$prefix . 'list_attrib'] = $attrib;
// For list in dialog(s) remove all option-like columns
if ($this->rc->task != 'files') {
$a_show_cols = array_intersect($a_show_cols, $this->{$type . '_sort_cols'});
}
// set default sort col/order to session
if (!isset($_SESSION[$prefix . 'sort_col']))
$_SESSION[$prefix . 'sort_col'] = $this->rc->config->get($c_prefix . 'sort_col') ?: 'name';
if (!isset($_SESSION[$prefix . 'sort_order']))
$_SESSION[$prefix . 'sort_order'] = strtoupper($this->rc->config->get($c_prefix . 'sort_order') ?: 'asc');
// set client env
$this->rc->output->add_gui_object($type . 'list', $attrib['id']);
$this->rc->output->set_env($type . '_sort_col', $_SESSION[$prefix . 'sort_col']);
$this->rc->output->set_env($type . '_sort_order', $_SESSION[$prefix . 'sort_order']);
$this->rc->output->set_env($type . '_coltypes', $a_show_cols);
$this->rc->output->include_script('list.js');
$this->rc->output->add_label('kolab_files.abort', 'searching');
// attach css rules for mimetype icons
if (!$this->filetypes_style) {
$this->plugin->include_stylesheet($this->url . '/skins/default/images/mimetypes/style.css');
$this->filetypes_style = true;
}
$thead = '';
foreach ($this->list_head($attrib, $a_show_cols, $type) as $cell) {
$thead .= html::tag('th', array('class' => $cell['className'], 'id' => $cell['id']), $cell['html']);
}
return html::tag('table', $attrib,
html::tag('thead', null, html::tag('tr', null, $thead)) . html::tag('tbody', null, ''),
array('style', 'class', 'id', 'cellpadding', 'cellspacing', 'border', 'summary'));
}
/**
* Creates <THEAD> for message list table
*/
protected function list_head($attrib, $a_show_cols, $type = 'files')
{
$prefix = 'kolab_' . $type . '_';
$c_prefix = 'kolab_files_' . ($type != 'files' ? $type : '') . '_';
- $skin_path = $_SESSION['skin_path'];
+ $skin_path = $_SESSION['skin_path'] ?? null;
// check to see if we have some settings for sorting
$sort_col = $_SESSION[$prefix . 'sort_col'];
$sort_order = $_SESSION[$prefix . 'sort_order'];
$dont_override = (array)$this->rc->config->get('dont_override');
$disabled_sort = in_array($c_prefix . 'sort_col', $dont_override);
$disabled_order = in_array($c_prefix . 'sort_order', $dont_override);
$this->rc->output->set_env($prefix . 'disabled_sort_col', $disabled_sort);
$this->rc->output->set_env($prefix . 'disabled_sort_order', $disabled_order);
// define sortable columns
if ($disabled_sort)
$a_sort_cols = $sort_col && !$disabled_order ? array($sort_col) : array();
else
$a_sort_cols = $this->{$type . '_sort_cols'};
if (!empty($attrib['optionsmenuicon'])) {
$onclick = 'return ' . rcmail_output::JS_OBJECT_NAME . ".command('menu-open', '{$type}listmenu', this, event)";
$inner = $this->rc->gettext('listoptions');
if (is_string($attrib['optionsmenuicon']) && $attrib['optionsmenuicon'] != 'true') {
$inner = html::img(array('src' => $skin_path . $attrib['optionsmenuicon'], 'alt' => $this->rc->gettext('listoptions')));
}
$list_menu = html::a(array(
'href' => '#list-options',
'onclick' => $onclick,
'class' => 'listmenu',
'id' => $type . 'listmenulink',
'title' => $this->rc->gettext('listoptions'),
'tabindex' => '0',
), $inner);
}
else {
$list_menu = '';
}
$cells = array();
foreach ($a_show_cols as $col) {
// get column name
switch ($col) {
case 'options':
$col_name = $list_menu;
break;
default:
$col_name = rcube::Q($this->plugin->gettext($col));
}
// make sort links
if (in_array($col, $a_sort_cols)) {
$col_name = html::a(array(
'href' => "#sort",
'onclick' => 'return ' . rcmail_output::JS_OBJECT_NAME . ".command('$type-sort','$col',this)",
'title' => $this->plugin->gettext('sortby')
), $col_name);
}
- else if ($col_name[0] != '<') {
+ else if (empty($col_name) || $col_name[0] != '<') {
$col_name = '<span class="' . $col .'">' . $col_name . '</span>';
}
$sort_class = $col == $sort_col && !$disabled_order ? " sorted$sort_order" : '';
$class_name = $col.$sort_class;
// put it all together
$cells[] = array('className' => $class_name, 'id' => "rcm$col", 'html' => $col_name);
}
return $cells;
}
/**
* Update files|sessions list object
*/
protected function list_update($prefs, $type = 'files')
{
$prefix = 'kolab_' . $type . '_list_';
$c_prefix = 'kolab_files' . ($type != 'files' ? '_' . $type : '') . '_list_';
$attrib = $_SESSION[$prefix . 'attrib'];
if (!empty($prefs[$c_prefix . 'cols'])) {
$attrib['columns'] = $prefs[$c_prefix . 'cols'];
$_SESSION[$prefix . 'attrib'] = $attrib;
}
$a_show_cols = $attrib['columns'];
$head = '';
foreach ($this->list_head($attrib, $a_show_cols, $type) as $cell) {
$head .= html::tag('th', array('class' => $cell['className'], 'id' => $cell['id']), $cell['html']);
}
$head = html::tag('tr', null, $head);
$this->rc->output->set_env($type . '_coltypes', $a_show_cols);
$this->rc->output->command($type . '_list_update', $head);
}
/**
* Template object for file info box
*/
public function file_info_box($attrib)
{
// print_r($this->file_data, true);
$table = new html_table(array('cols' => 2, 'class' => $attrib['class']));
// file name
$table->add('title', $this->plugin->gettext('name').':');
$table->add('data filename', $this->file_data['name']);
// file type
// @TODO: human-readable type name
$table->add('title', $this->plugin->gettext('type').':');
$table->add('data filetype', $this->file_data['type']);
// file size
$table->add('title', $this->plugin->gettext('size').':');
$table->add('data filesize', $this->rc->show_bytes($this->file_data['size']));
// file modification time
$table->add('title', $this->plugin->gettext('mtime').':');
$table->add('data filemtime', $this->file_data['mtime']);
// @TODO: for images: width, height, color depth, etc.
// @TODO: for text files: count of characters, lines, words
return $table->show();
}
/**
* Template object for file preview frame
*/
public function file_preview_frame($attrib)
{
if (empty($attrib['id'])) {
$attrib['id'] = 'filepreviewframe';
}
- if ($frame = $this->file_data['viewer']['frame']) {
+ if ($frame = ($this->file_data['viewer']['frame'] ?? null)) {
return $frame;
}
if ($href = $this->file_data['viewer']['href']) {
// file href attribute must be an absolute URL (Bug #2063)
if (!empty($href)) {
if (!preg_match('|^https?://|', $href)) {
$href = $this->url . '/api/' . $href;
}
}
}
else {
$token = $this->get_api_token();
$href = $this->url . '/api/?method=file_get'
. '&file=' . urlencode($this->file_data['filename'])
. '&token=' . urlencode($token);
}
$this->rc->output->add_gui_object('preview_frame', $attrib['id']);
$attrib['allowfullscreen'] = true;
$attrib['src'] = $href;
$attrib['onload'] = 'kolab_files_frame_load(this)';
+ $form = null;
// editor requires additional arguments via POST
if (!empty($this->file_data['viewer']['post'])) {
$attrib['src'] = 'program/resources/blank.gif';
$form_content = new html_hiddenfield();
$form_attrib = array(
'action' => $href,
'id' => $attrib['id'] . '-form',
'target' => $attrib['name'],
'method' => 'post',
);
foreach ($this->file_data['viewer']['post'] as $name => $value) {
$form_content->add(array('name' => $name, 'value' => $value));
}
$form = html::tag('form', $form_attrib, $form_content->show())
. html::script(array(), "\$('#{$attrib['id']}-form').submit()");
}
return html::iframe($attrib) . $form;
}
/**
* Template object for quota display
*/
public function quota_display($attrib)
{
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmquotadisplay';
}
$quota_type = !empty($attrib['display']) ? $attrib['display'] : 'text';
$this->rc->output->add_gui_object('quotadisplay', $attrib['id']);
$this->rc->output->set_env('quota_type', $quota_type);
// get quota
$token = $this->get_api_token();
$request = $this->get_request(array('method' => 'quota'), $token);
// send request to the API
try {
$response = $request->send();
$status = $response->getStatus();
$body = @json_decode($response->getBody(), true);
if ($status == 200 && $body['status'] == 'OK') {
$quota = $body['result'];
}
else {
throw new Exception($body['reason'] ?: "Failed to get quota. Status: $status");
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
$quota = array('total' => 0, 'percent' => 0);
}
$quota = rcube_output::json_serialize($quota);
$this->rc->output->add_script(rcmail_output::JS_OBJECT_NAME . ".files_set_quota($quota);", 'docready');
return html::span($attrib, '');
}
/**
* Get API token for current user session, authenticate if needed
*/
public function get_api_token($configure = true)
{
- $token = $_SESSION['kolab_files_token'];
- $time = $_SESSION['kolab_files_time'];
+ $token = $_SESSION['kolab_files_token'] ?? null;
+ $time = $_SESSION['kolab_files_time'] ?? null;
if ($token && time() - $this->timeout < $time) {
if (time() - $time <= $this->timeout / 2) {
return $token;
}
}
$request = $this->get_request(array('method' => 'ping'), $token);
try {
$url = $request->getUrl();
// Send ping request
if ($token) {
$url->setQueryVariables(array('method' => 'ping'));
$request->setUrl($url);
$response = $request->send();
$status = $response->getStatus();
if ($status == 200 && ($body = json_decode($response->getBody(), true))) {
if ($body['status'] == 'OK') {
$_SESSION['kolab_files_time'] = time();
return $token;
}
}
}
// Go with authenticate request
$url->setQueryVariables(array('method' => 'authenticate', 'version' => self::API_VERSION));
$request->setUrl($url);
$request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
// Allow plugins (e.g. kolab_sso) to modify the request
$this->rc->plugins->exec_hook('chwala_authenticate', array('request' => $request));
$response = $request->send();
$status = $response->getStatus();
if ($status == 200 && ($body = json_decode($response->getBody(), true))) {
$token = $body['result']['token'];
if ($token) {
$_SESSION['kolab_files_token'] = $token;
$_SESSION['kolab_files_time'] = time();
$_SESSION['kolab_files_caps'] = $body['result']['capabilities'];
}
}
else {
throw new Exception(sprintf("Authenticate error (Status: %d)", $status));
}
// Configure session
if ($configure && $token) {
$this->configure($token);
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
}
return $token;
}
protected function capabilities()
{
if (empty($_SESSION['kolab_files_caps'])) {
$token = $this->get_api_token();
if (empty($_SESSION['kolab_files_caps'])) {
$request = $this->get_request(array('method' => 'capabilities'), $token);
// send request to the API
try {
$response = $request->send();
$status = $response->getStatus();
$body = @json_decode($response->getBody(), true);
if ($status == 200 && $body['status'] == 'OK') {
$_SESSION['kolab_files_caps'] = $body['result'];
}
else {
throw new Exception($body['reason'] ?: "Failed to get capabilities. Status: $status");
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
return array();
}
}
}
- if ($_SESSION['kolab_files_caps']['MANTICORE'] || $_SESSION['kolab_files_caps']['WOPI']) {
+ if (($_SESSION['kolab_files_caps']['MANTICORE'] ?? false) || ($_SESSION['kolab_files_caps']['WOPI'] ?? false)) {
$_SESSION['kolab_files_caps']['DOCEDIT'] = true;
$_SESSION['kolab_files_caps']['DOCTYPE'] = $_SESSION['kolab_files_caps']['MANTICORE'] ? 'manticore' : 'wopi';
}
if (!empty($_SESSION['kolab_files_caps']) && !isset($_SESSION['kolab_files_caps']['MOUNTPOINTS'])) {
$_SESSION['kolab_files_caps']['MOUNTPOINTS'] = array();
}
return $_SESSION['kolab_files_caps'];
}
/**
* Initialize HTTP_Request object
*/
protected function get_request($get = null, $token = null)
{
$url = $this->url_srv . '/api/';
if (empty($this->request)) {
$config = array(
'store_body' => true,
'follow_redirects' => true,
);
$this->request = libkolab::http_request($url, 'GET', $config);
}
else {
// cleanup
try {
$this->request->setBody('');
$this->request->setUrl($url);
$this->request->setMethod(HTTP_Request2::METHOD_GET);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
}
if ($token) {
$this->request->setHeader('X-Session-Token', $token);
}
if (!empty($get)) {
$url = $this->request->getUrl();
$url->setQueryVariables($get);
$this->request->setUrl($url);
}
// some HTTP server configurations require this header
$this->request->setHeader('accept', "application/json,text/javascript,*/*");
// Localization
$this->request->setHeader('accept-language', $_SESSION['language']);
// set Referer which is used as an origin for cross-window
// communication with document editor iframe
$host = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'];
$this->request->setHeader('referer', $host);
return $this->request;
}
/**
* Configure chwala session
*/
public function configure($token = null, $prefs = array())
{
if (!$token) {
$token = $this->get_api_token(false);
}
try {
// Configure session
$query = array(
'method' => 'configure',
- 'timezone' => $prefs['timezone'] ?: $this->rc->config->get('timezone'),
- 'date_format' => $prefs['date_long'] ?: $this->rc->config->get('date_long', 'Y-m-d H:i'),
+ 'timezone' => $prefs['timezone'] ?? $this->rc->config->get('timezone'),
+ 'date_format' => $prefs['date_long'] ?? $this->rc->config->get('date_long', 'Y-m-d H:i'),
);
$request = $this->get_request($query, $token);
$response = $request->send();
$status = $response->getStatus();
if ($status != 200) {
throw new Exception(sprintf("Failed to configure chwala session (Status: %d)", $status));
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
}
}
/**
* Handler for main files interface (Files task)
*/
protected function action_index()
{
$this->plugin->add_label(
'uploading', 'attaching', 'uploadsizeerror',
'filedeleting', 'filedeletenotice', 'filedeleteconfirm',
'filemoving', 'filemovenotice', 'filemoveconfirm', 'filecopying', 'filecopynotice',
'fileskip', 'fileskipall', 'fileoverwrite', 'fileoverwriteall'
);
$this->folder_list_env();
if ($this->rc->task == 'files') {
$this->rc->output->set_env('folder', rcube_utils::get_input_value('folder', rcube_utils::INPUT_GET));
$this->rc->output->set_env('collection', rcube_utils::get_input_value('collection', rcube_utils::INPUT_GET));
}
$caps = $this->capabilities();
$this->rc->output->add_label('uploadprogress', 'GB', 'MB', 'KB', 'B');
$this->rc->output->set_pagetitle($this->plugin->gettext('files'));
$this->rc->output->set_env('file_mimetypes', $this->get_mimetypes());
$this->rc->output->set_env('files_quota', $caps['QUOTA']);
$this->rc->output->set_env('files_max_upload', $caps['MAX_UPLOAD']);
$this->rc->output->set_env('files_progress_name', $caps['PROGRESS_NAME'] ?? null);
$this->rc->output->set_env('files_progress_time', $caps['PROGRESS_TIME'] ?? null);
$this->rc->output->send('kolab_files.files');
}
/**
* Handler for resetting some session/cached information
*/
protected function action_reset()
{
$this->rc->session->remove('kolab_files_caps');
if (($caps = $this->capabilities()) && !empty($caps)) {
$this->rc->output->set_env('files_caps', $caps);
}
}
/**
* Handler for preferences save action
*/
protected function action_prefs()
{
$dont_override = (array)$this->rc->config->get('dont_override');
$prefs = array();
$type = rcube_utils::get_input_value('type', rcube_utils::INPUT_POST);
$opts = array(
'kolab_files_sort_col' => true,
'kolab_files_sort_order' => true,
'kolab_files_list_cols' => false,
);
foreach ($opts as $o => $sess) {
if (isset($_POST[$o])) {
$value = rcube_utils::get_input_value($o, rcube_utils::INPUT_POST);
$session_key = $o;
$config_key = $o;
if ($type != 'files') {
$config_key = str_replace('files', 'files_' . $type, $config_key);
}
if (in_array($config_key, $dont_override)) {
continue;
}
if ($o == 'kolab_files_list_cols') {
$update_list = true;
}
$prefs[$config_key] = $value;
if ($sess) {
$_SESSION[$session_key] = $prefs[$config_key];
}
}
}
// save preference values
if (!empty($prefs)) {
$this->rc->user->save_prefs($prefs);
}
if (!empty($update_list)) {
$this->list_update($prefs, $type);
}
$this->rc->output->send();
}
/**
* Handler for file open action
*/
protected function action_open()
{
$this->rc->output->set_env('file_mimetypes', $this->get_mimetypes());
$this->file_opener(intval($_GET['_viewer']) & ~4);
}
/**
* Handler for file open action
*/
protected function action_edit()
{
$this->plugin->add_label('sessionterminating', 'unsavedchanges', 'documentinviting',
'documentcancelling', 'removeparticipant', 'sessionterminated', 'sessionterminatedtitle');
$this->file_opener(intval($_GET['_viewer']));
}
/**
* Handler for folder sharing action
*/
protected function action_share()
{
$this->rc->output->add_handler('share-form', array($this, 'folder_share_form'));
$this->rc->output->send('kolab_files.share');
}
/**
* Handler for "save all attachments into cloud" action
*/
protected function action_save_file()
{
// $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
$uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
$dest = rcube_utils::get_input_value('dest', rcube_utils::INPUT_POST);
$id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
$name = rcube_utils::get_input_value('name', rcube_utils::INPUT_POST);
$temp_dir = unslashify($this->rc->config->get('temp_dir'));
$message = new rcube_message($uid);
$request = $this->get_request();
$url = $request->getUrl();
$files = array();
$errors = array();
$attachments = array();
$request->setMethod(HTTP_Request2::METHOD_POST);
$request->setHeader('X-Session-Token', $this->get_api_token());
$url->setQueryVariables(array('method' => 'file_upload', 'folder' => $dest));
$request->setUrl($url);
foreach ($message->attachments as $attach_prop) {
if (empty($id) || $id == $attach_prop->mime_id) {
$filename = strlen($name) ? $name : rcmail_attachment_name($attach_prop, true);
$attachments[$filename] = $attach_prop;
}
}
// @TODO: handle error
// @TODO: implement file upload using file URI instead of body upload
foreach ($attachments as $attach_name => $attach_prop) {
$path = tempnam($temp_dir, 'rcmAttmnt');
// save attachment to file
if ($fp = fopen($path, 'w+')) {
$message->get_part_body($attach_prop->mime_id, false, 0, $fp);
}
else {
$errors[] = true;
rcube::raise_error(array(
'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
'message' => "Unable to save attachment into file $path"),
true, false);
continue;
}
fclose($fp);
// send request to the API
try {
$request->setBody('');
$request->addUpload('file[]', $path, $attach_name, $attach_prop->mimetype);
$response = $request->send();
$status = $response->getStatus();
$body = @json_decode($response->getBody(), true);
if ($status == 200 && $body['status'] == 'OK') {
$files[] = $attach_name;
}
else {
throw new Exception($body['reason'] ?: "Failed to post file_upload. Status: $status");
}
}
catch (Exception $e) {
unlink($path);
$errors[] = $e->getMessage();
rcube::raise_error(array(
'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
'message' => $e->getMessage()),
true, false);
continue;
}
// clean up
unlink($path);
$request->setBody('');
}
if ($count = count($files)) {
$msg = $this->plugin->gettext(array('name' => 'saveallnotice', 'vars' => array('n' => $count)));
$this->rc->output->show_message($msg, 'confirmation');
}
if ($count = count($errors)) {
$msg = $this->plugin->gettext(array('name' => 'saveallerror', 'vars' => array('n' => $count)));
$this->rc->output->show_message($msg, 'error');
}
// @TODO: update quota indicator, make this optional in case files aren't stored in IMAP
$this->rc->output->send();
}
/**
* Handler for "add attachments from the cloud" action
*/
protected function action_attach_file()
{
$files = rcube_utils::get_input_value('files', rcube_utils::INPUT_POST);
$uploadid = rcube_utils::get_input_value('uploadid', rcube_utils::INPUT_POST);
$COMPOSE_ID = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
$COMPOSE = null;
$errors = array();
$attachments = array();
if ($this->rc->task == 'mail') {
if ($COMPOSE_ID && $_SESSION['compose_data_'.$COMPOSE_ID]) {
$COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID];
}
if (!$COMPOSE) {
die("Invalid session var!");
}
// attachment upload action
if (!is_array($COMPOSE['attachments'])) {
$COMPOSE['attachments'] = array();
}
}
// clear all stored output properties (like scripts and env vars)
$this->rc->output->reset();
$temp_dir = unslashify($this->rc->config->get('temp_dir'));
$request = $this->get_request();
$url = $request->getUrl();
// Use observer object to store HTTP response into a file
require_once $this->plugin->home . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'kolab_files_observer.php';
$observer = new kolab_files_observer();
$request->setHeader('X-Session-Token', $this->get_api_token());
// download files from the API and attach them
foreach ($files as $file) {
// decode filename
$file = urldecode($file);
// get file information
try {
$url->setQueryVariables(array('method' => 'file_info', 'file' => $file));
$request->setUrl($url);
$response = $request->send();
$status = $response->getStatus();
$body = @json_decode($response->getBody(), true);
if ($status == 200 && $body['status'] == 'OK') {
$file_params = $body['result'];
}
else {
throw new Exception($body['reason'] ?: "Failed to get file_info. Status: $status");
}
}
catch (Exception $e) {
$errors[] = $e->getMessage();
rcube::raise_error(array(
'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
'message' => $e->getMessage()),
true, false);
continue;
}
// set location of downloaded file
$path = tempnam($temp_dir, 'rcmAttmnt');
$observer->set_file($path);
// download file
try {
$url->setQueryVariables(array('method' => 'file_get', 'file' => $file));
$request->setUrl($url);
$request->attach($observer);
$response = $request->send();
$status = $response->getStatus();
$response->getBody(); // returns nothing
$request->detach($observer);
if ($status != 200 || !file_exists($path)) {
throw new Exception("Unable to save file");
}
}
catch (Exception $e) {
$errors[] = $e->getMessage();
rcube::raise_error(array(
'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
'message' => $e->getMessage()),
true, false);
continue;
}
$attachment = array(
'path' => $path,
'size' => $file_params['size'],
'name' => $file_params['name'],
'mimetype' => $file_params['type'],
'group' => $COMPOSE_ID,
);
if ($this->rc->task != 'mail') {
$attachments[] = $attachment;
continue;
}
$attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status'] && !$attachment['abort']) {
$this->compose_attach_success($attachment, $COMPOSE, $COMPOSE_ID, $uploadid);
}
else if ($attachment['error']) {
$errors[] = $attachment['error'];
}
else {
$errors[] = $this->plugin->gettext('attacherror');
}
}
if (!empty($errors)) {
$this->rc->output->command('display_message', $this->plugin->gettext('attacherror'), 'error');
$this->rc->output->command('remove_from_attachment_list', $uploadid);
}
else if ($this->rc->task == 'calendar' || $this->rc->task == 'tasks') {
// for uploads in events/tasks we'll use its standard upload handler,
// for this we have to fake $_FILES and some other POST args
foreach ($attachments as $attach) {
$_FILES['_attachments']['tmp_name'][] = $attachment['path'];
$_FILES['_attachments']['name'][] = $attachment['name'];
$_FILES['_attachments']['size'][] = $attachment['size'];
$_FILES['_attachments']['type'][] = $attachment['mimetype'];
$_FILES['_attachments']['error'][] = null;
}
$_GET['_uploadid'] = $uploadid;
$_GET['_id'] = $COMPOSE_ID;
switch ($this->rc->task) {
case 'tasks':
$handler = new kolab_attachments_handler();
$handler->attachment_upload(tasklist::SESSION_KEY);
break;
case 'calendar':
$handler = new kolab_attachments_handler();
$handler->attachment_upload(calendar::SESSION_KEY, 'cal-');
break;
}
}
// send html page with JS calls as response
$this->rc->output->command('auto_save_start', false);
$this->rc->output->send();
}
protected function compose_attach_success($attachment, $COMPOSE, $COMPOSE_ID, $uploadid)
{
$id = $attachment['id'];
// store new attachment in session
unset($attachment['data'], $attachment['status'], $attachment['abort']);
$this->rc->session->append('compose_data_' . $COMPOSE_ID . '.attachments', $id, $attachment);
if (($icon = $COMPOSE['deleteicon']) && is_file($icon)) {
$button = html::img(array(
'src' => $icon,
'alt' => $this->rc->gettext('delete')
));
}
else if ($COMPOSE['textbuttons']) {
$button = rcube::Q($this->rc->gettext('delete'));
}
else {
$button = '';
}
if (version_compare(version_parse(RCMAIL_VERSION), '1.3.0', '>=')) {
$link_content = sprintf('%s <span class="attachment-size"> (%s)</span>',
rcube::Q($attachment['name']), $this->rc->show_bytes($attachment['size']));
$content_link = html::a(array(
'href' => "#load",
'class' => 'filename',
'onclick' => sprintf("return %s.command('load-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
), $link_content);
$delete_link = html::a(array(
'href' => "#delete",
'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
'title' => $this->rc->gettext('delete'),
'class' => 'delete',
'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'],
), $button);
$content = $COMPOSE['icon_pos'] == 'left' ? $delete_link.$content_link : $content_link.$delete_link;
}
else {
$content = html::a(array(
'href' => "#delete",
'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this)", rcmail_output::JS_OBJECT_NAME, $id),
'title' => $this->rc->gettext('delete'),
'class' => 'delete',
), $button);
$content .= rcube::Q($attachment['name']);
}
$this->rc->output->command('add2attachment_list', "rcmfile$id", array(
'html' => $content,
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
'complete' => true), $uploadid);
}
/**
* Handler for file open/edit action
*/
protected function file_opener($viewer)
{
$file = rcube_utils::get_input_value('_file', rcube_utils::INPUT_GET);
$session = rcube_utils::get_input_value('_session', rcube_utils::INPUT_GET);
// get file info
$token = $this->get_api_token();
$request = $this->get_request(array(
'method' => 'file_info',
'file' => $file,
'viewer' => $viewer,
'session' => $session,
), $token);
// send request to the API
try {
$response = $request->send();
$status = $response->getStatus();
$body = @json_decode($response->getBody(), true);
if ($status == 200 && $body['status'] == 'OK') {
$this->file_data = $body['result'];
}
else {
throw new Exception($body['reason'] ?: "Failed to get file_info. Status: $status");
}
}
catch (Exception $e) {
rcube::raise_error(array(
'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
'message' => $e->getMessage()),
true, true);
}
if ($file === null || $file === '') {
$file = $this->file_data['file'];
}
$this->file_data['filename'] = $file;
$this->plugin->add_label('filedeleteconfirm', 'filedeleting', 'filedeletenotice', 'terminate');
// register template objects for dialogs (and main interface)
$this->rc->output->add_handlers(array(
'fileinfobox' => array($this, 'file_info_box'),
'filepreviewframe' => array($this, 'file_preview_frame'),
));
$placeholder = $this->rc->output->asset_url('program/resources/blank.gif');
- if ($this->file_data['viewer']['wopi']) {
+ $editor_type = null;
+ $got_editor = null;
+ if ($this->file_data['viewer']['wopi'] ?? false) {
$editor_type = 'wopi';
$got_editor = ($viewer & 4);
}
- else if ($this->file_data['viewer']['manticore']) {
+ else if ($this->file_data['viewer']['manticore'] ?? false) {
$editor_type = 'manticore';
$got_editor = ($viewer & 4);
}
// this one is for styling purpose
$this->rc->output->set_env('extwin', true);
$this->rc->output->set_env('file', $file);
$this->rc->output->set_env('file_data', $this->file_data);
$this->rc->output->set_env('mimetype', $this->file_data['type']);
$this->rc->output->set_env('filename', pathinfo($file, PATHINFO_BASENAME));
$this->rc->output->set_env('editor_type', $editor_type);
$this->rc->output->set_env('photo_placeholder', $placeholder);
$this->rc->output->set_pagetitle(rcube::Q($file));
$this->rc->output->send('kolab_files.' . ($got_editor ? 'docedit' : 'filepreview'));
}
/**
* Returns mimetypes supported by File API viewers
*/
protected function get_mimetypes($type = 'view')
{
$mimetypes = array();
// send request to the API
try {
if ($this->mimetypes === null) {
$this->mimetypes = false;
$token = $this->get_api_token();
$caps = $this->capabilities();
$request = $this->get_request(array('method' => 'mimetypes'), $token);
$response = $request->send();
$status = $response->getStatus();
$body = @json_decode($response->getBody(), true);
if ($status == 200 && $body['status'] == 'OK') {
$this->mimetypes = $body['result'];
}
else {
throw new Exception($body['reason'] ?: "Failed to get mimetypes. Status: $status");
}
}
if (is_array($this->mimetypes)) {
if (array_key_exists($type, $this->mimetypes)) {
$mimetypes = $this->mimetypes[$type];
}
// fallback to static definition if old Chwala is used
else if ($type == 'edit') {
$mimetypes = array(
'text/plain' => 'txt',
'text/html' => 'html',
);
if (!empty($caps['MANTICORE'])) {
$mimetypes = array_merge(array('application/vnd.oasis.opendocument.text' => 'odt'), $mimetypes);
}
foreach (array_keys($mimetypes) as $type) {
list ($app, $label) = explode('/', $type);
$label = preg_replace('/[^a-z]/', '', $label);
$mimetypes[$type] = array(
'ext' => $mimetypes[$type],
'label' => $this->plugin->gettext('type.' . $label),
);
}
}
else {
$mimetypes = $this->mimetypes;
}
}
}
catch (Exception $e) {
rcube::raise_error(array(
'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
'message' => $e->getMessage()),
true, false);
}
return $mimetypes;
}
/**
* Get list of available external storage drivers
*/
protected function get_external_storage_drivers()
{
// first get configured sources from Chwala
$token = $this->get_api_token();
$request = $this->get_request(array('method' => 'folder_types'), $token);
// send request to the API
try {
$response = $request->send();
$status = $response->getStatus();
$body = @json_decode($response->getBody(), true);
if ($status == 200 && $body['status'] == 'OK') {
$sources = $body['result'];
}
else {
throw new Exception($body['reason'] ?: "Failed to get folder_types. Status: $status");
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
return;
}
$this->rc->output->set_env('external_sources', $sources);
}
/**
* Get folder share dialog data
*/
protected function get_share_info($folder)
{
// first get configured sources from Chwala
$token = $this->get_api_token();
$request = $this->get_request(array('method' => 'sharing', 'folder' => $folder), $token);
// send request to the API
try {
$response = $request->send();
$status = $response->getStatus();
$body = @json_decode($response->getBody(), true);
if ($status == 200 && $body['status'] == 'OK') {
$info = $body['result'];
}
else if ($body['code'] == 530) {
return false;
}
else {
throw new Exception($body['reason'] ?: "Failed to get sharing form information. Status: $status");
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
return;
}
return $info;
}
/**
* Registers translation labels for folder lists in UI
*/
protected function folder_list_env()
{
// folder list and actions
$this->plugin->add_label(
'folderdeleting', 'folderdeleteconfirm', 'folderdeletenotice',
'collection_audio', 'collection_video', 'collection_image', 'collection_document',
'additionalfolders', 'listpermanent', 'storageautherror'
);
$this->rc->output->add_label('foldersubscribing', 'foldersubscribed',
'folderunsubscribing', 'folderunsubscribed', 'searching'
);
}
}
diff --git a/plugins/kolab_folders/kolab_folders.php b/plugins/kolab_folders/kolab_folders.php
index e68c436a..369bf26e 100644
--- a/plugins/kolab_folders/kolab_folders.php
+++ b/plugins/kolab_folders/kolab_folders.php
@@ -1,853 +1,853 @@
<?php
/**
* Type-aware folder management/listing for Kolab
*
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011-2017, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_folders extends rcube_plugin
{
public $task = '?(?!login).*';
public $types = array('mail', 'event', 'journal', 'task', 'note', 'contact', 'configuration', 'file', 'freebusy');
public $subtypes = array(
'mail' => array('inbox', 'drafts', 'sentitems', 'outbox', 'wastebasket', 'junkemail'),
'event' => array('default'),
'task' => array('default'),
'journal' => array('default'),
'note' => array('default'),
'contact' => array('default'),
'configuration' => array('default'),
'file' => array('default'),
'freebusy' => array('default'),
);
public $act_types = array('event', 'task');
private $rc;
private static $instance;
private $expire_annotation = '/shared/vendor/cmu/cyrus-imapd/expire';
private $is_processing = false;
/**
* Plugin initialization.
*/
function init()
{
self::$instance = $this;
$this->rc = rcube::get_instance();
// load required plugin
$this->require_plugin('libkolab');
// Folder listing hooks
$this->add_hook('storage_folders', array($this, 'mailboxes_list'));
// Folder manager hooks
$this->add_hook('folder_form', array($this, 'folder_form'));
$this->add_hook('folder_update', array($this, 'folder_save'));
$this->add_hook('folder_create', array($this, 'folder_save'));
$this->add_hook('folder_delete', array($this, 'folder_save'));
$this->add_hook('folder_rename', array($this, 'folder_save'));
$this->add_hook('folders_list', array($this, 'folders_list'));
// Special folders setting
$this->add_hook('preferences_save', array($this, 'prefs_save'));
// ACL plugin hooks
$this->add_hook('acl_rights_simple', array($this, 'acl_rights_simple'));
$this->add_hook('acl_rights_supported', array($this, 'acl_rights_supported'));
// Resolving other user folder names
$this->add_hook('render_mailboxlist', array($this, 'render_folderlist'));
$this->add_hook('render_folder_selector', array($this, 'render_folderlist'));
$this->add_hook('folders_list', array($this, 'render_folderlist'));
}
/**
* Handler for mailboxes_list hook. Enables type-aware lists filtering.
*/
function mailboxes_list($args)
{
// infinite loop prevention
if ($this->is_processing) {
return $args;
}
if (!$this->metadata_support()) {
return $args;
}
$this->is_processing = true;
// get folders
$folders = kolab_storage::list_folders($args['root'], $args['name'], $args['filter'], $args['mode'] == 'LSUB', $folderdata);
$this->is_processing = false;
if (!is_array($folders)) {
return $args;
}
// Create default folders
if ($args['root'] == '' && $args['name'] = '*') {
$this->create_default_folders($folders, $args['filter'], $folderdata, $args['mode'] == 'LSUB');
}
$args['folders'] = $folders;
return $args;
}
/**
* Handler for folders_list hook. Add css classes to folder rows.
*/
function folders_list($args)
{
if (!$this->metadata_support()) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// Add javascript script to the client
$this->include_script('kolab_folders.js');
$this->add_label('folderctype');
foreach ($this->types as $type) {
$this->add_label('foldertype' . $type);
}
$skip_namespace = $this->rc->config->get('kolab_skip_namespace');
$skip_roots = array();
if (!empty($skip_namespace)) {
$storage = $this->rc->get_storage();
foreach ((array)$skip_namespace as $ns) {
foreach((array)$storage->get_namespace($ns) as $root) {
$skip_roots[] = rtrim($root[0], $root[1]);
}
}
}
$this->rc->output->set_env('skip_roots', $skip_roots);
$this->rc->output->set_env('foldertypes', $this->types);
// get folders types
$folderdata = kolab_storage::folders_typedata();
if (!is_array($folderdata)) {
return $args;
}
// Add type-based style for table rows
// See kolab_folders::folder_class_name()
if (!empty($args['table'])) {
$table = $args['table'];
for ($i=1, $cnt=$table->size(); $i<=$cnt; $i++) {
$attrib = $table->get_row_attribs($i);
$folder = $attrib['foldername']; // UTF7-IMAP
$type = $folderdata[$folder];
if (!$type) {
$type = 'mail';
}
$class_name = self::folder_class_name($type);
$attrib['class'] = trim($attrib['class'] . ' ' . $class_name);
$table->set_row_attribs($attrib, $i);
}
}
// Add type-based class for list items
if (!empty($args['list']) && is_array($args['list'])) {
foreach ($args['list'] as $k => $item) {
$folder = $item['folder_imap']; // UTF7-IMAP
$type = $folderdata[$folder] ?? null;
if (!$type) {
$type = 'mail';
}
$class_name = self::folder_class_name($type);
$args['list'][$k]['class'] = trim($item['class'] . ' ' . $class_name);
}
}
return $args;
}
/**
* Handler for folder info/edit form (folder_form hook).
* Adds folder type selector.
*/
function folder_form($args)
{
if (!$this->metadata_support()) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// INBOX folder is of type mail.inbox and this cannot be changed
if ($args['name'] == 'INBOX') {
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => sprintf('%s (%s)', $this->gettext('foldertypemail'), $this->gettext('inbox')),
);
$this->add_expire_input($args['form'], 'INBOX');
return $args;
}
if ($args['options']['is_root']) {
return $args;
}
$mbox = strlen($args['name']) ? $args['name'] : $args['parent_name'];
if (isset($_POST['_ctype'])) {
$new_ctype = trim(rcube_utils::get_input_value('_ctype', rcube_utils::INPUT_POST));
$new_subtype = trim(rcube_utils::get_input_value('_subtype', rcube_utils::INPUT_POST));
}
// Get type of the folder or the parent
if (strlen($mbox)) {
list($ctype, $subtype) = $this->get_folder_type($mbox);
if (strlen($args['parent_name']) && $subtype == 'default')
$subtype = ''; // there can be only one
}
if (!$ctype) {
$ctype = 'mail';
}
$storage = $this->rc->get_storage();
// Don't allow changing type of shared folder, according to ACL
if (strlen($mbox)) {
$options = $storage->folder_info($mbox);
if ($options['namespace'] != 'personal' && !in_array('a', (array)$options['rights'])) {
if (in_array($ctype, $this->types)) {
$value = $this->gettext('foldertype'.$ctype);
}
else {
$value = $ctype;
}
if ($subtype) {
$value .= ' ('. ($subtype == 'default' ? $this->gettext('default') : $subtype) .')';
}
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => $value,
);
return $args;
}
}
// Add javascript script to the client
$this->include_script('kolab_folders.js');
// build type SELECT fields
$type_select = new html_select(array('name' => '_ctype', 'id' => '_folderctype',
'onchange' => "\$('[name=\"_expire\"]').attr('disabled', \$(this).val() != 'mail')"
));
$sub_select = new html_select(array('name' => '_subtype', 'id' => '_subtype'));
$sub_select->add('', '');
foreach ($this->types as $type) {
$type_select->add($this->gettext('foldertype'.$type), $type);
}
// add non-supported type
if (!in_array($ctype, $this->types)) {
$type_select->add($ctype, $ctype);
}
$sub_types = array();
foreach ($this->subtypes as $ftype => $subtypes) {
$sub_types[$ftype] = array_combine($subtypes, array_map(array($this, 'gettext'), $subtypes));
// fill options for the current folder type
if ($ftype == $ctype || (isset($new_ctype) && $ftype == $new_ctype)) {
$sub_select->add(array_values($sub_types[$ftype]), $subtypes);
}
}
$args['form']['props']['fieldsets']['settings']['content']['folderctype'] = array(
'label' => $this->gettext('folderctype'),
'value' => html::div('input-group',
$type_select->show(isset($new_ctype) ? $new_ctype : $ctype)
. $sub_select->show(isset($new_subtype) ? $new_subtype : $subtype)
),
);
$this->rc->output->set_env('kolab_folder_subtypes', $sub_types);
$this->rc->output->set_env('kolab_folder_subtype', isset($new_subtype) ? $new_subtype : $subtype);
$this->add_expire_input($args['form'], $args['name'], $ctype);
return $args;
}
/**
* Handler for folder update/create action (folder_update/folder_create hook).
*/
function folder_save($args)
{
// Folder actions from folders list
if (empty($args['record'])) {
return $args;
}
// Folder create/update with form
$ctype = trim(rcube_utils::get_input_value('_ctype', rcube_utils::INPUT_POST));
$subtype = trim(rcube_utils::get_input_value('_subtype', rcube_utils::INPUT_POST));
$mbox = $args['record']['name'];
- $old_mbox = $args['record']['oldname'];
+ $old_mbox = $args['record']['oldname'] ?? null;
$subscribe = $args['record']['subscribe'];
if (empty($ctype)) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// Skip folder creation/rename in core
// @TODO: Maybe we should provide folder_create_after and folder_update_after hooks?
// Using create_mailbox/rename_mailbox here looks bad
$args['abort'] = true;
// There can be only one default folder of specified type
if ($subtype == 'default') {
$default = $this->get_default_folder($ctype);
if ($default !== null && $old_mbox != $default) {
$args['result'] = false;
$args['message'] = $this->gettext('defaultfolderexists');
return $args;
}
}
// Subtype sanity-checks
else if ($subtype && (!($subtypes = $this->subtypes[$ctype]) || !in_array($subtype, $subtypes))) {
$subtype = '';
}
$ctype .= $subtype ? '.'.$subtype : '';
$storage = $this->rc->get_storage();
// Create folder
if (!strlen($old_mbox)) {
// By default don't subscribe to non-mail folders
if ($subscribe)
$subscribe = (bool) preg_match('/^mail/', $ctype);
$result = $storage->create_folder($mbox, $subscribe);
// Set folder type
if ($result) {
$this->set_folder_type($mbox, $ctype);
}
}
// Rename folder
else {
if ($old_mbox != $mbox) {
$result = $storage->rename_folder($old_mbox, $mbox);
}
else {
$result = true;
}
if ($result) {
list($oldtype, $oldsubtype) = $this->get_folder_type($mbox);
$oldtype .= $oldsubtype ? '.'.$oldsubtype : '';
if ($ctype != $oldtype) {
$this->set_folder_type($mbox, $ctype);
}
}
}
// Set messages expiration in days
if ($result && isset($_POST['_expire'])) {
$expire = trim(rcube_utils::get_input_value('_expire', rcube_utils::INPUT_POST));
$expire = intval($expire) && preg_match('/^mail/', $ctype) ? intval($expire) : null;
$storage->set_metadata($mbox, array($this->expire_annotation => $expire));
}
$args['record']['class'] = self::folder_class_name($ctype);
$args['record']['subscribe'] = $subscribe;
$args['result'] = $result;
return $args;
}
/**
* Handler for user preferences save (preferences_save hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_save($args)
{
if ($args['section'] != 'folders') {
return $args;
}
$dont_override = (array) $this->rc->config->get('dont_override', array());
// map config option name to kolab folder type annotation
$opts = array(
'drafts_mbox' => 'mail.drafts',
'sent_mbox' => 'mail.sentitems',
'junk_mbox' => 'mail.junkemail',
'trash_mbox' => 'mail.wastebasket',
);
// check if any of special folders has been changed
foreach ($opts as $opt_name => $type) {
$new = $args['prefs'][$opt_name];
$old = $this->rc->config->get($opt_name);
if (!strlen($new) || $new === $old || in_array($opt_name, $dont_override)) {
unset($opts[$opt_name]);
}
}
if (empty($opts)) {
return $args;
}
$folderdata = kolab_storage::folders_typedata();
if (!is_array($folderdata)) {
return $args;
}
foreach ($opts as $opt_name => $type) {
$foldername = $args['prefs'][$opt_name];
// get all folders of specified type
$folders = array_intersect($folderdata, array($type));
// folder already annotated with specified type
if (!empty($folders[$foldername])) {
continue;
}
// set type to the new folder
$this->set_folder_type($foldername, $type);
// unset old folder(s) type annotation
list($maintype, $subtype) = explode('.', $type);
foreach (array_keys($folders) as $folder) {
$this->set_folder_type($folder, $maintype);
}
}
return $args;
}
/**
* Handler for ACL permissions listing (acl_rights_simple hook)
*
* This shall combine the write and delete permissions into one item for
* groupware folders as updating groupware objects is an insert + delete operation.
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function acl_rights_simple($args)
{
if ($args['folder']) {
list($type,) = $this->get_folder_type($args['folder']);
// we're dealing with a groupware folder here...
if ($type && $type !== 'mail') {
if ($args['rights']['write'] && $args['rights']['delete']) {
$write_perms = $args['rights']['write'] . $args['rights']['delete'];
$rw_perms = $write_perms . $args['rights']['read'];
$args['rights']['write'] = $write_perms;
$args['rights']['other'] = preg_replace("/[$rw_perms]/", '', $args['rights']['other']);
// add localized labels and titles for the altered items
$args['labels'] = array(
'other' => $this->rc->gettext('shortacla','acl'),
);
$args['titles'] = array(
'other' => $this->rc->gettext('longaclother','acl'),
);
}
}
}
return $args;
}
/**
* Handler for ACL permissions listing (acl_rights_supported hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function acl_rights_supported($args)
{
if ($args['folder']) {
list($type,) = $this->get_folder_type($args['folder']);
// we're dealing with a groupware folder here...
if ($type && $type !== 'mail') {
// remove some irrelevant (for groupware objects) rights
$args['rights'] = str_split(preg_replace('/[p]/', '', join('', $args['rights'])));
}
}
return $args;
}
/**
* Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
*
* @return boolean
*/
function metadata_support()
{
$storage = $this->rc->get_storage();
return $storage->get_capability('METADATA') ||
$storage->get_capability('ANNOTATEMORE') ||
$storage->get_capability('ANNOTATEMORE2');
}
/**
* Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
*
* @param string $folder Folder name
*
* @return array Folder content-type
*/
function get_folder_type($folder)
{
return explode('.', (string)kolab_storage::folder_type($folder));
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return boolean True on success
*/
function set_folder_type($folder, $type = 'mail')
{
return kolab_storage::set_folder_type($folder, $type);
}
/**
* Returns the name of default folder
*
* @param string $type Folder type
*
* @return string Folder name
*/
function get_default_folder($type)
{
$folderdata = kolab_storage::folders_typedata();
if (!is_array($folderdata)) {
return null;
}
// get all folders of specified type
$folderdata = array_intersect($folderdata, array($type.'.default'));
return key($folderdata);
}
/**
* Returns CSS class name for specified folder type
*
* @param string $type Folder type
*
* @return string Class name
*/
static function folder_class_name($type)
{
if ($type && strpos($type, '.')) {
list($ctype, $subtype) = explode('.', $type);
return 'type-' . $ctype . ' subtype-' . $subtype;
}
return 'type-' . ($type ? $type : 'mail');
}
/**
* Creates default folders if they doesn't exist
*/
private function create_default_folders(&$folders, $filter, $folderdata = null, $lsub = false)
{
$storage = $this->rc->get_storage();
$namespace = $storage->get_namespace();
$defaults = array();
$prefix = '';
// Find personal namespace prefix
if (is_array($namespace['personal']) && count($namespace['personal']) == 1) {
$prefix = $namespace['personal'][0][0];
}
$this->load_config();
// get configured defaults
foreach ($this->types as $type) {
foreach ((array)$this->subtypes[$type] as $subtype) {
$opt_name = 'kolab_folders_' . $type . '_' . $subtype;
if ($folder = $this->rc->config->get($opt_name)) {
// convert configuration value to UTF7-IMAP charset
$folder = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP');
// and namespace prefix if needed
if ($prefix && strpos($folder, $prefix) === false && $folder != 'INBOX') {
$folder = $prefix . $folder;
}
$defaults[$type . '.' . $subtype] = $folder;
}
}
}
if (empty($defaults)) {
return;
}
if ($folderdata === null) {
$folderdata = kolab_storage::folders_typedata();
}
if (!is_array($folderdata)) {
return;
}
// find default folders
foreach ($defaults as $type => $foldername) {
// get all folders of specified type
$_folders = array_intersect($folderdata, array($type));
// default folder found
if (!empty($_folders)) {
continue;
}
list($type1, $type2) = explode('.', $type);
$activate = in_array($type1, $this->act_types);
$exists = false;
$result = false;
// check if folder exists
if (!empty($folderdata[$foldername]) || $foldername == 'INBOX') {
$exists = true;
}
else if ((!$filter || $filter == $type1) && in_array($foldername, $folders)) {
// this assumes also that subscribed folder exists
$exists = true;
}
else {
$exists = $storage->folder_exists($foldername);
}
// create folder
if (!$exists) {
$exists = $storage->create_folder($foldername);
}
// set type + subscribe + activate
if ($exists) {
if ($result = kolab_storage::set_folder_type($foldername, $type)) {
// check if folder is subscribed
if ((!$filter || $filter == $type1) && $lsub && in_array($foldername, $folders)) {
// already subscribed
$subscribed = true;
}
else {
$subscribed = $storage->subscribe($foldername);
}
// activate folder
if ($activate) {
kolab_storage::folder_activate($foldername, true);
}
}
}
// add new folder to the result
if ($result && (!$filter || $filter == $type1) && (!$lsub || $subscribed)) {
$folders[] = $foldername;
}
}
}
/**
* Static getter for default folder of the given type
*
* @param string $type Folder type
*
* @return string Folder name
*/
public static function default_folder($type)
{
return self::$instance->get_default_folder($type);
}
/**
* Get /shared/vendor/cmu/cyrus-imapd/expire value
*
* @param string $folder IMAP folder name
*
* @return int|false The annotation value or False if not supported
*/
private function get_expire_annotation($folder)
{
$storage = $this->rc->get_storage();
if ($storage->get_vendor() != 'cyrus') {
return false;
}
if (!strlen($folder)) {
return 0;
}
$value = $storage->get_metadata($folder, $this->expire_annotation);
if (is_array($value)) {
return !empty($value[$folder]) ? intval($value[$folder][$this->expire_annotation] ?? 0) : 0;
}
return false;
}
/**
* Add expiration time input to the form if supported
*/
private function add_expire_input(&$form, $folder, $type = null)
{
if (($expire = $this->get_expire_annotation($folder)) !== false) {
$post = trim(rcube_utils::get_input_value('_expire', rcube_utils::INPUT_POST));
$is_mail = empty($type) || preg_match('/^mail/i', $type);
$label = $this->gettext('xdays');
$input = new html_inputfield(array(
'id' => '_kolabexpire',
'name' => '_expire',
'size' => 3,
'disabled' => !$is_mail
));
if ($post && $is_mail) {
$expire = (int) $post;
}
if (strpos($label, '$') === 0) {
$label = str_replace('$x', '', $label);
$html = $input->show($expire ?: '')
. html::span('input-group-append', html::span('input-group-text', rcube::Q($label)));
}
else {
$label = str_replace('$x', '', $label);
$html = html::span('input-group-prepend', html::span('input-group-text', rcube::Q($label)))
. $input->show($expire ?: '');
}
$form['props']['fieldsets']['settings']['content']['kolabexpire'] = array(
'label' => $this->gettext('folderexpire'),
'value' => html::div('input-group', $html),
);
}
}
/**
* Handler for various folders list widgets (hooks)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function render_folderlist($args)
{
$storage = $this->rc->get_storage();
$ns_other = $storage->get_namespace('other');
$is_fl = $this->rc->plugins->is_processing('folders_list');
foreach ((array) $ns_other as $root) {
$delim = $root[1];
$prefix = rtrim($root[0], $delim);
$length = strlen($prefix);
if (!$length) {
continue;
}
// folders_list hook mode
if ($is_fl) {
foreach ((array) $args['list'] as $folder_name => $folder) {
if (strpos($folder_name, $root[0]) === 0 && !substr_count($folder_name, $root[1], $length+1)) {
if ($name = kolab_storage::folder_id2user(substr($folder_name, $length+1), true)) {
$old = $args['list'][$folder_name]['display'];
$content = $args['list'][$folder_name]['content'];
$name = rcube::Q($name);
$content = str_replace(">$old<", ">$name<", $content);
$args['list'][$folder_name]['display'] = $name;
$args['list'][$folder_name]['content'] = $content;
}
}
}
// TODO: Re-sort the list
}
// render_* hooks mode
else if (!empty($args['list'][$prefix]) && !empty($args['list'][$prefix]['folders'])) {
$map = array();
foreach ($args['list'][$prefix]['folders'] as $folder_name => $folder) {
if ($name = kolab_storage::folder_id2user($folder_name, true)) {
$args['list'][$prefix]['folders'][$folder_name]['name'] = $name;
}
$map[$folder_name] = $name ?: $args['list'][$prefix]['folders'][$folder_name]['name'];
}
// Re-sort the list
uasort($map, 'strcoll');
$args['list'][$prefix]['folders'] = array_replace($map, $args['list'][$prefix]['folders']);
}
}
return $args;
}
}
diff --git a/plugins/kolab_notes/kolab_notes.php b/plugins/kolab_notes/kolab_notes.php
index b0674f86..994dcc9a 100644
--- a/plugins/kolab_notes/kolab_notes.php
+++ b/plugins/kolab_notes/kolab_notes.php
@@ -1,1485 +1,1485 @@
<?php
/**
* Kolab notes module
*
* Adds simple notes management features to the web client
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_notes extends rcube_plugin
{
public $task = '?(?!login|logout).*';
public $allowed_prefs = array('kolab_notes_sort_col');
public $rc;
private $ui;
private $lists;
private $folders;
private $cache = array();
private $message_notes = array();
private $bonnie_api = false;
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
$this->require_plugin('libkolab');
$this->rc = rcube::get_instance();
// proceed initialization in startup hook
$this->add_hook('startup', array($this, 'startup'));
}
/**
* Startup hook
*/
public function startup($args)
{
// the notes module can be enabled/disabled by the kolab_auth plugin
if ($this->rc->config->get('kolab_notes_disabled', false) || !$this->rc->config->get('kolab_notes_enabled', true)) {
return;
}
$this->register_task('notes');
// load plugin configuration
$this->load_config();
// load localizations
$this->add_texts('localization/', $args['task'] == 'notes' && (!$args['action'] || $args['action'] == 'dialog-ui'));
$this->rc->load_language($_SESSION['language'], array('notes.notes' => $this->gettext('navtitle'))); // add label for task title
if ($args['task'] == 'notes') {
$this->add_hook('storage_init', array($this, 'storage_init'));
// register task actions
$this->register_action('index', array($this, 'notes_view'));
$this->register_action('fetch', array($this, 'notes_fetch'));
$this->register_action('get', array($this, 'note_record'));
$this->register_action('action', array($this, 'note_action'));
$this->register_action('list', array($this, 'list_action'));
$this->register_action('dialog-ui', array($this, 'dialog_view'));
$this->register_action('print', array($this, 'print_note'));
if (!$this->rc->output->ajax_call && in_array($args['action'], array('dialog-ui', 'list'))) {
$this->load_ui();
}
}
else if ($args['task'] == 'mail') {
$this->add_hook('storage_init', array($this, 'storage_init'));
$this->add_hook('message_compose', array($this, 'mail_message_compose'));
if (in_array($args['action'], array('show', 'preview', 'print'))) {
$this->add_hook('message_load', array($this, 'mail_message_load'));
$this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
}
// add 'Append note' item to message menu
- if ($this->api->output->type == 'html' && $_REQUEST['_rel'] != 'note') {
+ if ($this->api->output->type == 'html' && ($_REQUEST['_rel'] ?? null) != 'note') {
$this->api->add_content(html::tag('li', array('role' => 'menuitem'),
$this->api->output->button(array(
'command' => 'append-kolab-note',
'label' => 'kolab_notes.appendnote',
'type' => 'link',
'classact' => 'icon appendnote active',
'class' => 'icon appendnote disabled',
'innerclass' => 'icon note',
))),
'messagemenu');
$this->api->output->add_label('kolab_notes.appendnote', 'kolab_notes.editnote', 'kolab_notes.deletenotesconfirm', 'kolab_notes.entertitle', 'save', 'delete', 'cancel', 'close');
$this->include_script('notes_mail.js');
}
}
- if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
+ if (!$this->rc->output->ajax_call && !($this->rc->output->env['framed'] ?? null)) {
$this->load_ui();
}
// get configuration for the Bonnie API
$this->bonnie_api = libkolab::get_bonnie_api();
// notes use fully encoded identifiers
kolab_storage::$encode_ids = true;
}
/**
* Hook into IMAP FETCH HEADER.FIELDS command and request MESSAGE-ID
*/
public function storage_init($p)
{
$p['fetch_headers'] = trim($p['fetch_headers'] . ' MESSAGE-ID');
return $p;
}
/**
* Load and initialize UI class
*/
private function load_ui()
{
if (!$this->ui) {
require_once($this->home . '/kolab_notes_ui.php');
$this->ui = new kolab_notes_ui($this);
$this->ui->init();
}
}
/**
* Read available calendars for the current user and store them internally
*/
private function _read_lists($force = false)
{
// already read sources
if (isset($this->lists) && !$force)
return $this->lists;
// get all folders that have type "task"
$folders = kolab_storage::sort_folders(kolab_storage::get_folders('note'));
$this->lists = $this->folders = array();
// find default folder
$default_index = 0;
foreach ($folders as $i => $folder) {
if ($folder->default)
$default_index = $i;
}
// put default folder on top of the list
if ($default_index > 0) {
$default_folder = $folders[$default_index];
unset($folders[$default_index]);
array_unshift($folders, $default_folder);
}
foreach ($folders as $folder) {
$item = $this->folder_props($folder);
$this->lists[$item['id']] = $item;
$this->folders[$item['id']] = $folder;
$this->folders[$folder->name] = $folder;
}
}
/**
* Get a list of available folders from this source
*/
public function get_lists(&$tree = null)
{
$this->_read_lists();
// attempt to create a default folder for this user
if (empty($this->lists)) {
$folder = array('name' => 'Notes', 'type' => 'note', 'default' => true, 'subscribed' => true);
if (kolab_storage::folder_update($folder)) {
$this->_read_lists(true);
}
}
$folders = array();
foreach ($this->lists as $id => $list) {
if (!empty($this->folders[$id])) {
$folders[] = $this->folders[$id];
}
}
// include virtual folders for a full folder tree
if (!is_null($tree)) {
$folders = kolab_storage::folder_hierarchy($folders, $tree);
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$lists = array();
foreach ($folders as $folder) {
$list_id = $folder->id;
$imap_path = explode($delim, $folder->name);
// find parent
do {
array_pop($imap_path);
$parent_id = kolab_storage::folder_id(join($delim, $imap_path));
}
while (count($imap_path) > 1 && !$this->folders[$parent_id]);
// restore "real" parent ID
if ($parent_id && !$this->folders[$parent_id]) {
$parent_id = kolab_storage::folder_id($folder->get_parent());
}
$fullname = $folder->get_name();
$listname = $folder->get_foldername();
// special handling for virtual folders
if ($folder instanceof kolab_storage_folder_user) {
$lists[$list_id] = array(
'id' => $list_id,
'name' => $fullname,
'listname' => $listname,
'title' => $folder->get_title(),
'virtual' => true,
'editable' => false,
'rights' => 'l',
'group' => 'other virtual',
'class' => 'user',
'parent' => $parent_id,
);
}
- else if ($folder->virtual) {
+ else if (!empty($folder->virtual)) {
$lists[$list_id] = array(
'id' => $list_id,
'name' => $fullname,
'listname' => $listname,
'virtual' => true,
'editable' => false,
'rights' => 'l',
'group' => $folder->get_namespace(),
'parent' => $parent_id,
);
}
else {
if (!$this->lists[$list_id]) {
$this->lists[$list_id] = $this->folder_props($folder);
$this->folders[$list_id] = $folder;
}
$this->lists[$list_id]['parent'] = $parent_id;
$lists[$list_id] = $this->lists[$list_id];
}
}
return $lists;
}
/**
* Search for shared or otherwise not listed folders the user has access
*
* @param string Search string
* @param string Section/source to search
* @return array List of notes folders
*/
protected function search_lists($query, $source)
{
if (!kolab_storage::setup()) {
return array();
}
$this->search_more_results = false;
$this->lists = $this->folders = array();
// find unsubscribed IMAP folders that have "event" type
if ($source == 'folders') {
foreach ((array)kolab_storage::search_folders('note', $query, array('other')) as $folder) {
$this->folders[$folder->id] = $folder;
$this->lists[$folder->id] = $this->folder_props($folder);
}
}
// search other user's namespace via LDAP
else if ($source == 'users') {
$limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
$folders = array();
// search for note folders shared by this user
foreach (kolab_storage::list_user_folders($user, 'note', false) as $foldername) {
$folders[] = new kolab_storage_folder($foldername, 'note');
}
if (count($folders)) {
$userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
$this->folders[$userfolder->id] = $userfolder;
$this->lists[$userfolder->id] = $this->folder_props($userfolder);
foreach ($folders as $folder) {
$this->folders[$folder->id] = $folder;
$this->lists[$folder->id] = $this->folder_props($folder);
$count++;
}
}
if ($count >= $limit) {
$this->search_more_results = true;
break;
}
}
}
return $this->get_lists();
}
/**
* Derive list properties from the given kolab_storage_folder object
*/
protected function folder_props($folder)
{
if ($folder->get_namespace() == 'personal') {
$norename = false;
$editable = true;
$rights = 'lrswikxtea';
$alarms = true;
}
else {
$alarms = false;
$rights = 'lr';
$editable = false;
if (($myrights = $folder->get_myrights()) && !PEAR::isError($myrights)) {
$rights = $myrights;
if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false)
$editable = strpos($rights, 'i');
}
$info = $folder->get_folder_info();
$norename = $readonly || $info['norename'] || $info['protected'];
}
$list_id = $folder->id;
return array(
'id' => $list_id,
'name' => $folder->get_name(),
'listname' => $folder->get_foldername(),
'editname' => $folder->get_foldername(),
'editable' => $editable,
'rights' => $rights,
'norename' => $norename,
'parentfolder' => $folder->get_parent(),
'subscribed' => (bool)$folder->is_subscribed(),
'default' => $folder->default,
'group' => $folder->default ? 'default' : $folder->get_namespace(),
'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
);
}
/**
* Get the kolab_calendar instance for the given calendar ID
*
* @param string List identifier (encoded imap folder name)
* @return object kolab_storage_folder Object nor null if list doesn't exist
*/
public function get_folder($id)
{
// create list and folder instance if necesary
if (!$this->lists[$id]) {
$folder = kolab_storage::get_folder(kolab_storage::id_decode($id));
if ($folder->type) {
$this->folders[$id] = $folder;
$this->lists[$id] = $this->folder_props($folder);
}
}
return $this->folders[$id];
}
/******* UI functions ********/
/**
* Render main view of the tasklist task
*/
public function notes_view()
{
$this->ui->init();
$this->ui->init_templates();
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('kolab_notes.notes');
}
/**
* Deliver a rediced UI for inline (dialog)
*/
public function dialog_view()
{
// resolve message reference
if ($msgref = rcube_utils::get_input_value('_msg', rcube_utils::INPUT_GPC, true)) {
$storage = $this->rc->get_storage();
list($uid, $folder) = explode('-', $msgref, 2);
if ($message = $storage->get_message_headers($msgref)) {
$this->rc->output->set_env('kolab_notes_template', array(
'_from_mail' => true,
'title' => $message->get('subject'),
'links' => array(kolab_storage_config::get_message_reference(
kolab_storage_config::get_message_uri($message, $folder),
'note'
)),
));
}
}
$this->ui->init_templates();
$this->rc->output->send('kolab_notes.dialogview');
}
/**
* Handler to retrieve note records for the given list and/or search query
*/
public function notes_fetch()
{
$search = rcube_utils::get_input_value('_q', rcube_utils::INPUT_GPC, true);
$list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC);
$data = $this->notes_data($this->list_notes($list, $search), $tags);
$this->rc->output->command('plugin.data_ready', array(
'list' => $list,
'search' => $search,
'data' => $data,
'tags' => array_values($tags)
));
}
/**
* Convert the given note records for delivery to the client
*/
protected function notes_data($records, &$tags)
{
$config = kolab_storage_config::get_instance();
$tags = $config->apply_tags($records);
$config->apply_links($records);
foreach ($records as $i => $rec) {
unset($records[$i]['description']);
$this->_client_encode($records[$i]);
}
return $records;
}
/**
* Read note records for the given list from the storage backend
*/
protected function list_notes($list_id, $search = null)
{
$results = array();
// query Kolab storage
$query = array();
// full text search (only works with cache enabled)
if (strlen($search)) {
$words = array_filter(rcube_utils::normalize_string(mb_strtolower($search), true));
foreach ($words as $word) {
if (strlen($word) > 2) { // only words > 3 chars are stored in DB
$query[] = array('words', '~', $word);
}
}
}
$this->_read_lists();
if ($folder = $this->get_folder($list_id)) {
foreach ($folder->select($query, empty($query)) as $record) {
// post-filter search results
if (strlen($search)) {
$matches = 0;
$contents = mb_strtolower(
$record['title'] .
($this->is_html($record) ? strip_tags($record['description']) : $record['description'])
);
foreach ($words as $word) {
if (mb_strpos($contents, $word) !== false) {
$matches++;
}
}
// skip records not matching all search words
if ($matches < count($words)) {
continue;
}
}
$record['list'] = $list_id;
$results[] = $record;
}
}
return $results;
}
/**
* Handler for delivering a full note record to the client
*/
public function note_record()
{
$data = $this->get_note(array(
'uid' => rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC),
'list' => rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC),
));
// encode for client use
if (is_array($data)) {
$this->_client_encode($data);
}
$this->rc->output->command('plugin.render_note', $data);
}
/**
* Get the full note record identified by the given UID + Lolder identifier
*/
public function get_note($note)
{
if (is_array($note)) {
$uid = $note['uid'] ?: $note['id'];
$list_id = $note['list'];
}
else {
$uid = $note;
}
// deliver from in-memory cache
$key = $list_id . ':' . $uid;
if (!empty($this->cache[$key])) {
return $this->cache[$key];
}
$result = false;
$this->_read_lists();
if ($list_id) {
if ($folder = $this->get_folder($list_id)) {
$result = $folder->get_object($uid);
}
}
// iterate over all calendar folders and search for the event ID
else {
foreach ($this->folders as $list_id => $folder) {
if ($result = $folder->get_object($uid)) {
$result['list'] = $list_id;
break;
}
}
}
if ($result) {
// get note tags
$result['tags'] = $this->get_tags($result['uid']);
// get note links
$result['links'] = $this->get_links($result['uid']);
}
return $result;
}
/**
* Helper method to encode the given note record for use in the client
*/
private function _client_encode(&$note)
{
foreach ($note as $key => $prop) {
if ($key[0] == '_' || $key == 'x-custom') {
unset($note[$key]);
}
}
foreach (array('created','changed') as $key) {
if (is_object($note[$key]) && $note[$key] instanceof DateTime) {
$note[$key.'_'] = $note[$key]->format('U');
$note[$key] = $this->rc->format_date($note[$key]);
}
}
// clean HTML contents
if (!empty($note['description']) && $this->is_html($note)) {
$note['html'] = $this->_wash_html($note['description']);
}
// convert link URIs references into structs
if (array_key_exists('links', $note)) {
foreach ((array)$note['links'] as $i => $link) {
if (strpos($link, 'imap://') === 0 && ($msgref = kolab_storage_config::get_message_reference($link, 'note'))) {
$note['links'][$i] = $msgref;
}
}
}
return $note;
}
/**
* Handler for client-initiated actions on a single note record
*/
public function note_action()
{
$action = rcube_utils::get_input_value('_do', rcube_utils::INPUT_POST);
$note = rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST, true);
$success = $silent = false;
switch ($action) {
case 'new':
case 'edit':
if ($success = $this->save_note($note)) {
$refresh = $this->get_note($note);
}
break;
case 'move':
$uids = explode(',', $note['uid']);
foreach ($uids as $uid) {
$note['uid'] = $uid;
if (!($success = $this->move_note($note, $note['to']))) {
$refresh = $this->get_note($note);
break;
}
}
break;
case 'delete':
$uids = explode(',', $note['uid']);
foreach ($uids as $uid) {
$note['uid'] = $uid;
if (!($success = $this->delete_note($note))) {
$refresh = $this->get_note($note);
break;
}
}
break;
case 'changelog':
$data = $this->get_changelog($note);
if (is_array($data) && !empty($data)) {
$rcmail = $this->rc;
$dtformat = $rcmail->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
array_walk($data, function(&$change) use ($lib, $rcmail, $dtformat) {
if ($change['date']) {
$dt = rcube_utils::anytodatetime($change['date']);
if ($dt instanceof DateTime) {
$change['date'] = $rcmail->format_date($dt, $dtformat);
}
}
});
$this->rc->output->command('plugin.note_render_changelog', $data);
}
else {
$this->rc->output->command('plugin.note_render_changelog', false);
}
$silent = true;
break;
case 'diff':
$silent = true;
$data = $this->get_diff($note, $note['rev1'], $note['rev2']);
if (is_array($data)) {
$this->rc->output->command('plugin.note_show_diff', $data);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
}
break;
case 'show':
if ($rec = $this->get_revison($note, $note['rev'])) {
$this->rc->output->command('plugin.note_show_revision', $this->_client_encode($rec));
}
else {
$this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
}
$silent = true;
break;
case 'restore':
if ($this->restore_revision($note, $note['rev'])) {
$refresh = $this->get_note($note);
$this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $note['rev']))), 'confirmation');
$this->rc->output->command('plugin.close_history_dialog');
}
else {
$this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
}
$silent = true;
break;
}
// show confirmation/error message
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
}
else if (!$silent) {
$this->rc->output->show_message('errorsaving', 'error');
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
if ($refresh) {
$this->rc->output->command('plugin.update_note', $this->_client_encode($refresh));
}
}
/**
* Update an note record with the given data
*
* @param array Hash array with note properties (id, list)
* @return boolean True on success, False on error
*/
private function save_note(&$note)
{
$this->_read_lists();
$list_id = $note['list'];
if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
// moved from another folder
if (!empty($note['_fromlist']) && ($fromfolder = $this->get_folder($note['_fromlist']))) {
if (!$fromfolder->move($note['uid'], $folder->name))
return false;
unset($note['_fromlist']);
}
// load previous version of this record to merge
$old = null;
if (!empty($note['uid'])) {
$old = $folder->get_object($note['uid']);
if (!$old || PEAR::isError($old))
return false;
// merge existing properties if the update isn't complete
if (!isset($note['title']) || !isset($note['description']))
$note += $old;
}
// generate new note object from input
$object = $this->_write_preprocess($note, $old);
// email links and tags are handled separately
$links = $object['links'] ?? null;
$tags = $object['tags'] ?? null;
unset($object['links']);
unset($object['tags']);
$saved = $folder->save($object, 'note', $note['uid']);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving note object to Kolab server"),
true, false);
$saved = false;
}
else {
// save links in configuration.relation object
$this->save_links($object['uid'], $links);
// save tags in configuration.relation object
$this->save_tags($object['uid'], $tags);
$note = $object;
$note['list'] = $list_id;
$note['tags'] = (array) $tags;
// cache this in memory for later read
$key = $list_id . ':' . $note['uid'];
$this->cache[$key] = $note;
}
return $saved;
}
/**
* Move the given note to another folder
*/
function move_note($note, $list_id)
{
$this->_read_lists();
$tofolder = $this->get_folder($list_id);
$fromfolder = $this->get_folder($note['list']);
if ($fromfolder && $tofolder) {
return $fromfolder->move($note['uid'], $tofolder->name);
}
return false;
}
/**
* Remove a single note record from the backend
*
* @param array Hash array with note properties (id, list)
* @param boolean Remove record irreversible (mark as deleted otherwise)
* @return boolean True on success, False on error
*/
public function delete_note($note, $force = true)
{
$this->_read_lists();
$list_id = $note['list'];
if (!$list_id || !($folder = $this->get_folder($list_id))) {
return false;
}
$status = $folder->delete($note['uid'], $force);
if ($status) {
$this->save_links($note['uid'], null);
$this->save_tags($note['uid'], null);
}
return $status;
}
/**
* Render the template for printing with placeholders
*/
public function print_note()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);
$list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GET);
$this->note = $this->get_note(array('uid' => $uid, 'list' => $list));
// encode for client use
if (is_array($this->note)) {
$this->_client_encode($this->note);
}
$this->rc->output->set_pagetitle($this->note['title']);
$this->rc->output->add_handlers(array(
'noteheader' => array($this, 'print_note_header'),
'notebody' => array($this, 'print_note_body'),
));
$this->include_script('notes.js');
$this->rc->output->send('kolab_notes.print');
}
public function print_note_header()
{
$tags = array_map(array('rcube', 'Q'), (array) $this->note['tags']);
$tags = implode(' ', $tags);
return html::tag('h1', array('id' => 'notetitle'), rcube::Q($this->note['title']))
. html::div(array('id' => 'notetags', 'class' => 'tagline'), $tags)
. html::div('dates',
html::label(null, rcube::Q($this->gettext('created')))
. html::span(array('id' => 'notecreated'), rcube::Q($this->note['created']))
. html::label(null, rcube::Q($this->gettext('changed')))
. html::span(array('id' => 'notechanged'), rcube::Q($this->note['changed']))
);
}
public function print_note_body()
{
return isset($this->note['html']) ? $this->note['html'] : rcube::Q($this->note['description']);
}
/**
* Provide a list of revisions for the given object
*
* @param array $note Hash array with note properties
* @return array List of changes, each as a hash array
*/
public function get_changelog($note)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
$result = $uid && $mailbox ? $this->bonnie_api->changelog('note', $uid, $mailbox, $msguid) : null;
if (is_array($result) && $result['uid'] == $uid) {
return $result['changes'];
}
return false;
}
/**
* Return full data of a specific revision of a note record
*
* @param mixed $note UID string or hash array with note properties
* @param mixed $rev Revision number
*
* @return array Note object as hash array
*/
public function get_revison($note, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
// call Bonnie API
$result = $this->bonnie_api->get('note', $uid, $rev, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
$format = kolab_format::factory('note');
$format->load($result['xml']);
$rec = $format->to_array();
if ($format->is_valid()) {
$rec['rev'] = $result['rev'];
return $rec;
}
}
return false;
}
/**
* Get a list of property changes beteen two revisions of a note object
*
* @param array $$note Hash array with note properties
* @param mixed $rev Revisions: "from:to"
*
* @return array List of property changes, each as a hash array
*/
public function get_diff($note, $rev1, $rev2)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
// call Bonnie API
$result = $this->bonnie_api->diff('note', $uid, $rev1, $rev2, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid) {
$result['rev1'] = $rev1;
$result['rev2'] = $rev2;
// convert some properties, similar to self::_client_encode()
$keymap = array(
'summary' => 'title',
'lastmodified-date' => 'changed',
);
// map kolab object properties to keys and values the client expects
array_walk($result['changes'], function(&$change, $i) use ($keymap) {
if (array_key_exists($change['property'], $keymap)) {
$change['property'] = $keymap[$change['property']];
}
if ($change['property'] == 'created' || $change['property'] == 'changed') {
if ($old_ = rcube_utils::anytodatetime($change['old'])) {
$change['old_'] = $this->rc->format_date($old_);
}
if ($new_ = rcube_utils::anytodatetime($change['new'])) {
$change['new_'] = $this->rc->format_date($new_);
}
}
// compute a nice diff of note contents
if ($change['property'] == 'description') {
$change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
if (!empty($change['diff_'])) {
unset($change['old'], $change['new']);
$change['diff_'] = preg_replace(array('!^.*<body[^>]*>!Uims','!</body>.*$!Uims'), '', $change['diff_']);
$change['diff_'] = preg_replace("!</(p|li|span)>\n!", '</\\1>', $change['diff_']);
}
}
});
return $result;
}
return false;
}
/**
* Command the backend to restore a certain revision of a note.
* This shall replace the current object with an older version.
*
* @param array $note Hash array with note properties (id, list)
* @param mixed $rev Revision number
*
* @return boolean True on success, False on failure
*/
public function restore_revision($note, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
$folder = $this->get_folder($note['list']);
$success = false;
if ($folder && ($raw_msg = $this->bonnie_api->rawdata('note', $uid, $rev, $mailbox))) {
$imap = $this->rc->get_storage();
// insert $raw_msg as new message
if ($imap->save_message($folder->name, $raw_msg, null, false)) {
$success = true;
// delete old revision from imap and cache
$imap->delete_message($msguid, $folder->name);
$folder->cache->set($msguid, false);
$this->cache = array();
}
}
return $success;
}
/**
* Helper method to resolved the given note identifier into uid and mailbox
*
* @return array (uid,mailbox,msguid) tuple
*/
private function _resolve_note_identity($note)
{
$mailbox = $msguid = null;
if (!is_array($note)) {
$note = $this->get_note($note);
}
if (is_array($note)) {
$uid = $note['uid'] ?: $note['id'];
$list = $note['list'];
}
else {
return array(null, $mailbox, $msguid);
}
if ($folder = $this->get_folder($list)) {
$mailbox = $folder->get_mailbox_id();
// get object from storage in order to get the real object uid an msguid
if ($rec = $folder->get_object($uid)) {
$msguid = $rec['_msguid'];
$uid = $rec['uid'];
}
}
return array($uid, $mailbox, $msguid);
}
/**
* Handler for client requests to list (aka folder) actions
*/
public function list_action()
{
$action = rcube_utils::get_input_value('_do', rcube_utils::INPUT_GPC);
$list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC, true);
$success = $update_cmd = false;
if (empty($action)) {
$action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
}
switch ($action) {
case 'form-new':
case 'form-edit':
$this->_read_lists();
$this->ui->list_editform($action, $this->lists[$list['id']], $this->folders[$list['id']]);
exit;
case 'new':
$list['type'] = 'note';
$list['subscribed'] = true;
$folder = kolab_storage::folder_update($list);
if ($folder === false) {
$save_error = $this->gettext(kolab_storage::$last_error);
}
else {
$success = true;
$update_cmd = 'plugin.update_list';
$list['id'] = kolab_storage::folder_id($folder);
$list['_reload'] = true;
}
break;
case 'edit':
$this->_read_lists();
$oldparent = $this->lists[$list['id']]['parentfolder'];
$newfolder = kolab_storage::folder_update($list);
if ($newfolder === false) {
$save_error = $this->gettext(kolab_storage::$last_error);
}
else {
$success = true;
$update_cmd = 'plugin.update_list';
$list['newid'] = kolab_storage::folder_id($newfolder);
$list['_reload'] = $list['parent'] != $oldparent;
// compose the new display name
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$path_imap = explode($delim, $newfolder);
$list['name'] = kolab_storage::object_name($newfolder);
$list['editname'] = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');
$list['listname'] = $list['editname'];
}
break;
case 'delete':
$this->_read_lists();
$folder = $this->get_folder($list['id']);
if ($folder && kolab_storage::folder_delete($folder->name)) {
$success = true;
$update_cmd = 'plugin.destroy_list';
}
else {
$save_error = $this->gettext(kolab_storage::$last_error);
}
break;
case 'search':
$this->load_ui();
$results = array();
foreach ((array)$this->search_lists(rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC), rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC)) as $id => $prop) {
$editname = $prop['editname'];
unset($prop['editname']); // force full name to be displayed
// let the UI generate HTML and CSS representation for this calendar
$html = $this->ui->folder_list_item($id, $prop, $jsenv, true);
$prop += (array)$jsenv[$id];
$prop['editname'] = $editname;
$prop['html'] = $html;
$results[] = $prop;
}
// report more results available
if ($this->driver->search_more_results) {
$this->rc->output->show_message('autocompletemore', 'notice');
}
$this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
return;
case 'subscribe':
$success = false;
if ($list['id'] && ($folder = $this->get_folder($list['id']))) {
if (isset($list['permanent']))
$success |= $folder->subscribe(intval($list['permanent']));
if (isset($list['active']))
$success |= $folder->activate(intval($list['active']));
// apply to child folders, too
if ($list['recursive']) {
foreach ((array)kolab_storage::list_folders($folder->name, '*', 'node') as $subfolder) {
if (isset($list['permanent']))
($list['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
if (isset($list['active']))
($list['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder));
}
}
}
break;
}
$this->rc->output->command('plugin.unlock_saving');
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
if ($update_cmd) {
$this->rc->output->command($update_cmd, $list);
}
}
else {
$error_msg = $this->gettext('errorsaving') . ($save_error ? ': ' . $save_error :'');
$this->rc->output->show_message($error_msg, 'error');
}
}
/**
* Hook to add note attachments to message compose if the according parameter is present.
* This completes the 'send note by mail' feature.
*/
public function mail_message_compose($args)
{
if (!empty($args['param']['with_notes'])) {
$uids = explode(',', $args['param']['with_notes']);
$list = $args['param']['notes_list'];
foreach ($uids as $uid) {
if ($note = $this->get_note(array('uid' => $uid, 'list' => $list))) {
$data = $this->note2message($note);
$args['attachments'][] = array(
'name' => abbreviate_string($note['title'], 50, ''),
'mimetype' => 'message/rfc822',
'data' => $data,
'size' => strlen($data),
);
if (empty($args['param']['subject'])) {
$args['param']['subject'] = $note['title'];
}
}
}
unset($args['param']['with_notes'], $args['param']['notes_list']);
}
return $args;
}
/**
* Lookup backend storage and find notes associated with the given message
*/
public function mail_message_load($p)
{
if (!$p['object']->headers->others['x-kolab-type']) {
$this->message_notes = $this->get_message_notes($p['object']->headers, $p['object']->folder);
}
}
/**
* Handler for 'messagebody_html' hook
*/
public function mail_messagebody_html($args)
{
$html = '';
foreach ($this->message_notes as $note) {
$html .= html::a(array(
'href' => $this->rc->url(array('task' => 'notes', '_list' => $note['list'], '_id' => $note['uid'])),
'class' => 'kolabnotesref',
'rel' => $note['uid'] . '@' . $note['list'],
'target' => '_blank',
), rcube::Q($note['title']));
}
// prepend note links to message body
if ($html) {
$this->load_ui();
$args['content'] = html::div('kolabmessagenotes boxinformation', $html) . $args['content'];
}
return $args;
}
/**
* Determine whether the given note is HTML formatted
*/
private function is_html($note)
{
// check for opening and closing <html> or <body> tags
return (preg_match('/<(html|body)(\s+[a-z]|>)/', $note['description'], $m) && strpos($note['description'], '</'.$m[1].'>') > 0);
}
/**
* Build an RFC 822 message from the given note
*/
private function note2message($note)
{
$message = new Mail_mime("\r\n");
$message->setParam('text_encoding', '8bit');
$message->setParam('html_encoding', 'quoted-printable');
$message->setParam('head_encoding', 'quoted-printable');
$message->setParam('head_charset', RCUBE_CHARSET);
$message->setParam('html_charset', RCUBE_CHARSET);
$message->setParam('text_charset', RCUBE_CHARSET);
$message->headers(array(
'Subject' => $note['title'],
'Date' => $note['changed']->format('r'),
));
if ($this->is_html($note)) {
$message->setHTMLBody($note['description']);
// add a plain text version of the note content as an alternative part.
$h2t = new rcube_html2text($note['description'], false, true, 0, RCUBE_CHARSET);
$plain_part = rcube_mime::wordwrap($h2t->get_text(), $this->rc->config->get('line_length', 72), "\r\n", false, RCUBE_CHARSET);
$plain_part = trim(wordwrap($plain_part, 998, "\r\n", true));
// make sure all line endings are CRLF
$plain_part = preg_replace('/\r?\n/', "\r\n", $plain_part);
$message->setTXTBody($plain_part);
}
else {
$message->setTXTBody($note['description']);
}
return $message->getMessage();
}
private function save_links($uid, $links)
{
$config = kolab_storage_config::get_instance();
return $config->save_object_links($uid, (array) $links);
}
/**
* Find messages assigned to specified note
*/
private function get_links($uid)
{
$config = kolab_storage_config::get_instance();
return $config->get_object_links($uid);
}
/**
* Get note tags
*/
private function get_tags($uid)
{
$config = kolab_storage_config::get_instance();
$tags = $config->get_tags($uid);
$tags = array_map(function($v) { return $v['name']; }, $tags);
return $tags;
}
/**
* Find notes assigned to specified message
*/
private function get_message_notes($message, $folder)
{
$config = kolab_storage_config::get_instance();
$result = $config->get_message_relations($message, $folder, 'note');
foreach ($result as $idx => $note) {
$result[$idx]['list'] = kolab_storage::folder_id($note['_mailbox']);
}
return $result;
}
/**
* Update note tags
*/
private function save_tags($uid, $tags)
{
$config = kolab_storage_config::get_instance();
$config->save_tags($uid, $tags);
}
/**
* Process the given note data (submitted by the client) before saving it
*/
private function _write_preprocess($note, $old = array())
{
$object = $note;
// TODO: handle attachments
// convert link references into simple URIs
if (array_key_exists('links', $note)) {
$object['links'] = array_map(function($link){ return is_array($link) ? $link['uri'] : strval($link); }, $note['links']);
}
else {
if ($old) {
$object['links'] = $old['links'] ?? null;
}
}
// clean up HTML content
$object['description'] = $this->_wash_html($note['description']);
$is_html = true;
// try to be smart and convert to plain-text if no real formatting is detected
if (preg_match('!<body><(?:p|pre)>(.*)</(?:p|pre)></body>!Uims', $object['description'], $m)) {
if (!preg_match('!<(a|b|i|strong|em|p|span|div|pre|li|img)(\s+[a-z]|>)!im', $m[1], $n)
|| ($n[1] != 'img' && !strpos($m[1], '</'.$n[1].'>'))
) {
// $converter = new rcube_html2text($m[1], false, true, 0);
// $object['description'] = rtrim($converter->get_text());
$object['description'] = html_entity_decode(preg_replace('!<br(\s+/)>!', "\n", $m[1]));
$is_html = false;
}
}
// Add proper HTML header, otherwise Kontact renders it as plain text
if ($is_html) {
$object['description'] = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">'."\n" .
str_replace('<head>', '<head><meta name="qrichtext" content="1" />', $object['description']);
}
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// make list of categories unique
if (!empty($object['tags'])) {
$object['tags'] = array_unique(array_filter($object['tags']));
}
unset($object['list'], $object['tempid'], $object['created'], $object['changed'], $object['created_'], $object['changed_']);
return $object;
}
/**
* Sanity checks/cleanups HTML content
*/
private function _wash_html($html)
{
// Add header with charset spec., washtml cannot work without that
$html = '<html><head>'
. '<meta http-equiv="Content-Type" content="text/html; charset='.RCUBE_CHARSET.'" />'
. '</head><body>' . $html . '</body></html>';
// clean HTML with washtml by Frederic Motte
$wash_opts = array(
'show_washed' => false,
'allow_remote' => 1,
'charset' => RCUBE_CHARSET,
'html_elements' => array('html', 'head', 'meta', 'body', 'link'),
'html_attribs' => array('rel', 'type', 'name', 'http-equiv'),
);
// initialize HTML washer
$washer = new rcube_washtml($wash_opts);
$washer->add_callback('form', array($this, '_washtml_callback'));
$washer->add_callback('a', array($this, '_washtml_callback'));
// Remove non-UTF8 characters
$html = rcube_charset::clean($html);
$html = $washer->wash($html);
// remove unwanted comments (produced by washtml)
$html = preg_replace('/<!--[^>]+-->/', '', $html);
return $html;
}
/**
* Callback function for washtml cleaning class
*/
public function _washtml_callback($tagname, $attrib, $content, $washtml)
{
switch ($tagname) {
case 'form':
$out = html::div('form', $content);
break;
case 'a':
// strip temporary link tags from plain-text markup
$attrib = html::parse_attrib_string($attrib);
if (!empty($attrib['class']) && strpos($attrib['class'], 'x-templink') !== false) {
// remove link entirely
if (strpos($attrib['href'], html_entity_decode($content)) !== false) {
$out = $content;
break;
}
$attrib['class'] = trim(str_replace('x-templink', '', $attrib['class']));
}
$out = html::a($attrib, $content);
break;
default:
$out = '';
}
return $out;
}
}
diff --git a/plugins/libkolab/lib/kolab_ldap.php b/plugins/libkolab/lib/kolab_ldap.php
index c51fede4..46085b86 100644
--- a/plugins/libkolab/lib/kolab_ldap.php
+++ b/plugins/libkolab/lib/kolab_ldap.php
@@ -1,613 +1,613 @@
<?php
/**
* Kolab Authentication and User Base
*
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011-2019, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Wrapper class for rcube_ldap_generic
*/
class kolab_ldap extends rcube_ldap_generic
{
private $conf = array();
private $fieldmap = array();
private $rcache;
function __construct($p)
{
$rcmail = rcube::get_instance();
$this->conf = $p;
$this->conf['kolab_auth_user_displayname'] = $rcmail->config->get('kolab_auth_user_displayname', '{name}');
$this->fieldmap = $p['fieldmap'];
$this->fieldmap['uid'] = 'uid';
$p['attributes'] = array_values($this->fieldmap);
$p['debug'] = (bool) $rcmail->config->get('ldap_debug');
if ($cache_type = $rcmail->config->get('ldap_cache', 'db')) {
$cache_ttl = $rcmail->config->get('ldap_cache_ttl', '10m');
$this->cache = $rcmail->get_cache('LDAP.kolab_cache', $cache_type, $cache_ttl);
}
// Connect to the server (with bind)
parent::__construct($p);
$this->_connect();
$rcmail->add_shutdown_function(array($this, 'close'));
}
/**
* Establish a connection to the LDAP server
*/
private function _connect()
{
// try to connect + bind for every host configured
// with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
// see http://www.php.net/manual/en/function.ldap-connect.php
foreach ((array)$this->config['hosts'] as $host) {
// skip host if connection failed
if (!$this->connect($host)) {
continue;
}
$bind_pass = $this->config['bind_pass'] ?? null;
$bind_user = $this->config['bind_user'] ?? null;
$bind_dn = $this->config['bind_dn'];
$base_dn = $this->config['base_dn'];
$groups_base_dn = $this->config['groups']['base_dn'] ?: $base_dn;
// User specific access, generate the proper values to use.
if ($this->config['user_specific']) {
$rcube = rcube::get_instance();
// No password set, use the session password
if (empty($bind_pass)) {
$bind_pass = $rcube->get_user_password();
}
$u = null;
// Get the pieces needed for variable replacement.
if ($fu = ($rcube->get_user_email() ?: ($this->config['username'] ?? null))) {
list($u, $d) = explode('@', $fu);
}
else {
$d = $this->config['mail_domain'] ?? null;
}
$dc = 'dc=' . strtr($d, array('.' => ',dc=')); // hierarchal domain string
// resolve $dc through LDAP
if (!empty($this->config['domain_filter']) && !empty($this->config['search_bind_dn'])) {
$this->bind($this->config['search_bind_dn'], $this->config['search_bind_pw']);
$dc = $this->domain_root_dn($d);
}
$replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
// Search for the dn to use to authenticate
if (($this->config['search_base_dn'] ?? false) && ($this->config['search_filter'] ?? false)
&& (strstr($bind_dn, '%dn') || strstr($base_dn, '%dn') || strstr($groups_base_dn, '%dn'))
) {
$search_attribs = array('uid');
if ($search_bind_attrib = (array) $this->config['search_bind_attrib']) {
foreach ($search_bind_attrib as $r => $attr) {
$search_attribs[] = $attr;
$replaces[$r] = '';
}
}
$search_bind_dn = strtr($this->config['search_bind_dn'], $replaces);
$search_base_dn = strtr($this->config['search_base_dn'], $replaces);
$search_filter = strtr($this->config['search_filter'], $replaces);
$cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:" . $this->config['search_bind_pw']);
if ($this->cache && ($dn = $this->cache->get($cache_key))) {
$replaces['%dn'] = $dn;
}
else {
$ldap = $this;
if (!empty($search_bind_dn) && !empty($this->config['search_bind_pw'])) {
// To protect from "Critical extension is unavailable" error
// we need to use a separate LDAP connection
if (!empty($this->config['vlv'])) {
$ldap = new rcube_ldap_generic($this->config);
$ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug));
if (!$ldap->connect($host)) {
continue;
}
}
if (!$ldap->bind($search_bind_dn, $this->config['search_bind_pw'])) {
continue; // bind failed, try next host
}
}
$res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs);
if ($res) {
$res->rewind();
$replaces['%dn'] = key($res->entries(true));
// add more replacements from 'search_bind_attrib' config
if ($search_bind_attrib) {
$res = $res->current();
foreach ($search_bind_attrib as $r => $attr) {
$replaces[$r] = $res[$attr][0];
}
}
}
if ($ldap != $this) {
$ldap->close();
}
}
// DN not found
if (empty($replaces['%dn'])) {
if (!empty($this->config['search_dn_default']))
$replaces['%dn'] = $this->config['search_dn_default'];
else {
rcube::raise_error(array(
'code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "DN not found using LDAP search."), true);
continue;
}
}
if ($this->cache && !empty($replaces['%dn'])) {
$this->cache->set($cache_key, $replaces['%dn']);
}
}
// Replace the bind_dn and base_dn variables.
$bind_dn = strtr($bind_dn, $replaces);
$base_dn = strtr($base_dn, $replaces);
$groups_base_dn = strtr($groups_base_dn, $replaces);
// replace placeholders in filter settings
if (!empty($this->config['filter'])) {
$this->config['filter'] = strtr($this->config['filter'], $replaces);
}
foreach (array('base_dn', 'filter', 'member_filter') as $k) {
if (!empty($this->config['groups'][$k])) {
$this->config['groups'][$k] = strtr($this->config['groups'][$k], $replaces);
}
}
if (empty($bind_user)) {
$bind_user = $u;
}
}
if (empty($bind_pass)) {
$this->ready = true;
}
else {
if (!empty($this->config['auth_cid'])) {
$this->ready = $this->sasl_bind($this->config['auth_cid'], $bind_pass, $bind_dn);
}
else if (!empty($bind_dn)) {
$this->ready = $this->bind($bind_dn, $bind_pass);
}
else {
$this->ready = $this->sasl_bind($bind_user, $bind_pass);
}
}
// connection established, we're done here
if ($this->ready) {
break;
}
} // end foreach hosts
if (!is_resource($this->conn)) {
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not connect to any LDAP server, last tried $host"), true);
$this->ready = false;
}
return $this->ready;
}
/**
* Fetches user data from LDAP addressbook
*/
function get_user_record($user, $host)
{
$rcmail = rcube::get_instance();
$filter = $rcmail->config->get('kolab_auth_filter');
$filter = $this->parse_vars($filter, $user, $host);
$base_dn = $this->parse_vars($this->config['base_dn'], $user, $host);
$scope = $this->config['scope'];
// @TODO: print error if filter is empty
// get record
if ($result = parent::search($base_dn, $filter, $scope, $this->attributes)) {
if ($result->count() == 1) {
$entries = $result->entries(true);
$dn = key($entries);
$entry = array_pop($entries);
$entry = $this->field_mapping($dn, $entry);
return $entry;
}
}
}
/**
* Fetches user data from LDAP addressbook
*/
function get_user_groups($dn, $user, $host)
{
if (empty($dn) || empty($this->config['groups'])) {
return array();
}
$base_dn = $this->parse_vars($this->config['groups']['base_dn'], $user, $host);
$name_attr = $this->config['groups']['name_attr'] ? $this->config['groups']['name_attr'] : 'cn';
$member_attr = $this->get_group_member_attr();
$filter = "(member=$dn)(uniqueMember=$dn)";
if ($member_attr != 'member' && $member_attr != 'uniqueMember')
$filter .= "($member_attr=$dn)";
$filter = strtr("(|$filter)", array("\\" => "\\\\"));
$result = parent::search($base_dn, $filter, 'sub', array('dn', $name_attr));
if (!$result) {
return array();
}
$groups = array();
foreach ($result as $entry) {
$dn = $entry['dn'];
$entry = rcube_ldap_generic::normalize_entry($entry);
$groups[$dn] = $entry[$name_attr];
}
return $groups;
}
/**
* Get a specific LDAP record
*
* @param string DN
*
* @return array Record data
*/
function get_record($dn)
{
if (!$this->ready) {
return;
}
if ($rec = $this->get_entry($dn, $this->attributes)) {
$rec = rcube_ldap_generic::normalize_entry($rec);
$rec = $this->field_mapping($dn, $rec);
}
return $rec;
}
/**
* Replace LDAP record data items
*
* @param string $dn DN
* @param array $entry LDAP entry
*
* return bool True on success, False on failure
*/
function replace($dn, $entry)
{
// fields mapping
foreach ($this->fieldmap as $field => $attr) {
if (array_key_exists($field, $entry)) {
$entry[$attr] = $entry[$field];
if ($attr != $field) {
unset($entry[$field]);
}
}
}
return $this->mod_replace($dn, $entry);
}
/**
* Search records (simplified version of rcube_ldap::search)
*
* @param mixed $fields The field name or array of field names to search in
* @param string $value Search value
* @param int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* @param array $required List of fields that cannot be empty
* @param int $limit Number of records
* @param int $count Returns the number of records found
*
* @return array List of LDAP records found
*/
function dosearch($fields, $value, $mode=1, $required = array(), $limit = 0, &$count = 0)
{
if (empty($fields)) {
return array();
}
$mode = intval($mode);
// try to resolve field names into ldap attributes
$fieldmap = $this->fieldmap;
$attrs = array_map(function($f) use ($fieldmap) {
return array_key_exists($f, $fieldmap) ? $fieldmap[$f] : $f;
}, (array)$fields);
// compose a full-text-search-like filter
if (count($attrs) > 1 || $mode != 1) {
$filter = self::fulltext_search_filter($value, $attrs, $mode);
}
// direct search
else {
$field = $attrs[0];
$filter = "($field=" . self::quote_string($value) . ")";
}
// add required (non empty) fields filter
$req_filter = '';
foreach ((array)$required as $field) {
$attr = array_key_exists($field, $this->fieldmap) ? $this->fieldmap[$field] : $field;
// only add if required field is not already in search filter
if (!in_array($attr, $attrs)) {
$req_filter .= "($attr=*)";
}
}
if (!empty($req_filter)) {
$filter = '(&' . $req_filter . $filter . ')';
}
// avoid double-wildcard if $value is empty
$filter = preg_replace('/\*+/', '*', $filter);
// add general filter to query
if (!empty($this->config['filter'])) {
$filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->config['filter']) . ')' . $filter . ')';
}
$base_dn = $this->parse_vars($this->config['base_dn']);
$scope = $this->config['scope'];
$attrs = array_values($this->fieldmap);
$list = array();
if ($result = $this->search($base_dn, $filter, $scope, $attrs)) {
$count = $result->count();
$i = 0;
foreach ($result as $entry) {
if ($limit && $limit <= $i) {
break;
}
$dn = $entry['dn'];
$entry = rcube_ldap_generic::normalize_entry($entry);
$list[$dn] = $this->field_mapping($dn, $entry);
$i++;
}
}
return $list;
}
/**
* Set filter used in search()
*/
function set_filter($filter)
{
$this->config['filter'] = $filter;
}
/**
* Maps LDAP attributes to defined fields
*/
protected function field_mapping($dn, $entry)
{
$entry['dn'] = $dn;
// fields mapping
foreach ($this->fieldmap as $field => $attr) {
// $entry might be indexed by lower-case attribute names
$attr_lc = strtolower($attr);
if (isset($entry[$attr_lc])) {
$entry[$field] = $entry[$attr_lc];
}
else if (isset($entry[$attr])) {
$entry[$field] = $entry[$attr];
}
}
// compose display name according to config
if (empty($this->fieldmap['displayname'])) {
$entry['displayname'] = rcube_addressbook::compose_search_name(
$entry,
$entry['email'],
- $entry['name'],
+ $entry['name'] ?? null,
$this->conf['kolab_auth_user_displayname']
);
}
return $entry;
}
/**
* Detects group member attribute name
*/
private function get_group_member_attr($object_classes = array())
{
if (empty($object_classes)) {
$object_classes = $this->config['groups']['object_classes'];
}
if (!empty($object_classes)) {
foreach ((array)$object_classes as $oc) {
switch (strtolower($oc)) {
case 'group':
case 'groupofnames':
case 'kolabgroupofnames':
$member_attr = 'member';
break;
case 'groupofuniquenames':
case 'kolabgroupofuniquenames':
$member_attr = 'uniqueMember';
break;
}
}
}
if (!empty($member_attr)) {
return $member_attr;
}
if (!empty($this->config['groups']['member_attr'])) {
return $this->config['groups']['member_attr'];
}
return 'member';
}
/**
* Prepares filter query for LDAP search
*/
function parse_vars($str, $user = null, $host = null)
{
// When authenticating user $user is always set
// if not set it means we use this LDAP object for other
// purposes, e.g. kolab_delegation, then username with
// correct domain is in a session
if (!$user) {
$user = $_SESSION['username'];
}
$dc = null;
if (isset($this->icache[$user])) {
list($user, $dc) = $this->icache[$user];
}
else {
$orig_user = $user;
$rcmail = rcube::get_instance();
// get default domain
if ($username_domain = $rcmail->config->get('username_domain')) {
if ($host && is_array($username_domain) && isset($username_domain[$host])) {
$domain = rcube_utils::parse_host($username_domain[$host], $host);
}
else if (is_string($username_domain)) {
$domain = rcube_utils::parse_host($username_domain, $host);
}
}
// realmed username (with domain)
if (strpos($user, '@')) {
list($usr, $dom) = explode('@', $user);
// unrealm domain, user login can contain a domain alias
if ($dom != $domain && ($dc = $this->domain_root_dn($dom))) {
// @FIXME: we should replace domain in $user, I suppose
}
}
else if ($domain) {
$user .= '@' . $domain;
}
$this->icache[$orig_user] = array($user, $dc);
}
// replace variables in filter
list($u, $d) = explode('@', $user);
// hierarchal domain string
if (empty($dc)) {
$dc = 'dc=' . strtr($d, array('.' => ',dc='));
}
$replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
$this->parse_replaces = $replaces;
return strtr($str, $replaces);
}
/**
* Returns variables used for replacement in (last) parse_vars() call
*
* @return array Variable-value hash array
*/
public function get_parse_vars()
{
return $this->parse_replaces;
}
/**
* Register additional fields
*/
public function extend_fieldmap($map)
{
foreach ((array)$map as $name => $attr) {
if (!in_array($attr, $this->attributes)) {
$this->attributes[] = $attr;
$this->fieldmap[$name] = $attr;
}
}
}
/**
* HTML-safe DN string encoding
*
* @param string $str DN string
*
* @return string Encoded HTML identifier string
*/
static function dn_encode($str)
{
return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
}
/**
* Decodes DN string encoded with _dn_encode()
*
* @param string $str Encoded HTML identifier string
*
* @return string DN string
*/
static function dn_decode($str)
{
$str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
return base64_decode($str);
}
}
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 1ef2138e..e0551000 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -1,1810 +1,1811 @@
<?php
/**
* Kolab storage class providing static methods to access groupware objects on a Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage
{
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
const NAME_KEY_SHARED = '/shared/vendor/kolab/displayname';
const NAME_KEY_PRIVATE = '/private/vendor/kolab/displayname';
const UID_KEY_SHARED = '/shared/vendor/kolab/uniqueid';
const UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
const ERROR_IMAP_CONN = 1;
const ERROR_CACHE_DB = 2;
const ERROR_NO_PERMISSION = 3;
const ERROR_INVALID_FOLDER = 4;
public static $version = '3.0';
public static $last_error;
public static $encode_ids = false;
private static $ready = false;
private static $with_tempsubs = true;
private static $subscriptions;
private static $ldapcache = array();
private static $ldap = array();
private static $states;
private static $config;
private static $imap;
// Default folder names
private static $default_folders = array(
'event' => 'Calendar',
'contact' => 'Contacts',
'task' => 'Tasks',
'note' => 'Notes',
'file' => 'Files',
'configuration' => 'Configuration',
'journal' => 'Journal',
'mail.inbox' => 'INBOX',
'mail.drafts' => 'Drafts',
'mail.sentitems' => 'Sent',
'mail.wastebasket' => 'Trash',
'mail.outbox' => 'Outbox',
'mail.junkemail' => 'Junk',
);
/**
* Setup the environment needed by the libs
*/
public static function setup()
{
if (self::$ready)
return true;
$rcmail = rcube::get_instance();
self::$config = $rcmail->config;
self::$version = strval($rcmail->config->get('kolab_format_version', self::$version));
self::$imap = $rcmail->get_storage();
self::$ready = class_exists('kolabformat') &&
(self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
if (self::$ready) {
// do nothing
}
else if (!class_exists('kolabformat')) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabformat module not found"
), true);
}
else if (self::$imap->get_error_code()) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php', 'message' => "IMAP error"
), true);
}
// adjust some configurable settings
if ($event_scheduling_prop = $rcmail->config->get('kolab_event_scheduling_properties', null)) {
kolab_format_event::$scheduling_properties = (array)$event_scheduling_prop;
}
// adjust some configurable settings
if ($task_scheduling_prop = $rcmail->config->get('kolab_task_scheduling_properties', null)) {
kolab_format_task::$scheduling_properties = (array)$task_scheduling_prop;
}
return self::$ready;
}
/**
* Initializes LDAP object to resolve Kolab users
*
* @param string $name Name of the configuration option with LDAP config
*/
public static function ldap($name = 'kolab_users_directory')
{
self::setup();
$config = self::$config->get($name);
if (empty($config)) {
$name = 'kolab_auth_addressbook';
$config = self::$config->get($name);
}
if (!empty(self::$ldap[$name])) {
return self::$ldap[$name];
}
if (!is_array($config)) {
$ldap_config = (array)self::$config->get('ldap_public');
$config = $ldap_config[$config];
}
if (empty($config)) {
return null;
}
$ldap = new kolab_ldap($config);
// overwrite filter option
if ($filter = self::$config->get('kolab_users_filter')) {
self::$config->set('kolab_auth_filter', $filter);
}
$user_field = $user_attrib = self::$config->get('kolab_users_id_attrib');
// Fallback to kolab_auth_login, which is not attribute, but field name
if (!$user_field && ($user_field = self::$config->get('kolab_auth_login', 'email'))) {
$user_attrib = $config['fieldmap'][$user_field];
}
if ($user_field && $user_attrib) {
$ldap->extend_fieldmap(array($user_field => $user_attrib));
}
self::$ldap[$name] = $ldap;
return $ldap;
}
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
public static function get_folders($type, $subscribed = null)
{
$folders = $folderdata = array();
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
$folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Getter for the storage folder for the given type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @return object kolab_storage_folder The folder object
*/
public static function get_default_folder($type)
{
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
return new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return null;
}
/**
* Getter for a specific storage folder
*
* @param string IMAP folder to access (UTF7-IMAP)
* @param string Expected folder type
*
* @return object kolab_storage_folder The folder object
*/
public static function get_folder($folder, $type = null)
{
return self::setup() ? new kolab_storage_folder($folder, $type) : null;
}
/**
* Getter for a single Kolab object, identified by its UID.
* This will search all folders storing objects of the given type.
*
* @param string Object UID
* @param string Object type (contact,event,task,journal,file,note,configuration)
* @return array The Kolab object represented as hash array or false if not found
*/
public static function get_object($uid, $type)
{
self::setup();
$folder = null;
foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
if (!$folder)
$folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
else
$folder->set_folder($foldername, $type, $folderdata[$foldername]);
if ($object = $folder->get_object($uid))
return $object;
}
return false;
}
/**
* Execute cross-folder searches with the given query.
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* @param string Folder type (contact,event,task,journal,file,note,configuration)
* @param int Expected number of records or limit (for performance reasons)
*
* @return array List of Kolab data objects (each represented as hash array)
* @see kolab_storage_format::select()
*/
public static function select($query, $type, $limit = null)
{
self::setup();
$folder = null;
$result = array();
foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
$folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
if ($limit) {
$folder->set_order_and_limit(null, $limit);
}
foreach ($folder->select($query) as $object) {
$result[] = $object;
}
}
return $result;
}
/**
* Returns Free-busy server URL
*/
public static function get_freebusy_server()
{
$rcmail = rcube::get_instance();
$url = 'https://' . $_SESSION['imap_host'] . '/freebusy';
$url = $rcmail->config->get('kolab_freebusy_server', $url);
$url = rcube_utils::resolve_url($url);
return unslashify($url);
}
/**
* Compose an URL to query the free/busy status for the given user
*
* @param string Email address of the user to get free/busy data for
* @param object DateTime Start of the query range (optional)
* @param object DateTime End of the query range (optional)
*
* @return string Fully qualified URL to query free/busy data
*/
public static function get_freebusy_url($email, $start = null, $end = null)
{
$query = '';
$param = array();
$utc = new \DateTimeZone('UTC');
// https://www.calconnect.org/pubdocs/CD0903%20Freebusy%20Read%20URL.pdf
if ($start instanceof \DateTime) {
$start->setTimezone($utc);
$param['start'] = $param['dtstart'] = $start->format('Ymd\THis\Z');
}
if ($end instanceof \DateTime) {
$end->setTimezone($utc);
$param['end'] = $param['dtend'] = $end->format('Ymd\THis\Z');
}
if (!empty($param)) {
$query = '?' . http_build_query($param);
}
$url = self::get_freebusy_server();
if (strpos($url, '%u')) {
// Expected configured full URL, just replace the %u variable
// Note: Cyrus v3 Free-Busy service does not use .ifb extension
$url = str_replace('%u', rawurlencode($email), $url);
}
else {
$url .= '/' . $email . '.ifb';
}
return $url . $query;
}
/**
* Creates folder ID from folder name
*
* @param string $folder Folder name (UTF7-IMAP)
* @param boolean $enc Use lossless encoding
* @return string Folder ID string
*/
public static function folder_id($folder, $enc = null)
{
return $enc == true || ($enc === null && self::$encode_ids) ?
self::id_encode($folder) :
asciiwords(strtr($folder, '/.-', '___'));
}
/**
* Encode the given ID to a safe ascii representation
*
* @param string $id Arbitrary identifier string
*
* @return string Ascii representation
*/
public static function id_encode($id)
{
return rtrim(strtr(base64_encode($id), '+/', '-_'), '=');
}
/**
* Convert the given identifier back to it's raw value
*
* @param string $id Ascii identifier
* @return string Raw identifier string
*/
public static function id_decode($id)
{
return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
}
/**
* Return the (first) path of the requested IMAP namespace
*
* @param string Namespace name (personal, shared, other)
* @return string IMAP root path for that namespace
*/
public static function namespace_root($name)
{
self::setup();
foreach ((array)self::$imap->get_namespace($name) as $paths) {
if (strlen($paths[0]) > 1) {
return $paths[0];
}
}
return '';
}
/**
* Deletes IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_delete($name)
{
// clear cached entries first
if ($folder = self::get_folder($name))
$folder->cache->purge();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_delete', array('name' => $name));
$success = self::$imap->delete_folder($name);
self::$last_error = self::$imap->get_error_str();
return $success;
}
/**
* Creates IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $subscribed Sets folder subscription
* @param bool $active Sets folder state (client-side subscription)
*
* @return bool True on success, false on failure
*/
public static function folder_create($name, $type = null, $subscribed = false, $active = false)
{
self::setup();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_create', array('record' => array(
'name' => $name,
'subscribe' => $subscribed,
)));
if ($saved = self::$imap->create_folder($name, $subscribed)) {
// set metadata for folder type
if ($type) {
$saved = self::set_folder_type($name, $type);
// revert if metadata could not be set
if (!$saved) {
self::$imap->delete_folder($name);
}
// activate folder
else if ($active) {
self::set_state($name, true);
}
}
}
if ($saved) {
return true;
}
self::$last_error = self::$imap->get_error_str();
return false;
}
/**
* Renames IMAP folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_rename($oldname, $newname)
{
self::setup();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_rename', array(
'oldname' => $oldname, 'newname' => $newname));
$oldfolder = self::get_folder($oldname);
$active = self::folder_is_active($oldname);
$success = self::$imap->rename_folder($oldname, $newname);
self::$last_error = self::$imap->get_error_str();
// pass active state to new folder name
if ($success && $active) {
self::set_state($oldname, false);
self::set_state($newname, true);
}
// assign existing cache entries to new resource uri
if ($success && $oldfolder) {
$oldfolder->cache->rename($newname);
}
return $success;
}
/**
* Rename or Create a new IMAP folder.
*
* Does additional checks for permissions and folder name restrictions
*
* @param array &$prop Hash array with folder properties and metadata
* - name: Folder name
* - oldname: Old folder name when changed
* - parent: Parent folder to create the new one in
* - type: Folder type to create
* - subscribed: Subscribed flag (IMAP subscription)
* - active: Activation flag (client-side subscription)
*
* @return string|false New folder name or False on failure
*
* @see self::set_folder_props() for list of other properties
*/
public static function folder_update(&$prop)
{
self::setup();
$folder = rcube_charset::convert($prop['name'], RCUBE_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
$delimiter = self::$imap->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
$options = self::$imap->folder_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
self::$last_error = 'cannotbeempty';
return false;
}
else if (strlen($folder) > 128) {
self::$last_error = 'nametoolong';
return false;
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $char) !== false) {
self::$last_error = 'forbiddencharacter';
return false;
}
}
}
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($parent)) {
$folder = $parent . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$folder = self::$imap->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$parent_opts = self::$imap->folder_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
self::$last_error = 'No permission to create folder';
return false;
}
}
// update the folder name
if (strlen($oldfolder)) {
if ($oldfolder != $folder) {
$result = self::folder_rename($oldfolder, $folder);
}
else {
$result = true;
}
}
// create new folder
else {
$result = self::folder_create($folder, $prop['type'], $prop['subscribed'], $prop['active']);
}
if ($result) {
self::set_folder_props($folder, $prop);
}
return $result ? $folder : false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* with kolab_custom_display_names support.
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_name($folder, &$folder_ns=null)
{
// find custom display name in folder METADATA
if ($name = self::custom_displayname($folder)) {
return $name;
}
return self::object_prettyname($folder, $folder_ns);
}
/**
* Get custom display name (saved in metadata) for the given folder
*/
public static function custom_displayname($folder)
{
static $_metadata;
// find custom display name in folder METADATA
if (self::$config->get('kolab_custom_display_names', true) && self::setup()) {
if ($_metadata !== null) {
$metadata = $_metadata;
}
else {
// For performance reasons ask for all folders, it will be cached as one cache entry
$metadata = self::$imap->get_metadata("*", array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
// If cache is disabled store result in memory
if (!self::$config->get('imap_cache')) {
$_metadata = $metadata;
}
}
- if ($data = $metadata[$folder]) {
+ if ($data = $metadata[$folder] ?? null) {
if (($name = $data[self::NAME_KEY_PRIVATE]) || ($name = $data[self::NAME_KEY_SHARED])) {
return $name;
}
}
}
return false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_prettyname($folder, &$folder_ns=null)
{
self::setup();
$found = false;
$namespace = self::$imap->get_namespace();
+ $prefix = null;
if (!empty($namespace['shared'])) {
foreach ($namespace['shared'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
$prefix = '';
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
$found = true;
$folder_ns = 'shared';
break;
}
}
}
if (!$found && !empty($namespace['other'])) {
foreach ($namespace['other'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix and extract username
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
// get username part and map it to user name
$pos = strpos($folder, $delim);
$fid = $pos ? substr($folder, 0, $pos) : $folder;
if ($user = self::folder_id2user($fid, true)) {
$fid = str_replace($delim, '', $user);
}
$prefix = "($fid)";
$folder = $pos ? substr($folder, $pos + 1) : '';
$found = true;
$folder_ns = 'other';
break;
}
}
}
if (!$found && !empty($namespace['personal'])) {
foreach ($namespace['personal'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$prefix = '';
$delim = $ns[1];
$found = true;
break;
}
}
}
if (empty($delim))
$delim = self::$imap->get_hierarchy_delimiter();
$folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$folder = html::quote($folder);
$folder = str_replace(html::quote($delim), ' » ', $folder);
if ($prefix)
$folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : '');
if (!$folder_ns)
$folder_ns = 'personal';
return $folder;
}
/**
* Helper method to generate a truncated folder name to display.
* Note: $origname is a string returned by self::object_name()
*/
public static function folder_displayname($origname, &$names)
{
$name = $origname;
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i] . ' » ') === 0) {
$length = strlen($names[$i] . ' » ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$diff = 1;
// check if prefix folder is in other users namespace
for ($n = count($names)-1; $n >= 0; $n--) {
if (strpos($prefix, '(' . $names[$n] . ') ') === 0) {
$diff = 0;
break;
}
}
$name = str_repeat(' ', $count - $diff) . '» ' . substr($name, $length);
break;
}
// other users namespace and parent folder exists
else if (strpos($name, '(' . $names[$i] . ') ') === 0) {
$length = strlen('(' . $names[$i] . ') ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$name = str_repeat(' ', $count) . '» ' . substr($name, $length);
break;
}
}
$names[] = $origname;
return $name;
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public static function folder_selector($type, $attrs, $current = '')
{
// get all folders of specified type (sorted)
$folders = self::get_folders($type, true);
$delim = self::$imap->get_hierarchy_delimiter();
$names = array();
$len = strlen($current);
if ($len && ($rpos = strrpos($current, $delim))) {
$parent = substr($current, 0, $rpos);
$p_len = strlen($parent);
}
// Filter folders list
foreach ($folders as $c_folder) {
$name = $c_folder->name;
// skip current folder and it's subfolders
if ($len) {
if ($name == $current) {
// Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
if ($p_len && !isset($names[$parent])) {
$names[$parent] = self::object_name($parent);
}
continue;
}
if (strpos($name, $current.$delim) === 0) {
continue;
}
}
// always show the parent of current folder
if ($p_len && $name == $parent) {
}
// skip folders where user have no rights to create subfolders
else if ($c_folder->get_owner() != $_SESSION['username']) {
$rights = $c_folder->get_myrights();
if (!preg_match('/[ck]/', $rights)) {
continue;
}
}
$names[$name] = $c_folder->get_name();
}
// Build SELECT field of parent folder
$attrs['is_escaped'] = true;
$select = new html_select($attrs);
$select->add('---', '');
$listnames = array();
foreach (array_keys($names) as $imap_name) {
$name = $origname = $names[$imap_name];
// find folder prefix to truncate
for ($i = count($listnames)-1; $i >= 0; $i--) {
if (strpos($name, $listnames[$i].' » ') === 0) {
$length = strlen($listnames[$i].' » ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$name = str_repeat(' ', $count-1) . '» ' . substr($name, $length);
break;
}
}
$listnames[] = $origname;
$select->add($name, $imap_name);
}
return $select;
}
/**
* Returns a list of folder names
*
* @param string Optional root folder
* @param string Optional name pattern
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
{
if (!self::setup()) {
return null;
}
// use IMAP subscriptions
if ($subscribed === null && self::$config->get('kolab_use_subscriptions')) {
$subscribed = true;
}
if (!$filter) {
// Get ALL folders list, standard way
if ($subscribed) {
$folders = self::_imap_list_subscribed($root, $mbox, $filter);
}
else {
$folders = self::_imap_list_folders($root, $mbox);
}
return $folders;
}
$prefix = $root . $mbox;
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
// get folders types for all folders
$folderdata = self::folders_typedata($prefix);
if (!is_array($folderdata)) {
return array();
}
// If we only want groupware folders and don't care about the subscription state,
// then the metadata will already contain all folder names and we can avoid the LIST below.
if (!$subscribed && $filter != 'mail' && $prefix == '*') {
foreach ($folderdata as $folder => $type) {
if (!preg_match($regexp, $type)) {
unset($folderdata[$folder]);
}
}
return self::$imap->sort_folder_list(array_keys($folderdata), true);
}
// Get folders list
if ($subscribed) {
$folders = self::_imap_list_subscribed($root, $mbox, $filter);
}
else {
$folders = self::_imap_list_folders($root, $mbox);
}
// In case of an error, return empty list (?)
if (!is_array($folders)) {
return array();
}
// Filter folders list
foreach ($folders as $idx => $folder) {
- $type = $folderdata[$folder];
+ $type = $folderdata[$folder] ?? null;
if ($filter == 'mail' && empty($type)) {
continue;
}
if (empty($type) || !preg_match($regexp, $type)) {
unset($folders[$idx]);
}
}
return $folders;
}
/**
* Wrapper for rcube_imap::list_folders() with optional post-filtering
*/
protected static function _imap_list_folders($root, $mbox)
{
$postfilter = null;
// compose a post-filter expression for the excluded namespaces
if ($root . $mbox == '*' && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
$excludes = array();
foreach ((array)$skip_ns as $ns) {
if ($ns_root = self::namespace_root($ns)) {
$excludes[] = $ns_root;
}
}
if (count($excludes)) {
$postfilter = '!^(' . join(')|(', array_map('preg_quote', $excludes)) . ')!';
}
}
// use normal LIST command to return all folders, it's fast enough
$folders = self::$imap->list_folders($root, $mbox, null, null, !empty($postfilter));
if (!empty($postfilter)) {
$folders = array_filter($folders, function($folder) use ($postfilter) { return !preg_match($postfilter, $folder); });
$folders = self::$imap->sort_folder_list($folders);
}
return $folders;
}
/**
* Wrapper for rcube_imap::list_folders_subscribed()
* with support for temporarily subscribed folders
*/
protected static function _imap_list_subscribed($root, $mbox, $filter = null)
{
$folders = self::$imap->list_folders_subscribed($root, $mbox);
// add temporarily subscribed folders
- if ($filter != 'mail' && self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
+ if ($filter != 'mail' && self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'] ?? null)) {
$folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
}
return $folders;
}
/**
* Search for shared or otherwise not listed groupware folders the user has access
*
* @param string Folder type of folders to search for
* @param string Search string
* @param array Namespace(s) to exclude results from
*
* @return array List of matching kolab_storage_folder objects
*/
public static function search_folders($type, $query, $exclude_ns = array())
{
if (!self::setup()) {
return array();
}
$folders = array();
$query = str_replace('*', '', $query);
// find unsubscribed IMAP folders of the given type
foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
// FIXME: only consider the last part of the folder path for searching?
$realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
if (($query == '' || strpos($realname, $query) !== false) &&
!self::folder_is_subscribed($foldername, true) &&
!in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns)
) {
$folders[] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Sort the given list of kolab folders by namespace/name
*
* @param array List of kolab_storage_folder objects
* @return array Sorted list of folders
*/
public static function sort_folders($folders)
{
$pad = ' ';
$out = array();
$nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
foreach ($folders as $folder) {
$_folders[$folder->name] = $folder;
$ns = $folder->get_namespace();
$nsnames[$ns][$folder->name] = strtolower(html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET)) . $pad; // decode »
}
// $folders is a result of get_folders() we can assume folders were already sorted
foreach (array_keys($nsnames) as $ns) {
asort($nsnames[$ns], SORT_LOCALE_STRING);
foreach (array_keys($nsnames[$ns]) as $utf7name) {
$out[] = $_folders[$utf7name];
}
}
return $out;
}
/**
* Check the folder tree and add the missing parents as virtual folders
*
* @param array $folders Folders list
* @param object $tree Reference to the root node of the folder tree
*
* @return array Flat folders list
*/
public static function folder_hierarchy($folders, &$tree = null)
{
if (!self::setup()) {
return array();
}
$_folders = array();
$delim = self::$imap->get_hierarchy_delimiter();
$other_ns = rtrim(self::namespace_root('other'), $delim);
$tree = new kolab_storage_folder_virtual('', '<root>', ''); // create tree root
$refs = array('' => $tree);
foreach ($folders as $idx => $folder) {
$path = explode($delim, $folder->name);
array_pop($path);
$folder->parent = join($delim, $path);
$folder->children = array(); // reset list
// skip top folders or ones with a custom displayname
if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) {
$tree->children[] = $folder;
}
else {
$parents = array();
$depth = $folder->get_namespace() == 'personal' ? 1 : 2;
while (count($path) >= $depth && ($parent = join($delim, $path))) {
array_pop($path);
$parent_parent = join($delim, $path);
if (!$refs[$parent]) {
if ($folder->type && self::folder_type($parent) == $folder->type) {
$refs[$parent] = new kolab_storage_folder($parent, $folder->type, $folder->type);
$refs[$parent]->parent = $parent_parent;
}
else if ($parent_parent == $other_ns) {
$refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent);
}
else {
$name = kolab_storage::object_name($parent);
$refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
}
$parents[] = $refs[$parent];
}
}
if (!empty($parents)) {
$parents = array_reverse($parents);
foreach ($parents as $parent) {
$parent_node = $refs[$parent->parent] ?: $tree;
$parent_node->children[] = $parent;
$_folders[] = $parent;
}
}
$parent_node = $refs[$folder->parent] ?: $tree;
$parent_node->children[] = $folder;
}
$refs[$folder->name] = $folder;
$_folders[] = $folder;
unset($folders[$idx]);
}
return $_folders;
}
/**
* Returns folder types indexed by folder name
*
* @param string $prefix Folder prefix (Default '*' for all folders)
*
* @return array|bool List of folders, False on failure
*/
public static function folders_typedata($prefix = '*')
{
if (!self::setup()) {
return false;
}
$type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE);
// fetch metadata from *some* folders only
if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
$delimiter = self::$imap->get_hierarchy_delimiter();
$folderdata = $blacklist = array();
foreach ((array)$skip_ns as $ns) {
if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) {
$blacklist[] = $ns_root;
}
}
foreach (array('personal','other','shared') as $ns) {
if (!in_array($ns, (array)$skip_ns)) {
$ns_root = rtrim(self::namespace_root($ns), $delimiter);
// list top-level folders and their childs one by one
// GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
if ($ns_root == '') {
foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
if (!in_array($folder, $blacklist)) {
$folderdata[$folder] = $metadata;
$opts = self::$imap->folder_attributes($folder);
if (!in_array('\\HasNoChildren', $opts) && ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
$folderdata += $data;
}
}
}
}
else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
$folderdata += $data;
}
}
}
}
else {
$folderdata = self::$imap->get_metadata($prefix, $type_keys);
}
if (!is_array($folderdata)) {
return false;
}
return array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
}
/**
* Callback for array_map to select the correct annotation value
*/
public static function folder_select_metadata($types)
{
if (!empty($types[self::CTYPE_KEY_PRIVATE])) {
return $types[self::CTYPE_KEY_PRIVATE];
}
else if (!empty($types[self::CTYPE_KEY])) {
list($ctype, ) = explode('.', $types[self::CTYPE_KEY]);
return $ctype;
}
return null;
}
/**
* Returns type of IMAP folder
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder type
*/
public static function folder_type($folder)
{
self::setup();
$metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
if (!is_array($metadata)) {
return null;
}
if (!empty($metadata[$folder])) {
return self::folder_select_metadata($metadata[$folder]);
}
return 'mail';
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return boolean True on success
*/
public static function set_folder_type($folder, $type='mail')
{
self::setup();
list($ctype, $subtype) = explode('.', $type);
$success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null));
if (!$success) // fallback: only set private annotation
$success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type));
return $success;
}
/**
* Check subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Include temporary/session subscriptions
*
* @return boolean True if subscribed, false if not
*/
public static function folder_is_subscribed($folder, $temp = false)
{
if (self::$subscriptions === null) {
self::setup();
self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
self::$with_tempsubs = true;
}
return in_array($folder, self::$subscriptions) ||
($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Only subscribe temporarily for the current session
*
* @return True on success, false on error
*/
public static function folder_subscribe($folder, $temp = false)
{
self::setup();
// temporary/session subscription
if ($temp) {
if (self::folder_is_subscribed($folder)) {
return true;
}
else if (!is_array($_SESSION['kolab_subscribed_folders']) || !in_array($folder, $_SESSION['kolab_subscribed_folders'])) {
$_SESSION['kolab_subscribed_folders'][] = $folder;
return true;
}
}
else if (self::$imap->subscribe($folder)) {
self::$subscriptions = null;
return true;
}
return false;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Only remove temporary subscription
*
* @return True on success, false on error
*/
public static function folder_unsubscribe($folder, $temp = false)
{
self::setup();
// temporary/session subscription
if ($temp) {
if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
unset($_SESSION['kolab_subscribed_folders'][$i]);
}
return true;
}
else if (self::$imap->unsubscribe($folder)) {
self::$subscriptions = null;
return true;
}
return false;
}
/**
* Check activation status of this folder
*
* @param string $folder Folder name
*
* @return boolean True if active, false if not
*/
public static function folder_is_active($folder)
{
$active_folders = self::get_states();
return in_array($folder, $active_folders);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_activate($folder)
{
// activation implies temporary subscription
self::folder_subscribe($folder, true);
return self::set_state($folder, true);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_deactivate($folder)
{
// remove from temp subscriptions, really?
self::folder_unsubscribe($folder, true);
return self::set_state($folder, false);
}
/**
* Return list of active folders
*/
private static function get_states()
{
if (self::$states !== null) {
return self::$states;
}
$rcube = rcube::get_instance();
$folders = $rcube->config->get('kolab_active_folders');
if ($folders !== null) {
self::$states = !empty($folders) ? explode('**', $folders) : array();
}
// for backward-compatibility copy server-side subscriptions to activation states
else {
self::setup();
if (self::$subscriptions === null) {
self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
self::$with_tempsubs = true;
}
self::$states = (array) self::$subscriptions;
$folders = implode('**', self::$states);
$rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
return self::$states;
}
/**
* Update list of active folders
*/
private static function set_state($folder, $state)
{
self::get_states();
// update in-memory list
$idx = array_search($folder, self::$states);
if ($state && $idx === false) {
self::$states[] = $folder;
}
else if (!$state && $idx !== false) {
unset(self::$states[$idx]);
}
// update user preferences
$folders = implode('**', self::$states);
return rcube::get_instance()->user->save_prefs(array('kolab_active_folders' => $folders));
}
/**
* Creates default folder of specified type
* To be run when none of subscribed folders (of specified type) is found
*
* @param string $type Folder type
* @param string $props Folder properties (color, etc)
*
* @return string Folder name
*/
public static function create_default_folder($type, $props = array())
{
if (!self::setup()) {
return;
}
$folders = self::$imap->get_metadata('*', array(kolab_storage::CTYPE_KEY_PRIVATE));
// from kolab_folders config
$folder_type = strpos($type, '.') ? str_replace('.', '_', $type) : $type . '_default';
$default_name = self::$config->get('kolab_folders_' . $folder_type);
$folder_type = str_replace('_', '.', $folder_type);
// check if we have any folder in personal namespace
// folder(s) may exist but not subscribed
foreach ((array)$folders as $f => $data) {
if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
$folder = $f;
break;
}
}
if (!$folder) {
if (!$default_name) {
$default_name = self::$default_folders[$type];
}
if (!$default_name) {
return;
}
$folder = rcube_charset::convert($default_name, RCUBE_CHARSET, 'UTF7-IMAP');
$prefix = self::$imap->get_namespace('prefix');
// add personal namespace prefix if needed
if ($prefix && strpos($folder, $prefix) !== 0 && $folder != 'INBOX') {
$folder = $prefix . $folder;
}
if (!self::$imap->folder_exists($folder)) {
if (!self::$imap->create_folder($folder)) {
return;
}
}
self::set_folder_type($folder, $folder_type);
}
self::folder_subscribe($folder);
if ($props['active']) {
self::set_state($folder, true);
}
if (!empty($props)) {
self::set_folder_props($folder, $props);
}
return $folder;
}
/**
* Sets folder metadata properties
*
* @param string $folder Folder name
* @param array &$prop Folder properties (color, displayname)
*/
public static function set_folder_props($folder, &$prop)
{
if (!self::setup()) {
return;
}
// TODO: also save 'showalarams' and other properties here
$ns = self::$imap->folder_namespace($folder);
$supported = array(
'color' => array(self::COLOR_KEY_SHARED, self::COLOR_KEY_PRIVATE),
'displayname' => array(self::NAME_KEY_SHARED, self::NAME_KEY_PRIVATE),
);
foreach ($supported as $key => $metakeys) {
if (array_key_exists($key, $prop)) {
$meta_saved = false;
if ($ns == 'personal') // save in shared namespace for personal folders
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[0] => $prop[$key]));
if (!$meta_saved) // try in private namespace
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[1] => $prop[$key]));
if ($meta_saved)
unset($prop[$key]); // unsetting will prevent fallback to local user prefs
}
}
}
/**
* Search users in Kolab LDAP storage
*
* @param mixed $query Search value (or array of field => value pairs)
* @param int $mode Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
* @param array $required List of fields that shall ot be empty
* @param int $limit Maximum number of records
* @param int $count Returns the number of records found
*
* @return array List of users
*/
public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
{
$query = str_replace('*', '', $query);
// requires a working LDAP setup
if (!strlen($query) || !($ldap = self::ldap())) {
return array();
}
$root = self::namespace_root('other');
$user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
$search_attrib = self::$config->get('kolab_users_search_attrib', array('cn','mail','alias'));
// search users using the configured attributes
$results = $ldap->dosearch($search_attrib, $query, $mode, $required, $limit, $count);
// exclude myself
if ($_SESSION['kolab_dn']) {
unset($results[$_SESSION['kolab_dn']]);
}
// resolve to IMAP folder name
array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
list($localpart, ) = explode('@', $user[$user_attrib]);
$user['kolabtargetfolder'] = $root . $localpart;
});
return $results;
}
/**
* Returns a list of IMAP folders shared by the given user
*
* @param array User entry from LDAP
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param int 1 - subscribed folders only, 0 - all folders, 2 - all non-active
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_user_folders($user, $type, $subscribed = 0, &$folderdata = array())
{
self::setup();
$folders = array();
// use localpart of user attribute as root for folder listing
$user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
if (!empty($user[$user_attrib])) {
list($mbox) = explode('@', $user[$user_attrib]);
$delimiter = self::$imap->get_hierarchy_delimiter();
$other_ns = self::namespace_root('other');
$prefix = $other_ns . $mbox . $delimiter;
$subscribed = (int) $subscribed;
$subs = $subscribed < 2 ? (bool) $subscribed : false;
$folders = self::list_folders($prefix, '*', $type, $subs, $folderdata);
if ($subscribed === 2 && !empty($folders)) {
$active = self::get_states();
if (!empty($active)) {
$folders = array_diff($folders, $active);
}
}
}
return $folders;
}
/**
* Get a list of (virtual) top-level folders from the other users namespace
*
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of kolab_storage_folder_user objects
*/
public static function get_user_folders($type, $subscribed)
{
$folders = $folderdata = array();
if (self::setup()) {
$delimiter = self::$imap->get_hierarchy_delimiter();
$other_ns = rtrim(self::namespace_root('other'), $delimiter);
$path_len = count(explode($delimiter, $other_ns));
foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
if ($foldername == 'INBOX') // skip INBOX which is added by default
continue;
$path = explode($delimiter, $foldername);
// compare folder type if a subfolder is listed
if ($type && count($path) > $path_len + 1 && $type != self::folder_type($foldername)) {
continue;
}
// truncate folder path to top-level folders of the 'other' namespace
$foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
if (!$folders[$foldername]) {
$folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
}
}
// for every (subscribed) user folder, list all (unsubscribed) subfolders
foreach ($folders as $userfolder) {
foreach ((array)self::list_folders($userfolder->name . $delimiter, '*', $type, false, $folderdata) as $foldername) {
if (!$folders[$foldername]) {
$folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
$userfolder->children[] = $folders[$foldername];
}
}
}
}
return $folders;
}
/**
* Handler for user_delete plugin hooks
*
* Remove all cache data from the local database related to the given user.
*/
public static function delete_user_folders($args)
{
$db = rcmail::get_instance()->get_dbh();
$prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
$db->query("DELETE FROM " . $db->table_name('kolab_folders', true) . " WHERE `resource` LIKE ?", $prefix);
}
/**
* Get folder METADATA for all supported keys
* Do this in one go for better caching performance
*/
public static function folder_metadata($folder)
{
if (self::setup()) {
$keys = array(
// For better performance we skip displayname here, see (self::custom_displayname())
// self::NAME_KEY_PRIVATE,
// self::NAME_KEY_SHARED,
self::CTYPE_KEY,
self::CTYPE_KEY_PRIVATE,
self::COLOR_KEY_PRIVATE,
self::COLOR_KEY_SHARED,
self::UID_KEY_SHARED,
self::UID_KEY_CYRUS,
);
$metadata = self::$imap->get_metadata($folder, $keys);
return $metadata[$folder];
}
}
/**
* Get user attributes for specified other user (imap) folder identifier.
*
* @param string $folder_id Folder name w/o path (imap user identifier)
* @param bool $as_string Return configured display name attribute value
*
* @return array User attributes
* @see self::ldap()
*/
public static function folder_id2user($folder_id, $as_string = false)
{
static $domain, $cache, $name_attr;
$rcube = rcube::get_instance();
if ($domain === null) {
list(, $domain) = explode('@', $rcube->get_user_name());
}
if ($name_attr === null) {
$name_attr = (array) ($rcube->config->get('kolab_users_name_field', $rcube->config->get('kolab_auth_name')) ?: 'name');
}
$token = $folder_id;
- if ($domain && strpos($find, '@') === false) {
+ if ($domain && strpos($token, '@') === false) {
$token .= '@' . $domain;
}
if ($cache === null) {
$cache = $rcube->get_cache_shared('kolab_users') ?: false;
}
// use value cached in memory for repeated lookups
if (!$cache && array_key_exists($token, self::$ldapcache)) {
$user = self::$ldapcache[$token];
}
if (empty($user) && $cache) {
$user = $cache->get($token);
}
if (empty($user) && ($ldap = self::ldap())) {
$user = $ldap->get_user_record($token, $_SESSION['imap_host']);
if (!empty($user)) {
$keys = array('displayname', 'name', 'mail'); // supported keys
$user = array_intersect_key($user, array_flip($keys));
if (!empty($user)) {
if ($cache) {
$cache->set($token, $user);
}
else {
self::$ldapcache[$token] = $user;
}
}
}
}
if (!empty($user)) {
if ($as_string) {
foreach ($name_attr as $attr) {
if ($display = $user[$attr]) {
break;
}
}
if (!$display) {
$display = $user['displayname'] ?: $user['name'];
}
if ($display && $display != $folder_id) {
$display = "$display ($folder_id)";
}
return $display;
}
return $user;
}
}
/**
* Chwala's 'folder_mod' hook handler for mapping other users folder names
*/
public static function folder_mod($args)
{
static $roots;
if ($roots === null) {
self::setup();
$roots = self::$imap->get_namespace('other');
}
// Note: We're working with UTF7-IMAP encoding here
if ($args['dir'] == 'in') {
foreach ((array) $roots as $root) {
if (strpos($args['folder'], $root[0]) === 0) {
// remove root and explode folder
$delim = $root[1];
$folder = explode($delim, substr($args['folder'], strlen($root[0])));
// compare first (user) part with a regexp, it's supposed
// to look like this: "Doe, Jane (uid)", so we can extract the uid
// and replace the folder with it
if (preg_match('~^[^/]+ \(([^)]+)\)$~', $folder[0], $m)) {
$folder[0] = $m[1];
$args['folder'] = $root[0] . implode($delim, $folder);
}
break;
}
}
}
else { // dir == 'out'
foreach ((array) $roots as $root) {
if (strpos($args['folder'], $root[0]) === 0) {
// remove root and explode folder
$delim = $root[1];
$folder = explode($delim, substr($args['folder'], strlen($root[0])));
// Replace uid with "Doe, Jane (uid)"
if ($user = self::folder_id2user($folder[0], true)) {
$user = str_replace($delim, '', $user);
$folder[0] = rcube_charset::convert($user, RCUBE_CHARSET, 'UTF7-IMAP');
$args['folder'] = $root[0] . implode($delim, $folder);
}
break;
}
}
}
return $args;
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_dav_folder.php b/plugins/libkolab/lib/kolab_storage_dav_folder.php
index 6877bdc5..2a1cda0c 100644
--- a/plugins/libkolab/lib/kolab_storage_dav_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_dav_folder.php
@@ -1,759 +1,759 @@
<?php
/**
* A class representing a DAV folder object.
*
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2014-2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#[AllowDynamicProperties]
class kolab_storage_dav_folder extends kolab_storage_folder
{
public $dav;
public $href;
public $attributes;
/**
* Object constructor
*/
public function __construct($dav, $attributes, $type = '')
{
$this->attributes = $attributes;
$this->href = $this->attributes['href'];
$this->id = kolab_storage_dav::folder_id($dav->url, $this->href);
$this->dav = $dav;
$this->valid = true;
list($this->type, $suffix) = strpos($type, '.') ? explode('.', $type) : [$type, ''];
$this->default = $suffix == 'default';
$this->subtype = $this->default ? '' : $suffix;
// Init cache
$this->cache = kolab_storage_dav_cache::factory($this);
}
/**
* Returns the owner of the folder.
*
* @param bool Return a fully qualified owner name (i.e. including domain for shared folders)
*
* @return string The owner of this folder.
*/
public function get_owner($fully_qualified = false)
{
// return cached value
if (isset($this->owner)) {
return $this->owner;
}
$rcube = rcube::get_instance();
$this->owner = $rcube->get_user_name();
$this->valid = true;
// TODO: Support shared folders
return $this->owner;
}
/**
* Get a folder Etag identifier
*/
public function get_ctag()
{
return $this->attributes['ctag'];
}
/**
* Getter for the name of the namespace to which the folder belongs
*
* @return string Name of the namespace (personal, other, shared)
*/
public function get_namespace()
{
// TODO: Support shared folders
return 'personal';
}
/**
* Get the display name value of this folder
*
* @return string Folder name
*/
public function get_name()
{
return kolab_storage_dav::object_name($this->attributes['name']);
}
/**
* Getter for the top-end folder name (not the entire path)
*
* @return string Name of this folder
*/
public function get_foldername()
{
return $this->attributes['name'];
}
public function get_folder_info()
{
return []; // todo ?
}
/**
* Getter for parent folder path
*
* @return string Full path to parent folder
*/
public function get_parent()
{
// TODO
return '';
}
/**
* Compose a unique resource URI for this folder
*/
public function get_resource_uri()
{
if (!empty($this->resource_uri)) {
return $this->resource_uri;
}
// compose fully qualified resource uri for this instance
$host = preg_replace('|^https?://|', 'dav://' . urlencode($this->get_owner(true)) . '@', $this->dav->url);
$path = $this->href[0] == '/' ? $this->href : "/{$this->href}";
$host_path = parse_url($host, PHP_URL_PATH);
if ($host_path && strpos($path, $host_path) === 0) {
$path = substr($path, strlen($host_path));
}
$this->resource_uri = unslashify($host) . $path;
return $this->resource_uri;
}
/**
* Getter for the Cyrus mailbox identifier corresponding to this folder
* (e.g. user/john.doe/Calendar/Personal@example.org)
*
* @return string Mailbox ID
*/
public function get_mailbox_id()
{
// TODO: This is used with Bonnie related features
return '';
}
/**
* Get the color value stored in metadata
*
* @param string Default color value to return if not set
*
* @return mixed Color value from the folder metadata or $default if not set
*/
public function get_color($default = null)
{
return !empty($this->attributes['color']) ? $this->attributes['color'] : $default;
}
/**
* Get ACL information for this folder
*
* @return string Permissions as string
*/
public function get_myrights()
{
// TODO
return '';
}
/**
* Helper method to extract folder UID
*
* @return string Folder's UID
*/
public function get_uid()
{
// TODO ???
return '';
}
/**
* Check activation status of this folder
*
* @return bool True if enabled, false if not
*/
public function is_active()
{
return true; // Unused
}
/**
* Change activation status of this folder
*
* @param bool The desired subscription status: true = active, false = not active
*
* @return bool True on success, false on error
*/
public function activate($active)
{
return true; // Unused
}
/**
* Check subscription status of this folder
*
* @return bool True if subscribed, false if not
*/
public function is_subscribed()
{
return true; // TODO
}
/**
* Change subscription status of this folder
*
* @param bool The desired subscription status: true = subscribed, false = not subscribed
*
* @return True on success, false on error
*/
public function subscribe($subscribed)
{
return true; // TODO
}
/**
* Delete the specified object from this folder.
*
* @param array|string $object The Kolab object to delete or object UID
* @param bool $expunge Should the folder be expunged?
*
* @return bool True if successful, false on error
*/
public function delete($object, $expunge = true)
{
if (!$this->valid) {
return false;
}
$uid = is_array($object) ? $object['uid'] : $object;
$success = $this->dav->delete($this->object_location($uid));
if ($success) {
$this->cache->set($uid, false);
}
return $success;
}
/**
* Delete all objects in a folder.
*
* Note: This method is used by kolab_addressbook plugin only
*
* @return bool True if successful, false on error
*/
public function delete_all()
{
if (!$this->valid) {
return false;
}
// TODO: Maybe just deleting and re-creating a folder would be
// better, but probably might not always work (ACL)
$this->cache->synchronize();
foreach (array_keys($this->cache->folder_index()) as $uid) {
$this->dav->delete($this->object_location($uid));
}
$this->cache->purge();
return true;
}
/**
* Restore a previously deleted object
*
* @param string $uid Object UID
*
* @return mixed Message UID on success, false on error
*/
public function undelete($uid)
{
if (!$this->valid) {
return false;
}
// TODO
return false;
}
/**
* Move a Kolab object message to another IMAP folder
*
* @param string Object UID
* @param kolab_storage_dav_folder Target folder to move object into
*
* @return bool True on success, false on failure
*/
public function move($uid, $target_folder)
{
if (!$this->valid) {
return false;
}
$source = $this->object_location($uid);
$target = $target_folder->object_location($uid);
$success = $this->dav->move($source, $target) !== false;
if ($success) {
$this->cache->set($uid, false);
}
return $success;
}
/**
* Save an object in this folder.
*
* @param array $object The array that holds the data of the object.
* @param string $type The type of the kolab object.
* @param string $uid The UID of the old object if it existed before
*
* @return mixed False on error or object UID on success
*/
public function save(&$object, $type = null, $uid = null)
{
if (!$this->valid || empty($object)) {
return false;
}
if (!$type) {
$type = $this->type;
}
$result = false;
if (empty($uid)) {
if (empty($object['created'])) {
$object['created'] = new DateTime('now');
}
}
else {
$object['changed'] = new DateTime('now');
}
// generate and save object message
if ($content = $this->to_dav($object)) {
$method = $uid ? 'update' : 'create';
$dav_type = $this->get_dav_type();
$result = $this->dav->{$method}($this->object_location($object['uid']), $content, $dav_type);
// Note: $result can be NULL if the request was successful, but ETag wasn't returned
if ($result !== false) {
// insert/update object in the cache
$object['etag'] = $result;
$object['_raw'] = $content;
$this->cache->save($object, $uid);
$result = true;
unset($object['_raw']);
}
}
return $result;
}
/**
* Fetch the object the DAV server and convert to internal format
*
* @param string The object UID to fetch
* @param string The object type expected (use wildcard '*' to accept all types)
* @param string Unused (kept for compat. with the parent class)
*
* @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
*/
public function read_object($uid, $type = null, $folder = null)
{
if (!$this->valid) {
return false;
}
$href = $this->object_location($uid);
$objects = $this->dav->getData($this->href, $this->get_dav_type(), [$href]);
if (!is_array($objects) || count($objects) != 1) {
rcube::raise_error([
'code' => 900,
'message' => "Failed to fetch {$href}"
], true);
return false;
}
return $this->from_dav($objects[0]);
}
/**
* Fetch multiple objects from the DAV server and convert to internal format
*
* @param array The object UIDs to fetch
*
* @return mixed Hash array representing the Kolab objects
*/
public function read_objects($uids)
{
if (!$this->valid) {
return false;
}
if (empty($uids)) {
return [];
}
foreach ($uids as $uid) {
$hrefs[] = $this->object_location($uid);
}
$objects = $this->dav->getData($this->href, $this->get_dav_type(), $hrefs);
if (!is_array($objects)) {
rcube::raise_error([
'code' => 900,
- 'message' => "Failed to fetch {$href}"
+ 'message' => "Failed to fetch {$this->href}"
], true);
return false;
}
$objects = array_map([$this, 'from_dav'], $objects);
foreach ($uids as $idx => $uid) {
foreach ($objects as $oidx => $object) {
if ($object && $object['uid'] == $uid) {
$uids[$idx] = $object;
unset($objects[$oidx]);
continue 2;
}
}
$uids[$idx] = false;
}
return $uids;
}
/**
* Convert DAV object into PHP array
*
* @param array Object data in kolab_dav_client::fetchData() format
*
* @return array|false Object properties, False on error
*/
public function from_dav($object)
{
if (empty($object ) || empty($object['data'])) {
return false;
}
if ($this->type == 'event' || $this->type == 'task') {
$ical = libcalendaring::get_ical();
$objects = $ical->import($object['data']);
if (!count($objects) || empty($objects[0]['uid'])) {
return false;
}
$result = $objects[0];
$result['_attachments'] = $result['attachments'] ?? [];
unset($result['attachments']);
}
else if ($this->type == 'contact') {
if (stripos($object['data'], 'BEGIN:VCARD') !== 0) {
return false;
}
// vCard properties not supported by rcube_vcard
$map = [
'uid' => 'UID',
'kind' => 'KIND',
'member' => 'MEMBER',
'x-kind' => 'X-ADDRESSBOOKSERVER-KIND',
'x-member' => 'X-ADDRESSBOOKSERVER-MEMBER',
];
// TODO: We should probably use Sabre/Vobject to parse the vCard
$vcard = new rcube_vcard($object['data'], RCUBE_CHARSET, false, $map);
if (!empty($vcard->displayname) || !empty($vcard->surname) || !empty($vcard->firstname) || !empty($vcard->email)) {
$result = $vcard->get_assoc();
// Contact groups
if (!empty($result['x-kind']) && implode($result['x-kind']) == 'group') {
$result['_type'] = 'group';
$members = isset($result['x-member']) ? $result['x-member'] : [];
unset($result['x-kind'], $result['x-member']);
}
else if (!empty($result['kind']) && implode($result['kind']) == 'group') {
$result['_type'] = 'group';
$members = isset($result['member']) ? $result['member'] : [];
unset($result['kind'], $result['member']);
}
if (isset($members)) {
$result['member'] = [];
foreach ($members as $member) {
if (strpos($member, 'urn:uuid:') === 0) {
$result['member'][] = ['uid' => substr($member, 9)];
}
else if (strpos($member, 'mailto:') === 0) {
$member = reset(rcube_mime::decode_address_list(urldecode(substr($member, 7))));
if (!empty($member['mailto'])) {
$result['member'][] = ['email' => $member['mailto'], 'name' => $member['name']];
}
}
}
}
if (!empty($result['uid'])) {
$result['uid'] = preg_replace('/^urn:uuid:/', '', implode($result['uid']));
}
}
else {
return false;
}
}
$result['etag'] = $object['etag'];
$result['href'] = !empty($object['href']) ? $object['href'] : null;
$result['uid'] = !empty($object['uid']) ? $object['uid'] : $result['uid'];
return $result;
}
/**
* Convert Kolab object into DAV format (iCalendar)
*/
public function to_dav($object)
{
$result = '';
if ($this->type == 'event' || $this->type == 'task') {
$ical = libcalendaring::get_ical();
if (!empty($object['exceptions'])) {
$object['recurrence']['EXCEPTIONS'] = $object['exceptions'];
}
$object['_type'] = $this->type;
// pre-process attachments
if (isset($object['_attachments']) && is_array($object['_attachments'])) {
foreach ($object['_attachments'] as $key => $attachment) {
if ($attachment === false) {
// Deleted attachment
unset($object['_attachments'][$key]);
continue;
}
// make sure size is set
if (!isset($attachment['size'])) {
if (!empty($attachment['data'])) {
if (is_resource($attachment['data'])) {
// this need to be a seekable resource, otherwise
// fstat() fails and we're unable to determine size
// here nor in rcube_imap_generic before IMAP APPEND
$stat = fstat($attachment['data']);
$attachment['size'] = $stat ? $stat['size'] : 0;
}
else {
$attachment['size'] = strlen($attachment['data']);
}
}
else if (!empty($attachment['path'])) {
$attachment['size'] = filesize($attachment['path']);
}
$object['_attachments'][$key] = $attachment;
}
}
}
$object['attachments'] = $object['_attachments'] ?? [];
unset($object['_attachments']);
$result = $ical->export([$object], null, false, [$this, 'get_attachment']);
}
else if ($this->type == 'contact') {
// copy values into vcard object
// TODO: We should probably use Sabre/Vobject to create the vCard
// vCard properties not supported by rcube_vcard
$map = ['uid' => 'UID', 'kind' => 'KIND'];
$vcard = new rcube_vcard('', RCUBE_CHARSET, false, $map);
if ((!empty($object['_type']) && $object['_type'] == 'group')
|| (!empty($object['type']) && $object['type'] == 'group')
) {
$object['kind'] = 'group';
}
foreach ($object as $key => $values) {
list($field, $section) = rcube_utils::explode(':', $key);
// avoid casting DateTime objects to array
if (is_object($values) && $values instanceof DateTimeInterface) {
$values = [$values];
}
foreach ((array) $values as $value) {
if (isset($value)) {
$vcard->set($field, $value, $section);
}
}
}
$result = $vcard->export(false);
if (!empty($object['kind']) && $object['kind'] == 'group') {
$members = '';
foreach ((array) $object['member'] as $member) {
$value = null;
if (!empty($member['uid'])) {
$value = 'urn:uuid:' . $member['uid'];
}
else if (!empty($member['email']) && !empty($member['name'])) {
$value = 'mailto:' . urlencode(sprintf('"%s" <%s>', addcslashes($member['name'], '"'), $member['email']));
}
else if (!empty($member['email'])) {
$value = 'mailto:' . $member['email'];
}
if ($value) {
$members .= "MEMBER:{$value}\r\n";
}
}
if ($members) {
$result = preg_replace('/\r\nEND:VCARD/', "\r\n{$members}END:VCARD", $result);
}
/**
Version 4.0 of the vCard format requires Cyrus >= 3.6.0, we'll use Version 3.0 for now
$result = preg_replace('/\r\nVERSION:3\.0\r\n/', "\r\nVERSION:4.0\r\n", $result);
$result = preg_replace('/\r\nN:[^\r]+/', '', $result);
$result = preg_replace('/\r\nUID:([^\r]+)/', "\r\nUID:urn:uuid:\\1", $result);
*/
$result = preg_replace('/\r\nMEMBER:([^\r]+)/', "\r\nX-ADDRESSBOOKSERVER-MEMBER:\\1", $result);
$result = preg_replace('/\r\nKIND:([^\r]+)/', "\r\nX-ADDRESSBOOKSERVER-KIND:\\1", $result);
}
}
if ($result) {
// The content must be UTF-8, otherwise if we try to fetch the object
// from server XML parsing would fail.
$result = rcube_charset::clean($result);
}
return $result;
}
public function object_location($uid)
{
return unslashify($this->href) . '/' . urlencode($uid) . '.' . $this->get_dav_ext();
}
/**
* Get a folder DAV content type
*/
public function get_dav_type()
{
return kolab_storage_dav::get_dav_type($this->type);
}
/**
* Get body of an attachment
*/
public function get_attachment($id, $event, $unused1 = null, $unused2 = false, $unused3 = null, $unused4 = false)
{
// Note: 'attachments' is defined when saving the data into the DAV server
// '_attachments' is defined after fetching the object from the DAV server
if (is_int($id) && isset($event['attachments'][$id])) {
$attachment = $event['attachments'][$id];
}
else if (is_int($id) && isset($event['_attachments'][$id])) {
$attachment = $event['_attachments'][$id];
}
else if (is_string($id) && !empty($event['attachments'])) {
foreach ($event['attachments'] as $att) {
if (!empty($att['id']) && $att['id'] === $id) {
$attachment = $att;
}
}
}
else if (is_string($id) && !empty($event['_attachments'])) {
foreach ($event['_attachments'] as $att) {
if (!empty($att['id']) && $att['id'] === $id) {
$attachment = $att;
}
}
}
if (empty($attachment)) {
return false;
}
if (!empty($attachment['path'])) {
return file_get_contents($attachment['path']);
}
return $attachment['data'] ?? null;
}
/**
* Get a DAV file extension for specified Kolab type
*/
public function get_dav_ext()
{
$types = [
'event' => 'ics',
'task' => 'ics',
'contact' => 'vcf',
];
return $types[$this->type];
}
/**
* Return folder name as string representation of this object
*
* @return string Folder display name
*/
public function __toString()
{
return $this->attributes['name'];
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_folder_api.php b/plugins/libkolab/lib/kolab_storage_folder_api.php
index 1879d6b9..c1cfd5d6 100644
--- a/plugins/libkolab/lib/kolab_storage_folder_api.php
+++ b/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -1,379 +1,379 @@
<?php
/**
* Abstract interface class for Kolab storage IMAP folder objects
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
abstract class kolab_storage_folder_api
{
/**
* Folder identifier
* @var string
*/
public $id;
/**
* The folder name.
* @var string
*/
public $name;
/**
* The type of this folder.
* @var string
*/
public $type;
/**
* The subtype of this folder.
* @var string
*/
public $subtype;
/**
* Is this folder set to be the default for its type
* @var boolean
*/
public $default = false;
/**
* List of direct child folders
* @var array
*/
public $children = array();
/**
* Name of the parent folder
* @var string
*/
public $parent = '';
protected $imap;
protected $owner;
protected $info;
protected $idata;
protected $namespace;
protected $metadata;
/**
* Private constructor
*/
protected function __construct($name)
{
$this->name = $name;
$this->id = kolab_storage::folder_id($name);
$this->imap = rcube::get_instance()->get_storage();
}
/**
* Returns the owner of the folder.
*
* @param boolean Return a fully qualified owner name (i.e. including domain for shared folders)
* @return string The owner of this folder.
*/
public function get_owner($fully_qualified = false)
{
// return cached value
if (isset($this->owner))
return $this->owner;
$info = $this->get_folder_info();
$rcmail = rcube::get_instance();
switch ($info['namespace']) {
case 'personal':
$this->owner = $rcmail->get_user_name();
break;
case 'shared':
$this->owner = 'anonymous';
break;
default:
list($prefix, $this->owner) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
$fully_qualified = true; // enforce email addresses (backwards compatibility)
break;
}
if ($fully_qualified && strpos($this->owner, '@') === false) {
// extract domain from current user name
$domain = strstr($rcmail->get_user_name(), '@');
// fall back to mail_domain config option
if (empty($domain) && ($mdomain = $rcmail->config->mail_domain($this->imap->options['host']))) {
$domain = '@' . $mdomain;
}
$this->owner .= $domain;
}
return $this->owner;
}
/**
* Getter for the name of the namespace to which the IMAP folder belongs
*
* @return string Name of the namespace (personal, other, shared)
*/
public function get_namespace()
{
if (!isset($this->namespace))
$this->namespace = $this->imap->folder_namespace($this->name);
return $this->namespace;
}
/**
* Get the display name value of this folder
*
* @return string Folder name
*/
public function get_name()
{
return kolab_storage::object_name($this->name);
}
/**
* Getter for the top-end folder name (not the entire path)
*
* @return string Name of this folder
*/
public function get_foldername()
{
$parts = explode($this->imap->get_hierarchy_delimiter(), $this->name);
return rcube_charset::convert(end($parts), 'UTF7-IMAP');
}
/**
* Getter for parent folder path
*
* @return string Full path to parent folder
*/
public function get_parent()
{
$delim = $this->imap->get_hierarchy_delimiter();
$path = explode($delim, $this->name);
array_pop($path);
// don't list top-level namespace folder
if (count($path) == 1 && in_array($this->get_namespace(), array('other', 'shared'))) {
$path = array();
}
return join($delim, $path);
}
/**
* Getter for the Cyrus mailbox identifier corresponding to this folder
* (e.g. user/john.doe/Calendar/Personal@example.org)
*
* @return string Mailbox ID
*/
public function get_mailbox_id()
{
$info = $this->get_folder_info();
$owner = $this->get_owner();
list($user, $domain) = explode('@', $owner);
switch ($info['namespace']) {
case 'personal':
return sprintf('user/%s/%s@%s', $user, $this->name, $domain);
case 'shared':
$ns = $this->imap->get_namespace('shared');
$prefix = is_array($ns) ? $ns[0][0] : '';
list(, $domain) = explode('@', rcube::get_instance()->get_user_name());
return substr($this->name, strlen($prefix)) . '@' . $domain;
default:
$ns = $this->imap->get_namespace('other');
$prefix = is_array($ns) ? $ns[0][0] : '';
list($user, $folder) = explode($this->imap->get_hierarchy_delimiter(), substr($info['name'], strlen($prefix)), 2);
if (strpos($user, '@')) {
list($user, $domain) = explode('@', $user);
}
return sprintf('user/%s/%s@%s', $user, $folder, $domain);
}
}
/**
* Get the color value stored in metadata
*
* @param string Default color value to return if not set
* @return mixed Color value from IMAP metadata or $default is not set
*/
public function get_color($default = null)
{
// color is defined in folder METADATA
$metadata = $this->get_metadata();
- if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
+ if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE] ?? null) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED] ?? null)) {
return $color;
}
return $default;
}
/**
* Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
* supported by kolab_storage
*
* @return array Metadata entry-value hash array on success, NULL on error
*/
public function get_metadata()
{
if ($this->metadata === null) {
$this->metadata = kolab_storage::folder_metadata($this->name);
}
return $this->metadata;
}
/**
* Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param array $entries Entry-value array (use NULL value as NIL)
* @return boolean True on success, False on failure
*/
public function set_metadata($entries)
{
$this->metadata = null;
return $this->imap->set_metadata($this->name, $entries);
}
/**
*
*/
public function get_folder_info()
{
if (!isset($this->info))
$this->info = $this->imap->folder_info($this->name);
return $this->info;
}
/**
* Make IMAP folder data available for this folder
*/
public function get_imap_data()
{
if (!isset($this->idata))
$this->idata = $this->imap->folder_data($this->name);
return $this->idata;
}
/**
* Returns (full) type of IMAP folder
*
* @return string Folder type
*/
public function get_type()
{
$metadata = $this->get_metadata();
if (!empty($metadata)) {
return kolab_storage::folder_select_metadata($metadata);
}
return $this->type;
}
/**
* Get IMAP ACL information for this folder
*
* @return string Permissions as string
*/
public function get_myrights()
{
$rights = $this->info['rights'];
if (!is_array($rights))
$rights = $this->imap->my_rights($this->name);
return join('', (array)$rights);
}
/**
* Helper method to extract folder UID metadata
*
* @return string Folder's UID
*/
public function get_uid()
{
// To be implemented by extending classes
return false;
}
/**
* Check activation status of this folder
*
* @return boolean True if enabled, false if not
*/
public function is_active()
{
return kolab_storage::folder_is_active($this->name);
}
/**
* Change activation status of this folder
*
* @param boolean The desired subscription status: true = active, false = not active
*
* @return True on success, false on error
*/
public function activate($active)
{
return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
}
/**
* Check subscription status of this folder
*
* @return boolean True if subscribed, false if not
*/
public function is_subscribed()
{
return kolab_storage::folder_is_subscribed($this->name);
}
/**
* Change subscription status of this folder
*
* @param boolean The desired subscription status: true = subscribed, false = not subscribed
*
* @return True on success, false on error
*/
public function subscribe($subscribed)
{
return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
}
/**
* Return folder name as string representation of this object
*
* @return string Full IMAP folder name
*/
public function __toString()
{
return $this->name;
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_folder_virtual.php b/plugins/libkolab/lib/kolab_storage_folder_virtual.php
index bf3ba554..8c6b3319 100644
--- a/plugins/libkolab/lib/kolab_storage_folder_virtual.php
+++ b/plugins/libkolab/lib/kolab_storage_folder_virtual.php
@@ -1,59 +1,58 @@
<?php
/**
* Helper class that represents a virtual IMAP folder
* with a subset of the kolab_storage_folder API.
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_folder_virtual extends kolab_storage_folder_api
{
- public $virtual = true;
-
protected $displayname;
public function __construct($name, $dispname, $ns, $parent = '')
{
parent::__construct($name);
$this->namespace = $ns;
$this->parent = $parent;
$this->displayname = $dispname;
+ $this->virtual = true;
}
/**
* Get the display name value of this folder
*
* @return string Folder name
*/
public function get_name()
{
return $this->displayname ?: parent::get_name();
}
/**
* Get the color value stored in metadata
*
* @param string Default color value to return if not set
* @return mixed Color value from IMAP metadata or $default is not set
*/
public function get_color($default = null)
{
return $default;
}
}
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index 53c72bcd..e770586c 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -1,392 +1,392 @@
<?php
/**
* Kolab core library
*
* Plugin to setup a basic environment for the interaction with a Kolab server.
* Other Kolab-related plugins will depend on it and can use the library classes
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class libkolab extends rcube_plugin
{
static $http_requests = array();
static $bonnie_api = false;
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
// load local config
$this->load_config();
$this->require_plugin('libcalendaring');
// extend include path to load bundled lib classes
$include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
set_include_path($include_path);
$this->add_hook('storage_init', array($this, 'storage_init'));
$this->add_hook('storage_connect', array($this, 'storage_connect'));
$this->add_hook('user_delete', array('kolab_storage', 'delete_user_folders'));
// For Chwala
$this->add_hook('folder_mod', array('kolab_storage', 'folder_mod'));
$rcmail = rcube::get_instance();
try {
kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
}
catch (Exception $e) {
rcube::raise_error($e, true);
kolab_format::$timezone = new DateTimeZone('GMT');
}
$this->add_texts('localization/', false);
if (!empty($rcmail->output->type) && $rcmail->output->type == 'html') {
$rcmail->output->add_handler('libkolab.folder_search_form', array($this, 'folder_search_form'));
$this->include_stylesheet($this->local_skin_path() . '/libkolab.css');
}
// embed scripts and templates for email message audit trail
if (property_exists($rcmail, 'task') && $rcmail->task == 'mail' && self::get_bonnie_api()) {
if ($rcmail->output->type == 'html') {
$this->add_hook('render_page', array($this, 'bonnie_render_page'));
$this->include_script('libkolab.js');
// add 'Show history' item to message menu
$this->api->add_content(html::tag('li', array('role' => 'menuitem'),
$this->api->output->button(array(
'command' => 'kolab-mail-history',
'label' => 'libkolab.showhistory',
'type' => 'link',
'classact' => 'icon history active',
'class' => 'icon history disabled',
'innerclass' => 'icon history',
))),
'messagemenu');
}
$this->register_action('plugin.message-changelog', array($this, 'message_changelog'));
}
}
/**
* Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers
*/
function storage_init($p)
{
$kolab_headers = 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION MESSAGE-ID';
if (!empty($p['fetch_headers'])) {
$p['fetch_headers'] .= ' ' . $kolab_headers;
}
else {
$p['fetch_headers'] = $kolab_headers;
}
return $p;
}
/**
* Hook into IMAP connection to replace client identity
*/
function storage_connect($p)
{
$client_name = 'Roundcube/Kolab';
if (empty($p['ident'])) {
$p['ident'] = array(
'name' => $client_name,
'version' => RCUBE_VERSION,
/*
'php' => PHP_VERSION,
'os' => PHP_OS,
'command' => $_SERVER['REQUEST_URI'],
*/
);
}
else {
$p['ident']['name'] = $client_name;
}
return $p;
}
/**
* Getter for a singleton instance of the Bonnie API
*
* @return mixed kolab_bonnie_api instance if configured, false otherwise
*/
public static function get_bonnie_api()
{
// get configuration for the Bonnie API
if (!self::$bonnie_api && ($bonnie_config = rcube::get_instance()->config->get('kolab_bonnie_api', false))) {
self::$bonnie_api = new kolab_bonnie_api($bonnie_config);
}
return self::$bonnie_api;
}
/**
* Hook to append the message history dialog template to the mail view
*/
function bonnie_render_page($p)
{
if (($p['template'] === 'mail' || $p['template'] === 'message') && !$p['kolab-audittrail']) {
// append a template for the audit trail dialog
$this->api->output->add_footer(
html::div(array('id' => 'mailmessagehistory', 'class' => 'uidialog', 'aria-hidden' => 'true', 'style' => 'display:none'),
self::object_changelog_table(array('class' => 'records-table changelog-table'))
)
);
$this->api->output->set_env('kolab_audit_trail', true);
$p['kolab-audittrail'] = true;
}
return $p;
}
/**
* Handler for message audit trail changelog requests
*/
public function message_changelog()
{
if (!self::$bonnie_api) {
return false;
}
$rcmail = rcube::get_instance();
$msguid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST, true);
$mailbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$result = $msguid && $mailbox ? self::$bonnie_api->changelog('mail', null, $mailbox, $msguid) : null;
if (is_array($result)) {
if (is_array($result['changes'])) {
$dtformat = $rcmail->config->get('date_format') . ' ' . $rcmail->config->get('time_format');
array_walk($result['changes'], function(&$change) use ($dtformat, $rcmail) {
if ($change['date']) {
$dt = rcube_utils::anytodatetime($change['date']);
if ($dt instanceof DateTimeInterface) {
$change['date'] = $rcmail->format_date($dt, $dtformat);
}
}
});
}
$this->api->output->command('plugin.message_render_changelog', $result['changes']);
}
else {
$this->api->output->command('plugin.message_render_changelog', false);
}
$this->api->output->send();
}
/**
* Wrapper function to load and initalize the HTTP_Request2 Object
*
* @param string|Net_Url2 Request URL
* @param string Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
* @param array Configuration for this Request instance, that will be merged
* with default configuration
*
* @return HTTP_Request2 Request object
*/
public static function http_request($url = '', $method = 'GET', $config = array())
{
$rcube = rcube::get_instance();
$http_config = (array) $rcube->config->get('kolab_http_request');
// deprecated configuration options
if (empty($http_config)) {
foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
$value = $rcube->config->get('kolab_' . $option, true);
if (is_bool($value)) {
$http_config[$option] = $value;
}
}
}
if (!empty($config)) {
$http_config = array_merge($http_config, $config);
}
// force CURL adapter, this allows to handle correctly
// compressed responses with SplObserver registered (kolab_files) (#4507)
$http_config['adapter'] = 'HTTP_Request2_Adapter_Curl';
$key = md5(serialize($http_config));
if (!empty(self::$http_requests[$key])) {
$request = self::$http_requests[$key];
}
else {
// load HTTP_Request2 (support both composer-installed and system-installed package)
if (!class_exists('HTTP_Request2')) {
require_once 'HTTP/Request2.php';
}
try {
$request = new HTTP_Request2();
$request->setConfig($http_config);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
// proxy User-Agent string
$request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
self::$http_requests[$key] = $request;
}
// cleanup
try {
$request->setBody('');
$request->setUrl($url);
$request->setMethod($method);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
return $request;
}
/**
* Table oultine for object changelog display
*/
public static function object_changelog_table($attrib = array())
{
$rcube = rcube::get_instance();
$attrib += array('domain' => 'libkolab');
$table = new html_table(array('cols' => 5, 'border' => 0, 'cellspacing' => 0));
$table->add_header('diff', '');
$table->add_header('revision', $rcube->gettext('revision', $attrib['domain']));
$table->add_header('date', $rcube->gettext('date', $attrib['domain']));
$table->add_header('user', $rcube->gettext('user', $attrib['domain']));
$table->add_header('operation', $rcube->gettext('operation', $attrib['domain']));
$table->add_header('actions', ' ');
$rcube->output->add_label(
'libkolab.showrevision',
'libkolab.actionreceive',
'libkolab.actionappend',
'libkolab.actionmove',
'libkolab.actiondelete',
'libkolab.actionread',
'libkolab.actionflagset',
'libkolab.actionflagclear',
'libkolab.objectchangelog',
'libkolab.objectchangelognotavailable',
'close'
);
return $table->show($attrib);
}
/**
* Wrapper function for generating a html diff using the FineDiff class by Raymond Hill
*/
public static function html_diff($from, $to, $is_html = null)
{
// auto-detect text/html format
if ($is_html === null) {
$from_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $from, $m) && strpos($from, '</'.$m[1].'>') > 0);
$to_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $to, $m) && strpos($to, '</'.$m[1].'>') > 0);
$is_html = $from_html || $to_html;
// ensure both parts are of the same format
if ($is_html && !$from_html) {
$converter = new rcube_text2html($from, false, array('wrap' => true));
$from = $converter->get_html();
}
if ($is_html && !$to_html) {
$converter = new rcube_text2html($to, false, array('wrap' => true));
$to = $converter->get_html();
}
}
// compute diff from HTML
if ($is_html) {
include_once __dir__ . '/vendor/Caxy/HtmlDiff/Match.php';
include_once __dir__ . '/vendor/Caxy/HtmlDiff/Operation.php';
include_once __dir__ . '/vendor/Caxy/HtmlDiff/HtmlDiff.php';
// replace data: urls with a transparent image to avoid memory problems
$from = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $from);
$to = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $to);
$diff = new Caxy\HtmlDiff\HtmlDiff($from, $to);
$diffhtml = $diff->build();
// remove empty inserts (from tables)
return preg_replace('!<ins class="diff\w+">\s*</ins>!Uims', '', $diffhtml);
}
else {
include_once __dir__ . '/vendor/finediff.php';
$diff = new FineDiff($from, $to, FineDiff::$wordGranularity);
return $diff->renderDiffToHTML();
}
}
/**
* Return a date() format string to render identifiers for recurrence instances
*
* @param array Hash array with event properties
* @return string Format string
*/
public static function recurrence_id_format($event)
{
return $event['allday'] ? 'Ymd' : 'Ymd\THis';
}
/**
* Returns HTML code for folder search widget
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public function folder_search_form($attrib)
{
$rcmail = rcube::get_instance();
$attrib += array(
'gui-object' => false,
'wrapper' => true,
'form-name' => 'foldersearchform',
'command' => 'non-extsing-command',
'reset-command' => 'non-existing-command',
);
- if ($attrib['label-domain'] && !strpos($attrib['buttontitle'], '.')) {
+ if (($attrib['label-domain'] ?? null) && !strpos($attrib['buttontitle'], '.')) {
$attrib['buttontitle'] = $attrib['label-domain'] . '.' . $attrib['buttontitle'];
}
if ($attrib['buttontitle']) {
$attrib['placeholder'] = $rcmail->gettext($attrib['buttontitle']);
}
return $rcmail->output->search_form($attrib);
}
}
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 8f65ffdc..548cf1fe 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -1,1739 +1,1739 @@
<?php
/**
* Kolab Groupware driver for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class tasklist_kolab_driver extends tasklist_driver
{
// features supported by the backend
public $alarms = false;
public $attachments = true;
public $attendees = true;
public $undelete = false; // task undelete action
public $alarm_types = array('DISPLAY','AUDIO');
public $search_more_results;
private $rc;
private $plugin;
private $lists;
private $folders = array();
private $tasks = array();
private $tags = array();
private $bonnie_api = false;
/**
* Default constructor
*/
public function __construct($plugin)
{
$this->rc = $plugin->rc;
$this->plugin = $plugin;
if (kolab_storage::$version == '2.0') {
$this->alarm_absolute = false;
}
// tasklist use fully encoded identifiers
kolab_storage::$encode_ids = true;
// get configuration for the Bonnie API
$this->bonnie_api = libkolab::get_bonnie_api();
$this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
}
/**
* Read available calendars for the current user and store them internally
*/
private function _read_lists($force = false)
{
// already read sources
if (isset($this->lists) && !$force) {
return $this->lists;
}
// get all folders that have type "task"
$folders = kolab_storage::sort_folders(kolab_storage::get_folders('task'));
$this->lists = $this->folders = array();
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
// find default folder
$default_index = 0;
foreach ($folders as $i => $folder) {
if ($folder->default && strpos($folder->name, $delim) === false)
$default_index = $i;
}
// put default folder (aka INBOX) on top of the list
if ($default_index > 0) {
$default_folder = $folders[$default_index];
unset($folders[$default_index]);
array_unshift($folders, $default_folder);
}
$prefs = $this->rc->config->get('kolab_tasklists', array());
foreach ($folders as $folder) {
$tasklist = $this->folder_props($folder, $prefs);
$this->lists[$tasklist['id']] = $tasklist;
$this->folders[$tasklist['id']] = $folder;
$this->folders[$folder->name] = $folder;
}
return $this->lists;
}
/**
* Derive list properties from the given kolab_storage_folder object
*/
protected function folder_props($folder, $prefs)
{
if ($folder->get_namespace() == 'personal') {
$norename = false;
$editable = true;
$rights = 'lrswikxtea';
$alarms = true;
}
else {
$alarms = false;
$rights = 'lr';
$editable = false;
if ($myrights = $folder->get_myrights()) {
$rights = $myrights;
if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false)
$editable = strpos($rights, 'i') !== false;
}
$info = $folder->get_folder_info();
$norename = $readonly || $info['norename'] || $info['protected'];
}
$list_id = $folder->id; #kolab_storage::folder_id($folder->name);
$old_id = kolab_storage::folder_id($folder->name, false);
if (!isset($prefs[$list_id]['showalarms']) && isset($prefs[$old_id]['showalarms'])) {
$prefs[$list_id]['showalarms'] = $prefs[$old_id]['showalarms'];
}
return array(
'id' => $list_id,
'name' => $folder->get_name(),
'listname' => $folder->get_foldername(),
'editname' => $folder->get_foldername(),
'color' => $folder->get_color('0000CC'),
'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms,
'editable' => $editable,
'rights' => $rights,
'norename' => $norename,
'active' => $folder->is_active(),
'owner' => $folder->get_owner(),
'parentfolder' => $folder->get_parent(),
'default' => $folder->default,
- 'virtual' => $folder->virtual,
+ 'virtual' => !empty($folder->virtual),
'children' => true, // TODO: determine if that folder indeed has child folders
'subscribed' => (bool)$folder->is_subscribed(),
'removable' => !$folder->default,
'subtype' => $folder->subtype,
'group' => $folder->default ? 'default' : $folder->get_namespace(),
'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
'caldavuid' => $folder->get_uid(),
'history' => !empty($this->bonnie_api),
);
}
/**
* Get a list of available task lists from this source
*
* @param integer Bitmask defining filter criterias.
* See FILTER_* constants for possible values.
*/
public function get_lists($filter = 0, &$tree = null)
{
$this->_read_lists();
// attempt to create a default list for this user
if (empty($this->lists) && !isset($this->search_more_results)) {
$prop = array('name' => 'Tasks', 'color' => '0000CC', 'default' => true);
if ($this->create_list($prop))
$this->_read_lists(true);
}
$folders = $this->filter_folders($filter);
// include virtual folders for a full folder tree
if (!is_null($tree)) {
$folders = kolab_storage::folder_hierarchy($folders, $tree);
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$prefs = $this->rc->config->get('kolab_tasklists', array());
$lists = array();
foreach ($folders as $folder) {
$list_id = $folder->id; // kolab_storage::folder_id($folder->name);
$imap_path = explode($delim, $folder->name);
// find parent
do {
array_pop($imap_path);
$parent_id = kolab_storage::folder_id(join($delim, $imap_path));
}
while (count($imap_path) > 1 && !$this->folders[$parent_id]);
// restore "real" parent ID
if ($parent_id && !$this->folders[$parent_id]) {
$parent_id = kolab_storage::folder_id($folder->get_parent());
}
$fullname = $folder->get_name();
$listname = $folder->get_foldername();
// special handling for virtual folders
if ($folder instanceof kolab_storage_folder_user) {
$lists[$list_id] = array(
'id' => $list_id,
'name' => $fullname,
'listname' => $listname,
'title' => $folder->get_title(),
'virtual' => true,
'editable' => false,
'rights' => 'l',
'group' => 'other virtual',
'class' => 'user',
'parent' => $parent_id,
);
}
- else if ($folder->virtual) {
+ else if (!empty($folder->virtual)) {
$lists[$list_id] = array(
'id' => $list_id,
'name' => $fullname,
'listname' => $listname,
'virtual' => true,
'editable' => false,
'rights' => 'l',
'group' => $folder->get_namespace(),
'class' => 'folder',
'parent' => $parent_id,
);
}
else {
if (!$this->lists[$list_id]) {
$this->lists[$list_id] = $this->folder_props($folder, $prefs);
$this->folders[$list_id] = $folder;
}
$this->lists[$list_id]['parent'] = $parent_id;
$lists[$list_id] = $this->lists[$list_id];
}
}
return $lists;
}
/**
* Get list of folders according to specified filters
*
* @param integer Bitmask defining restrictions. See FILTER_* constants for possible values.
*
* @return array List of task folders
*/
protected function filter_folders($filter)
{
$this->_read_lists();
$folders = array();
foreach ($this->lists as $id => $list) {
if (!empty($this->folders[$id])) {
$folder = $this->folders[$id];
if ($folder->get_namespace() == 'personal') {
$folder->editable = true;
}
else if ($rights = $folder->get_myrights()) {
if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) {
$folder->editable = strpos($rights, 'i') !== false;
}
}
$folders[] = $folder;
}
}
$plugin = $this->rc->plugins->exec_hook('tasklist_list_filter', array(
'list' => $folders,
'filter' => $filter,
'tasklists' => $folders,
));
if ($plugin['abort'] || !$filter) {
return $plugin['tasklists'];
}
$personal = $filter & self::FILTER_PERSONAL;
$shared = $filter & self::FILTER_SHARED;
$tasklists = array();
foreach ($folders as $folder) {
if (($filter & self::FILTER_WRITEABLE) && !$folder->editable) {
continue;
}
/*
if (($filter & self::FILTER_INSERTABLE) && !$folder->insert) {
continue;
}
if (($filter & self::FILTER_ACTIVE) && !$folder->is_active()) {
continue;
}
if (($filter & self::FILTER_PRIVATE) && $folder->subtype != 'private') {
continue;
}
if (($filter & self::FILTER_CONFIDENTIAL) && $folder->subtype != 'confidential') {
continue;
}
*/
if ($personal || $shared) {
$ns = $folder->get_namespace();
if (!(($personal && $ns == 'personal') || ($shared && $ns == 'shared'))) {
continue;
}
}
$tasklists[$folder->id] = $folder;
}
return $tasklists;
}
/**
* Get the kolab_calendar instance for the given calendar ID
*
* @param string List identifier (encoded imap folder name)
* @return object kolab_storage_folder Object nor null if list doesn't exist
*/
protected function get_folder($id)
{
$this->_read_lists();
// create list and folder instance if necesary
if (!$this->lists[$id]) {
$folder = kolab_storage::get_folder(kolab_storage::id_decode($id));
if ($folder->type) {
$this->folders[$id] = $folder;
$this->lists[$id] = $this->folder_props($folder, $this->rc->config->get('kolab_tasklists', array()));
}
}
return $this->folders[$id];
}
/**
* Create a new list assigned to the current user
*
* @param array Hash array with list properties
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled
* @return mixed ID of the new list on success, False on error
*/
public function create_list(&$prop)
{
$prop['type'] = 'task' . ($prop['default'] ? '.default' : '');
$prop['active'] = true; // activate folder by default
$prop['subscribed'] = true;
$folder = kolab_storage::folder_update($prop);
if ($folder === false) {
$this->last_error = kolab_storage::$last_error;
return false;
}
// create ID
$id = kolab_storage::folder_id($folder);
$prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array());
if (isset($prop['showalarms']))
$prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_tasklists'][$id])
$this->rc->user->save_prefs($prefs);
// force page reload to properly render folder hierarchy
if (!empty($prop['parent'])) {
$prop['_reload'] = true;
}
else {
$folder = kolab_storage::get_folder($folder);
$prop += $this->folder_props($folder, array());
}
return $id;
}
/**
* Update properties of an existing tasklist
*
* @param array Hash array with list properties
* id: List Identifier
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled (if supported)
* @return boolean True on success, Fales on failure
*/
public function edit_list(&$prop)
{
if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
$prop['oldname'] = $folder->name;
$prop['type'] = 'task';
$newfolder = kolab_storage::folder_update($prop);
if ($newfolder === false) {
$this->last_error = kolab_storage::$last_error;
return false;
}
// create ID
$id = kolab_storage::folder_id($newfolder);
// fallback to local prefs
$prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array());
unset($prefs['kolab_tasklists'][$prop['id']]);
if (isset($prop['showalarms']))
$prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_tasklists'][$id])
$this->rc->user->save_prefs($prefs);
// force page reload if folder name/hierarchy changed
if ($newfolder != $prop['oldname'])
$prop['_reload'] = true;
return $id;
}
return false;
}
/**
* Set active/subscribed state of a list
*
* @param array Hash array with list properties
* id: List Identifier
* active: True if list is active, false if not
* permanent: True if list is to be subscribed permanently
* @return boolean True on success, Fales on failure
*/
public function subscribe_list($prop)
{
if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
$ret = false;
if (isset($prop['permanent']))
$ret |= $folder->subscribe(intval($prop['permanent']));
if (isset($prop['active']))
$ret |= $folder->activate(intval($prop['active']));
// apply to child folders, too
if ($prop['recursive']) {
foreach ((array)kolab_storage::list_folders($folder->name, '*', 'task') as $subfolder) {
if (isset($prop['permanent']))
($prop['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
if (isset($prop['active']))
($prop['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder));
}
}
return $ret;
}
return false;
}
/**
* Delete the given list with all its contents
*
* @param array Hash array with list properties
* id: list Identifier
* @return boolean True on success, Fales on failure
*/
public function delete_list($prop)
{
if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
if (kolab_storage::folder_delete($folder->name)) {
return true;
}
$this->last_error = kolab_storage::$last_error;
}
return false;
}
/**
* Search for shared or otherwise not listed tasklists the user has access
*
* @param string Search string
* @param string Section/source to search
* @return array List of tasklists
*/
public function search_lists($query, $source)
{
if (!kolab_storage::setup()) {
return array();
}
$this->search_more_results = false;
$this->lists = $this->folders = array();
// find unsubscribed IMAP folders that have "event" type
if ($source == 'folders') {
foreach ((array)kolab_storage::search_folders('task', $query, array('other')) as $folder) {
$this->folders[$folder->id] = $folder;
$this->lists[$folder->id] = $this->folder_props($folder, array());
}
}
// search other user's namespace via LDAP
else if ($source == 'users') {
$limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
$folders = array();
// search for tasks folders shared by this user
foreach (kolab_storage::list_user_folders($user, 'task', false) as $foldername) {
$folders[] = new kolab_storage_folder($foldername, 'task');
}
if (count($folders)) {
$userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
$this->folders[$userfolder->id] = $userfolder;
$this->lists[$userfolder->id] = $this->folder_props($userfolder, array());
foreach ($folders as $folder) {
$this->folders[$folder->id] = $folder;
$this->lists[$folder->id] = $this->folder_props($folder, array());
$count++;
}
}
if ($count >= $limit) {
$this->search_more_results = true;
break;
}
}
}
return $this->get_lists();
}
/**
* Get a list of tags to assign tasks to
*
* @return array List of tags
*/
public function get_tags()
{
$config = kolab_storage_config::get_instance();
$tags = $config->get_tags();
$backend_tags = array_map(function($v) { return $v['name']; }, $tags);
return array_values(array_unique(array_merge($this->tags, $backend_tags)));
}
/**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
* @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
*/
public function count_tasks($lists = null)
{
if (empty($lists)) {
$lists = $this->_read_lists();
$lists = array_keys($lists);
}
else if (is_string($lists)) {
$lists = explode(',', $lists);
}
$today_date = new DateTime('now', $this->plugin->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
$counts = array('all' => 0, 'today' => 0, 'tomorrow' => 0, 'later' => 0, 'overdue' => 0);
foreach ($lists as $list_id) {
if (!$folder = $this->get_folder($list_id)) {
continue;
}
foreach ($folder->select(array(array('tags','!~','x-complete')), true) as $record) {
$rec = $this->_to_rcube_task($record, $list_id, false);
if ($this->is_complete($rec)) // don't count complete tasks
continue;
$counts['all']++;
if (empty($rec['date']))
$counts['later']++;
else if ($rec['date'] == $today)
$counts['today']++;
else if ($rec['date'] == $tomorrow)
$counts['tomorrow']++;
else if ($rec['date'] < $today)
$counts['overdue']++;
else if ($rec['date'] > $tomorrow)
$counts['later']++;
}
}
// avoid session race conditions that will loose temporary subscriptions
$this->plugin->rc->session->nowrite = true;
return $counts;
}
/**
* Get all task records matching the given filter
*
* @param array Hash array with filter criterias:
* - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
* - from: Date range start as string (Y-m-d)
* - to: Date range end as string (Y-m-d)
* - search: Search query string
* - uid: Task UIDs
* @param array List of lists to get tasks from
* @return array List of tasks records matchin the criteria
*/
public function list_tasks($filter, $lists = null)
{
if (empty($lists)) {
$lists = $this->_read_lists();
$lists = array_keys($lists);
}
else if (is_string($lists)) {
$lists = explode(',', $lists);
}
$config = kolab_storage_config::get_instance();
$results = array();
// query Kolab storage
$query = array();
if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
$query[] = array('tags','~','x-complete');
else if (empty($filter['since']))
$query[] = array('tags','!~','x-complete');
// full text search (only works with cache enabled)
if ($filter['search']) {
$search = mb_strtolower($filter['search']);
foreach (rcube_utils::normalize_string($search, true) as $word) {
$query[] = array('words', '~', $word);
}
}
if (!empty($filter['since'])) {
$query[] = array('changed', '>=', $filter['since']);
}
if (!empty($filter['uid'])) {
$query[] = array('uid', '=', (array) $filter['uid']);
}
foreach ($lists as $list_id) {
if (!$folder = $this->get_folder($list_id)) {
continue;
}
foreach ($folder->select($query) as $record) {
// TODO: post-filter tasks returned from storage
$record['list_id'] = $list_id;
$results[] = $record;
}
}
$config->apply_tags($results, true);
$config->apply_links($results);
foreach (array_keys($results) as $idx) {
$results[$idx] = $this->_to_rcube_task($results[$idx], $results[$idx]['list_id']);
}
// avoid session race conditions that will loose temporary subscriptions
$this->plugin->rc->session->nowrite = true;
return $results;
}
/**
* Return data of a specific task
*
* @param mixed Hash array with task properties or task UID
* @param integer Bitmask defining filter criterias for folders.
* See FILTER_* constants for possible values.
*
* @return array Hash array with task properties or false if not found
*/
public function get_task($prop, $filter = 0)
{
$this->_parse_id($prop);
$id = $prop['uid'];
$list_id = $prop['list'];
$folders = $list_id ? array($list_id => $this->get_folder($list_id)) : $this->get_lists($filter);
// find task in the available folders
foreach ($folders as $list_id => $folder) {
if (is_array($folder))
$folder = $this->folders[$list_id];
if (is_numeric($list_id) || !$folder)
continue;
if (empty($this->tasks[$id]) && ($object = $folder->get_object($id))) {
$this->load_tags($object);
$this->tasks[$id] = $this->_to_rcube_task($object, $list_id);
break;
}
}
return $this->tasks[$id];
}
/**
* Get all decendents of the given task record
*
* @param mixed Hash array with task properties or task UID
* @param boolean True if all childrens children should be fetched
* @return array List of all child task IDs
*/
public function get_childs($prop, $recursive = false)
{
if (is_string($prop)) {
$task = $this->get_task($prop);
$prop = array('uid' => $task['uid'], 'list' => $task['list']);
}
else {
$this->_parse_id($prop);
}
$childs = array();
$list_id = $prop['list'];
$task_ids = array($prop['uid']);
$folder = $this->get_folder($list_id);
// query for childs (recursively)
while ($folder && !empty($task_ids)) {
$query_ids = array();
foreach ($task_ids as $task_id) {
$query = array(array('tags','=','x-parent:' . $task_id));
foreach ($folder->select($query) as $record) {
// don't rely on kolab_storage_folder filtering
if ($record['parent_id'] == $task_id) {
$childs[] = $list_id . ':' . $record['uid'];
$query_ids[] = $record['uid'];
}
}
}
if (!$recursive)
break;
$task_ids = $query_ids;
}
return $childs;
}
/**
* Provide a list of revisions for the given task
*
* @param array $task Hash array with task properties
* @return array List of changes, each as a hash array
* @see tasklist_driver::get_task_changelog()
*/
public function get_task_changelog($prop)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
$result = $uid && $mailbox ? $this->bonnie_api->changelog('task', $uid, $mailbox, $msguid) : null;
if (is_array($result) && $result['uid'] == $uid) {
return $result['changes'];
}
return false;
}
/**
* Return full data of a specific revision of an event
*
* @param mixed $task UID string or hash array with task properties
* @param mixed $rev Revision number
*
* @return array Task object as hash array
* @see tasklist_driver::get_task_revision()
*/
public function get_task_revison($prop, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
$this->_parse_id($prop);
$uid = $prop['uid'];
$list_id = $prop['list'];
list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
// call Bonnie API
$result = $this->bonnie_api->get('task', $uid, $rev, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
$format = kolab_format::factory('task');
$format->load($result['xml']);
$rec = $format->to_array();
$format->get_attachments($rec, true);
if ($format->is_valid()) {
$rec = self::_to_rcube_task($rec, $list_id, false);
$rec['rev'] = $result['rev'];
return $rec;
}
}
return false;
}
/**
* Command the backend to restore a certain revision of a task.
* This shall replace the current object with an older version.
*
* @param mixed $task UID string or hash array with task properties
* @param mixed $rev Revision number
*
* @return boolean True on success, False on failure
* @see tasklist_driver::restore_task_revision()
*/
public function restore_task_revision($prop, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
$this->_parse_id($prop);
$uid = $prop['uid'];
$list_id = $prop['list'];
list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
$folder = $this->get_folder($list_id);
$success = false;
if ($folder && ($raw_msg = $this->bonnie_api->rawdata('task', $uid, $rev, $mailbox))) {
$imap = $this->rc->get_storage();
// insert $raw_msg as new message
if ($imap->save_message($folder->name, $raw_msg, null, false)) {
$success = true;
// delete old revision from imap and cache
$imap->delete_message($msguid, $folder->name);
$folder->cache->set($msguid, false);
}
}
return $success;
}
/**
* Get a list of property changes beteen two revisions of a task object
*
* @param array $task Hash array with task properties
* @param mixed $rev Revisions: "from:to"
*
* @return array List of property changes, each as a hash array
* @see tasklist_driver::get_task_diff()
*/
public function get_task_diff($prop, $rev1, $rev2)
{
$this->_parse_id($prop);
$uid = $prop['uid'];
$list_id = $prop['list'];
list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
// call Bonnie API
$result = $this->bonnie_api->diff('task', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id);
if (is_array($result) && $result['uid'] == $uid) {
$result['rev1'] = $rev1;
$result['rev2'] = $rev2;
$keymap = array(
'start' => 'start',
'due' => 'date',
'dstamp' => 'changed',
'summary' => 'title',
'alarm' => 'alarms',
'attendee' => 'attendees',
'attach' => 'attachments',
'rrule' => 'recurrence',
'related-to' => 'parent_id',
'percent-complete' => 'complete',
'lastmodified-date' => 'changed',
);
$prop_keymaps = array(
'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'),
'attendees' => array('partstat' => 'status'),
);
$special_changes = array();
// map kolab event properties to keys the client expects
array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
if (array_key_exists($change['property'], $keymap)) {
$change['property'] = $keymap[$change['property']];
}
if ($change['property'] == 'priority') {
$change['property'] = 'flagged';
$change['old'] = $change['old'] == 1 ? $this->plugin->gettext('yes') : null;
$change['new'] = $change['new'] == 1 ? $this->plugin->gettext('yes') : null;
}
// map alarms trigger value
if ($change['property'] == 'alarms') {
if (is_array($change['old']) && is_array($change['old']['trigger']))
$change['old']['trigger'] = $change['old']['trigger']['value'];
if (is_array($change['new']) && is_array($change['new']['trigger']))
$change['new']['trigger'] = $change['new']['trigger']['value'];
}
// make all property keys uppercase
if ($change['property'] == 'recurrence') {
$special_changes['recurrence'] = $i;
foreach (array('old','new') as $m) {
if (is_array($change[$m])) {
$props = array();
foreach ($change[$m] as $k => $v) {
$props[strtoupper($k)] = $v;
}
$change[$m] = $props;
}
}
}
// map property keys names
if (is_array($prop_keymaps[$change['property']])) {
foreach ($prop_keymaps[$change['property']] as $k => $dest) {
if (is_array($change['old']) && array_key_exists($k, $change['old'])) {
$change['old'][$dest] = $change['old'][$k];
unset($change['old'][$k]);
}
if (is_array($change['new']) && array_key_exists($k, $change['new'])) {
$change['new'][$dest] = $change['new'][$k];
unset($change['new'][$k]);
}
}
}
if ($change['property'] == 'exdate') {
$special_changes['exdate'] = $i;
}
else if ($change['property'] == 'rdate') {
$special_changes['rdate'] = $i;
}
});
// merge some recurrence changes
foreach (array('exdate','rdate') as $prop) {
if (array_key_exists($prop, $special_changes)) {
$exdate = $result['changes'][$special_changes[$prop]];
if (array_key_exists('recurrence', $special_changes)) {
$recurrence = &$result['changes'][$special_changes['recurrence']];
}
else {
$i = count($result['changes']);
$result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array());
$recurrence = &$result['changes'][$i]['recurrence'];
}
$key = strtoupper($prop);
$recurrence['old'][$key] = $exdate['old'];
$recurrence['new'][$key] = $exdate['new'];
unset($result['changes'][$special_changes[$prop]]);
}
}
return $result;
}
return false;
}
/**
* Helper method to resolved the given task identifier into uid and folder
*
* @return array (uid,folder,msguid) tuple
*/
private function _resolve_task_identity($prop)
{
$mailbox = $msguid = null;
$this->_parse_id($prop);
$uid = $prop['uid'];
$list_id = $prop['list'];
if ($folder = $this->get_folder($list_id)) {
$mailbox = $folder->get_mailbox_id();
// get task object from storage in order to get the real object uid an msguid
if ($rec = $folder->get_object($uid)) {
$msguid = $rec['_msguid'];
$uid = $rec['uid'];
}
}
return array($uid, $mailbox, $msguid);
}
/**
* Get a list of pending alarms to be displayed to the user
*
* @param integer Current time (unix timestamp)
* @param mixed List of list IDs to show alarms for (either as array or comma-separated string)
* @return array A list of alarms, each encoded as hash array with task properties
* @see tasklist_driver::pending_alarms()
*/
public function pending_alarms($time, $lists = null)
{
$interval = 300;
$time -= $time % 60;
$slot = $time;
$slot -= $slot % $interval;
$last = $time - max(60, $this->rc->config->get('refresh_interval', 0));
$last -= $last % $interval;
// only check for alerts once in 5 minutes
if ($last == $slot)
return array();
if ($lists && is_string($lists))
$lists = explode(',', $lists);
$time = $slot + $interval;
$candidates = array();
$query = array(
array('tags', '=', 'x-has-alarms'),
array('tags', '!=', 'x-complete')
);
$this->_read_lists();
foreach ($this->lists as $lid => $list) {
// skip lists with alarms disabled
if (!$list['showalarms'] || ($lists && !in_array($lid, $lists)))
continue;
$folder = $this->get_folder($lid);
foreach ($folder->select($query) as $record) {
if (!($record['valarms'] || $record['alarms']) || $record['status'] == 'COMPLETED' || $record['complete'] == 100) // don't trust query :-)
continue;
$task = $this->_to_rcube_task($record, $lid, false);
// add to list if alarm is set
$alarm = libcalendaring::get_next_alarm($task, 'task');
if ($alarm && $alarm['time'] && $alarm['time'] <= $time && in_array($alarm['action'], $this->alarm_types)) {
$id = $alarm['id']; // use alarm-id as primary identifier
$candidates[$id] = array(
'id' => $id,
'title' => $task['title'],
'date' => $task['date'],
'time' => $task['time'],
'notifyat' => $alarm['time'],
'action' => $alarm['action'],
);
}
}
}
// get alarm information stored in local database
if (!empty($candidates)) {
$alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates));
$result = $this->rc->db->query("SELECT *"
. " FROM " . $this->rc->db->table_name('kolab_alarms', true)
. " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")"
. " AND `user_id` = ?",
$this->rc->user->ID
);
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
$dbdata[$rec['alarm_id']] = $rec;
}
}
$alarms = array();
foreach ($candidates as $id => $task) {
// skip dismissed
if ($dbdata[$id]['dismissed'])
continue;
// snooze function may have shifted alarm time
$notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $task['notifyat'];
if ($notifyat <= $time)
$alarms[] = $task;
}
return $alarms;
}
/**
* (User) feedback after showing an alarm notification
* This should mark the alarm as 'shown' or snooze it for the given amount of time
*
* @param string Task identifier
* @param integer Suspend the alarm for this number of seconds
*/
public function dismiss_alarm($id, $snooze = 0)
{
// delete old alarm entry
$this->rc->db->query(
"DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . "
WHERE `alarm_id` = ? AND `user_id` = ?",
$id,
$this->rc->user->ID
);
// set new notifyat time or unset if not snoozed
$notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query(
"INSERT INTO " . $this->rc->db->table_name('kolab_alarms', true) . "
(`alarm_id`, `user_id`, `dismissed`, `notifyat`)
VALUES (?, ?, ?, ?)",
$id,
$this->rc->user->ID,
$snooze > 0 ? 0 : 1,
$notifyat
);
return $this->rc->db->affected_rows($query);
}
/**
* Remove alarm dismissal or snooze state
*
* @param string Task identifier
*/
public function clear_alarms($id)
{
// delete alarm entry
$this->rc->db->query(
"DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . "
WHERE `alarm_id` = ? AND `user_id` = ?",
$id,
$this->rc->user->ID
);
return true;
}
/**
* Get task tags
*/
private function load_tags(&$object)
{
// this task hasn't been migrated yet
if (!empty($object['categories'])) {
// OPTIONAL: call kolab_storage_config::apply_tags() to migrate the object
$object['tags'] = (array)$object['categories'];
if (!empty($object['tags'])) {
$this->tags = array_merge($this->tags, $object['tags']);
}
}
else {
$config = kolab_storage_config::get_instance();
$tags = $config->get_tags($object['uid']);
$object['tags'] = array_map(function($v) { return $v['name']; }, $tags);
}
}
/**
* Update task tags
*/
private function save_tags($uid, $tags)
{
$config = kolab_storage_config::get_instance();
$config->save_tags($uid, $tags);
}
/**
* Find messages linked with a task record
*/
private function get_links($uid)
{
$config = kolab_storage_config::get_instance();
return $config->get_object_links($uid);
}
/**
*
*/
private function save_links($uid, $links)
{
$config = kolab_storage_config::get_instance();
return $config->save_object_links($uid, (array) $links);
}
/**
* Extract uid + list identifiers from the given input
*
* @param mixed array or string with task identifier(s)
*/
private function _parse_id(&$prop)
{
$id_ = null;
if (is_array($prop)) {
// 'uid' + 'list' available, nothing to be done
if (!empty($prop['uid']) && !empty($prop['list'])) {
return;
}
// 'id' is given
if (!empty($prop['id'])) {
if (!empty($prop['list'])) {
$list_id = $prop['_fromlist'] ?: $prop['list'];
if (strpos($prop['id'], $list_id.':') === 0) {
$prop['uid'] = substr($prop['id'], strlen($list_id)+1);
}
else {
$prop['uid'] = $prop['id'];
}
}
else {
$id_ = $prop['id'];
}
}
}
else {
$id_ = strval($prop);
$prop = array();
}
// split 'id' into list + uid
if (!empty($id_)) {
list($list, $uid) = explode(':', $id_, 2);
if (!empty($uid)) {
$prop['uid'] = $uid;
$prop['list'] = $list;
}
else {
$prop['uid'] = $id_;
}
}
}
/**
* Convert from Kolab_Format to internal representation
*/
private function _to_rcube_task($record, $list_id, $all = true)
{
$id_prefix = $list_id . ':';
$task = array(
'id' => $id_prefix . $record['uid'],
'uid' => $record['uid'],
'title' => $record['title'] ?? null,
// 'location' => $record['location'],
'description' => $record['description'] ?? null,
'flagged' => ($record['priority'] ?? null) == 1,
'complete' => floatval(($record['complete'] ?? null) / 100),
'status' => $record['status'] ?? null,
'parent_id' => ($record['parent_id'] ?? null) ? $id_prefix . $record['parent_id'] : null,
'recurrence' => $record['recurrence'] ?? null,
'attendees' => $record['attendees'] ?? null,
'organizer' => $record['organizer'] ?? null,
'sequence' => $record['sequence'] ?? null,
'tags' => $record['tags'] ?? null,
'list' => $list_id,
'links' => $record['links'] ?? null,
);
// we can sometimes skip this expensive operation
if ($all && !array_key_exists('links', $task)) {
$task['links'] = $this->get_links($task['uid']);
}
// convert from DateTime to internal date format
if (($record['due'] ?? null) instanceof DateTimeInterface) {
$due = $this->plugin->lib->adjust_timezone($record['due']);
$task['date'] = $due->format('Y-m-d');
if (empty($record['due']->_dateonly)) {
$task['time'] = $due->format('H:i');
}
}
// convert from DateTime to internal date format
if (($record['start'] ?? null) instanceof DateTimeInterface) {
$start = $this->plugin->lib->adjust_timezone($record['start']);
$task['startdate'] = $start->format('Y-m-d');
if (empty($record['start']->_dateonly)) {
$task['starttime'] = $start->format('H:i');
}
}
if (($record['changed'] ?? null) instanceof DateTimeInterface) {
$task['changed'] = $record['changed'];
}
if (($record['created'] ?? null) instanceof DateTimeInterface) {
$task['created'] = $record['created'];
}
if (!empty($record['valarms'])) {
$task['valarms'] = $record['valarms'];
}
else if (!empty($record['alarms'])) {
$task['alarms'] = $record['alarms'];
}
if (!empty($task['attendees'])) {
foreach ((array)$task['attendees'] as $i => $attendee) {
if (isset($attendee['delegated-from']) && is_array($attendee['delegated-from'])) {
$task['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']);
}
if (isset($attendee['delegated-to']) && is_array($attendee['delegated-to'])) {
$task['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']);
}
}
}
if (!empty($record['_attachments'])) {
foreach ($record['_attachments'] as $key => $attachment) {
if ($attachment !== false) {
if (empty($attachment['name'])) {
$attachment['name'] = $key;
}
$attachments[] = $attachment;
}
}
$task['attachments'] = $attachments;
}
return $task;
}
/**
* Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
* (opposite of self::_to_rcube_event())
*/
private function _from_rcube_task($task, $old = [])
{
$object = $task;
$id_prefix = $task['list'] . ':';
$toDT = function($date) {
// Convert DateTime into libcalendaring_datetime
return libcalendaring_datetime::createFromFormat(
'Y-m-d\\TH:i:s',
$date->format('Y-m-d\\TH:i:s'),
$date->getTimezone()
);
};
if (!empty($task['date'])) {
$object['due'] = $toDT(rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->plugin->timezone));
if (empty($task['time'])) {
$object['due']->_dateonly = true;
}
unset($object['date']);
}
if (!empty($task['startdate'])) {
$object['start'] = $toDT(rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone));
if (empty($task['starttime'])) {
$object['start']->_dateonly = true;
}
unset($object['startdate']);
}
// as per RFC (and the Kolab schema validation), start and due dates need to be of the same type (#3614)
// this should be catched in the client already but just make sure we don't write invalid objects
if (!empty($object['start']) && !empty($object['due']) && $object['due']->_dateonly != $object['start']->_dateonly) {
$object['start']->_dateonly = true;
$object['due']->_dateonly = true;
}
$object['complete'] = $task['complete'] * 100;
if ($task['complete'] == 1.0 && empty($task['complete']))
$object['status'] = 'COMPLETED';
if (!empty($task['flagged']))
$object['priority'] = 1;
else
$object['priority'] = ($old['priority'] ?? 0) > 1 ? $old['priority'] : 0;
// remove list: prefix from parent_id
if (!empty($task['parent_id']) && strpos($task['parent_id'], $id_prefix) === 0) {
$object['parent_id'] = substr($task['parent_id'], strlen($id_prefix));
}
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// copy recurrence rules if the client didn't submit it (#2713)
if (!array_key_exists('recurrence', $object) && $old['recurrence']) {
$object['recurrence'] = $old['recurrence'];
}
unset($task['attachments']);
kolab_format::merge_attachments($object, $old);
// allow sequence increments if I'm the organizer
if ($this->plugin->is_organizer($object) && empty($object['_method'])) {
unset($object['sequence']);
}
else if (isset($old['sequence']) && empty($object['_method'])) {
$object['sequence'] = $old['sequence'];
}
unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['created']);
return $object;
}
/**
* Add a single task to the database
*
* @param array Hash array with task properties (see header of tasklist_driver.php)
* @return mixed New task ID on success, False on error
*/
public function create_task($task)
{
return $this->edit_task($task);
}
/**
* Update an task entry with the given data
*
* @param array Hash array with task properties (see header of tasklist_driver.php)
* @return boolean True on success, False on error
*/
public function edit_task($task)
{
$this->_parse_id($task);
$list_id = $task['list'];
if (!$list_id || !($folder = $this->get_folder($list_id))) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid list identifer to save task: " . print_r($list_id, true)),
true, false);
return false;
}
// email links and tags are stored separately
$links = $task['links'] ?? null;
$tags = $task['tags'] ?? null;
unset($task['tags'], $task['links']);
// moved from another folder
if (($task['_fromlist'] ?? false) && ($fromfolder = $this->get_folder($task['_fromlist']))) {
if (!$fromfolder->move($task['uid'], $folder))
return false;
unset($task['_fromlist']);
}
// load previous version of this task to merge
$old = null;
if (!empty($task['id'])) {
$old = $folder->get_object($task['uid']);
if (!$old || PEAR::isError($old))
return false;
// merge existing properties if the update isn't complete
if (!isset($task['title']) || !isset($task['complete']))
$task += $this->_to_rcube_task($old, $list_id);
}
// generate new task object from RC input
$object = $this->_from_rcube_task($task, $old);
$saved = $folder->save($object, 'task', $task['uid']);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving task object to Kolab server"),
true, false);
$saved = false;
}
else {
// save links in configuration.relation object
$this->save_links($object['uid'], $links);
// save tags in configuration.relation object
$this->save_tags($object['uid'], $tags);
$task = $this->_to_rcube_task($object, $list_id);
$task['tags'] = (array) $tags;
$this->tasks[$task['uid']] = $task;
}
return $saved;
}
/**
* Move a single task to another list
*
* @param array Hash array with task properties:
* @return boolean True on success, False on error
* @see tasklist_driver::move_task()
*/
public function move_task($task)
{
$this->_parse_id($task);
$list_id = $task['list'];
if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
// execute move command
if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
return $fromfolder->move($task['uid'], $folder);
}
return false;
}
/**
* Remove a single task from the database
*
* @param array Hash array with task properties:
* id: Task identifier
* @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
* @return boolean True on success, False on error
*/
public function delete_task($task, $force = true)
{
$this->_parse_id($task);
$list_id = $task['list'];
if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
$status = $folder->delete($task['uid']);
if ($status) {
// remove tag assignments
// @TODO: don't do this when undelete feature will be implemented
$this->save_tags($task['uid'], null);
}
return $status;
}
/**
* Restores a single deleted task (if supported)
*
* @param array Hash array with task properties:
* id: Task identifier
* @return boolean True on success, False on error
*/
public function undelete_task($prop)
{
// TODO: implement this
return false;
}
/**
* Get attachment properties
*
* @param string $id Attachment identifier
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
* rev: Revision (optional)
*
* @return array Hash array with attachment properties:
* id: Attachment identifier
* name: Attachment name
* mimetype: MIME content type of the attachment
* size: Attachment size
*/
public function get_attachment($id, $task)
{
// get old revision of the object
if ($task['rev']) {
$task = $this->get_task_revison($task, $task['rev']);
}
else {
$task = $this->get_task($task);
}
if ($task && !empty($task['attachments'])) {
foreach ($task['attachments'] as $att) {
if ($att['id'] == $id)
return $att;
}
}
return null;
}
/**
* Get attachment body
*
* @param string $id Attachment identifier
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
* rev: Revision (optional)
*
* @return string Attachment body
*/
public function get_attachment_body($id, $task)
{
$this->_parse_id($task);
// get old revision of event
if ($task['rev']) {
if (empty($this->bonnie_api)) {
return false;
}
$cid = substr($id, 4);
// call Bonnie API and get the raw mime message
list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($task);
if ($msg_raw = $this->bonnie_api->rawdata('task', $uid, $task['rev'], $mailbox, $msguid)) {
// parse the message and find the part with the matching content-id
$message = rcube_mime::parse_message($msg_raw);
foreach ((array)$message->parts as $part) {
if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) {
return $part->body;
}
}
}
return false;
}
if ($storage = $this->get_folder($task['list'])) {
return $storage->get_attachment($task['uid'], $id);
}
return false;
}
/**
* Build a struct representing the given message reference
*
* @see tasklist_driver::get_message_reference()
*/
public function get_message_reference($uri_or_headers, $folder = null)
{
if (is_object($uri_or_headers)) {
$uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder);
}
if (is_string($uri_or_headers)) {
return kolab_storage_config::get_message_reference($uri_or_headers, 'task');
}
return false;
}
/**
* Find tasks assigned to a specified message
*
* @see tasklist_driver::get_message_related_tasks()
*/
public function get_message_related_tasks($headers, $folder)
{
$config = kolab_storage_config::get_instance();
$result = $config->get_message_relations($headers, $folder, 'task');
foreach ($result as $idx => $rec) {
$result[$idx] = $this->_to_rcube_task($rec, kolab_storage::folder_id($rec['_mailbox']));
}
return $result;
}
/**
*
*/
public function tasklist_edit_form($action, $list, $fieldprop)
{
$this->_read_lists();
if ($list['id'] && ($list = $this->lists[$list['id']])) {
$folder_name = $this->get_folder($list['id'])->name; // UTF7
}
else {
$folder_name = '';
}
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder_name)) {
$path_imap = explode($delim, $folder_name);
array_pop($path_imap); // pop off name part
$path_imap = implode($delim, $path_imap);
$options = $storage->folder_info($folder_name);
}
else {
$path_imap = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder_name);
// folder name (default field)
$input_name = new html_inputfield(array('name' => 'name', 'id' => 'taskedit-tasklistname', 'size' => 20));
$fieldprop['name']['value'] = $input_name->show($list['editname'], array('disabled' => ($options['norename'] || $options['protected'])));
// prevent user from moving folder
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
}
else {
$select = kolab_storage::folder_selector('task', array('name' => 'parent', 'id' => 'taskedit-parentfolder'), $folder_name);
$fieldprop['parent'] = array(
'id' => 'taskedit-parentfolder',
'label' => $this->plugin->gettext('parentfolder'),
'value' => $select->show($path_imap),
);
}
// General tab
$form['properties'] = array(
'name' => $this->rc->gettext('properties'),
'fields' => array(),
);
foreach (array('name','parent','showalarms') as $f) {
$form['properties']['fields'][$f] = $fieldprop[$f];
}
return kolab_utils::folder_form($form, $folder_name, 'tasklist', $hidden_fields);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Aug 25, 9:50 PM (7 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
257815
Default Alt Text
(473 KB)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment