* @author Thomas Bruederli * * Copyright (C) 2011-2012, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ class kolab_activesync extends rcube_plugin { public $task = 'settings'; public $urlbase; public $backend; private $rc; private $ui; private $folder_meta; private $root_meta; const ROOT_MAILBOX = 'INBOX'; const ASYNC_KEY = '/private/vendor/kolab/activesync'; const CTYPE_KEY = '/shared/vendor/kolab/folder-type'; /** * Plugin initialization. */ public function init() { $this->rc = rcmail::get_instance(); $this->require_plugin('jqueryui'); $this->require_plugin('libkolab'); $this->register_action('plugin.activesyncconfig', array($this, 'config_view')); $this->register_action('plugin.activesyncjson', array($this, 'json_command')); $this->add_texts('localization/', true); $this->include_script('kolab_activesync.js'); } /** * Handle JSON requests */ public function json_command() { $cmd = get_input_value('cmd', RCUBE_INPUT_GPC); $imei = get_input_value('id', RCUBE_INPUT_GPC); switch ($cmd) { case 'load': $devices = $this->list_devices(); if ($device = $devices[$imei]) { $result['id'] = $imei; $result['devicealias'] = $device['ALIAS']; // $result['syncmode'] = intval($device['MODE']); // $result['laxpic'] = intval($device['LAXPIC']); $result['subscribed'] = array(); foreach ($this->folder_meta() as $folder => $meta) { if ($meta['FOLDER'][$imei]['S']) { $result['subscribed'][$folder] = intval($meta['FOLDER'][$imei]['S']); } } $this->rc->output->command('plugin.activesync_data_ready', $result); } else { $this->rc->output->show_message($this->gettext('devicenotfound'), 'error'); } break; case 'save': $devices = $this->list_devices(); $device = $devices[$imei]; $subscriptions = (array) get_input_value('subscribed', RCUBE_INPUT_POST); $devicealias = get_input_value('devicealias', RCUBE_INPUT_POST, true); // $syncmode = intval(get_input_value('syncmode', RCUBE_INPUT_POST)); // $laxpic = intval(get_input_value('laxpic', RCUBE_INPUT_POST)); $device['ALIAS'] = $devicealias; // $device['MODE'] = $syncmode; // $device['LAXPIC'] = $laxpic; $err = !$this->device_update($device, $imei); if (!$err) { // iterate over folders list and update metadata if necessary // old subscriptions foreach ($this->folder_meta() as $folder => $meta) { $err |= !$this->folder_set($folder, $imei, intval($subscriptions[$folder])); unset($subsciptions[$folder]); } // new subscription foreach ($subscriptions as $folder => $flag) { $err |= !$this->folder_set($folder, $imei, intval($flag)); } $this->rc->output->command('plugin.activesync_save_complete', array( 'success' => !$err, 'id' => $imei, 'devicealias' => Q($devicealias))); } if ($err) $this->rc->output->show_message($this->gettext('savingerror'), 'error'); else $this->rc->output->show_message($this->gettext('successfullysaved'), 'confirmation'); break; case 'delete': $success = $this->device_delete($imei); if ($success) { $this->rc->output->show_message($this->gettext('successfullydeleted'), 'confirmation'); $this->rc->output->redirect(array('action' => 'plugin.activesyncconfig')); // reload UI } else $this->rc->output->show_message($this->gettext('savingerror'), 'error'); break; } $this->rc->output->send(); } /** * Render main UI for device configuration */ 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->register_handler('plugin.devicelist', array($this->ui, 'device_list')); $this->register_handler('plugin.deviceconfigform', array($this->ui, 'device_config_form')); $this->register_handler('plugin.foldersubscriptions', array($this->ui, 'folder_subscriptions')); $this->rc->output->send('kolab_activesync.config'); } /** * 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(); } /** * Returns list of folders with assigned type * * @return array List of folder types indexed by folder name */ public function list_types() { if ($this->folder_types === null) { $storage = $this->rc->get_storage(); $folderdata = $storage->get_metadata('*', self::CTYPE_KEY); $this->folder_types = array(); foreach ($folderdata as $folder => $data) { if ($data[self::CTYPE_KEY]) { $this->folder_types[$folder] = $data[self::CTYPE_KEY]; } } } return $this->folder_types; } /** * 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 = array(); } } if (!empty($this->root_meta['DEVICE']) && is_array($this->root_meta['DEVICE'])) { return $this->root_meta['DEVICE']; } return array(); } /** * 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 = array(); $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) { // get folders activesync config $metadata = $this->folder_meta(); $metadata = $metadata[$name]; if ($flag) { if (empty($metadata)) { $metadata = array(); } if (empty($metadata['FOLDER'])) { $metadata['FOLDER'] = array(); } if (empty($metadata['FOLDER'][$deviceid])) { $metadata['FOLDER'][$deviceid] = array(); } // 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], $metadata)) { return true; } $this->folder_meta[$name] = $metadata; $storage = $this->rc->get_storage(); return $storage->set_metadata($name, array( self::ASYNC_KEY => $this->serialize_metadata($metadata))); } 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 = array(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 = array(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 = array(self::ASYNC_KEY => $this->serialize_metadata($meta)); $res = $storage->set_metadata($folder, $metadata); if ($res && $meta) { $this->folder_meta[$folder] = $meta; } } } } return $result; } /** * Helper method to decode saved IMAP metadata */ private function unserialize_metadata($str) { if (!empty($str)) { $data = json_decode($str, true); return $data; } 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; } 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; } }