diff --git a/doc/SQL/mysql.initial.sql b/doc/SQL/mysql.initial.sql new file mode 100644 index 0000000..533379a --- /dev/null +++ b/doc/SQL/mysql.initial.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS `copenhagen_fai` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `folder` varchar(64) BINARY NOT NULL, + `data` mediumtext, + PRIMARY KEY (`id`), + INDEX `folder` (`folder`) +) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; + +INSERT INTO `system` (`name`, `value`) VALUES ('copenhagen-version', '2015121000'); diff --git a/lib/filter/mapistore.php b/lib/filter/mapistore.php index 0458027..d74affe 100644 --- a/lib/filter/mapistore.php +++ b/lib/filter/mapistore.php @@ -1,799 +1,851 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore extends kolab_api_filter { public $input; public $api; public $attrs_filter = array(); /** * Modify request path * * @param array (Exploded) request path */ public function path(&$path) { // handle differences between OpenChange API and Kolab API // here we do only very basic modifications, just to be able // to select apprioprate api action class if ($path[0] == 'calendars') { $path[0] = 'events'; } + // Mapistore-specific requests we'll use an existing API class + // and handle this separately, see self::input() + else if ($path[0] == 'fai') { + $path[0] = 'info'; // use existing API path + $path[] = 'mapi-fai'; + } } /** * Executed before every api action * * @param kolab_api_input Request */ public function input(&$input) { + // bring back fai action + if ($input->action == 'info' && ($size = count($input->path)) && $input->path[$size-1] == 'mapi-fai') { + $input->action = 'fai'; + array_pop($input->path); + } + // handle differences between OpenChange API and Kolab API $this->input = $input; $this->api = $input->api; $this->common_action = !in_array($input->action, array('folders', 'info')); // convert / to // or /// if ($this->common_action && ($uid = $input->path[0])) { list($folder, $msg, $attach) = self::uid_decode($uid); $path = array($folder, $msg); if ($attach) { $path[] = $attach; } array_splice($input->path, 0, 1, $path); } // convert parent_id into path on object create request if ($input->method == 'POST' && $this->common_action && !count($input->path)) { $data = $input->input(null, true); if ($data['parent_id']) { $input->path[0] = $data['parent_id']; } else { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } } // convert parent_id into path on object update request else if ($input->method == 'PUT' && $folder && count($input->path) == 2) { $data = $input->input(null, true); if ($data['parent_id'] && $data['parent_id'] != $folder) { $this->parent_change_handler($data); } } // properties filter, map MAPI attribute names to Kolab attributes else if ($input->method == 'GET' && $input->args['properties']) { if ($input->action == 'folders' && $input->path[1] == 'messages') { if (!$this->is_builtin_folder($input->path[0])) { $type = $this->api->backend->folder_type($input->path[0]); list($type, ) = explode('.', $type); } } else { $type = $input->action[strlen($input->action)-1] == 's' ? substr($input->action, 0, -1) : $input->action; } $this->attrs_filter = explode(',', $input->args['properties']); $properties = $this->attributes_filter($this->attrs_filter, $type); $input->args['properties'] = implode(',', $properties); } - // handle actions on contact photo attachments switch ($input->action) { case 'attachments': $this->attachment_actions_handler(); break; case 'folders': $this->folder_actions_handler(); break; + case 'fai': + $fai = $this->get_model_class('fai'); + $fai->actions_handler($input); + break; + case 'notes': // Notes do not have attachments in Exchange if ($input->path[1] === 'attachments' || count($input->path) > 2) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } break; } } /** * Executed when parsing request body * * @param string Request data * @param string Expected object type * @param string Original object data (set on update requests) */ public function input_body(&$data, $type = null, $original_object = null) { // handle differences between OpenChange API and Kolab API // Note: input->path is already modified by input() and path() above switch ($this->input->action) { case 'folders': // folders//deletemessages input if ($this->input->path[1] == 'deleteobjects') { // Kolab API expects just a list of identifiers, I.e.: // [{"id": "1"}, {"id": "2"}] => ["1", "2"] foreach ((array) $data as $idx => $element) { $data[$idx] = $element['id']; } } break; } switch ($type) { case 'attachment': case 'event': case 'note': case 'task': case 'contact': case 'mail': case 'folder': $model = $this->get_model_class($type); $data = $model->input($data, $original_object); break; } } /** * Apply filter on output data * * @param array Result data * @param string Object type * @param array Context (folder_uid, object_uid, object) * @param array Optional attributes filter */ public function output(&$result, $type, $context = null, $attrs_filter = array()) { // handle differences between OpenChange API and Kolab API $model = $this->get_model_class($type); if (!empty($this->attrs_filter)) { $attrs_filter = array_combine($this->attrs_filter, $this->attrs_filter); } else if (!empty($attrs_filter)) { $attrs_filter = $this->attributes_filter($attrs_filter, $type, true); if (!empty($attrs_filter)) { $attrs_filter = array_combine($attrs_filter, $attrs_filter); } } // Add contact photo to attachments list if ($this->input->action == 'contacts' && $this->input->path[2] == 'attachments' && $context && !empty($context['object']) ) { if ($attachment = kolab_api_filter_mapistore_contact::photo_attachment($context['object'])) { $result[] = $attachment; } } // Add event exceptions to attachments list else if ($this->input->action == 'events' && $this->input->path[2] == 'attachments' && $context && !empty($context['object']) ) { $event_model = $this->get_model_class('event'); $event_output = new kolab_api_output_json_event($this->api->output); $event = $event_output->element($context['object']); $attachments = $event_model->exception_attachments($event); if (!empty($attachments)) { $result = array_merge($result, $attachments); } } foreach ($result as $idx => $data) { if ($filtered = $model->output($data, $context)) { // apply properties filter (again) if (!empty($attrs_filter)) { $filtered = array_intersect_key($filtered, $attrs_filter); } $result[$idx] = $filtered; } else { unset($result[$idx]); $unset = true; } } if ($unset) { $result = array_values($result); } // cleanup unset($_SESSION['uploads']['MAPIATTACH']); } /** * Executed for response headers * * @param array Response headers * @param array Context (folder_uid, object_uid, object) */ public function headers(&$headers, $context = null) { // handle differences between OpenChange API and Kolab API foreach ($headers as $name => $value) { switch ($name) { case 'X-Count': // Add contact photo to attachments count if ($this->input->action == 'contacts' && $this->input->path[2] == 'attachments' && $context && !empty($context['object']) && kolab_api_filter_mapistore_contact::photo_attachment($context['object']) ) { $value += 1; } // Add event exceptions to attachments count if ($this->input->action == 'events' && $this->input->path[2] == 'attachments' && $context && !empty($context['object']) ) { $event_model = $this->get_model_class('event'); $event_output = new kolab_api_output_json_event($this->api->output); $event = $event_output->element($context['object']); $value += count($event_model->exception_attachments($event)); } $headers['X-mapistore-rowcount'] = $value; unset($headers[$name]); break; } } } /** * Executed for empty response status * * @param int Status code */ public function send_status(&$status) { // handle differences between OpenChange API and Kolab API if ($this->input->method == 'PUT' && !in_array($input->action, array('info'))) { // Mapistore expects 204 on object updates // however, we'd like to send modified UID of the object sometimes // $status = kolab_api_output::STATUS_EMPTY; } } /** * Converts kolab identifiers describind the object into * MAPI identifier that can be easily used in URL. * * @param string Folder UID * @param string Object UID * @param string Optional attachment identifier * * @return string Object identifier */ public static function uid_encode($folder_uid, $msg_uid, $attach_id = null) { $result = array($folder_uid, $msg_uid); if ($attach_id) { $result[] = $attach_id; } $result = array_map(array('kolab_api_filter_mapistore', 'uid_encode_item'), $result); return implode('.', $result); } /** * Converts back the MAPI identifier into kolab folder/object/attachment IDs * * @param string Object identifier * * @return array Object identifiers */ public static function uid_decode($uid) { $result = explode('.', $uid); $result = array_map(array('kolab_api_filter_mapistore', 'uid_decode_item'), $result); return $result; } /** * Encodes UID element */ protected static function uid_encode_item($str) { $fn = function($match) { return '_' . ord($match[1]); }; $str = preg_replace_callback('/([^0-9a-zA-Z-])/', $fn, $str); return $str; } /** * Decodes UID element */ protected static function uid_decode_item($str) { $fn = function($match) { return chr($match[1]); }; $str = preg_replace_callback('/_([0-9]{2})/', $fn, $str); return $str; } /** * Filter property names */ protected function attributes_filter($attrs, $type = null, $reverse = false) { $model = $this->get_model_class($type); return $model->attributes_filter($attrs, $reverse); } /** * Return instance of model class object */ protected function get_model_class($type) { $class = "kolab_api_filter_mapistore_$type"; return new $class; } /** * Handles object parent modification (move) */ protected function parent_change_handler($data) { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $target = $data['parent_id']; $api = kolab_api::get_instance(); // move the object $api->backend->objects_move($folder, $target, array($uid)); // replace folder uid in input arguments $this->input->path[0] = $target; // exit if the rest of input is empty if (count($data) < 2) { $api->output->send_status(kolab_api_output::STATUS_EMPTY); } } /** * Overwrite attachment actions for contact photos */ protected function attachment_actions_handler() { // contact photo attachment if ($this->input->path[2] == kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID) { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $attach_uid = $this->input->path[2]; $object = $this->api->backend->object_get($folder, $object_uid); $attachment = kolab_api_filter_mapistore_contact::photo_attachment($object); $context = array( 'folder_uid' => $folder, 'object_uid' => $object_uid, 'object' => $object ); if (!$attachment) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // fetch photo info/body if ($this->input->method == 'GET') { $this->api->output->send($attachment, 'attachment', $context); } // photo existence check else if ($this->input->method == 'HEAD') { $this->api->output->send_status(kolab_api_output::STATUS_OK); } // photo delete else if ($this->input->method == 'DELETE') { $object['photo'] = ''; $this->api->backend->object_update($folder, $object, 'contact'); $this->api->output->send_status(kolab_api_output::STATUS_OK); } // photo update else if ($this->input->method == 'PUT') { $data = file_get_contents('php://input'); $data = trim($data); $data = json_decode($data, true); if (empty($data) || empty($data['PidTagAttachDataBinary'])) { $error = "Invalid input for contact photo update request"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } $object['photo'] = base64_decode($data['PidTagAttachDataBinary']); $this->api->backend->object_update($folder, $object, 'contact'); $this->api->output->send_status(kolab_api_output::STATUS_OK); } } // event exception attachment else if ($this->input->path[2] >= 1000000 && $this->api->backend->folder_type($this->input->path[0]) == 'event') { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $attach_uid = $this->input->path[2]; $object = $this->api->backend->object_get($folder, $object_uid); $context = array( 'folder_uid' => $folder, 'object_uid' => $object_uid, 'object' => $object ); $model = $this->get_model_class('event'); $event_output = new kolab_api_output_json_event($this->api->output); $event = $event_output->element($object); $attachments = $model->exception_attachments($event); foreach ($attachments as $attach_idx => $att) { if ($att['id'] == $attach_uid) { $attachment = $att; break; } } if (!$attachment) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // fetch attachment info if ($this->input->method == 'GET') { $this->api->output->send($attachment, 'attachment', $context); } // attachment existence check else if ($this->input->method == 'HEAD') { $this->api->output->send_status(kolab_api_output::STATUS_OK); } // attachment delete else if ($this->input->method == 'DELETE') { unset($object['exceptions'][$attach_idx]); $this->api->backend->object_update($folder, $object, 'event'); $this->api->output->send_status(kolab_api_output::STATUS_OK); } // exception update else if ($this->input->method == 'PUT') { $data = file_get_contents('php://input'); $data = trim($data); $data = json_decode($data, true); if (empty($data['PidTagAttachDataObject'])) { $error = "Invalid input for event exception update request"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } if ($data['PidTagAttachDataObject']['PidTagMessageClass'] != kolab_api_filter_mapistore_event::EXCEPTION_CLASS) { // as of now we do not support other DataObject attachments $error = "Unsupported input for DataObject attachment"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } // parse exception data $exception = $model->input($data['PidTagAttachDataObject']); // Convert the exception into internal format $input = new kolab_api_input_json_event; $input->input($exception, $object['exceptions'][$attach_idx]); if ($exception['start']) { $dt = $exception['start']; } else if ($data['PidTagExceptionReplaceTime']) { $dt = $model->date_mapi2php($data['PidTagExceptionReplaceTime']); } if (empty($dt)) { $error = "Invalid input for event exception"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } $exception['recurrence_date'] = $dt; // Exception attachment id $id = $model->recurrence_id($dt, $event['dtstart']); // Replace the exception and remove other exceptions with the same new recurrence-id reset($attachments); foreach ($attachments as $idx => $att) { if ($idx == $attach_idx) { $object['exceptions'][$attach_idx] = $exception; } else if ($att['id'] == $id) { unset($object['exceptions'][$idx]); } } // Save the event $this->api->backend->object_update($folder, $object, 'event'); $this->api->output->send(array('id' => $id), 'attachment', $context, array('id')); } } // new attachment... else if ($this->input->path[0] && $this->input->path[1] && $this->input->method == 'POST') { $data = $this->input->input(null, true); // add photo to a contact? if ($data['PidTagAttachmentContactPhoto']) { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $object = $this->api->backend->object_get($folder, $object_uid); if (empty($data['PidTagAttachDataBinary'])) { $error = "Invalid input for contact photo create request"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } $id = kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID; $context = array( 'folder_uid' => $folder, 'object_uid' => $object_uid, 'object' => $object ); $object['photo'] = base64_decode($data['PidTagAttachDataBinary']); $this->api->backend->object_update($folder, $object, 'contact'); $this->api->output->send(array('id' => $id), 'attachment', $context, array('id')); } // embedded message, e.g. calendar event exception else if ($data['PidTagAttachMethod'] == 0x00000005) { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $object = $this->api->backend->object_get($folder, $object_uid); if (empty($data['PidTagAttachDataObject'])) { $error = "Invalid input for event exception create request"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } if ($data['PidTagAttachDataObject']['PidTagMessageClass'] != kolab_api_filter_mapistore_event::EXCEPTION_CLASS) { // as of now we do not support other DataObject attachments $error = "Unsupported input for DataObject attachment"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } $context = array( 'folder_uid' => $folder, 'object_uid' => $object_uid, 'object' => $object ); // parse exception data $model = $this->get_model_class('event'); $exception = $model->input($data['PidTagAttachDataObject']); $dt = kolab_api_input_json::to_datetime($exception['recurrence-id'] ?: $exception['dtstart']); if (empty($dt) && $data['PidTagExceptionReplaceTime']) { $dt = $model->date_mapi2php($data['PidTagExceptionReplaceTime']); } if (empty($dt)) { $error = "Invalid input for event exception"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } // Convert the exception into internal format $input = new kolab_api_input_json_event; $input->input($exception); // Exception attachment id $id = $model->recurrence_id($dt, $object['start']); // Append the exception and save event $object['exceptions'][] = $exception; $this->api->backend->object_update($folder, $object, 'event'); $this->api->output->send(array('id' => $id), 'attachment', $context, array('id')); } } } /** * Overwrite folders actions */ protected function folder_actions_handler() { $input = $this->input; // in OpenChange folders/1/folders means get folders of the IPM Subtree if ($input->method == 'GET' && $input->path[0] === '1' && $input->path[1] == 'folders') { $input->path = array(); } // in OpenChange folders/0/folders means get the hierarchy of the NON-IPM Subtree else if ($input->method == 'GET' && $input->path[0] === '0' && $input->path[1] == 'folders') { $list = $this->get_builtin_folder_list(0); $this->api->output->send($list, 'folder-list', null); } else if ($input->path[1] == 'messages') { $input->path[1] = 'objects'; } else if ($input->path[1] == 'deletemessages') { $input->path[1] = 'deleteobjects'; } + // FAI + if ($input->path[1] === 'fai') { + // get FAI objects + if ($input->method == 'GET') { + $this->list_fai_objects($input->path[0]); + } + // count FAI objects + else if ($input->method == 'HEAD') { + $this->count_fai_objects($input->path[0]); + } + + throw new kolab_api_exception(kolab_api_exception::NOT_IMPLEMENTED); + } // request for built-in folder - if ($this->is_builtin_folder($input->path[0])) { + else if ($this->is_builtin_folder($input->path[0])) { $folder = $this->get_builtin_folder($input->path[0]); if (count($input->path) == 1) { // folder info if ($input->method == 'GET') { $this->api->output->send($folder, 'folder', null); } // folder exists else if ($input->method == 'HEAD') { $this->api->output->send_status(kolab_api_output::STATUS_OK); } throw new kolab_api_exception(kolab_api_exception::NOT_IMPLEMENTED); } else { switch (strtolower((string) $input->path[1])) { case 'objects': if ($input->method == 'HEAD') { $this->builtin_folder_count_objects(); } else if ($input->method == 'GET') { $this->builtin_folder_list_objects(); } break; case 'folders': if ($input->method == 'HEAD') { $this->builtin_folder_count_folders(); } else if ($input->method == 'GET') { $this->builtin_folder_list_folders(); } break; case 'empty': if ($input->method == 'POST') { $this->builtin_folder_empty(); } break; case 'deleteobjects': if ($input->method == 'POST') { $this->builtin_folder_delete_objects(); } break; } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } } /** * Returns list of built-in folders (NON-IPM subtree) */ protected function get_builtin_folder_list($parent = null) { $folders = kolab_api_filter_mapistore_folder::$builtin_folders; foreach ($folders as $idx => $folder) { if ($parent !== null && $parent != $folder['parent']) { unset($folders[$idx]); continue; } $folders[$idx] = array_merge(array( 'comment' => $folder['name'], 'uid' => $idx, 'hidden' => true, 'role' => 10, 'system_idx' => $idx, ), $folder); } return $folders; } /** * Returns built-in folder information */ protected function get_builtin_folder($uid) { $list = $this->get_builtin_folder_list(); $result = $list[$uid]; if (!$result) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } return $result; } /** * Check if specified uid is an uid of builtin folder */ protected function is_builtin_folder($uid) { return is_numeric($uid) && strlen($uid) < 4; } /** * Coun objects in built-in folder */ protected function builtin_folder_count_objects() { // @TODO $this->api->output->headers(array('X-Count' => 0)); $this->api->output->send_status(kolab_api_output::STATUS_OK); } /** * List objects in built-in folder */ protected function builtin_folder_list_objects() { // @TODO $this->api->output->send(array(), 'folder-list'); } /** * Count sub-folders of built-in folder */ protected function builtin_folder_count_folders() { // @TODO $this->api->output->headers(array('X-Count' => 0)); $this->api->output->send_status(kolab_api_output::STATUS_OK); } /** * List sub-folders in built-in folder */ protected function builtin_folder_list_folders() { // @TODO $this->api->output->send(array(), 'folder-list'); } /** * Delete all objects in built-in folder */ protected function builtin_folder_empty() { throw new kolab_api_exception(kolab_api_exception::NOT_IMPLEMENTED); } /** * Delete objects in built0in folder */ protected function builtin_folder_delete_objects() { throw new kolab_api_exception(kolab_api_exception::NOT_IMPLEMENTED); } + + /** + * Count FAI objects in a folder + */ + protected function count_fai_objects($folder) + { + $fai = $this->get_model_class('fai'); + $count = $fai->count_objects($folder); + + $this->api->output->headers(array('X-Count' => $count)); + $this->api->output->send_status(kolab_api_output::STATUS_OK); + } + + /** + * List FAI objects in a folder + */ + protected function list_fai_objects($folder) + { + $fai = $this->get_model_class('fai'); + $list = $fai->list_objects($folder); + + $fai->send($list, true); + } } diff --git a/lib/filter/mapistore/fai.php b/lib/filter/mapistore/fai.php new file mode 100644 index 0000000..854c543 --- /dev/null +++ b/lib/filter/mapistore/fai.php @@ -0,0 +1,273 @@ + | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak | + +--------------------------------------------------------------------------+ +*/ + +class kolab_api_filter_mapistore_fai +{ + protected $output; + protected $db; + protected $table = 'copenhagen_fai'; + + + public function __construct() + { + $api = kolab_api::get_instance(); + + $this->db = $api->get_dbh(); + $this->output = $api->output; + } + + /** + * Handle FAI actions + */ + public function actions_handler($input) + { + $method = $input->method; + + if ($input->path[0] && $input->path[1] === null && $method == 'POST') { + $object = $input->input(null, true); + $this->object_create($object); + } + else if ($input->path[0]) { + switch (strtolower($input->path[1])) { +/* + case 'attachments': + if ($method == 'HEAD') { + $this->count_attachments(); + } + else if ($method == 'GET') { + $this->list_attachments(); + } + break; +*/ + case '': + if ($method == 'GET') { + $this->object_info($input->path[0]); + } + else if ($method == 'PUT') { + $object = $input->input(null, true); + $object['id'] = $input->path[0]; + $this->object_update($object); + } + else if ($method == 'HEAD') { + $this->object_exists($input->path[0]); + } + else if ($method == 'DELETE') { + $this->object_delete($input->path[0]); + } + } + } + + throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); + } + + /** + * Count FAI objects in a folder + */ + public function count_objects($folder) + { + $res = $this->db->query("SELECT COUNT(*) FROM `{$this->table}`" + . " WHERE `folder` = ?", (string) $folder); + + if ($this->db->is_error($res)) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + + $row = $this->db->fetch_array($res); + + return $row ? (int) $row[0] : 0; + } + + /** + * List FAI objects in a folder + */ + public function list_objects($folder) + { + $res = $this->db->query("SELECT * FROM `{$this->table}`" + . " WHERE `folder` = ? ORDER BY `id`", (string) $folder); + + if ($this->db->is_error($res)) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + + $result = array(); + + while ($row = $this->db->fetch_assoc($res)) { + if (($object = json_decode($row['data'], true)) !== false) { + $object['id'] = $row['id']; + $object['parent_id'] = $row['folder']; + + $result[] = $object; + } + } + + return $result; + } + + /** + * Create FAI object + */ + public function object_create($object) + { + $folder = $object['parent_id']; + + $res = $this->db->query("INSERT INTO `{$this->table}`" + . " (`folder`, `data`) VALUES (?, ?) ", + $folder, json_encode($object)); + + if ($this->db->is_error($res)) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + + $id = $this->db->insert_id($this->table); + + $this->send(array('id' => $id)); + } + + /** + * Update FAI object + */ + public function object_update($object) + { + $id = $object['id']; + + if (!$id) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + + if ($object['parent_id']) { + $folder = $object['parent_id']; + } + + // @FIXME: do we need to merge with the old data on update? + + $res = $this->db->query("UPDATE `{$this->table}`" + . " SET `data` = ?" + . ($folder ? ", `folder` = " . $this->db->quote($folder) : '') + . " WHERE `id` = ?", + json_encode($object), $id); + + if ($this->db->is_error($res)) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + + if (!$this->db->affected_rows($res)) { + throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); + } + + $this->send(array('id' => $id)); + } + + /** + * Delete FAI object + */ + public function object_delete($id) + { + $res = $this->db->query("DELETE FROM `{$this->table}`" + . " WHERE `id` = ?", $id); + + if ($this->db->is_error($res)) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + + if (!$this->db->affected_rows($res)) { + throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); + } + + $this->output->send_status(kolab_api_output::STATUS_EMPTY); + } + + /** + * Check if FAI object exists + */ + public function object_exists($id) + { + $res = $this->db->query("SELECT 1 FROM `{$this->table}`" + . " WHERE `id` = ?", $id); + + if ($this->db->is_error($res)) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + + $row = $this->db->fetch_assoc($res); + + if (empty($row)) { + throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); + } + + $this->output->send_status(kolab_api_output::STATUS_OK); + } + + /** + * Get FAI object data + */ + public function object_info($id) + { + $res = $this->db->query("SELECT * FROM `{$this->table}`" + . " WHERE `id` = ?", $id); + + if ($this->db->is_error($res)) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + + if ($row = $this->db->fetch_assoc($res)) { + if (($object = json_decode($row['data'], true)) !== false) { + $object['id'] = $row['id']; + $object['parent_id'] = $row['folder']; + } + } + + if (empty($object)) { + throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); + } + + $this->send($object); + } + + /** + * Send successful response + * + * @param mixed Response data + */ + public function send($data) + { + $api = kolab_api::get_instance(); + $debug = $api->config->get('kolab_api_debug'); + + // generate JSON output + $opts = $debug && defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0; + $result = json_encode($data, $opts); + + if ($debug) { + rcube::console($result); + } + + header('Content-Type: "application/json; charset=utf-8"'); + header('HTTP/1.1 200 OK'); + + // send JSON output + echo $result; + exit; + } +} diff --git a/tests/Mapistore/FAI.php b/tests/Mapistore/FAI.php new file mode 100644 index 0000000..8298a10 --- /dev/null +++ b/tests/Mapistore/FAI.php @@ -0,0 +1,189 @@ + 'SomeValue', + 'parent_id' => 1, + )); + self::$api->post('fai', array(), $post); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + $this->assertNotEmpty($body['id']); + + self::$id = $body['id']; +/* + // parent folder does not exists + $post = json_encode(array( + 'SomeProperty' => 'Test', + 'parent_id' => '123456789', + )); + self::$api->post('fai', array(), $post); + + $code = self::$api->response_code(); + $this->assertEquals(404, $code); +*/ + } + + /** + * Test listing all FAI objects + */ + function test_list_objects() + { + // get all objects + self::$api->get('folders/1/fai'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + + $this->assertTrue(count($body) == 1); + $this->assertSame('1', $body[0]['parent_id']); + } + + /** + * Test listing all FAI objects + */ + function test_count_objects() + { + // get all folders + self::$api->head('folders/1/fai'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $count = self::$api->response_header('X-mapistore-rowcount'); + + $this->assertEquals(200, $code); + $this->assertSame('', $body); + $this->assertSame(1, (int) $count); + } + /** + * Test object existence + */ + function test_object_exists() + { + self::$api->head('fai/' . self::$id); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(200, $code); + $this->assertSame('', $body); + + // and non-existing object + self::$api->get('fai/abcde'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(404, $code); + $this->assertSame('', $body); + } + + /** + * Test folder update + */ + function test_object_update() + { + $post = json_encode(array( + 'SomeOtherProp' => 'Test2', + )); + self::$api->put('fai/' . self::$id, array(), $post); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(200, $code); +/* + // change parent to an existing folder + $post = json_encode(array( + 'parent_id' => kolab_api_tests::folder_uid('Trash'), + )); + self::$api->put('fai/' . self::$id, array(), $post); + + $code = self::$api->response_code(); + $this->assertEquals(200, $code); +*/ + } + + /** + * Test FAI object info + */ + function test_object_info() + { + self::$api->get('fai/' . self::$id); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); +// $this->assertSame('SomeValue', $body['SomeProperty']); + $this->assertSame('Test2', $body['SomeOtherProp']); + $this->assertSame('1', $body['parent_id']); + + // non-existing object + self::$api->get('fai/abcdse'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(404, $code); + } + + /** + * Test object delete + */ + function test_object_delete() + { + // delete existing object + self::$api->delete('fai/' . self::$id); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(204, $code); + $this->assertSame('', $body); + + // and non-existing object + self::$api->get('fai/abcde'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(404, $code); + $this->assertSame('', $body); + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 6a246ae..6b04e24 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,67 +1,68 @@ Unit/Output/Json.php Unit/Output/Json/Attachment.php Unit/Output/Json/Contact.php Unit/Output/Json/Event.php Unit/Output/Json/Folder.php Unit/Output/Json/Info.php Unit/Output/Json/Mail.php Unit/Output/Json/Note.php Unit/Output/Json/Task.php Unit/Input/Json.php Unit/Input/Json/Attachment.php Unit/Input/Json/Contact.php Unit/Input/Json/Event.php Unit/Input/Json/Folder.php Unit/Input/Json/Mail.php Unit/Input/Json/Note.php Unit/Input/Json/Folder.php Unit/Input/Json/Task.php Unit/Filter/Mapistore.php Unit/Filter/Mapistore/Structure/Appointmentrecurrencepattern.php Unit/Filter/Mapistore/Structure/Changehighlight.php Unit/Filter/Mapistore/Structure/Exceptioninfo.php Unit/Filter/Mapistore/Structure/Extendedexception.php Unit/Filter/Mapistore/Structure/Recipientrow.php Unit/Filter/Mapistore/Structure/Recurrencepattern.php Unit/Filter/Mapistore/Structure/Systemtime.php Unit/Filter/Mapistore/Structure/Timezonedefinition.php Unit/Filter/Mapistore/Structure/Timezonestruct.php Unit/Filter/Mapistore/Structure/Tzrule.php Unit/Filter/Mapistore/Common.php Unit/Filter/Mapistore/Attachment.php Unit/Filter/Mapistore/Contact.php Unit/Filter/Mapistore/Event.php Unit/Filter/Mapistore/Folder.php Unit/Filter/Mapistore/Info.php Unit/Filter/Mapistore/Mail.php Unit/Filter/Mapistore/Note.php Unit/Filter/Mapistore/Task.php Unit/Filter/Mapistore/Timezone.php API/Folders.php API/Attachments.php API/Contacts.php API/Events.php API/Info.php API/Mails.php API/Notes.php API/Tasks.php + Mapistore/FAI.php Mapistore/Folders.php Mapistore/Attachments.php Mapistore/Contacts.php Mapistore/Events.php Mapistore/Info.php Mapistore/Mails.php Mapistore/Notes.php Mapistore/Tasks.php