Page MenuHomePhorge

No OneTemporary

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_] .= '&nbsp;' . 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), ' &raquo; ', $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] . ' &raquo; ') === 0) {
$length = strlen($names[$i] . ' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $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('&nbsp;&nbsp;&nbsp;', $count - $diff) . '&raquo; ' . 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(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;&nbsp;', $count) . '&raquo; ' . 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].' &raquo; ') === 0) {
$length = strlen($listnames[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . 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 &raquo;
}
// $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', '&nbsp;');
$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

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)

Event Timeline