Page MenuHomePhorge

D4683.1775320391.diff
No OneTemporary

Authored By
Unknown
Size
61 KB
Referenced Files
None
Subscribers
None

D4683.1775320391.diff

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 <machniak@kolabsys.com>
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
- * Copyright (C) 2011-2013, Kolab Systems AG <contact@kolabsys.com>
+ * Copyright (C) 2024, 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
@@ -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] . ' &raquo; ') === 0) {
$length = strlen($names[$i] . ' &raquo; ');
@@ -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 = '<?xml version="1.0" encoding="utf-8"?>'
. '<d:propfind xmlns:d="DAV:">'
. '<d:prop>'
@@ -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 = '<?xml version="1.0" encoding="utf-8"?>'
. '<d:propfind xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:card="urn:ietf:params:xml:ns:carddav">'
@@ -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/<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/<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 @@
+<?php
+
+/**
+ * A utility class to manage ActiveSync subscriptions (for both IMAP and DAV folders).
+ *
+ * @author Aleksander Machniak <machniak@apheleia-it.ch>
+ *
+ * Copyright (C) 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/>.
+ */
+
+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<string,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<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<string,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<string,int> 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<string,int> $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) {

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 4:33 PM (2 h, 40 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18830390
Default Alt Text
D4683.1775320391.diff (61 KB)

Event Timeline