diff --git a/lib/output/json.php b/lib/output/json.php index 9cac88a..5ac03a6 100644 --- a/lib/output/json.php +++ b/lib/output/json.php @@ -1,289 +1,309 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json extends kolab_api_output { /** * Send successful response * * @param mixed Response data * @param string Data type * @param array Context (folder_uid, object_uid, object) * @param array Optional attributes filter */ public function send($data, $type, $context = null, $attrs_filter = array()) { // Set output type $this->headers(array('Content-Type' => "application/json; charset=utf-8")); list($type, $mode) = explode('-', $type); if ($mode != 'list') { $data = array($data); } $class = "kolab_api_output_json_$type"; $model = new $class($this); $result = array(); $debug = $this->api->config->get('kolab_api_debug'); foreach ($data as $idx => $item) { if ($element = $model->element($item, $attrs_filter)) { $result[] = $element; } else { unset($data[$idx]); } } // apply output filter if ($this->api->filter) { $this->api->filter->output($result, $type, $context, $attrs_filter); } // generate JSON output $opts = $debug && defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0; $result = json_encode($result, $opts); if ($mode != 'list') { $result = trim($result, '[]'); } if ($debug) { rcube::console($result); } $this->send_status(kolab_api_output::STATUS_OK, false); // send JSON output echo $result; exit; } /** * Convert object data into JSON API format * * @param array Object data * @param string Object type * * @return array Object data in JSON API format */ public function convert($data, $type) { $class = "kolab_api_output_json_$type"; $model = new $class($this); return $model->element($data); } /** * Convert (part of) kolab_format object into an array * * @param array Kolab object * @param string Object type * @param string Data element name * @param array Optional list of return properties + * @param array List of array properties (to force their type) * * @return array Object data */ public function object_to_array($object, $type, $element, $properties = array(), $array_elements = array()) { // load old object to preserve data we don't understand/process if (is_object($object['_formatobj'])) { $format = $object['_formatobj']; } // create new kolab_format instance if (!$format) { $ftype = $object['_type'] ?: $type; $format = kolab_format::factory($ftype, kolab_storage::$version); if (PEAR::isError($format)) { return; } $format->set($object); } $xml = $format->write(kolab_storage::$version); if (empty($xml) || !$format->is_valid() || !$format->uid) { return; } // Modify which has been set to current date-time by write() call above // Workaround for #5026 if ($object['changed'] instanceof DateTime && $type == 'contact') { $xml = preg_replace( '/([\r\n\s]*)[TZ0-9]+(<\/timestamp>[\r\n\s]*<\/rev>)/m', '' . $object['changed']->format('Ymd\THis\Z') . '', $xml, 1); } // The simplest way of "normalizing object properties // is to use its XML representation $doc = new DOMDocument(); // LIBXML_NOBLANKS is required for xml_to_array() below $doc->loadXML($xml, LIBXML_NOBLANKS); - $node = $doc->getElementsByTagName($element)->item(0); - $node = $this->xml_to_array($node); - $node = array_filter($node); - - unset($node['prodid']); + $result = array(); + foreach ($doc->getElementsByTagName($element) as $node) { + $result[] = $this->node_to_array($node, $properties, $array_elements); + } // faked 'categories' property (we need this for unit-tests // @TODO: find a better way - if (array_key_exists('categories', $object)) { - if ($node['properties']) { - $node['properties']['categories'] = $object['categories']; + if (array_key_exists('categories', $object) + && (empty($properties) || in_array('categories', $properties)) + ) { + if ($result[0]['properties']) { + $result[0]['properties']['categories'] = $object['categories']; } else { - $node['categories'] = $object['categories']; + $result[0]['categories'] = $object['categories']; } } + return $result; + } + + /** + * Convert XML element into an array + * This is intended to use with Kolab XML format + * + * @param DOMElement XML element + * + * @return mixed Conversion result + */ + protected function node_to_array($node, $properties, $array_elements) + { + $node = $this->xml_to_array($node); + $node = array_filter($node); + + unset($node['prodid']); + if (!empty($properties)) { $node = array_intersect_key($node, array_combine($properties, $properties)); } // force some elements to be arrays if (!empty($array_elements)) { self::parse_array_result($node, $array_elements); } return $node; } /** * Convert XML element into an array * This is intended to use with Kolab XML format * * @param DOMElement XML element * * @return mixed Conversion result */ public function xml_to_array($node) { $children = $node->childNodes; if (!$children->length) { return; } if ($children->length == 1) { if ($node->firstChild->nodeType == XML_TEXT_NODE || !$node->firstChild->childNodes->length ) { return (string) $node->textContent; } if ($node->firstChild->nodeType == XML_ELEMENT_NODE && $node->firstChild->childNodes->length == 1 && $node->firstChild->firstChild->nodeType == XML_TEXT_NODE ) { switch ($node->firstChild->nodeName) { case 'integer': return (int) $node->textContent; case 'boolean': return strtoupper($node->textContent) == 'TRUE'; case 'date-time': case 'timestamp': case 'date': case 'text': case 'uri': case 'sex': return (string) $node->textContent; } } } $result = array(); foreach ($children as $child) { $value = $child->nodeType == XML_TEXT_NODE ? $child->nodeValue : $this->xml_to_array($child); if (!isset($result[$child->nodeName])) { $result[$child->nodeName] = $value; } else { if (!is_array($result[$child->nodeName]) || !isset($result[$child->nodeName][0])) { $result[$child->nodeName] = array($result[$child->nodeName]); } $result[$child->nodeName][] = $value; } } if (is_array($result['text']) && count($result) == 1) { $result = $result['text']; } return $result; } public static function parse_array_result(&$data, $array_elements = array()) { foreach ($array_elements as $key) { $items = explode('/', $key); if (count($items) > 1 && !empty($data[$items[0]])) { $key = array_shift($items); self::parse_array_result($data[$key], array(implode('/', $items))); } else if (!empty($data[$key]) && (!is_array($data[$key]) || !array_key_exists(0, $data[$key]))) { $data[$key] = array($data[$key]); } else if (empty($data[$key])) { unset($data[$key]); } } } /** * Makes sure exdate/rdate output is consistent/unified */ public static function parse_recurrence(&$data) { foreach (array('exdate', 'rdate') as $key) { if ($data[$key]) { if (is_string($data[$key])) { $idx = strlen($data[$key]) > 10 ? 'date-time' : 'date'; $data[$key] = array($idx => array($data[$key])); } else if (array_key_exists('date', $data[$key]) && !is_array($data[$key]['date'])) { $data[$key]['date'] = (array) $data[$key]['date']; } else if (array_key_exists('date-time', $data[$key]) && !is_array($data[$key]['date-time'])) { $data[$key]['date-time'] = (array) $data[$key]['date-time']; } } } } } diff --git a/lib/output/json/configuration.php b/lib/output/json/configuration.php index 4e8014e..8613824 100644 --- a/lib/output/json/configuration.php +++ b/lib/output/json/configuration.php @@ -1,60 +1,60 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_configuration { protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data if (count($data) == 1) { $attrs_filter = array(key($data)); } $result = $this->output->object_to_array($data, 'configuration', 'configuration', $attrs_filter); - return $result; + return $result[0]; } } diff --git a/lib/output/json/contact.php b/lib/output/json/contact.php index 90ccecf..d19b3e7 100644 --- a/lib/output/json/contact.php +++ b/lib/output/json/contact.php @@ -1,80 +1,81 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_contact { protected $output; protected $array_elements = array( // 'group', // @TODO 'adr', 'related', 'url', 'lang', 'tel', 'impp', 'email', 'geo', 'key', 'title', 'categories', 'member', //dist-list 'x-custom', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data if (is_array($data) && count($data) == 1) { $attrs_filter = array(key($data)); } $result = $this->output->object_to_array($data, 'contact', 'vcard', $attrs_filter, $this->array_elements); + $result = $result[0]; if ($result['uid'] && strpos($result['uid'], 'urn:uuid:') === 0) { $result['uid'] = substr($result['uid'], 9); } return $result; } } diff --git a/lib/output/json/event.php b/lib/output/json/event.php index c73fad9..36a3517 100644 --- a/lib/output/json/event.php +++ b/lib/output/json/event.php @@ -1,84 +1,102 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_event { protected $output; + protected $attrs_filter; protected $array_elements = array( 'attach', 'attendee', 'categories', 'x-custom', 'valarm', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data if (is_array($data) && count($data) == 1) { return $data; } + $this->attrs_filter = $attrs_filter; + $result = $this->output->object_to_array($data, 'event', 'vevent'); + $result = array_map(array($this, 'subelement'), $result); + + $event = array_shift($result); - if (!empty($attrs_filter)) { - $result['properties'] = array_intersect_key($result['properties'], - array_combine($attrs_filter, $attrs_filter)); + if (!empty($result) && (empty($attrs_filter) || in_array('exceptions', $attrs_filter))) { + $event['exceptions'] = $result; + } + + return $event; + } + + /** + * Event properties converter + */ + protected function subelement($element) + { + if (!empty($this->attrs_filter)) { + $element['properties'] = array_intersect_key($element['properties'], + array_combine($this->attrs_filter, $this->attrs_filter)); } // add 'components' to the result - if (!empty($result['components'])) { - $result['properties'] += (array) $result['components']; + if (!empty($element['components'])) { + $element['properties'] += (array) $element['components']; } - $result = $result['properties']; + $element = $element['properties']; - kolab_api_output_json::parse_array_result($result, $this->array_elements); + kolab_api_output_json::parse_array_result($element, $this->array_elements); // make sure exdate/rdate format is unified - kolab_api_output_json::parse_recurrence($result); + kolab_api_output_json::parse_recurrence($element); - return $result; + return $element; } } diff --git a/lib/output/json/file.php b/lib/output/json/file.php index 9f2c728..961d77d 100644 --- a/lib/output/json/file.php +++ b/lib/output/json/file.php @@ -1,63 +1,63 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_file { protected $output; protected $array_elements = array( 'x-custom', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data if (is_array($data) && count($data) == 1) { $attrs_filter = array(key($data)); } $result = $this->output->object_to_array($data, 'file', 'file', $attrs_filter, $this->array_elements); - return $result; + return $result[0]; } } diff --git a/lib/output/json/note.php b/lib/output/json/note.php index d40819d..6d3632d 100644 --- a/lib/output/json/note.php +++ b/lib/output/json/note.php @@ -1,65 +1,65 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_note { protected $output; protected $array_elements = array( 'attachment', 'categories', 'x-custom', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data if (is_array($data) && count($data) == 1) { $attrs_filter = array(key($data)); } $result = $this->output->object_to_array($data, 'note', 'note', $attrs_filter, $this->array_elements); - return $result; + return $result[0]; } } diff --git a/lib/output/json/task.php b/lib/output/json/task.php index 1460114..d18d188 100644 --- a/lib/output/json/task.php +++ b/lib/output/json/task.php @@ -1,85 +1,102 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_task { protected $output; protected $array_elements = array( 'attach', 'attendee', 'related-to', 'x-custom', 'categories', 'valarm', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data if (is_array($data) && count($data) == 1) { $attrs_filter = array(key($data)); } + $this->attrs_filter = $attrs_filter; + $result = $this->output->object_to_array($data, 'task', 'vtodo'); + $result = array_map(array($this, 'subelement'), $result); + + $task = array_shift($result); - if (!empty($attrs_filter)) { - $result['properties'] = array_intersect_key($result['properties'], - array_combine($attrs_filter, $attrs_filter)); + if (!empty($result) && (empty($attrs_filter) || in_array('exceptions', $attrs_filter))) { + $task['exceptions'] = $result; + } + + return $task; + } + + /** + * Event properties converter + */ + protected function subelement($element) + { + if (!empty($this->attrs_filter)) { + $element['properties'] = array_intersect_key($element['properties'], + array_combine($this->attrs_filter, $this->attrs_filter)); } // add 'components' to the result - if (!empty($result['components'])) { - $result['properties'] += (array) $result['components']; + if (!empty($element['components'])) { + $element['properties'] += (array) $element['components']; } - $result = $result['properties']; + $element = $element['properties']; - kolab_api_output_json::parse_array_result($result, $this->array_elements); + kolab_api_output_json::parse_array_result($element, $this->array_elements); // make sure exdate/rdate format is unified - kolab_api_output_json::parse_recurrence($result); + kolab_api_output_json::parse_recurrence($element); - return $result; + return $element; } } diff --git a/tests/Unit/Output/Json/Event.php b/tests/Unit/Output/Json/Event.php index 4db0705..27e083a 100644 --- a/tests/Unit/Output/Json/Event.php +++ b/tests/Unit/Output/Json/Event.php @@ -1,69 +1,78 @@ element($object); $this->assertSame('100-100-100-100', $result['uid']); $this->assertSame('2015-05-14T13:03:33Z', $result['created']); $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); $this->assertSame(2, $result['sequence']); $this->assertSame('CONFIDENTIAL', $result['class']); $this->assertSame('tag1', $result['categories'][0]); $this->assertSame('/kolab.org/Europe/Berlin', $result['dtstart']['parameters']['tzid']); $this->assertSame('2015-05-15T10:00:00', $result['dtstart']['date-time']); $this->assertSame('/kolab.org/Europe/Berlin', $result['dtend']['parameters']['tzid']); $this->assertSame('2015-05-15T10:30:00', $result['dtend']['date-time']); $this->assertSame('Summary', $result['summary']); $this->assertSame('Description', $result['description']); $this->assertSame(1, $result['priority']); $this->assertSame('Location', $result['location']); $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); $this->assertSame('mailto:%3Cmark.german%40example.org%3E', $result['organizer']['cal-address']); $this->assertSame('https://some.url', $result['url']); $this->assertSame('Manager, Jane', $result['attendee'][0]['parameters']['cn']); $this->assertSame('NEEDS-ACTION', $result['attendee'][0]['parameters']['partstat']); $this->assertSame('REQ-PARTICIPANT', $result['attendee'][0]['parameters']['role']); $this->assertSame(true, $result['attendee'][0]['parameters']['rsvp']); $this->assertSame('mailto:%3Cjane.manager%40example.org%3E', $result['attendee'][0]['cal-address']); $this->assertSame('image/jpeg', $result['attach'][0]['parameters']['fmttype']); $this->assertSame('photo-mini.jpg', $result['attach'][0]['parameters']['x-label']); $this->assertSame('cid:photo-mini.1431611291.28810.jpg', $result['attach'][0]['uri']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('Summary', $result['valarm'][0]['properties']['description']); $this->assertSame('START', $result['valarm'][0]['properties']['trigger']['parameters']['related']); $this->assertSame('-PT15M', $result['valarm'][0]['properties']['trigger']['duration']); $object = kolab_api_tests::get_data('101-101-101-101', 'Calendar', 'event', null, $context); $result = $output->element($object); $this->assertSame('101-101-101-101', $result['uid']); $this->assertSame('PUBLIC', $result['class']); $this->assertSame('2015-05-15', $result['dtstart']); $this->assertSame('2015-05-15', $result['dtend']); $this->assertSame('WEEKLY', $result['rrule']['recur']['freq']); $this->assertSame('MO', $result['rrule']['recur']['byday']); $this->assertSame('2015-06-05', $result['exdate']['date'][0]); $this->assertSame('2015-06-12', $result['exdate']['date'][1]); $object = kolab_api_tests::get_data('102-102-102-102', 'Calendar', 'event', null, $context); $result = $output->element($object); $this->assertSame('102-102-102-102', $result['uid']); $this->assertSame('2015-06-25', $result['rdate']['date'][0]); $this->assertSame('2015-06-28', $result['rdate']['date'][1]); + + // Event with multiple tags for recurrences + $object = kolab_api_tests::get_data('103-103-103-103', 'Calendar', 'event', null, $context); + $result = $output->element($object); + + $this->assertSame('103-103-103-103', $result['uid']); + $this->assertCount(2, $result['exceptions']); + $this->assertSame('2015-06-22', $result['exceptions'][0]['recurrence-id']); + $this->assertSame('2015-06-29', $result['exceptions'][1]['recurrence-id']); } }