diff --git a/doc/contacts.rst b/doc/contacts.rst index c22b273..fbfb560 100644 --- a/doc/contacts.rst +++ b/doc/contacts.rst @@ -1,98 +1,104 @@ ============= ``/contacts`` ============= ``GET /contacts//`` ======================================================= Get the contact object properties. .. NOTE:: Obtain the folder uid from ``/folders``, and the contact uid from ``/folders//objects`` +.. NOTE:: + + The response includes properties that can be filtered. For example, + ``GET /contacts/6c11cd1e5283576e/0fcf492a-3dab-4ec1-99ee-119a6e517a3f?properties=uid,email`` + will return only the uid and email(s) of the contact. + **Example Result** ``GET /contacts/6c11cd1e5283576e/0fcf492a-3dab-4ec1-99ee-119a6e517a3f`` .. parsed-literal:: { "uid": "0fcf492a-3dab-4ec1-99ee-119a6e517a3f", "rev": "20171018T104904Z", "kind": "individual", "fn": "Firstname Lastname", "n": { "surname": "Lastname", "given": "Firstname" }, "group": { "org": "Kolab Groupware" }, "url": [ "https:\/\/kolab.org" ], "adr": [ { "parameters": {"type": "home"}, "street": "Broadway Awenue 123", "locality": "New York", "code": "123456", "country": "USA" } ], "tel": [ { "parameters": {"type": "home"}, "text": "+48 500000012" } ], "email": [ { "parameters": {"type": "home"}, "text": "home@kolab.org" } ], "categories":[] } ``GET /contacts///attachments`` ======================================================= List the attachments on the object, if any. .. NOTE:: Kolab does not really support contact attachments. ``HEAD /contacts///attachments`` ======================================================== Count the attachments on the object. Returns `X-Count` header with a numeric value. .. NOTE:: Kolab does not really support contact attachments. ``DELETE /contacts//`` ============================================== Delete an existing contact object. On success code 204 is returned. ``POST /contacts`` ================== Create a new contact in the default address book folder. .. NOTE:: Not yet implemented. ``POST /contacts/`` =============================== Create a new contact in specified folder. ``HEAD /contacts//`` ============================================ Check if the contact object exists. If object exists code 200 is returned, 404 otherwise. ``PUT /contacts//`` =========================================== Update an existing contact object. diff --git a/doc/events.rst b/doc/events.rst index a2b9812..d50fb23 100644 --- a/doc/events.rst +++ b/doc/events.rst @@ -1,143 +1,149 @@ =========== ``/events`` =========== ``GET /events//`` ======================================== Request an event. .. NOTE:: Obtain the folder uid from ``/folders``, and the event uid from ``/folders//objects`` +.. NOTE:: + + The response includes properties that can be filtered. For example, + ``GET /events/6c11cd1e5283576e/1D41B2DB805B93596B85F6B7BCAD5700-9EFCC9880FAF1EAB?properties=uid,summary`` + will return only the uid and summary of the event. + **Example** ``GET /events/6c11cd1e5283576e/1D41B2DB805B93596B85F6B7BCAD5700-9EFCC9880FAF1EAB`` .. parsed-literal:: { "class": "PUBLIC", "created": "2015-02-05T05:26:20Z", "dtend": { "date-time": "2015-02-06T12:30:00", "parameters": { "tzid": "/kolab.org/Europe/Berlin" } }, "dtstamp": "2015-02-05T05:26:20Z", "dtstart": { "date-time": "2015-02-06T06:30:00", "parameters": { "tzid": "/kolab.org/Europe/Berlin" } }, "organizer": { "cal-address": "mailto:%3Cjeroen%40kolab.org%3E", "parameters": { "cn": "van Meeuwen, Jeroen" } }, "attendee": [ { "parameters": { "cn": "Doe, Jane", "partstat": "NEEDS-ACTION", "role": "REQ-PARTICIPANT", "rsvp": true }, "cal-address": "mailto:%3Cjane.doe%40example.org%3E" } ], "sequence": 0, "summary": "test", "description": "test description with a line\nthis is the line", "uid": "1D41B2DB805B93596B85F6B7BCAD5700-9EFCC9880FAF1EAB" } ``GET /events///attachments`` ===================================================== List the attachments on the object, if any. **Example** ``GET /events/6c11cd1e5283576e/1D41B2DB805B93596B85F6B7BCAD5700-9EFCC9880FAF1EAB/attachments`` .. parsed-literal:: [ { "id": "3", "mimetype": "application\/pdf", "size": 239932, "filename": "doc.pdf", "disposition":"attachment" } ] ``HEAD /events///attachments`` ====================================================== Count the attachments on the object. Returns `X-Count` header with a numeric value. ``DELETE /events//`` ============================================ Delete an existing event object. On success code 204 is returned. ``POST /events`` ================ Create a new event in the default calendar folder. .. NOTE:: Not yet implemented. ``POST /events/`` ============================= Create a new event in specified folder. ``HEAD /events//`` ========================================== Check if the event object exists. If object exists code 200 is returned, 404 otherwise. ``PUT /events//`` ========================================= Update an existing event object. ``GET /events/inbox`` ===================== Request a list of iTip objects from user INBOX folder. List will contain entries in an event format with additional ``itip`` item describing the mail message. .. NOTE:: The response includes properties that can be filtered. For example, ``GET /events/inbox?properties=uid`` will return only the uids of events. **Example** ``GET /events/inbox`` .. parsed-literal:: [ { "summary": "test", "uid": "1D41B2DB805B93596B85F6B7BCAD5700-9EFCC9880FAF1EAB", ..., "itip": { "method": "REQUEST", "uri": "mails/35d7656c-d70e-4443-94ac-d8ae1dd45ed3/61", "from": "\"Sand, Carol\" ", "subject": "You have been invited to \"test\"" } } ] diff --git a/doc/folders.rst b/doc/folders.rst index 9f4b0c7..35fd776 100644 --- a/doc/folders.rst +++ b/doc/folders.rst @@ -1,242 +1,248 @@ ============ ``/folders`` ============ ``GET /folders`` ================ Request a full list of folders. +.. NOTE:: + + The response includes properties that can be filtered. For example, + ``GET /folders?properties=name`` will return only the names of the + folders. + +.. NOTE:: + + This API call requires the client to filter the folders by type. + **Example Result** .. parsed-literal:: [ (...) { "fullpath": "Calendar", "name": "Calendar", "type": "event.default", "uid": "6c11cd1e5283576e" }, { "fullpath": "Calendar/Personal", "name": "Personal", "parent": "6c11cd1e5283576e", "type": "event", "uid": "2faf3307-7d32-4d22-bb1b-9a8a40fb3872" }, (...) { "fullpath": "Drafts", "name": "Drafts", "type": "mail.drafts", "uid": "16dd16e25283576d" }, (...) { "fullpath": "INBOX", "name": "INBOX", "type": "mail.inbox", "uid": "169aad0b52725a31" }, (...) { "fullpath": "Sent", "name": "Sent", "type": "mail.sentitems", "uid": "5deaff235283576d" }, { "fullpath": "Spam", "name": "Spam", "type": "mail.junkemail", "uid": "5df585705283576e" }, (...) { "fullpath": "Trash", "name": "Trash", "type": "mail.wastebasket", "uid": "0e307e915283576e" } ] +``GET /folders/`` +============================= + +Get information about a specific folder. + .. NOTE:: The response includes properties that can be filtered. For example, - ``GET /folders?properties=name`` will return only the names of the + ``GET /folders/169aad0b52725a31?properties=name`` will return only the names of the folders. .. NOTE:: - This API call requires the client to filter the folders by type. - -``GET /folders/`` -============================= - -Get information about a specific folder. + The API supports additional folder metadata, but for performance reasons + that isn't returned by default and have to be requested using ``properties`` + argument. Supported extra properties are: exists, unseen, unread, highestmodseq, + uidvalidity, size, deleted, children. **Example** ``GET /folders/169aad0b52725a31`` .. parsed-literal:: { "fullpath": "INBOX", "name": "INBOX", "type": "mail.inbox", "uid": "169aad0b52725a31" } -.. NOTE:: - - The response includes properties that can be filtered. For example, - ``GET /folders/169aad0b52725a31?properties=name`` will return only the names of the - folders. - -.. NOTE:: - - The API supports additional folder metadata, but for performance reasons - that isn't returned by default and have to be requested using ``properties`` - argument. Supported extra properties are: exists, unseen, unread, highestmodseq, - uidvalidity, size, deleted, children. - ``DELETE /folders/`` ================================ Delete a folder ``POST /folders`` ================= Create a folder. **Example** ``POST /folders -d '{"name":"Test"}'`` .. parsed-literal: { "uid": "92ba0f35-40fe-4d0c-9b2a-81052e4e2cec" } ``POST /folders//deleteobjects`` ============================================ Delete selected objects from the folder. Supply a list of object UIDs. **Example** ``POST /folders/169aad0b52725a31/deleteobjects`` with ``[112,113]`` will delete objects 112 and 113. ``POST /folders//empty`` ==================================== Clear out the contents of the folder. ``POST /folders//move/`` ================================================ Move objects to another folder. The first folder uid is the source, the second folder uid the target. Supply a list of object uids to move. ``HEAD /folders/`` ============================== Verify the folder exists. ``PUT /folders/`` ============================= Update the folder properties. ``GET /folders//folders`` ===================================== Retrieve a list of sub-folders, if any. ``HEAD /folders//folders`` ====================================== Retrieve the number of sub-folders (as an ``X-Count`` response header value). ``GET /folders//objects`` ===================================== Obtain a list of objects in the folder specified with ``uid``. +.. NOTE:: + + The response includes properties that can be filtered. For example, + ``POST /folders/169aad0b52725a31/search?properties=uid`` will return only the uids of + objects. + **Example** ``GET /folders/169aad0b52725a31/objects`` .. parsed-literal:: [ (...) { "bcc": [], "categories": [], "cc": [], "date": "Mon, 4 Sep 2017 01:09:20 +0100", "flags": [ "seen" ], "from": { "address": "noreply_support@comodo.com", "name": "Comodo Security Services" }, "has-attach": false, "internaldate": " 4-Sep-2017 02:16:09 +0200", "message-id": "mid:44", "reply-to": [], "sender": [], "size": 2332, "subject": "Comodo Domain Validation for \*.kolab.org", "to": [ { "address": "administrator@kolab.org", "name": "administrator@kolab.org" } ], "uid": "112" }, (...) ] ``HEAD /folders//objects`` ====================================== Retrieve an object count (as an ``X-Count`` response header value). ``POST /folders//search`` ===================================== Search objects in the folder specified with ``uid``. Supported search fields for mail search are: TEXT, BODY, and any header name. For other object types it depends on the object type, e.g. for contact data you can use: firstname, surname, name, email, tags. .. NOTE:: The response includes properties that can be filtered. For example, ``POST /folders/169aad0b52725a31/search?properties=uid`` will return only the uids of objects. **Example** ``POST --data '{"subject":"valid"}' /folders/169aad0b52725a31/search?properties=subject,uid`` .. parsed-literal:: [ (...) { "subject": "Domain Validation", "uid": "112" }, (...) ] diff --git a/doc/notes.rst b/doc/notes.rst index a442349..fb2c0fd 100644 --- a/doc/notes.rst +++ b/doc/notes.rst @@ -1,71 +1,77 @@ ========== ``/notes`` ========== ``GET /notes//`` ======================================== Get the note object properties. .. NOTE:: Obtain the folder uid from ``/folders``, and the note uid from ``/folders//objects`` -**Example Result** +.. NOTE:: + + The response includes properties that can be filtered. For example, + ``GET /notes/6c11cd1e5283576e/23a145ca-f384-4564-8754-d7ae1d2824b4?properties=description`` + will return only the description of the note. + +**Example** ``GET /notes/6c11cd1e5283576e/23a145ca-f384-4564-8754-d7ae1d2824b4`` .. parsed-literal:: { "uid": "23a145ca-f384-4564-8754-d7ae1d2824b4", "creation-date": "2017-10-18T11:14:22Z", "last-modification-date": "2017-10-18T11:14:22Z", "classification": "PUBLIC", "summary": "This is a note", "description": "Note body", "categories": [ "tag" ] } ``GET /notes///attachments`` ==================================================== List the attachments on the object, if any. .. NOTE:: Kolab does not really support note attachments. ``HEAD /notes///attachments`` ===================================================== Count the attachments on the object. Returns `X-Count` header with a numeric value. .. NOTE:: Kolab does not really support note attachments. ``DELETE /notes//`` =========================================== Delete an existing note object. On success code 204 is returned. ``POST /notes`` =============== Create a new note in the default notes folder. .. NOTE:: Not yet implemented. ``POST /notes/`` ============================ Create a new note in specified folder. ``HEAD /notes//`` ========================================= Check if the note object exists. If object exists code 200 is returned, 404 otherwise. ``PUT /notes//`` ======================================== Update an existing note object. diff --git a/doc/tasks.rst b/doc/tasks.rst index e6e871b..0f1a78e 100644 --- a/doc/tasks.rst +++ b/doc/tasks.rst @@ -1,93 +1,99 @@ ========== ``/tasks`` ========== ``GET /tasks//`` ======================================== Get the task object properties. .. NOTE:: Obtain the folder uid from ``/folders``, and the task uid from ``/folders//objects`` -**Example Result** +.. NOTE:: + + The response includes properties that can be filtered. For example, + ``GET /tasks/6c11cd1e5283576e/631766755B73A29579871195D9326286-8FE68B2E68E1B348?properties=uid,summary`` + will return only uid and summary of the task. + +**Example** ``GET /tasks/6c11cd1e5283576e/631766755B73A29579871195D9326286-8FE68B2E68E1B348`` .. parsed-literal:: { "uid": "631766755B73A29579871195D9326286-8FE68B2E68E1B348", "created": "2017-10-18T11:19:06Z", "dtstamp": "2017-10-18T11:19:06Z", "sequence": 0, "class": "PUBLIC", "due": "2017-10-20", "rrule": { "recur": {"freq": "DAILY"} }, "summary": "Simple Task", "description": "This is task description", "percent-complete": 45, "organizer": { "parameters": {"cn": "Aleksander Machniak"}, "cal-address": "mailto:%3Ctest%40alec.pl%3E" }, "categories": [ "tag" ] } ``GET /tasks///attachments`` ==================================================== List the attachments on the object, if any. -**Example Result** +**Example** ``GET /tasks/6c11cd1e5283576e/631766755B73A29579871195D9326286-8FE68B2E68E1B348/attachments`` .. parsed-literal:: [ { "id": "3", "mimetype": "application\/pdf", "size": 239932, "filename": "doc.pdf", "disposition": "attachment" } ] ``HEAD /tasks///attachments`` ===================================================== Count the attachments on the object. Returns `X-Count` header with a numeric value. ``DELETE /tasks//`` =========================================== Delete an existing task object. On success code 204 is returned. ``POST /tasks`` =============== Create a new task in the default tasks folder. .. NOTE:: Not yet implemented. ``POST /tasks/`` ============================ Create a new task in specified folder. ``HEAD /tasks//`` ========================================= Check if the task object exists. If object exists code 200 is returned, 404 otherwise. ``PUT /tasks//`` ======================================== Update an existing task object. diff --git a/lib/kolab_api.php b/lib/kolab_api.php index 0ec6d78..ca9c9a8 100644 --- a/lib/kolab_api.php +++ b/lib/kolab_api.php @@ -1,486 +1,486 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api extends rcube { const APP_NAME = 'Kolab REST API'; const VERSION = '0.1'; public $backend; public $filter; public $input; public $output; /** * Current time in UTC. Use it to override * system time, e.g. for unit-testing. * * @var DateTime */ public static $now; protected $model; protected $initialized = false; /** * This implements the 'singleton' design pattern * * @return kolab_api The one and only instance */ public static function get_instance() { if (!self::$instance || !is_a(self::$instance, 'kolab_api')) { $path = kolab_api_input::request_path(); $request = array_shift($path) ?: 'info'; $class = 'kolab_api_' . $request; if (!$request || !class_exists($class)) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Invalid request method: $request" )); } self::$instance = new $class(); self::$instance->startup(); } return self::$instance; } /** * Initial startup function * to register session, create database and imap connections */ protected function startup() { $this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS); // Get list of plugins // WARNING: We can use only plugins that are prepared for this // e.g. are not using output or rcmail objects or // doesn't throw errors when using them $plugins = (array) $this->config->get('kolab_api_plugins', array('kolab_auth')); $plugins = array_unique(array_merge($plugins, array('libkolab', 'libcalendaring'))); // this way we're compatible with Roundcube Framework 1.2 // we can't use load_plugins() here foreach ($plugins as $plugin) { $this->plugins->load_plugin($plugin, true); } } /** * Exception handler * * @param kolab_api_exception Exception */ public static function exception_handler($exception) { $code = $exception->getCode(); $message = $exception->getMessage(); if ($code == 401) { header('WWW-Authenticate: Basic realm="' . self::APP_NAME .'"'); } if (!$exception instanceof kolab_api_exception) { rcube::raise_error($exception, true, false); } header("HTTP/1.1 $code $message"); exit; } /** * Program execution handler */ protected function initialize_handler() { if ($this->initialized) { return; } $this->initialized = true; // Handle request input $this->input = kolab_api_input::factory($this); // Get input/output filter $this->filter = $this->input->filter; // Start session, validate it and authenticate the user if needed if (!$this->session_validate()) { $this->authenticate(); $authenticated = true; } // Initialize backend $this->backend = kolab_api_backend::get_instance(); // set response output class $this->output = kolab_api_output::factory($this); // Filter the input, we want this after authentication if ($this->filter) { $this->filter->input($this->input); } if ($authenticated) { $this->output->headers(array('X-Session-Token' => session_id())); } } /** * Script shutdown handler */ public function shutdown() { parent::shutdown(); // write performance stats to logs/console if ($this->config->get('devel_mode')) { if (function_exists('memory_get_peak_usage')) $mem = memory_get_peak_usage(); else if (function_exists('memory_get_usage')) $mem = memory_get_usage(); $log = trim(kolab_api_input::request_uri() . ($mem ? sprintf(' [%.1f MB]', $mem/1024/1024) : '')); if (defined('KOLAB_API_START')) { rcube::print_timer(KOLAB_API_START, $log); } else { rcube::console($log); } } } /** * Validate the submitted session token */ protected function session_validate() { $sess_id = $this->input->request_header('X-Session-Token'); if (empty($sess_id)) { session_start(); return false; } session_id($sess_id); session_start(); // Session timeout $timeout = $this->config->get('kolab_api_session_timeout'); if ($timeout && $_SESSION['time'] && $_SESSION['time'] < time() - $timeout) { $_SESSION = array(); return false; } // update session time $_SESSION['time'] = time(); return true; } /** * Authentication request handler (HTTP Auth) */ protected function authenticate() { if (!empty($_SERVER['PHP_AUTH_USER'])) { $username = $_SERVER['PHP_AUTH_USER']; $password = $_SERVER['PHP_AUTH_PW']; } // when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule else if (!isset($_SERVER['PHP_AUTH_USER'])) { // "Basic didhfiefdhfu4fjfjdsa34drsdfterrde..." if (isset($_SERVER['REMOTE_USER'])) { $basicAuthData = base64_decode(substr($_SERVER['REMOTE_USER'], 6)); } else if (isset($_SERVER['REDIRECT_REMOTE_USER'])) { $basicAuthData = base64_decode(substr($_SERVER['REDIRECT_REMOTE_USER'], 6)); } else if (isset($_SERVER['Authorization'])) { $basicAuthData = base64_decode(substr($_SERVER['Authorization'], 6)); } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { $basicAuthData = base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)); } if (isset($basicAuthData) && !empty($basicAuthData)) { list($username, $password) = explode(':', $basicAuthData); } } if (!empty($username)) { $backend = kolab_api_backend::get_instance(); $result = $backend->authenticate($username, $password); } if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::UNAUTHORIZED); } $_SESSION['time'] = time(); } /** * Handle API request */ public function run() { $this->initialize_handler(); $path = $this->input->path; $method = $this->input->method; if (!$path[1] && $path[0] && $method == 'POST') { $this->api_object_create(); } else if ($path[1]) { switch (strtolower($path[2])) { case 'attachments': if ($method == 'HEAD') { $this->api_object_count_attachments(); } else if ($method == 'GET') { $this->api_object_list_attachments(); } break; case '': if ($method == 'GET') { $this->api_object_info(); } else if ($method == 'PUT') { $this->api_object_update(); } else if ($method == 'HEAD') { $this->api_object_exists(); } else if ($method == 'DELETE') { $this->api_object_delete(); } } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Fetch object info */ protected function api_object_info() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $context = array('folder_uid' => $folder, 'object' => $object); - $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; + $props = $this->input->get_list_arg('properties'); $this->output->send($object, $this->model, $context, $props); } /** * Create an object */ protected function api_object_create() { $folder = $this->input->path[0]; $input = $this->input->input($this->model); $context = array('folder_uid' => $folder); $uid = $this->backend->object_create($folder, $input, $this->model); $this->output->send(array('uid' => $uid), $this->model, $context, array('uid')); } /** * Update specified object */ protected function api_object_update() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $context = array( 'folder_uid' => $folder, 'object_uid' => $uid, 'object' => $object, ); // parse input and merge with current data (result is in kolab_format/kolab_api_mail) $input = $this->input->input($this->model, false, $object); // update object on the backend $uid = $this->backend->object_update($folder, $input, $this->model); $this->output->send(array('uid' => $uid), $this->model, $context, array('uid')); } /** * Check if specified object exists */ protected function api_object_exists() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Remove specified object */ protected function api_object_delete() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $this->backend->objects_delete($folder, array($uid)); $this->output->send_status(kolab_api_output::STATUS_EMPTY); } /** * Count object attachments */ protected function api_object_count_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $context = array( 'folder_uid' => $folder, 'object_uid' => $uid, 'object' => $object, ); $count = !empty($object['_attachments']) ? count($object['_attachments']) : 0; $this->output->headers(array('X-Count' => $count), $context); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * List object attachments */ protected function api_object_list_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); - $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; + $props = $this->input->get_list_arg('properties'); $context = array( 'folder_uid' => $folder, 'object_uid' => $uid, 'object' => $object, ); // @TODO: currently Kolab format (libkolabxml) allows attachments // in events, tasks and notes. We should support them also in contacts $list = $this->get_object_attachments($object); $this->output->send($list, 'attachment-list', $context, $props); } /** * Extract attachments from the object, depending if it's * Kolab object or email message */ protected function get_object_attachments($object) { // this is a kolab_format object data if (is_array($object)) { $list = (array) $object['_attachments']; foreach ($list as $idx => $att) { $attachment = new rcube_message_part; $attachment->mime_id = $att['id']; $attachment->filename = $att['name']; $attachment->mimetype = $att['mimetype']; $attachment->size = $att['size']; $attachment->disposition = 'attachment'; $attachment->encoding = $att['encoding']; $list[$idx] = $attachment; } } // this is kolab_api_mail or rcube_message(_header) else { $list = (array) $object->attachments; } return $list; } /** * Convert kolab_format object into API format * * @param array Object data in kolab_format * @param string Object type * * @return array Object data in API format */ public function get_object_data($object, $type) { $output = $this->output; if (!$this->output instanceof kolab_api_output_json) { $class = "kolab_api_output_json"; $output = new $class($this); } return $output->convert($object, $type); } /** * Returns RFC2822 formatted current date in user's timezone * * @return string Date */ public function user_date() { // get user's timezone try { $tz = new DateTimeZone($this->config->get('timezone')); $date = self::$now ?: new DateTime('now'); $date->setTimezone($tz); } catch (Exception $e) { $date = new DateTime(); } return $date->format('r'); } }