Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117863251
D4683.1775320391.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
61 KB
Referenced Files
None
Subscribers
None
D4683.1775320391.diff
View Options
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] . ' » ') === 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 = '<?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
Details
Attached
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)
Attached To
Mode
D4683: Activesync subscriptions (in SQL database)
Attached
Detach File
Event Timeline