diff --git a/plugins/kolab_activesync/composer.json b/plugins/kolab_activesync/composer.json --- a/plugins/kolab_activesync/composer.json +++ b/plugins/kolab_activesync/composer.json @@ -4,7 +4,7 @@ "description": "ActiveSync configuration utility for Kolab accounts", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPLv3", - "version": "3.5.6", + "version": "3.5.7", "authors": [ { "name": "Thomas Bruederli", @@ -24,8 +24,8 @@ } ], "require": { - "php": ">=5.3.0", + "php": ">=7.2.0", "roundcube/plugin-installer": ">=0.1.3", - "kolab/libkolab": ">=3.4.0" + "kolab/libkolab": ">=3.5.12" } } diff --git a/plugins/kolab_activesync/config.inc.php.dist b/plugins/kolab_activesync/config.inc.php.dist --- a/plugins/kolab_activesync/config.inc.php.dist +++ b/plugins/kolab_activesync/config.inc.php.dist @@ -2,6 +2,7 @@ // The page with Activesync clients configuration manual $config['activesync_setup_url'] = 'https://kb.kolabenterprise.com/documentation/setting-up-an-activesync-client'; + // Force a subscription state per devicetype (lowercase) and folder // States can be: 0 => not subscribed, 1 => subscribed, 2 => subscribed with alarm $config['activesync_force_subscriptions'] = ['windowsoutlook15' => ['INBOX' => 1, 'Sent' => 1, 'Trash' => 1, 'Calendar' => 1, 'Contacts' => 1, 'Tasks' => 1]]; diff --git a/plugins/kolab_activesync/kolab_activesync.js b/plugins/kolab_activesync/kolab_activesync.js --- a/plugins/kolab_activesync/kolab_activesync.js +++ b/plugins/kolab_activesync/kolab_activesync.js @@ -180,28 +180,36 @@ { elem.name.match(/^_(subscriptions|alarms)\[(.+)\]$/); - var flag, type = RegExp.$1, device = RegExp.$2, - http_lock = rcmail.set_busy(true, 'kolab_activesync.savingdata'); + var type = RegExp.$1, + device = RegExp.$2, + http_lock = rcmail.set_busy(true, 'kolab_activesync.savingdata'), + post = { + cmd: 'update', + id: device, + flag: 0, + folder: rcmail.env.activesync_folder || rcmail.env.folder, + type: rcmail.env.activesync_type + }; // set subscription flag if (elem.checked) { - flag = type == 'alarms' ? 2 : 1; + post.flag = type == 'alarms' ? 2 : 1; } else { - flag = type == 'alarms' ? 1 : 0; + post.flag = type == 'alarms' ? 1 : 0; } // make sure subscription checkbox is checked if alarms is checked - if (flag == 2) { + if (post.flag == 2) { $('input[name="_subscriptions[' + device + ']"]').prop('checked', true); } // make sure alarms checkbox is unchecked if subscription is unchecked - else if (flag == 0) { + else if (post.flag == 0) { $('input[name="_alarms[' + device + ']"]').prop('checked', false); } // send the request - rcmail.http_post('plugin.activesync-json', {cmd: 'update', id: device, flag: flag, folder: rcmail.env.folder}, http_lock); + rcmail.http_post('plugin.activesync-json', post, http_lock); }; }; diff --git a/plugins/kolab_activesync/kolab_activesync.php b/plugins/kolab_activesync/kolab_activesync.php --- a/plugins/kolab_activesync/kolab_activesync.php +++ b/plugins/kolab_activesync/kolab_activesync.php @@ -7,7 +7,7 @@ * @author Aleksander Machniak * @author Thomas Bruederli * - * Copyright (C) 2011-2013, Kolab Systems AG + * Copyright (C) 2024, Apheleia IT AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -25,18 +25,14 @@ class kolab_activesync extends rcube_plugin { - public $task = 'settings'; - public $urlbase; - public $backend; + /** @var string Defines when the plugin is being used */ + public $task = '(addressbook|calendar|settings|tasks)'; + + /** @var ?kolab_subscriptions Subscriptions storage */ + public $engine; private $rc; private $ui; - private $folder_meta; - private $root_meta; - - public const ROOT_MAILBOX = 'INBOX'; - public const ASYNC_KEY = '/private/vendor/kolab/activesync'; - /** * Plugin initialization. @@ -47,18 +43,16 @@ $this->require_plugin('libkolab'); - $this->register_action('plugin.activesync', [$this, 'config_view']); - $this->register_action('plugin.activesync-config', [$this, 'config_frame']); $this->register_action('plugin.activesync-json', [$this, 'json_command']); - $this->add_hook('settings_actions', [$this, 'settings_actions']); - $this->add_hook('folder_form', [$this, 'folder_form']); - - $this->add_texts('localization/'); + if ($this->rc->task == 'settings') { + $this->register_action('plugin.activesync', [$this, 'config_view']); + $this->register_action('plugin.activesync-config', [$this, 'config_frame']); - if (preg_match('/^(plugin.activesync|edit-folder|save-folder)/', $this->rc->action)) { - $this->add_label('devicedeleteconfirm', 'savingdata'); - $this->include_script('kolab_activesync.js'); + $this->add_hook('settings_actions', [$this, 'settings_actions']); + $this->add_hook('folder_form', [$this, 'folder_form']); + } else { + $this->add_hook('kolab_folder_form', [$this, 'folder_form']); } } @@ -67,6 +61,8 @@ */ public function settings_actions($args) { + $this->init_ui(); + $args['actions'][] = [ 'action' => 'plugin.activesync', 'class' => 'activesync', @@ -84,33 +80,46 @@ */ public function folder_form($args) { - $mbox_imap = $args['options']['name'] ?? ''; + // Note: 'folder' arg from kolab_folder_form, 'options' arg from kolab_form hook + $folder = $args['folder'] ?? ($args['options']['name'] ?? null); // Edited folder name (empty in create-folder mode) - if (!strlen($mbox_imap)) { + if ($folder === null || $folder === '') { return $args; } - $devices = $this->list_devices(); + $this->init_engine(); + + $devices = $this->engine->list_devices(); // no registered devices if (empty($devices)) { return $args; } - [$type, ] = explode('.', (string) kolab_storage::folder_type($mbox_imap)); + if (is_object($folder)) { + $type = $folder->type; + } else { + [$type, ] = explode('.', (string) kolab_storage::folder_type($folder)); + } + if ($type && !in_array($type, ['mail', 'event', 'contact', 'task', 'note'])) { return $args; } - require_once $this->home . '/kolab_activesync_ui.php'; - $this->ui = new kolab_activesync_ui($this); + $this->init_ui(); - if ($content = $this->ui->folder_options_table($mbox_imap, $devices, $type)) { + if ($content = $this->ui->folder_options_table($folder, $devices, $type)) { $args['form']['activesync'] = [ 'name' => $this->gettext('tabtitle'), 'content' => $content, ]; + + $this->add_label('savingdata'); + $this->include_script('kolab_activesync.js'); + + $this->rc->output->set_env('activesync_folder', is_object($folder) ? $folder->href : $folder); + $this->rc->output->set_env('activesync_type', $type); } return $args; @@ -124,26 +133,38 @@ $cmd = rcube_utils::get_input_value('cmd', rcube_utils::INPUT_POST); $imei = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST); + $this->init_engine(); + $this->add_texts('localization/'); + switch ($cmd) { case 'save': - $devices = $this->list_devices(); - $device = $devices[$imei]; $subscriptions = (array) rcube_utils::get_input_value('subscribed', rcube_utils::INPUT_POST); - $devicealias = rcube_utils::get_input_value('devicealias', rcube_utils::INPUT_POST, true); - $device['ALIAS'] = $devicealias; + $devicealias = rcube_utils::get_input_value('devicealias', rcube_utils::INPUT_POST, true); - $err = !$this->device_update($device, $imei); + $err = !$this->engine->device_update($imei, ['friendlyname' => $devicealias]); if (!$err) { - // iterate over folders list and update metadata if necessary - // old subscriptions - foreach (array_keys($this->folder_meta()) as $folder) { - $err |= !$this->folder_set($folder, $imei, intval($subscriptions[$folder] ?? 0)); - unset($subscriptions[$folder]); - } - // new subscription + $subs = ['mail' => [], 'contact' => [], 'event' => [], 'task' => [], 'note' => []]; + $regexp = '/^(' . implode('|', array_keys($subs)) . ')#(.*)$/'; + foreach ($subscriptions as $folder => $flag) { - $err |= !$this->folder_set($folder, $imei, intval($flag)); + if (empty($flag)) { + continue; + } + + if (preg_match($regexp, $folder, $m)) { + $type = $m[1]; + $folder = $m[2]; + + $subs[$type][$folder] = (int) $flag; + } + } + + foreach ($subs as $type => $list) { + $err = !$this->engine->set_subscriptions($imei, $type, $list); + if ($err) { + break; + } } $this->rc->output->command('plugin.activesync_save_complete', [ @@ -160,7 +181,7 @@ case 'delete': foreach ((array) $imei as $id) { - $success = $this->device_delete($id); + $success = $this->engine->device_delete($id); } if (!empty($success)) { @@ -177,10 +198,11 @@ break; case 'update': - $subscription = (int) rcube_utils::get_input_value('flag', rcube_utils::INPUT_POST); - $folder = rcube_utils::get_input_value('folder', rcube_utils::INPUT_POST); + $flag = (int) rcube_utils::get_input_value('flag', rcube_utils::INPUT_POST); + $folder = rcube_utils::get_input_value('folder', rcube_utils::INPUT_POST); + $type = rcube_utils::get_input_value('type', rcube_utils::INPUT_POST); - $err = !$this->folder_set($folder, $imei, $subscription); + $err = !$this->engine->folder_subscribe($imei, $folder, $flag, $type); if ($err) { $this->rc->output->show_message($this->gettext('savingerror'), 'error'); @@ -199,19 +221,13 @@ */ public function config_view() { - $storage = $this->rc->get_storage(); - - // checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2 - if (!($storage->get_capability('METADATA') || $storage->get_capability('ANNOTATEMORE') || $storage->get_capability('ANNOTATEMORE2'))) { - $this->rc->output->show_message($this->gettext('notsupported'), 'error'); - } - - require_once $this->home . '/kolab_activesync_ui.php'; - - $this->ui = new kolab_activesync_ui($this); + $this->init_ui(); $this->register_handler('plugin.devicelist', [$this->ui, 'device_list']); + $this->add_label('devicedeleteconfirm', 'savingdata'); + $this->include_script('kolab_activesync.js'); + $this->rc->output->send('kolab_activesync.config'); } @@ -220,30 +236,21 @@ */ public function config_frame() { - $storage = $this->rc->get_storage(); - - // checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2 - if (!($storage->get_capability('METADATA') || $storage->get_capability('ANNOTATEMORE') || $storage->get_capability('ANNOTATEMORE2'))) { - $this->rc->output->show_message($this->gettext('notsupported'), 'error'); - } - - require_once $this->home . '/kolab_activesync_ui.php'; - - $this->ui = new kolab_activesync_ui($this); + $this->init_ui(); if (!empty($_GET['_init'])) { return $this->ui->init_message(); } + $this->include_script('kolab_activesync.js'); + $this->register_handler('plugin.deviceconfigform', [$this->ui, 'device_config_form']); $this->register_handler('plugin.foldersubscriptions', [$this->ui, 'folder_subscriptions']); - $imei = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); - $devices = $this->list_devices(); + $imei = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); - if ($device = $devices[$imei]) { - $this->ui->device = $device; - $this->ui->device['_id'] = $imei; + if ($device = $this->engine->device_info($imei)) { + $this->ui->device = $device; $this->rc->output->set_env('active_device', $imei); $this->rc->output->command('parent.enable_command', 'plugin.delete-device', true); } else { @@ -254,332 +261,27 @@ } /** - * Get list of all folders available for sync - * - * @return array List of mailbox folders - */ - public function list_folders() - { - $storage = $this->rc->get_storage(); - - return $storage->list_folders(); - } - - /** - * List known devices - * - * @return array Device list as hash array - */ - public function list_devices() - { - if ($this->root_meta === null) { - $storage = $this->rc->get_storage(); - // @TODO: consider server annotation instead of INBOX - if ($meta = $storage->get_metadata(self::ROOT_MAILBOX, self::ASYNC_KEY)) { - $this->root_meta = $this->unserialize_metadata($meta[self::ROOT_MAILBOX][self::ASYNC_KEY]); - } else { - $this->root_meta = []; - } - } - - if (!empty($this->root_meta['DEVICE']) && is_array($this->root_meta['DEVICE'])) { - return $this->root_meta['DEVICE']; - } - - return []; - } - - /** - * Getter for folder metadata - * - * @return array Hash array with meta data for each folder - */ - public function folder_meta() - { - if (!isset($this->folder_meta)) { - $this->folder_meta = []; - $storage = $this->rc->get_storage(); - - // get folders activesync config - $folderdata = $storage->get_metadata("*", self::ASYNC_KEY); - - foreach ($folderdata as $folder => $meta) { - if ($asyncdata = $meta[self::ASYNC_KEY]) { - if ($metadata = $this->unserialize_metadata($asyncdata)) { - $this->folder_meta[$folder] = $metadata; - } - } - } - } - - return $this->folder_meta; - } - - /** - * Sets ActiveSync subscription flag on a folder - * - * @param string $name Folder name (UTF7-IMAP) - * @param string $deviceid Device identifier - * @param int $flag Flag value (0|1|2) - */ - public function folder_set($name, $deviceid, $flag) - { - if (empty($deviceid)) { - return false; - } - - // get folders activesync config - $metadata = $this->folder_meta(); - $metadata = $metadata[$name] ?? null; - - if ($flag) { - if (empty($metadata)) { - $metadata = []; - } - - if (empty($metadata['FOLDER'])) { - $metadata['FOLDER'] = []; - } - - if (empty($metadata['FOLDER'][$deviceid])) { - $metadata['FOLDER'][$deviceid] = []; - } - - // Z-Push uses: - // 1 - synchronize, no alarms - // 2 - synchronize with alarms - $metadata['FOLDER'][$deviceid]['S'] = $flag; - } - - if (!$flag) { - unset($metadata['FOLDER'][$deviceid]['S']); - - if (empty($metadata['FOLDER'][$deviceid])) { - unset($metadata['FOLDER'][$deviceid]); - } - - if (empty($metadata['FOLDER'])) { - unset($metadata['FOLDER']); - } - - if (empty($metadata)) { - $metadata = null; - } - } - - // Return if nothing's been changed - if (!self::data_array_diff($this->folder_meta[$name] ?? null, $metadata)) { - return true; - } - - $this->folder_meta[$name] = $metadata; - - $storage = $this->rc->get_storage(); - - return $storage->set_metadata($name, [ - self::ASYNC_KEY => $this->serialize_metadata($metadata)]); - } - - /** - * Device update - * - * @param array $device Device data - * @param string $id Device ID - * - * @return bool True on success, False on failure - */ - public function device_update($device, $id) - { - $devices_list = $this->list_devices(); - $old_device = $devices_list[$id]; - - if (!$old_device) { - return false; - } - - // Do nothing if nothing is changed - if (!self::data_array_diff($old_device, $device)) { - return true; - } - - $device = array_merge($old_device, $device); - - $metadata = $this->root_meta; - $metadata['DEVICE'][$id] = $device; - $metadata = [self::ASYNC_KEY => $this->serialize_metadata($metadata)]; - $storage = $this->rc->get_storage(); - - $result = $storage->set_metadata(self::ROOT_MAILBOX, $metadata); - - if ($result) { - // Update local cache - $this->root_meta['DEVICE'][$id] = $device; - } - - return $result; - } - - /** - * Device delete. - * - * @param string $id Device ID - * - * @return bool True on success, False on failure - */ - public function device_delete($id) - { - $devices_list = $this->list_devices(); - $old_device = $devices_list[$id]; - - if (!$old_device) { - return false; - } - - unset($this->root_meta['DEVICE'][$id], $this->root_meta['FOLDER'][$id]); - - if (empty($this->root_meta['DEVICE'])) { - unset($this->root_meta['DEVICE']); - } - if (empty($this->root_meta['FOLDER'])) { - unset($this->root_meta['FOLDER']); - } - - $metadata = $this->serialize_metadata($this->root_meta); - $metadata = [self::ASYNC_KEY => $metadata]; - $storage = $this->rc->get_storage(); - - // update meta data - $result = $storage->set_metadata(self::ROOT_MAILBOX, $metadata); - - if ($result) { - // remove device annotation for every folder - foreach ($this->folder_meta() as $folder => $meta) { - // skip root folder (already handled above) - if ($folder == self::ROOT_MAILBOX) { - continue; - } - - if (!empty($meta['FOLDER']) && isset($meta['FOLDER'][$id])) { - unset($meta['FOLDER'][$id]); - - if (empty($meta['FOLDER'])) { - unset($this->folder_meta[$folder]['FOLDER']); - unset($meta['FOLDER']); - } - if (empty($meta)) { - unset($this->folder_meta[$folder]); - $meta = null; - } - - $metadata = [self::ASYNC_KEY => $this->serialize_metadata($meta)]; - $res = $storage->set_metadata($folder, $metadata); - - if ($res && $meta) { - $this->folder_meta[$folder] = $meta; - } - } - } - - // remove device data from syncroton database - $db = $this->rc->get_dbh(); - $table = $db->table_name('syncroton_device'); - - if (in_array($table, $db->list_tables())) { - $db->query( - "DELETE FROM $table WHERE owner_id = ? AND deviceid = ?", - $this->rc->user->ID, - $id - ); - } - } - - return $result; - } - - /** - * Device information (from syncroton database) - * - * @param string $id Device ID - * - * @return array|null Device data + * Initialize the subscriptions engine */ - public function device_info($id) + private function init_engine() { - $db = $this->rc->get_dbh(); - $table = $db->table_name('syncroton_device'); - - if (in_array($table, $db->list_tables())) { - $fields = ['devicetype', 'acsversion', 'useragent', 'friendlyname', 'os', - 'oslanguage', 'phonenumber']; - - $result = $db->query( - "SELECT " . $db->array2list($fields, 'ident') - . " FROM $table WHERE owner_id = ? AND id = ?", - $this->rc->user->ID, - $id - ); - - if ($result && ($sql_arr = $db->fetch_assoc($result))) { - return $sql_arr; - } + if (!$this->engine) { + $this->load_config(); + $this->engine = new kolab_subscriptions($this->rc->config->get('activesync_dav_server')); } - - return null; } /** - * Helper method to decode saved IMAP metadata + * Initialize the UI engine */ - private function unserialize_metadata($str) + private function init_ui() { - if (!empty($str)) { - $data = @json_decode($str, true); - return $data; - } + if (!$this->ui) { + $this->init_engine(); + require_once $this->home . '/kolab_activesync_ui.php'; + $this->ui = new kolab_activesync_ui($this); - return null; - } - - /** - * Helper method to encode IMAP metadata for saving - */ - private function serialize_metadata($data) - { - if (!empty($data) && is_array($data)) { - $data = json_encode($data); - return $data; + $this->add_texts('localization/'); } - - return null; - } - - /** - * Compares two arrays - * - * @param array $array1 - * @param array $array2 - * - * @return bool True if arrays differs, False otherwise - */ - private static function data_array_diff($array1, $array2) - { - if (!is_array($array1) || !is_array($array2)) { - return $array1 != $array2; - } - - if (count($array1) != count($array2)) { - return true; - } - - foreach ($array1 as $key => $val) { - if (!array_key_exists($key, $array2)) { - return true; - } - if ($val !== $array2[$key]) { - return true; - } - } - - return false; } } diff --git a/plugins/kolab_activesync/kolab_activesync_ui.php b/plugins/kolab_activesync/kolab_activesync_ui.php --- a/plugins/kolab_activesync/kolab_activesync_ui.php +++ b/plugins/kolab_activesync/kolab_activesync_ui.php @@ -42,7 +42,6 @@ $skin_path = $this->plugin->local_skin_path() . '/'; $this->skin_path = 'plugins/kolab_activesync/' . $skin_path; - $this->plugin->load_config(); $this->force_subscriptions = $this->rc->config->get('activesync_force_subscriptions', []); $this->plugin->include_stylesheet($skin_path . 'config.css'); @@ -52,14 +51,14 @@ { $attrib += ['id' => 'devices-list']; - $devices = $this->plugin->list_devices(); + $devices = $this->plugin->engine->list_devices(); $table = new html_table(); foreach ($devices as $id => $device) { - $name = $device['ALIAS'] ? $device['ALIAS'] : $id; + $name = $device['friendlyname'] ? $device['friendlyname'] : $id; $table->add_row(['id' => 'rcmrow' . $id]); $table->add(null, html::span('devicealias', rcube::Q($name)) - . ' ' . html::span('devicetype secondary', rcube::Q($device['TYPE']))); + . ' ' . html::span('devicetype secondary', rcube::Q($device['devicetype']))); } $this->rc->output->add_gui_object('devicelist', $attrib['id']); @@ -70,7 +69,6 @@ return $table->show($attrib); } - public function device_config_form($attrib = []) { $table = new html_table(['cols' => 2]); @@ -78,17 +76,18 @@ $field_id = 'config-device-alias'; $input = new html_inputfield(['name' => 'devicealias', 'id' => $field_id, 'size' => 40]); $table->add('title', html::label($field_id, $this->plugin->gettext('devicealias'))); - $table->add(null, $input->show($this->device['ALIAS'] ? $this->device['ALIAS'] : $this->device['_id'])); + $table->add(null, $input->show(!empty($this->device['friendlyname']) ? $this->device['friendlyname'] : $this->device['deviceid'])); - // read-only device information - $info = $this->plugin->device_info($this->device['ID']); + foreach (kolab_subscriptions::DEVICE_FIELDS as $key) { + if ($key == 'friendlyname') { + continue; + } - if (!empty($info)) { - foreach ($info as $key => $value) { - if ($value) { - $table->add('title', html::label(null, rcube::Q($this->plugin->gettext($key)))); - $table->add(null, rcube::Q($value)); - } + $value = $this->device[$key] ?? null; + + if ($value) { + $table->add('title', html::label(null, rcube::Q($this->plugin->gettext($key)))); + $table->add(null, rcube::Q($value)); } } @@ -99,7 +98,6 @@ return $table->show($attrib); } - private function is_protected($folder, $devicetype) { $devicetype = strtolower($devicetype); @@ -116,46 +114,35 @@ } // group folders by type (show only known types) - $folder_groups = ['mail' => [], 'contact' => [], 'event' => [], 'task' => [], 'note' => []]; - $folder_types = kolab_storage::folders_typedata(); $use_fieldsets = rcube_utils::get_boolean($attrib['use-fieldsets'] ?? ''); - $imei = $this->device['_id']; - $subscribed = []; - - if ($imei) { - $folder_meta = $this->plugin->folder_meta(); - } - - $devicetype = strtolower($this->device['TYPE']); + $imei = $this->device['deviceid']; + $devicetype = strtolower($this->device['devicetype'] ?? 'unknown'); $device_force_subscriptions = $this->force_subscriptions[$devicetype] ?? []; + $html = null; - foreach ($this->plugin->list_folders() as $folder) { - if (!empty($folder_types[$folder])) { - [$type, ] = explode('.', $folder_types[$folder]); - } else { - $type = 'mail'; - } + foreach (['mail', 'contact', 'event', 'task', 'note'] as $type) { + $subscriptions = $this->plugin->engine->list_subscriptions($imei, $type); + $folders = []; - if (array_key_exists($type, $folder_groups)) { - $folder_groups[$type][] = $folder; + foreach ($this->plugin->engine->list_folders($type) as $folder) { + $f = $folder[0]; + $subscribed = 0; - if ($device_force_subscriptions && array_key_exists($folder, $device_force_subscriptions)) { - $subscribed[$folder] = intval($device_force_subscriptions[$folder]); - } elseif (!empty($folder_meta[$folder]['FOLDER'][$imei]['S'])) { - $subscribed[$folder] = intval($folder_meta[$folder]['FOLDER'][$imei]['S']); + if ($device_force_subscriptions && array_key_exists($f, $device_force_subscriptions)) { + $subscribed = intval($device_force_subscriptions[$f]); + } elseif (!empty($subscriptions[$f])) { + $subscribed = (int) $subscriptions[$f][0]; } + + $folders[] = [$f, $folder[1], $subscribed]; } - } - // build block for every folder type - $html = null; - foreach ($folder_groups as $type => $group) { - if (empty($group)) { + if (empty($folders)) { continue; } $attrib['type'] = $type; - $table = $this->folder_subscriptions_block($group, $attrib, $subscribed); + $table = $this->folder_subscriptions_block($folders, $attrib); $label = $this->plugin->gettext($type); if ($use_fieldsets) { @@ -170,7 +157,7 @@ return html::div($attrib, $html); } - public function folder_subscriptions_block($a_folders, $attrib, $subscribed) + private function folder_subscriptions_block($a_folders, $attrib) { $alarms = ($attrib['type'] == 'event' || $attrib['type'] == 'task'); @@ -202,9 +189,10 @@ $names = []; foreach ($a_folders as $folder) { - $foldername = $origname = kolab_storage::object_prettyname($folder); + [$folder, $foldername, $subscribed] = $folder; // find folder prefix to truncate (the same code as in kolab_addressbook plugin) + $origname = $foldername; for ($i = count($names) - 1; $i >= 0; $i--) { if (strpos($foldername, $names[$i] . ' » ') === 0) { $length = strlen($names[$i] . ' » '); @@ -216,10 +204,13 @@ } $folder_id = 'rcmf' . rcube_utils::html_identifier($folder); + $folder_value = $attrib['type'] . '#' . $folder; + $names[] = $origname; $classes = ['mailbox']; - if ($folder_class = $this->rc->folder_classname($folder)) { + $folder_class = $attrib['type'] == 'mail' ? $this->rc->folder_classname($folder) : ''; + if ($folder_class) { if ($this->rc->text_exists($folder_class)) { $foldername = html::quote($this->rc->gettext($folder_class)); } @@ -228,17 +219,17 @@ $table->add_row(); - $disabled = $this->is_protected($folder, $this->device['TYPE']); + $disabled = $this->is_protected($folder, $this->device['devicetype'] ?? 'unknown'); $table->add('subscription checkbox-cell', $checkbox_sync->show( - !empty($subscribed[$folder]) ? $folder : null, - ['value' => $folder, 'id' => $folder_id, 'disabled' => $disabled] + $subscribed > 0 ? $folder_value : null, + ['value' => $folder_value, 'id' => $folder_id, 'disabled' => $disabled] )); if ($alarms) { $table->add('alarm checkbox-cell', $checkbox_alarm->show( - intval($subscribed[$folder] ?? 0) > 1 ? $folder : null, - ['value' => $folder, 'id' => $folder_id . '_alarm', 'disabled' => $disabled] + $subscribed > 1 ? $folder_value : null, + ['value' => $folder_value, 'id' => $folder_id . '_alarm', 'disabled' => $disabled] )); } @@ -248,11 +239,19 @@ return $table->show(); } - public function folder_options_table($folder_name, $devices, $type) + /** + * HTML table with a list of Activesync devices and an option to + * subscribe/unsubscribe to a specified folder. + * + * @param string|kolab_storage_dav_folder $folder Folder object or name + * @param array $devices Devices list + * @param string $type Folder type + * + * @return string HTMML table + */ + public function folder_options_table($folder, $devices, $type) { - $alarms = $type == 'event' || $type == 'task'; - $meta = $this->plugin->folder_meta(); - $folder_data = (array) (isset($meta[$folder_name]) ? $meta[$folder_name]['FOLDER'] : null); + $alarms = $type == 'event' || $type == 'task'; $table = new html_table(['cellspacing' => 0, 'id' => 'folder-sync-options', 'class' => 'records-table']); @@ -263,34 +262,37 @@ $table->add_header(['class' => 'alarm'], $this->plugin->gettext('withalarms')); } + $folder_name = is_object($folder) ? $folder->href : $folder; + $subscriptions = $this->plugin->engine->folder_subscriptions($folder_name, $type); + // table records foreach ($devices as $id => $device) { - $info = $this->plugin->device_info($device['ID']); - $name = $id; - $title = ''; - $checkbox = new html_checkbox(['name' => "_subscriptions[$id]", 'value' => 1, - 'onchange' => 'return activesync_object.update_sync_data(this)']); + $name = $id; + $title = ''; - if (!empty($info)) { - $_name = trim($info['friendlyname'] . ' ' . $info['os']); - $title = $info['useragent']; + $_name = trim(($device['friendlyname'] ?? '') . ' ' . ($device['os'] ?? '')); + $title = $device['useragent'] ?? ''; - if ($_name) { - $name .= " ($_name)"; - } + if ($_name) { + $name .= " ($_name)"; } - $disabled = $this->is_protected($folder_name, $device['TYPE']); + $disabled = $this->is_protected($folder_name, $device['devicetype'] ?? 'unknown'); + $flag = $subscriptions[$id] ?? 0; $table->add_row(); $table->add(['class' => 'device', 'title' => $title], $name); - $table->add('subscription checkbox-cell', $checkbox->show(!empty($folder_data[$id]['S']) ? 1 : 0, ['disabled' => $disabled])); + + $checkbox = new html_checkbox(['name' => "_subscriptions[$id]", 'value' => 1, + 'onchange' => 'return activesync_object.update_sync_data(this)']); + + $table->add('subscription checkbox-cell', $checkbox->show($flag ? 1 : 0, ['disabled' => $disabled])); if ($alarms) { $checkbox_alarm = new html_checkbox(['name' => "_alarms[$id]", 'value' => 1, 'onchange' => 'return activesync_object.update_sync_data(this)']); - $table->add('alarm checkbox-cell', $checkbox_alarm->show($folder_data[$id]['S'] > 1 ? 1 : 0, ['disabled' => $disabled])); + $table->add('alarm checkbox-cell', $checkbox_alarm->show($flag > 1 ? 1 : 0, ['disabled' => $disabled])); } } @@ -302,8 +304,6 @@ */ public function init_message() { - $this->plugin->load_config(); - $this->rc->output->add_handlers([ 'initmessage' => [$this, 'init_message_content'], ]); diff --git a/plugins/libkolab/composer.json b/plugins/libkolab/composer.json --- a/plugins/libkolab/composer.json +++ b/plugins/libkolab/composer.json @@ -4,7 +4,7 @@ "description": "Plugin to setup a basic environment for the interaction with a Kolab server.", "homepage": "https://git.kolab.org/diffusion/RPK/", "license": "AGPLv3", - "version": "3.5.11", + "version": "3.5.12", "authors": [ { "name": "Thomas Bruederli", diff --git a/plugins/libkolab/lib/kolab_dav_client.php b/plugins/libkolab/lib/kolab_dav_client.php --- a/plugins/libkolab/lib/kolab_dav_client.php +++ b/plugins/libkolab/lib/kolab_dav_client.php @@ -44,6 +44,7 @@ protected $user; protected $password; + protected $path; protected $rc; protected $responseHeaders = []; @@ -67,6 +68,7 @@ } $this->url = $url; + $this->path = $parsedUrl['path'] ?? ''; } /** @@ -84,9 +86,7 @@ $this->responseHeaders = []; - if ($path && ($rootPath = parse_url($this->url, PHP_URL_PATH)) && strpos($path, $rootPath) === 0) { - $path = substr($path, strlen($rootPath)); - } + $path = $this->normalize_location($path); try { $request = $this->initRequest($this->url . $path, $method, $request_config); @@ -144,8 +144,6 @@ } } - $path = parse_url($this->url, PHP_URL_PATH); - $body = '' . '' . '' @@ -170,9 +168,7 @@ } } - if ($path && strpos($principal_href, $path) === 0) { - $principal_href = substr($principal_href, strlen($path)); - } + $principal_href = $this->normalize_location($principal_href); $body = '' . '' @@ -197,12 +193,7 @@ foreach ($prop->childNodes as $home) { if ($home->firstChild && $home->firstChild->localName == 'href') { $href = $home->firstChild->nodeValue; - - if ($path && strpos($href, $path) === 0) { - $href = substr($href, strlen($path)); - } - - $homes[$home->localName] = $href; + $homes[$home->localName] = $this->normalize_location($href); } } } @@ -224,6 +215,16 @@ */ public function getHome($type) { + // FIXME: Can this be discovered? + if ($type == 'PRINCIPAL') { + $path = '/principals/user/'; + if ($this->path) { + $path = '/' . trim($this->path, '/') . $path; + } + + return $path; + } + $options = [ 'VEVENT' => 'calendar-home-set', 'VTODO' => 'calendar-home-set', @@ -814,6 +815,26 @@ return $response !== false; } + /** + * Normalize object location, by removing the path included the configured DAV server URI. + * + * @param string $href Location href + * + * @return string + */ + public function normalize_location($href) + { + if (!strlen($href)) { + return $href; + } + + if ($this->path && strpos($href, $this->path) === 0) { + $href = substr($href, strlen($this->path)); + } + + return $href; + } + /** * Set ACL on a DAV folder * @@ -970,12 +991,6 @@ { if ($href = $element->getElementsByTagName('href')->item(0)) { $href = $href->nodeValue; - /* - $path = parse_url($this->url, PHP_URL_PATH); - if ($path && strpos($href, $path) === 0) { - $href = substr($href, strlen($path)); - } - */ } if ($color = $element->getElementsByTagName('calendar-color')->item(0)) { @@ -1152,12 +1167,7 @@ $uid = null; if ($href = $element->getElementsByTagName('href')->item(0)) { $href = $href->nodeValue; - /* - $path = parse_url($this->url, PHP_URL_PATH); - if ($path && strpos($href, $path) === 0) { - $href = substr($href, strlen($path)); - } - */ + // Extract UID from the URL $href_parts = explode('/', $href); $uid = preg_replace('/\.[a-z]+$/', '', $href_parts[count($href_parts) - 1]); diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php --- a/plugins/libkolab/lib/kolab_storage.php +++ b/plugins/libkolab/lib/kolab_storage.php @@ -33,6 +33,7 @@ public const NAME_KEY_PRIVATE = '/private/vendor/kolab/displayname'; public const UID_KEY_SHARED = '/shared/vendor/kolab/uniqueid'; public const UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid'; + public const ASYNC_KEY = '/private/vendor/kolab/activesync'; public const ERROR_IMAP_CONN = 1; public const ERROR_CACHE_DB = 2; @@ -84,29 +85,25 @@ 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')); + self::$ready = self::$imap->get_capability('METADATA') + || self::$imap->get_capability('ANNOTATEMORE') + || self::$imap->get_capability('ANNOTATEMORE2'); if (self::$ready) { // do nothing - } elseif (!class_exists('kolabformat')) { - rcube::raise_error([ - 'code' => 900, 'type' => 'php', - 'message' => "required kolabformat module not found", - ], true); } elseif (self::$imap->get_error_code()) { - rcube::raise_error([ - 'code' => 900, 'type' => 'php', 'message' => "IMAP error", - ], true); + rcube::raise_error(['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; + if (class_exists('kolabformat')) { + // 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; @@ -1653,7 +1650,6 @@ $db = rcube::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); - } /** diff --git a/plugins/libkolab/lib/kolab_storage_dav.php b/plugins/libkolab/lib/kolab_storage_dav.php --- a/plugins/libkolab/lib/kolab_storage_dav.php +++ b/plugins/libkolab/lib/kolab_storage_dav.php @@ -29,8 +29,9 @@ public const ERROR_INVALID_FOLDER = 4; public static $last_error; + public $dav; + public $new_location; - protected $dav; protected $url; @@ -171,10 +172,6 @@ */ public static function folder_id($uri, $href) { - if (($rootPath = parse_url($uri, PHP_URL_PATH)) && strpos($href, $rootPath) === 0) { - $href = substr($href, strlen($rootPath)); - } - // Start with a letter to prevent from all kind of issues if it starts with a digit return 'f' . md5(rtrim($uri, '/') . '/' . trim($href, '/')); } @@ -281,6 +278,8 @@ $result = $this->dav->folderCreate($location, $type, $prop); if ($result) { + $this->new_location = $this->dav->normalize_location($location); + return self::folder_id($this->dav->url, $location); } diff --git a/plugins/libkolab/lib/kolab_storage_dav_folder.php b/plugins/libkolab/lib/kolab_storage_dav_folder.php --- a/plugins/libkolab/lib/kolab_storage_dav_folder.php +++ b/plugins/libkolab/lib/kolab_storage_dav_folder.php @@ -41,8 +41,8 @@ { $this->attributes = $attributes; - $this->href = $this->attributes['href']; - $this->id = kolab_storage_dav::folder_id($dav->url, $this->href); + $this->href = rtrim($this->attributes['href'], '/'); + $this->id = kolab_storage_dav::folder_id($dav->url, $dav->normalize_location($this->href)); $this->dav = $dav; $this->valid = true; @@ -932,12 +932,7 @@ return false; } - // In Kolab the users (principals) are under /principals/user/ - // TODO: This might need to be configurable or discovered somehow - $path = '/principals/user/'; - if ($host_path = parse_url($this->dav->url, PHP_URL_PATH)) { - $path = '/' . trim($host_path, '/') . $path; - } + $path = $this->dav->getHome('PRINCIPAL'); $specials = ['all', 'authenticated', 'self']; $request = []; @@ -967,12 +962,7 @@ return false; } - // In Kolab the users (principals) are under /principals/user/ - // TODO: This might need to be configurable or discovered somehow - $path = '/principals/user/'; - if ($host_path = parse_url($this->dav->url, PHP_URL_PATH)) { - $path = '/' . trim($host_path, '/') . $path; - } + $path = $this->dav->getHome('PRINCIPAL'); $request = []; diff --git a/plugins/libkolab/lib/kolab_subscriptions.php b/plugins/libkolab/lib/kolab_subscriptions.php new file mode 100644 --- /dev/null +++ b/plugins/libkolab/lib/kolab_subscriptions.php @@ -0,0 +1,507 @@ + + * + * Copyright (C) Apheleia IT AG + * + * 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 . + */ + +class kolab_subscriptions +{ + public const DEVICE_FIELDS = ['devicetype', 'acsversion', 'useragent', 'friendlyname', 'os', 'oslanguage', 'phonenumber']; + + /** @var ?kolab_storage_dav DAV storage handler */ + private $dav = null; + + private $rc; + private $folder_meta; + private $folders_list; + private $folders_type; + private $icache = []; + + /** + * Object constructor + * + * @param string $dav_url DAV server location. Enables DAV mode. + */ + public function __construct($dav_url = null) + { + $this->rc = rcube::get_instance(); + + if ($dav_url) { + $this->dav = new kolab_storage_dav($dav_url); + } + } + + /** + * List known devices + * + * @return array Device list as hash array + */ + public function list_devices() + { + $db = $this->rc->get_dbh(); + $table = $db->table_name('syncroton_device'); + $list = []; + + $query = $db->query( + "SELECT `id`, `deviceid`, " . $db->array2list(self::DEVICE_FIELDS, 'ident') + . " FROM {$table} WHERE `owner_id` = ?", + $this->rc->user->ID, + ); + + while ($record = $db->fetch_assoc($query)) { + $list[$record['deviceid']] = $record; + } + + return $list; + } + + /** + * Get list of all folders available for user + * + * @param string $type Folder type + * + * @return array List of folders (0 - path, 1 - displayname, 2 - optional folder object) + */ + public function list_folders($type) + { + if ($this->folders_list !== null && $this->folders_type == $type) { + return $this->folders_list; + } + + if ($this->dav) { + if ($type == 'note') { + $result = []; + } elseif ($type == 'mail') { + $storage = $this->rc->get_storage(); + $result = $storage->list_folders_subscribed(); + $result = array_map([$this, 'imap_folder_prop'], $result); + } else { + $result = $this->dav->get_folders($type); + $result = array_map([$this, 'dav_folder_prop'], $result); + } + } else { + $result = kolab_storage::list_folders('', '*', $type, true); + $result = array_map([$this, 'imap_folder_prop'], $result); + } + + $this->folders_list = $result; + $this->folders_type = $type; + + return $result; + } + + /** + * Get folder subscriptions + * + * @param string $deviceid Device IMEI identifier + * @param string $type Folder type + * + * @return array Folder subscription flags (0 - flag, 1 - display name, 2 - optional folder object) + */ + public function list_subscriptions($deviceid, $type) + { + if ($this->dav && $type == 'note') { + return []; + } + + $result = $this->get_subscriptions($deviceid, $type); + + // Verify if subscribed folders still exist + if (!empty($result)) { + $folders = $this->list_folders($type); + + foreach ($result as $idx => $flag) { + reset($folders); + foreach ($folders as $folder) { + if ($folder[0] === (string) $idx) { + // Folder exists, copy the properties + $folder[0] = $flag; + $result[$idx] = $folder; + continue 2; + } + } + + $update = true; + unset($result[$idx]); + } + + // Update subscriptions if any folder was removed from the list + if (!empty($update)) { + $data = array_map(function ($v) { return $v[0]; }, $result); + $this->update_subscriptions($deviceid, $type, $data); + } + } + + return $result ?? []; + } + + /** + * Get list of devices the folder is subscribed to + * + * @param string $folder Folder (IMAP path or DAV path) + * @param string $type Folder type + * + * @return array Index is a device IMEI, value is a subscription flag + */ + public function folder_subscriptions($folder, $type = null) + { + $db = $this->rc->get_dbh(); + $table = $db->table_name('syncroton_subscriptions'); + $device_table = $db->table_name('syncroton_device'); + $result = []; + + $query = $db->query( + "SELECT s.*, d.`deviceid` FROM {$table} s" + . " JOIN {$device_table} d ON (s.`device_id` = d.`id`)" + . " WHERE `owner_id` = ?" + . ($type ? " AND s.type = " . $db->quote($type) : ''), + $this->rc->user->ID + ); + + while ($record = $db->fetch_assoc($query)) { + $list = json_decode($record['data'], true); + if (!empty($list[$folder])) { + $result[$record['deviceid']] = $list[$folder]; + } + } + + return $result; + } + + /** + * Set folder subscription flag for specific device + * + * @param string $deviceid Device IMEI identifier + * @param string $folder Folder (an IMAP path or a DAV path) + * @param int $flag Subscription flag (1 or 2) + * @param ?string $type Folder type class + * + * @return bool True on success, False on failure + */ + public function folder_subscribe($deviceid, $folder, $flag, $type = null) + { + // For DAV folders it is required to use $type argument + // otherwise it's hard to get the folder type + if (empty($type)) { + $type = (string) kolab_storage::folder_type($folder); + } + + if (!$type) { + $type = 'mail'; + } + + [$type, ] = explode('.', $type); + + if (!in_array($type, ['mail', 'event', 'contact', 'task', 'note'])) { + return false; + } + + // Folder hrefs returned by kolab_dav_client aren't normalized, i.e. include path prefix + // We make sure here we use the same path + if ($this->dav && $type != 'mail') { + if ($type == 'note') { + return false; + } + + if ($path = parse_url($this->dav->dav->url, PHP_URL_PATH)) { + if (strpos($folder, $path) !== 0) { + $folder = '/' . trim($path, '/') . $folder; + } + } + + $folder = rtrim($folder, '/'); + } + + $subscribed = $this->get_subscriptions($deviceid, $type); + + if (isset($subscribed[$folder])) { + if ($subscribed[$folder] == $flag) { + return true; + } + + unset($subscribed[$folder]); + } + + if ($flag) { + $subscribed[$folder] = (int) $flag; + } + + return $this->update_subscriptions($deviceid, $type, $subscribed); + } + + /** + * Set folder subscriptions (in SQL database) + * + * @param string $deviceid Device IMEI identifier + * @param string $type Folder type class + * @param array $subscriptions Subscriptions + * + * @return bool True on success, False on failure + */ + public function set_subscriptions($deviceid, $type, $subscriptions) + { + $id = $this->imei_to_id($deviceid); + + if (empty($id)) { + return false; + } + + if ($this->dav && $type == 'note') { + return true; + } + + $data = json_encode($subscriptions); + + if ($data === false) { + return false; + } + + $db = $this->rc->get_dbh(); + $table = $db->table_name('syncroton_subscriptions'); + + $query = $db->query("SELECT 1 FROM $table WHERE `device_id` = ? AND `type` = ?", $id, $type); + + if ($record = $db->fetch_array($query)) { + $query = $db->query("UPDATE {$table} SET `data` = ? WHERE `device_id` = ? AND `type` = ?", $data, $id, $type); + } else { + $query = $db->query("INSERT INTO {$table} (`device_id`, `type`, `data`) VALUES (?, ?, ?)", $id, $type, $data); + } + + return $db->affected_rows($query) > 0; + } + + /** + * Device delete. + * + * @param string $id Device ID + * + * @return bool True on success, False on failure + */ + public function device_delete($id) + { + $db = $this->rc->get_dbh(); + $table = $db->table_name('syncroton_device'); + + $query = $db->query( + "DELETE FROM {$table} WHERE `owner_id` = ? AND `deviceid` = ?", + $this->rc->user->ID, + $id + ); + + return $db->affected_rows($query) > 0; + } + + /** + * Device information + * + * @param string $id Device ID + * + * @return array|null Device data + */ + public function device_info($id) + { + $db = $this->rc->get_dbh(); + $table = $db->table_name('syncroton_device'); + + $query = $db->query( + "SELECT `id`, `deviceid`, " . $db->array2list(self::DEVICE_FIELDS, 'ident') + . " FROM {$table} WHERE `owner_id` = ? AND `deviceid` = ?", + $this->rc->user->ID, + $id + ); + + if ($device = $db->fetch_assoc($query)) { + return $device; + } + + return null; + } + + /** + * Device update + * + * @param string $id Device ID + * @param array $device Device data + * + * @return bool True on success, False on failure + */ + public function device_update($id, $device) + { + $db = $this->rc->get_dbh(); + $cols = $params = []; + $allow = ['friendlyname']; + + foreach ((array) $device as $col => $value) { + $cols[] = $db->quote_identifier($col) . ' = ?'; + $params[] = $value; + } + + $params[] = $id; + $params[] = $this->rc->user->ID; + + $query = $db->query( + 'UPDATE ' . $db->table_name('syncroton_device', true) . + ' SET ' . implode(', ', $cols) . ' WHERE `deviceid` = ? AND `owner_id` = ?', + $params + ); + + return $db->affected_rows($query) > 0; + } + + /** + * Get subscriptions from database. + */ + private function get_subscriptions($deviceid, $type) + { + $id = $this->imei_to_id($deviceid); + + if ($id === null) { + return []; + } + + $db = $this->rc->get_dbh(); + $table = $db->table_name('syncroton_subscriptions'); + + // Get the subscriptions from database + $query = $db->query("SELECT `data` FROM {$table} WHERE `device_id` = ? AND `type` = ?", $id, $type); + + if ($record = $db->fetch_assoc($query)) { + $result = json_decode($record['data'], true); + } + + // No record yet... + if (!isset($result)) { + $result = []; + + // Get the old subscriptions from an IMAP annotations, create the record + if (!$this->dav || $type == 'mail') { + foreach ($this->folder_meta() as $folder => $meta) { + if ($meta[0] == $type && !empty($meta[1][$deviceid]['S'])) { + $result[$folder] = (int) $meta[1][$deviceid]['S']; + } + } + } + + $data = json_encode($result); + + $db->query("INSERT INTO {$table} (`device_id`, `type`, `data`) VALUES (?, ?, ?)", $id, $type, $data); + } + + return $result; + } + + /** + * Update subscriptions in the database. + */ + private function update_subscriptions($deviceid, $type, $list) + { + $id = $this->imei_to_id($deviceid); + + if ($id === null) { + return false; + } + + $db = $this->rc->get_dbh(); + $table = $db->table_name('syncroton_subscriptions'); + + $data = json_encode($list); + + $query = $db->query("UPDATE {$table} SET `data` = ? WHERE `device_id` = ? AND `type` = ?", $data, $id, $type); + + return $db->affected_rows($query) > 0; + } + + /** + * Getter for folders metadata (type and activesync subscription) + * + * @return array Hash array with meta data for each folder + */ + private function folder_meta() + { + if ($this->folder_meta === null) { + $this->folder_meta = []; + + $storage = $this->rc->get_storage(); + $keys = [ + kolab_storage::ASYNC_KEY, + kolab_storage::CTYPE_KEY, + kolab_storage::CTYPE_KEY_PRIVATE, + ]; + + // get folders activesync config + $folderdata = $storage->get_metadata('*', $keys); + + foreach ((array) $folderdata as $folder => $meta) { + $type = kolab_storage::folder_select_metadata($meta) ?? 'mail'; + [$type, ] = explode('.', $type); + $asyncdata = isset($meta[kolab_storage::ASYNC_KEY]) ? json_decode($meta[kolab_storage::ASYNC_KEY], true) : []; + $this->folder_meta[$folder] = [$type ?: 'mail', $asyncdata['FOLDER'] ?? []]; + } + } + + return $this->folder_meta; + } + + /** + * Get syncroton device_id from IMEI identifier + * + * @param string $imei IMEI identifier + * + * @return string|null Syncroton device identifier + */ + private function imei_to_id($imei) + { + $userid = $this->rc->user->ID; + + if (isset($this->icache["deviceid:{$userid}:{$imei}"])) { + return $this->icache["deviceid:{$userid}:{$imei}"]; + } + + $db = $this->rc->get_dbh(); + $table = $db->table_name('syncroton_device'); + + $result = $db->query("SELECT id FROM {$table} WHERE `owner_id` = ? AND `deviceid` = ?", $userid, $imei); + + return $this->icache["deviceid:{$userid}:{$imei}"] = $db->fetch_array($result)[0] ?? null; + } + + /** + * IMAP folder properties for list_folders/list_subscriptions output + */ + private static function imap_folder_prop($folder) + { + return [ + $folder, + kolab_storage::object_prettyname($folder), + ]; + } + + /** + * DAV folder properties for list_folders/list_subscriptions output + */ + private static function dav_folder_prop($folder) + { + return [ + $folder->href, + $folder->get_name(), + $folder, + ]; + } +} diff --git a/plugins/libkolab/lib/kolab_utils.php b/plugins/libkolab/lib/kolab_utils.php --- a/plugins/libkolab/lib/kolab_utils.php +++ b/plugins/libkolab/lib/kolab_utils.php @@ -49,8 +49,14 @@ } } + // Allow plugins to add something to the form (e.g. kolab_activesync) + $plugin = $rcmail->plugins->exec_hook('kolab_folder_form', [ + 'form' => $form, + 'folder' => $folder, + ]); + // create form output - foreach ($form as $tab) { + foreach ($plugin['form'] as $tab) { if (isset($tab['fields']) && is_array($tab['fields']) && empty($tab['content'])) { $table = new html_table(['cols' => 2, 'class' => 'propform']); foreach ($tab['fields'] as $col => $colprop) {