diff --git a/lib/filter/mapistore/contact.php b/lib/filter/mapistore/contact.php index d6f904b..4c01aa3 100644 --- a/lib/filter/mapistore/contact.php +++ b/lib/filter/mapistore/contact.php @@ -1,662 +1,684 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_contact extends kolab_api_filter_mapistore_common { const PHOTO_ATTACHMENT_ID = 9999; protected $model = 'contact'; protected $map = array( // contact name properties [MS-OXOCNTC] 'PidTagNickname' => 'nickname', // PtypString 'PidTagGeneration' => 'n.suffix', // PtypString 'PidTagDisplayNamePrefix' => 'n.prefix', // PtypString 'PidTagSurname' => 'n.surname', // PtypString 'PidTagMiddleName' => 'n.additional', // PtypString 'PidTagGivenName' => 'n.given', // PtypString 'PidTagInitials' => 'x-custom.MAPI:PidTagInitials', // PtypString 'PidTagDisplayName' => 'fn', // PtypString 'PidLidYomiFirstName' => '', // PtypString 'PidLidYomiLastName' => '', // PtypString 'PidLidFileUnder' => '', // PtypString 'PidLidFileUnderId' => '', // PtypInteger32 'PidLidFileUnderList' => '', // PtypMultipleInteger32 // electronic and phisical address properties 'PidTagPrimaryFaxNumber' => 'x-custom.MAPI:PidTagPrimaryFaxNumber', // PtypString 'PidTagBusinessFaxNumber' => 'x-custom.MAPI:PidTagBusinessFaxNumber', // PtypString 'PidTagHomeFaxNumber' => '', // PtypString 'PidTagHomeAddressStreet' => '', // PtypString 'PidTagHomeAddressCity' => '', // PtypString 'PidTagHomeAddressStateOrProvince' => '', // PtypString 'PidTagHomeAddressPostalCode' => '', // PtypString 'PidTagHomeAddressCountry' => '', // PtypString 'PidLidHomeAddressCountryCode' => '', // PtypString 'PidTagHomeAddressPostOfficeBox' => '', // PtypString 'PidLidHomeAddress' => '', // @TODO: ? 'PidLidWorkAddressStreet' => '', // PtypString 'PidLidWorkAddressCity' => '', // PtypString 'PidLidWorkAddressState' => '', // PtypString 'PidLidWorkAddressPostalCode' => '', // PtypString 'PidLidWorkAddressCountry' => '', // PtypString 'PidLidWorkAddressCountryCode' => '', // PtypString 'PidLidWorkAddressPostOfficeBox' => '', // PtypString 'PidLidWorkAddress' => '', // @TODO: ? 'PidTagOtherAddressStreet' => '', // PtypString 'PidTagOtherAddressCity' => '', // PtypString 'PidTagOtherAddressStateOrProvince' => '', // PtypString 'PidTagOtherAddressPostalCode' => '', // PtypString 'PidTagOtherAddressCountry' => '', // PtypString 'PidLidOtherAddressCountryCode' => '', // PtypString 'PidTagOtherAddressPostOfficeBox' => '', // PtypString 'PidLidOtherAddress' => '', // @TODO: ? // PtypString 'PidTagStreetAddress' => '', // @TODO: ? // PtypString 'PidTagLocality' => '', // @TODO: ? // PtypString 'PidTagStateOrProvince' => '', // @TODO: ? // PtypString 'PidTagPostalCode' => '', // @TODO: ? // PtypString 'PidTagCountry' => '', // @TODO: ? // PtypString 'PidLidAddressCountryCode' => '', // @TODO: ? // PtypString 'PidTagPostOfficeBox' => '', // @TODO: ? // PtypString 'PidTagPostalAddress' => '', // @TODO: ? // PtypString 'PidLidPostalAddressId' => '', // PtypInteger32 'PidTagPagerTelephoneNumber' => '', // PtypString 'PidTagCallbackTelephoneNumber' => '', // PtypString 'PidTagBusinessTelephoneNumber' => '', // PtypString 'PidTagHomeTelephoneNumber' => '', // PtypString 'PidTagPrimaryTelephoneNumber' => '', // PtypString 'PidTagBusiness2TelephoneNumber' => '', // PtypString 'PidTagMobileTelephoneNumber' => '', // PtypString 'PidTagRadioTelephoneNumber' => '', // PtypString 'PidTagCarTelephoneNumber' => '', // PtypString 'PidTagOtherTelephoneNumber' => '', // PtypString 'PidTagAssistantTelephoneNumber' => '', // PtypString 'PidTagHome2TelephoneNumber' => 'x-custom.MAPI:PidTagHome2TelephoneNumber', 'PidTagTelecommunicationsDeviceForDeafTelephoneNumber' => 'x-custom.MAPI:PidTagTelecommunicationsDeviceForDeafTelephoneNumber', 'PidTagCompanyMainTelephoneNumber' => 'x-custom.MAPI:PidTagCompanyMainTelephoneNumber', 'PidTagTelexNumber' => '', // PtypString 'PidTagIsdnNumber' => '', // PtypString 'PidLidAddressBookProviderEmailList' => '', // PtypMultipleInteger32, @TODO: ? 'PidLidAddressBookProviderArrayType' => '', // PtypInteger32, @TODO: ? // event properties 'PidTagBirthday' => 'bday', // PtypTime, UTC 'PidLidBirthdayLocal' => '', // PtypTime, @TODO 'PidLidBirthdayEventEntryId' => '', // PtypBinary 'PidTagWeddingAnniversary' => 'anniversary', // PtypTime, UTC 'PidLidWeddingAnniversaryLocal' => '', // PtypTime, @TODO 'PidLidAnniversaryEventEntryId' => '', // PtypBinary // professional properties 'PidTagTitle' => 'title', // PtypString 'PidTagCompanyName' => '', // PtypString 'PidLidYomiCompanyName' => '', // PtypString 'PidTagDepartmentName' => '', // PtypString 'PidTagOfficeLocation' => 'x-custom.MAPI:PidTagOfficeLocation', // PtypString 'PidTagManagerName' => '', // PtypString 'PidTagAssistant' => '', // PtypString 'PidTagProfession' => 'group.role', // PtypString 'PidLidHasPicture' => '', // PtypBoolean, more about photo attachments in MS-OXOCNTC // other properties 'PidTagHobbies' => 'x-custom.MAPI:PidTagHobbies', // PtypString 'PidTagSpouseName' => '', // PtypString 'PidTagLanguage' => 'lang', // PtypString 'PidTagLocation' => 'x-custom.MAPI:PidTagLocation', // PtypString 'PidLidInstantMessagingAddress' => 'impp', // PtypString 'PidTagOrganizationalIdNumber' => 'x-custom.MAPI:PidTagOrganizationalIdNumber',// PtypString 'PidTagCustomerId' => 'x-custom.MAPI:PidTagCustomerId', // PtypString 'PidTagGovernmentIdNumber' => 'x-custom.MAPI:PidTagGovernmentIdNumber',// PtypString 'PidTagPersonalHomePage' => 'url', // PtypString 'PidTagBusinessHomePage' => 'x-custom.MAPI:PidTagBussinessHomePage', // PtypString 'PidTagFtpSite' => 'x-custom.MAPI:PidTagFtpSite', // PtypString 'PidTagReferredByName' => 'x-custom.MAPI:PidTagReferredByName', // PtypString 'PidLidBilling' => 'x-custom.MAPI:PidLidBilling', // PtypString 'PidLidFreeBusyLocation' => 'fburl', // PtypString 'PidTagChildrenNames' => '', // PtypMultipleString 'PidTagGender' => 'gender', // PtypString 'PidTagUserX509Certificate' => 'key', // PtypMultipleBinary 'PidTagMessageClass' => '', // PtypString: IPM.Contact, IPM.DistList 'PidTagBody' => 'note', // PtypString // contact aggregation properties - skipped 'PidTagLastModificationTime' => 'rev', // PtypTime -/* // distribution lists [MS-OXOCNTC] 'PidLidDistributionListName' => '', // PtypString = PidTagDisplayName 'PidLidDistributionListMembers' => '', // PtypMultipleBinary 'PidLidDistributionListOneOffMembers' => '', // PtypMultipleBinary 'PidLidDistributionListChecksum' => '', // PtypInteger32 'PidLidDistributionListStream' => '', // PtypBinary -*/ ); protected $gender_map = array( 0 => '', 1 => 'F', 2 => 'M', ); protected $phone_map = array( 'PidTagPagerTelephoneNumber' => 'pager', 'PidTagBusinessTelephoneNumber' => 'work', 'PidTagHomeTelephoneNumber' => 'home', 'PidTagMobileTelephoneNumber' => 'cell', 'PidTagCarTelephoneNumber' => 'x-car', 'PidTagOtherTelephoneNumber' => 'textphone', 'PidTagBusinessFaxNumber' => 'faxwork', 'PidTagHomeFaxNumber' => 'faxhome', ); protected $email_map = array( 'PidLidEmail1EmailAddress' => 'home', 'PidLidEmail2EmailAddress' => 'work', 'PidLidEmail3EmailAddress' => 'other', ); protected $address_ids = array( 'home' => 0x00000001, 'work' => 0x00000002, 'other' => 0x00000003, ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array( - // @TODO: IPM.DistList for groups - 'PidTagMessageClass' => 'IPM.Contact', + 'PidTagMessageClass' => $data['kind'] == 'group' ? 'IPM.DistList' : 'IPM.Contact', // mapistore REST API specific properties 'collection' => 'contacts', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = $this->get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidTagGender': $value = (int) array_search($value, $this->gender_map); break; case 'PidTagPersonalHomePage': case 'PidLidInstantMessagingAddress': if (is_array($value)) { $value = $value[0]; } break; case 'PidTagBirthday': case 'PidTagWeddingAnniversary': case 'PidTagLastModificationTime': $value = $this->date_php2mapi($value, false); break; case 'PidTagUserX509Certificate': foreach ((array) $value as $val) { if ($val && preg_match('|^data:application/pkcs7-mime;base64,|i', $val, $m)) { $result[$mapi_idx] = substr($val, strlen($m[0])); continue 3; } } $value = null; break; case 'PidTagTitle': if (is_array($value)) { $value = $value[0]; } break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } // contact photo attachment [MS-OXVCARD 2.1.3.2.4] if (!empty($data['photo'])) { // @TODO: check if photo is one of .bmp, .gif, .jpeg, .png // Photo in MAPI is handled as attachment // Set PidTagAttachmentContactPhoto=true on attachment object $result['PidLidHasPicture'] = true; // @FIXME: should we set PidTagHasAttachments? } // Organization/Department $organization = $data['group']['org']; if (is_array($organization)) { $result['PidTagCompanyName'] = $organization[0]; $result['PidTagDepartmentName'] = $organization[1]; } else if ($organization !== null) { $result['PidTagCompanyName'] = $organization; } // Manager/Assistant $related = $data['group']['related']; if ($related && $related['parameters']) { $related = array($related); } foreach ((array) $related as $rel) { $type = $rel['parameters']['type']; if ($type == 'x-manager') { $result['PidTagManagerName'] = $rel['text']; } else if ($type == 'x-assistant') { $result['PidTagAssistant'] = $rel['text']; } } // Children, Spouse foreach ((array) $data['related'] as $rel) { $type = $rel['parameters']['type']; if ($type == 'child') { $result['PidTagChildrensNames'][] = $rel['text']; } else if ($type == 'spouse') { $result['PidTagSpouseName'] = $rel['text']; } } // Emails $email_map = array_flip($this->email_map); foreach ((array) $data['email'] as $email) { $type = is_array($email) ? $email['parameters']['type'] : 'other'; $key = $email_map[$type] ?: $email_map['other']; // @TODO: This may be addr-spec (RFC5322), we should parse it // and fill also *AddressType and *DisplayName $result[$key] = is_array($email) ? $email['text'] : $email; } // Phone(s) $phone_map = array_flip($this->phone_map); $phones = $data['tel']; if ($phones && $phones['parameters']) { $phones = array($phones); } foreach ((array) $phones as $phone) { $type = implode('', (array)$phone['parameters']['type']); if ($phone['text'] && ($idx = $phone_map[$type])) { $result[$idx] = $phone['text']; } } // Addresses(s) $addresses = $data['adr']; if ($addresses && $addresses['parameters']) { $addresses = array($addresses); } foreach ((array) $addresses as $addr) { $type = $addr['parameters']['type']; $pref = $addr['parameters']['pref']; $address = null; if ($type == 'home') { $address = array( 'PidTagHomeAddressStreet' => $addr['street'], 'PidTagHomeAddressCity' => $addr['locality'], 'PidTagHomeAddressStateOrProvince' => $addr['region'], 'PidTagHomeAddressPostalCode' => $addr['code'], 'PidTagHomeAddressCountry' => $addr['country'], 'PidTagHomeAddressPostOfficeBox' => $addr['pobox'], ); } else if ($type == 'work') { $address = array( 'PidLidWorkAddressStreet' => $addr['street'], 'PidLidWorkAddressCity' => $addr['locality'], 'PidLidWorkAddressState' => $addr['region'], 'PidLidWorkAddressPostalCode' => $addr['code'], 'PidLidWorkAddressCountry' => $addr['country'], 'PidLidWorkAddressPostOfficeBox' => $addr['pobox'], ); } if (!empty($address)) { $result = array_merge($result, array_filter($address)); if (!empty($pref)) { $result['PidLidPostalAddressId'] = $this->address_ids[$type]; } } } $other_adr_map = array( 'street' => 'PidTagOtherAddressStreet', 'locality' => 'PidTagOtherAddressCity', 'region' => 'PidTagOtherAddressStateOrProvince', 'code' => 'PidTagOtherAddressPostalCode', 'country' => 'PidTagOtherAddressCountry', 'pobox' => 'PidTagOtherAddressPostOfficeBox', ); foreach ((array) $data['group']['adr'] as $idx => $value) { if ($value && ($key = $other_adr_map[$idx])) { $result[$key] = $value; } } + // Group members + $this->members_from_kolab($data, $result); + $this->parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagBirthday': case 'PidTagWeddingAnniversary': if ($value) { $value = $this->date_mapi2php($value); $value = $value->format('Y-m-d'); } break; case 'PidTagLastModificationTime': if ($value) { $value = $this->date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; case 'PidTagGender': $value = $this->gender_map[(int)$value]; break; case 'PidTagUserX509Certificate': if (!empty($value)) { $value = array('data:application/pkcs7-mime;base64,' . $value); } break; case 'PidTagPersonalHomePage': case 'PidLidInstantMessagingAddress': if (!empty($value)) { $value = array($value); } break; } $this->set_kolab_value($result, $kolab_idx, $value); } // MS-OXVCARD 2.1.3.2.1 if (!empty($data['PidTagNormalizedSubject']) && empty($data['PidTagDisplayName'])) { $result['fn'] = $data['PidTagNormalizedSubject']; } // Organization/Department if ($data['PidTagCompanyName']) { $result['group']['org'][] = $data['PidTagCompanyName']; } if (!empty($data['PidTagDepartmentName'])) { $result['group']['org'][] = $data['PidTagDepartmentName']; } // Manager if ($data['PidTagManagerName']) { $result['group']['related'][] = array( 'parameters' => array('type' => 'x-manager'), 'text' => $data['PidTagManagerName'], ); } // Assistant if ($data['PidTagAssistant']) { $result['group']['related'][] = array( 'parameters' => array('type' => 'x-assistant'), 'text' => $data['PidTagAssistant'], ); } // Spouse if ($data['PidTagSpouseName']) { $result['related'][] = array( 'parameters' => array('type' => 'spouse'), 'text' => $data['PidTagSpouseName'], ); } // Children foreach ((array) $data['PidTagChildrensNames'] as $child) { $result['related'][] = array( 'parameters' => array('type' => 'child'), 'text' => $child, ); } // Emails foreach ($this->email_map as $mapi_idx => $type) { if ($email = $data[$mapi_idx]) { $result['email'][] = array( 'parameters' => array('type' => $type), 'text' => $email, ); } } // Phone(s) foreach ($this->phone_map as $mapi_idx => $type) { if (array_key_exists($mapi_idx, $data)) { // first remove the old phone... if (!empty($object['tel'])) { foreach ($object['tel'] as $idx => $phone) { $pt = implode('', (array) $phone['parameters']['type']); if ($pt == $type) { unset($object['tel'][$idx]); } } } if ($tel = $data[$mapi_idx]) { if (preg_match('/^fax(work|home)$/', $type, $m)) { $type = array('fax', $m[1]); } // and add it to the list $result['tel'][] = array( 'parameters' => array('type' => $type), 'text' => $tel, ); } } } if (!empty($object['tel'])) { $result['tel'] = array_merge((array) $result['tel'], (array) $object['tel']); } // Preferred (mailing) address if ($data['PidLidPostalAddressId']) { $map = array_flip($this->address_ids); $pref = $map[$data['PidLidPostalAddressId']]; } // Home address $address = array(); $adr_map = array( 'PidTagHomeAddressStreet' => 'street', 'PidTagHomeAddressCity' => 'locality', 'PidTagHomeAddressStateOrProvince' => 'region', 'PidTagHomeAddressPostalCode' => 'code', 'PidTagHomeAddressCountry' => 'country', 'PidTagHomeAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $type = array('parameters' => array('type' => 'home')); if ($pref == 'home') { $type['parameters']['pref'] = 1; } $result['adr'][] = array_merge($address, $type); } // Work address $address = array(); $adr_map = array( 'PidLidWorkAddressStreet' => 'street', 'PidLidWorkAddressCity' => 'locality', 'PidLidWorkAddressState' => 'region', 'PidLidWorkAddressPostalCode' => 'code', 'PidLidWorkAddressCountry' => 'country', 'PidLidWorkAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $type = array('parameters' => array('type' => 'work')); if ($pref == 'work') { $type['parameters']['pref'] = 1; } $result['adr'][] = array_merge($address, $type); } // Office address $address = array(); $adr_map = array( 'PidTagOtherAddressStreet' => 'street', 'PidTagOtherAddressCity' => 'locality', 'PidTagOtherAddressStateOrProvince' => 'region', 'PidTagOtherAddressPostalCode' => 'code', 'PidTagOtherAddressCountry' => 'country', 'PidTagOtherAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $type = array(); if ($pref == 'other') { $type['parameters']['pref'] = 1; } $result['group']['adr'] = array_merge($address, $type); } + // Group members + $this->members_to_kolab($data, $result); + $this->convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); return $map; } /** * Return attachment data for photo of the contact * * @param array $contact Contact data * * @return array Attachment data */ public static function photo_attachment($contact) { if (!empty($contact['photo'])) { $bin_data = $contact['photo']; $mimetype = rcube_mime::file_content_type($bin_data, 'ContactPicture.jpg', 'image/jpeg', true); list(, $type) = explode('/', $mimetype); $ext = $type == 'jpeg' ? 'jpg' : $type; // @TODO: Do we need to convert to JPEG? // [MS-OXOCNTC]: The value of the PidTagAttachDataBinary property, // which is the contents of the attachment, SHOULD be in JPEG format. // Support for other formats is as determined by the implementer. $attachment = array( 'is_photo' => true, 'filename' => 'ContactPicture.' . $ext, 'size' => strlen($bin_data), 'content' => $bin_data, 'mimetype' => $mimetype, 'id' => self::PHOTO_ATTACHMENT_ID, ); if ($attachment['size']) { return $attachment; } } } + + /** + * Convert Kolab members list into MAPI properties + */ + protected function members_from_kolab($data, &$result) + { + // @TODO + foreach ((array) $data['member'] as $member) { + + } + } + + /** + * Convert MAPI properties into Kolab member array + */ + protected function members_to_kolab($data, &$result) + { + // @TODO + } } diff --git a/lib/input/json/contact.php b/lib/input/json/contact.php index 79e18d7..885f5ad 100644 --- a/lib/input/json/contact.php +++ b/lib/input/json/contact.php @@ -1,338 +1,364 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_contact { // map xml/json attributes into internal (kolab_format) protected $field_map = array( // 'created' => 'creation-date', 'changed' => 'rev', 'categories' => 'categories', 'kind' => 'kind', // not supported by kolab_format_contact 'freebusyurl' => 'fburl', 'notes' => 'note', 'name' => 'fn', 'jobtitle' => 'title', 'nickname' => 'nickname', 'birthday' => 'bday', 'anniversary' => 'anniversary', 'photo' => 'photo', 'gender' => 'gender', 'im' => 'impp', 'lang' => 'lang', 'geo' => 'geo', // not supported by kolab_format_contact 'x-crypto' => 'x-crypto', // not supported by kolab_format_contact // the rest of properties is handled separately ); protected $address_props = array( // 'parameters', // 'pobox', // 'ext', 'street', 'locality', 'region', 'code', 'country' ); protected $gendermap = array( 'M' => 'male', 'F' => 'female', ); /** * Convert contact input array into an array that can * be handled by kolab_storage_folder::save() * * @param array Request body * @param array Original object data (on update) */ public function input(&$data, $original = null) { if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } foreach ($this->field_map as $kolab => $api) { if (!array_key_exists($api, $data)) { continue; } $value = $data[$api]; switch ($kolab) { case 'gender': $value = $this->gendermap[$value]; break; case 'created': case 'changed': case 'birthday': case 'anniversary': $value = kolab_api_input_json::to_datetime($value); break; case 'photo': if (preg_match('/^(data:image\/[a-z]+;base64,).+$/i', $value, $m)) { $value = base64_decode(substr($value, strlen($m[1]))); } else { continue 2; } break; case 'jobtitle': $value = (array) $value; break; + + case 'kind': + if ($value == 'group') { + $result['_type'] = 'distribution-list'; + } + break; } $result[$kolab] = $value; } // contact name properties if (array_key_exists('n', $data)) { $name_attrs = array( 'surname' => 'surname', 'firstname' => 'given', 'middlename' => 'additional', 'prefix' => 'prefix', 'suffix' => 'suffix', ); foreach ($name_attrs as $kolab => $api) { $result[$kolab] = $data['n'][$api]; } } // contact additional properties if (array_key_exists('group', $data)) { $group_changed = true; $result['organization'] = null; $result['department'] = null; $result['profession'] = null; $result['manager'] = null; $result['assistant'] = null; foreach ((array) $data['group'] as $idx => $entry) { // organization, department if ($idx == 'org') { if (is_array($entry) && count($entry) > 1) { $result['organization'] = $entry[0]; $result['department'] = $entry[1]; } else { $result['organization'] = is_array($entry) ? $entry[0] : $entry; } } // profession else if ($idx == 'role') { $result['profession'] = $entry; } // manager, assistant else if ($idx == 'related') { foreach ((array) $entry as $item) { if ($item['text'] !== null) { foreach (array('manager', 'assistant') as $i) { if ($item['parameters']['type'] == "x-$i") { $result[$i] = $item['text']; } } } } } // office address else if ($idx == 'adr') { $address = array('type' => 'office'); foreach ($this->address_props as $prop) { if ($entry[$prop] !== null) { $address[$prop] = $entry[$prop]; } } $result['address'][] = $address; } } } // website url if (array_key_exists('url', $data)) { $result['website'] = array(); foreach ((array) $data['url'] as $url) { if (is_array($url) && $url['url']) { $result['website'][] = array( 'url' => $url['url'], 'type' => $url['parameters']['type'], ); } else if ($url) { $result['website'][] = array('url' => $url); } } } // home and work address if (array_key_exists('adr', $data)) { $adr_changed = true; foreach ((array) $data['adr'] as $addr) { $address = array('type' => $addr['parameters']['type']); foreach ($this->address_props as $prop) { if ($addr[$prop] !== null) { $address[$prop] = $addr[$prop]; } } $result['address'][] = $address; } } // spouse, children if (array_key_exists('related', $data)) { $result['spouse'] = null; $result['children'] = array(); foreach ($data['related'] as $entry) { if (isset($entry['text'])) { $type = $entry['parameters']['type']; if ($type == 'spouse') { $result['spouse'] = $entry['text']; } else if ($type == 'child') { $result['children'][] = $entry['text']; } } } } // phone numbers if (array_key_exists('tel', $data)) { $result['phone'] = array(); foreach ((array) $data['tel'] as $phone) { if (!empty($phone) && isset($phone['text'])) { $type = implode('', (array) $phone['parameters']['type']); $aliases = array( 'faxhome' => 'homefax', 'faxwork' => 'workfax', 'x-car' => 'car', 'textphone' => 'other', 'cell' => 'mobile', 'voice' => 'main', ); $result['phone'][] = array( 'type' => $aliases[$type] ?: $type, 'number' => $phone['text'], ); } } } // email addresses if (array_key_exists('email', $data)) { $result['email'] = array(); foreach ($data['email'] as $email) { if (!empty($email)) { if (!is_array($email)) { $result['email'][] = array( 'type' => 'other', 'address' => $email, ); } else if (isset($email['text'])) { $type = implode('', (array) $email['parameters']['type']); $result['email'][] = array( 'type' => $type ? $type : 'other', 'address' => $email['text'], ); } } } } // PGP or S/MIME key if (array_key_exists('key', $data)) { $key_types = array( 'pgp-keys' => 'pgppublickey', 'pkcs7-mime' => 'pkcs7publickey', ); foreach ($key_types as $type) { $result[$type] = null; } foreach ((array) $data['key'] as $key) { if (preg_match('#^data:application/(pgp-keys|pkcs7-mime);base64,#', $key, $m)) { $result[$key_types[$m[1]]] = base64_decode(substr($key, strlen($m[0]))); } } } + // Group members + if (!empty($data['member'])) { + $result['member'] = array(); + + foreach ((array) $data['member'] as $member) { + $uri_params = array(); + if (stripos($member, 'urn:uuid:') === 0) { + if ($uid = substr($member, 9)) { + $result['member'][] = array('uid' => $uid); + } + } + else if ($email = kolab_api_input_json::parse_mailto_uri($member, $uri_params)) { + $result['member'][] = array( + 'email' => $email, + 'name' => $uri_params['cn'], + ); + } + } + } + // x-custom fields kolab_api_input_json::add_x_custom($data, $result); // @TODO: which contact properties should we require? if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } if (!empty($original)) { // fix addresses merging... $addresses = (array) $original['address']; // unset office address if ($group_changed) { foreach ($addresses as $idx => $adr) { if ($adr['type'] == 'office') { unset($addresses[$idx]); } } } // unset other addresses if ($adr_changed) { foreach ($addresses as $idx => $adr) { if ($adr['type'] != 'office') { unset($addresses[$idx]); } } } // merge old and new addresses if (isset($result['address']) && !empty($addresses)) { $result['address'] = array_merge($result['address'], $addresses); } $result = array_merge($original, $result); } $data = $result; } } diff --git a/lib/kolab_api_backend.php b/lib/kolab_api_backend.php index a08d079..dbc88d3 100644 --- a/lib/kolab_api_backend.php +++ b/lib/kolab_api_backend.php @@ -1,1326 +1,1332 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_backend { /** * Singleton instace of kolab_api_backend * * @var kolab_api_backend */ static protected $instance; public $api; public $storage; public $username; public $password; public $user; public $delimiter; protected $icache = array(); /** * This implements the 'singleton' design pattern * * @return kolab_api_backend The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new kolab_api_backend; self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $this->api = kolab_api::get_instance(); $this->storage = $this->api->get_storage(); // @TODO: reset cache? if we do this for every request the cache would be useless // There's no session here //$this->storage->clear_cache('mailboxes.', true); // set additional header used by libkolab $this->storage->set_options(array( // @TODO: there can be Roundcube plugins defining additional headers, // we maybe would need to add them here 'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION', 'skip_deleted' => true, 'threading' => false, )); // Disable paging $this->storage->set_pagesize(999999); $this->delimiter = $this->storage->get_hierarchy_delimiter(); if ($_SESSION['user_id']) { $this->user = new rcube_user($_SESSION['user_id']); $this->api->config->set_user_prefs((array)$this->user->get_prefs()); } } /** * Authenticate a user * * @param string Username * @param string Password * * @return bool */ public function authenticate($username, $password) { $host = $this->select_host($username); // use shared cache for kolab_auth plugin result (username canonification) $cache = $this->api->get_cache_shared('kolab_api_auth'); $cache_key = sha1($username . '::' . $host); if (!$cache || !($auth = $cache->get($cache_key))) { $auth = $this->api->plugins->exec_hook('authenticate', array( 'host' => $host, 'user' => $username, 'pass' => $password, )); if ($cache && !$auth['abort']) { $cache->set($cache_key, array( 'user' => $auth['user'], 'host' => $auth['host'], )); } // LDAP server failure... send 503 error if ($auth['kolab_ldap_error']) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } } else { $auth['pass'] = $password; } // authenticate user against the IMAP server $user_id = $auth['abort'] ? 0 : $this->login($auth['user'], $auth['pass'], $auth['host'], $error); if ($user_id) { $this->username = $auth['user']; $this->password = $auth['pass']; $this->delimiter = $this->storage->get_hierarchy_delimiter(); return true; } // IMAP server failure... send 503 error if ($error == rcube_imap_generic::ERROR_BAD) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } return false; } /** * Get list of folders * * @param string $type Folder type * * @return array|bool List of folders, False on backend failure */ public function folders_list($type = null) { $type_keys = array( kolab_storage::CTYPE_KEY_PRIVATE, kolab_storage::CTYPE_KEY, ); // get folder unique identifiers and types $uid_data = $this->folder_uids(); $type_data = $this->storage->get_metadata('*', $type_keys); $folders = array(); if (!is_array($type_data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } foreach ($uid_data as $folder => $uid) { $path = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); if (strpos($path, $this->delimiter)) { $list = explode($this->delimiter, $path); $name = array_pop($list); $parent = implode($this->delimiter, $list); $parent_id = null; if ($folders[$parent]) { $parent_id = $folders[$parent]['uid']; } // parent folder does not exist add it to the list else { for ($i=0; $idelimiter, $parent_arr); if ($folders[$parent]) { $parent_id = $folders[$parent]['uid']; } else { $fid = $this->folder_name2uid(rcube_charset::convert($parent, RCUBE_CHARSET, 'UTF7-IMAP')); $folders[$parent] = array( 'name' => array_pop($parent_arr), 'fullpath' => $parent, 'uid' => $fid, 'parent' => $parent_id, ); $parent_id = $fid; } } } } else { $parent_id = null; $name = $path; } $data = array( 'name' => $name, 'fullpath' => $path, 'parent' => $parent_id, 'uid' => $uid, ); // folder type reset($type_keys); foreach ($type_keys as $key) { if ($type = $type_data[$folder][$key]) { $data['type'] = $type; break; } } if (empty($data['type'])) { $data['type'] = 'mail'; } $folders[$path] = $data; } // sort folders uksort($folders, array($this, 'sort_folder_comparator')); return $folders; } /** * Returns folder type * * @param string $uid Folder unique identifier * @param string $with_suffix Enable to not remove the subtype * * @return string Folder type */ public function folder_type($uid, $with_suffix = false) { $folder = $this->folder_uid2name($uid); $type = kolab_storage::folder_type($folder); if ($type === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$with_suffix) { list($type, ) = explode('.', $type); } return $type; } /** * Returns objects in a folder * * @param string $uid Folder unique identifier * * @return array Objects (of type rcube_message_header or kolab_format) * @throws kolab_api_exception */ public function objects_list($uid) { $type = $this->folder_type($uid); // use IMAP to fetch mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); $result = $this->storage->list_messages($folder, 1, '', 'ASC'); foreach ($result as $idx => $mail) { $result[$idx] = new kolab_api_mail($mail); } } // otherwise use kolab_storage else { + // Make sure for contact folders we take also + // distribution-lists into account (see also #5209) + if ($type == 'contact') { + $filter = array(array('type', '=', array('contact', 'distribution-list'))); + } + $folder = $this->folder_get_by_uid($uid, $type); - $result = $folder->get_objects(); + $result = $folder->get_objects($filter); if ($result === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } return $result; } /** * Counts objects in a folder * * @param string $uid Folder unique identifier * * @return int Objects count * @throws kolab_api_exception */ public function objects_count($uid) { $type = $this->folder_type($uid); // use IMAP to count mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); // @TODO: error checking requires changes in rcube_imap $result = $this->storage->count($folder, 'ALL'); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $result = $folder->count(); } return $result; } /** * Delete objects in a folder * * @param string $uid Folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_delete($uid, $set) { $type = $this->folder_type($uid); if ($type === 'mail') { $is_mail = true; $folder = $this->folder_uid2name($uid); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); } // delete all if ($set === "*") { if ($is_mail) { $result = $this->storage->clear_folder($folder); } else { $result = $folder->delete_all(); } } else { if ($is_mail) { $result = $this->storage->delete_message($set, $folder); } else { foreach ($set as $uid) { $result = $folder->delete($uid); if ($result === false) { break; } } } } // @TODO: should we throw exception when deleting non-existing object? if ($result === false) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Move objects into another folder * * @param string $uid Folder unique identifier * @param string $target_uid Target folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_move($uid, $target_uid, $set) { $type = $this->folder_type($uid); $target_type = $this->folder_type($target_uid); if ($type === 'mail') { $is_mail = true; $folder = $this->folder_uid2name($uid); $target = $this->folder_uid2name($target_uid); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $target = $this->folder_get_by_uid($target_uid, $target_type); } if ($is_mail) { if ($set === "*") { $set = '1:*'; } $result = $this->storage->move_messages($set, $target, $folder); } else { if ($set === "*") { $set = $folder->get_uids(); } foreach ($set as $uid) { $result = $folder->move($uid, $target); if ($result === false) { break; } } } if ($result === false) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Get object data * * @param string $folder_uid Folder unique identifier * @param string $uid Object identifier * * @return kolab_api_mail|array Object data * @throws kolab_api_exception */ public function object_get($folder_uid, $uid) { $type = $this->folder_type($folder_uid); if ($type === 'mail') { $folder = $this->folder_uid2name($folder_uid); $object = new rcube_message($uid, $folder); if (!$object || empty($object->headers)) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $object = new kolab_api_mail($object); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $object = $folder->get_object($uid); if (!$object) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($type != 'configuration') { // get object categories (tag-relations) $object['categories'] = $this->get_tags($object, $object['categories']); } } return $object; } /** * Create an object * * @param string $folder_uid Folder unique identifier * @param mixed $data Object data (an array or kolab_api_mail) * @param string $type Object type * * @return string Object UID * @throws kolab_api_exception */ public function object_create($folder_uid, $data, $type) { $ftype = $this->folder_type($folder_uid); if ($type === 'mail') { if ($ftype !== 'mail') { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $folder = $this->folder_uid2name($folder_uid); return $data->save($folder); } // otherwise use kolab_storage else { if ($type != 'configuration') { // get object categories (tag-relations) $categories = (array) $data['categories']; $data['categories'] = array(); } $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } - if (!$folder->save($data)) { + if (!$folder->save($data, $data['_type'])) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!empty($categories)) { // create/assign categories (tag-relations) $this->set_tags($data['uid'], $categories); } return $data['uid']; } } /** * Update an object * * @param string $folder_uid Folder unique identifier * @param mixed $data Object data (array or kolab_api_mail) * @param string $type Object type * * @return string Object UID (it can change) * @throws kolab_api_exception */ public function object_update($folder_uid, $data, $type) { $ftype = $this->folder_type($folder_uid); if ($type === 'mail') { if ($ftype != 'mail') { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $folder = $this->folder_uid2name($folder_uid); return $data->save($folder); } // otherwise use kolab_storage else { if ($type != 'configuration') { // get object categories (tag-relations) $categories = (array) $data['categories']; $data['categories'] = array(); } $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } - if (!$folder->save($data)) { + if (!$folder->save($data, $data['_type'])) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (array_key_exists('categories', $data)) { // create/assign categories (tag-relations) $this->set_tags($data['uid'], $categories); } return $data['uid']; } } /** * Get attachment body * * @param mixed $object Object data (from self::object_get()) * @param string $part_id Attachment part identifier * @param mixed $mode NULL to return a string, -1 to print body * or file pointer to save the body into * * @return string Attachment body if $fp=null * @throws kolab_api_exception */ public function attachment_get($object, $part_id, $mode = null) { // object is a mail message if ($object instanceof kolab_api_mail) { return $object->get_part_body($part_id, false, 0, $mode); } // otherwise use kolab_storage else { $this->storage->set_folder($object['_mailbox']); return $this->storage->get_message_part($object['_msguid'], $part_id, null, $mode === -1, is_resource($mode) ? $mode : null, true, 0, false); } } /** * Delete an attachment from the message * * @param mixed $object Object data (from self::object_get()) * @param string $id Attachment identifier * * @return string Message/Object UID * @throws kolab_api_exception */ public function attachment_delete($object, $id) { // object is a mail message if (is_object($object)) { return $object->attachment_delete($id); } // otherwise use kolab_storage else { $folder = kolab_storage::get_folder($object['_mailbox']); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $found = false; // unset the attachment foreach ((array) $object['_attachments'] as $idx => $att) { if ($att['id'] == $id) { $object['_attachments'][$idx] = false; $found = true; } } if (!$found) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if (!$folder->save($object)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $object['uid']; } } /** * Create an attachment and add to a message/object * * @param mixed $object Object data (from self::object_get()) * @param rcube_message_part $attach Attachment data * * @return string Message/Object UID * @throws kolab_api_exception */ public function attachment_create($object, $attach) { // object is a mail message if (is_object($object)) { return $object->attachment_add($attach); } // otherwise use kolab_storage else { $folder = kolab_storage::get_folder($object['_mailbox']); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $object['_attachments'][] = array( 'name' => $attach->filename, 'mimetype' => $attach->mimetype, 'path' => $attach->path, 'size' => $attach->size, 'content' => $attach->data, ); if (!$folder->save($object)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $object['uid']; } } /** * Update an attachment in a message/object * * @param mixed $object Object data (from self::object_get()) * @param rcube_message_part $attach Attachment data * * @return string Message/Object UID * @throws kolab_api_exception */ public function attachment_update($object, $attach) { // object is a mail message if (is_object($object)) { return $object->attachment_update($attach); } // otherwise use kolab_storage else { $folder = kolab_storage::get_folder($object['_mailbox']); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $found = false; // unset the attachment foreach ((array) $object['_attachments'] as $idx => $att) { if ($att['id'] == $attach->mime_id) { $object['_attachments'][$idx] = false; $found = true; } } if (!$found) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $object['_attachments'][] = array( 'name' => $attach->filename, 'mimetype' => $attach->mimetype, 'path' => $attach->path, 'size' => $attach->size, 'content' => $attach->data, ); if (!$folder->save($object)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $object['uid']; } } /** * Creates a folder * * @param string $name Folder name (UTF-8) * @param string $parent Parent folder identifier * @param string $type Folder type * * @return bool Folder identifier on success */ public function folder_create($name, $parent = null, $type = null) { $name = rcube_charset::convert($name, RCUBE_CHARSET, 'UTF7-IMAP'); if ($parent) { $parent = $this->folder_uid2name($parent); $name = $parent . $this->delimiter . $name; } if ($this->storage->folder_exists($name)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $created = kolab_storage::folder_create($name, $type, false, false); if ($created) { $created = $this->folder_name2uid($name); } return $created; } /** * Subscribes a folder * * @param string $uid Folder identifier * @param array $updates Updates (array with keys type, subscribed, active) * * @throws kolab_api_exception */ public function folder_update($uid, $updates) { $folder = $this->folder_uid2name($uid); if (isset($updates['type'])) { $result = kolab_storage::set_folder_type($folder, $updates['type']); if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } if (isset($updates['subscribed'])) { if ($updates['subscribed']) { $result = $this->storage->subscribe($folder); } else { $result = $this->storage->unsubscribe($folder); } if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } // @TODO: active state } /** * Renames a folder * * @param string $old_name Folder name (UTF-8) * @param string $new_name New folder name (UTF-8) * * @throws kolab_api_exception */ public function folder_rename($old_name, $new_name) { $old_name = rcube_charset::convert($old_name, RCUBE_CHARSET, 'UTF7-IMAP'); $new_name = rcube_charset::convert($new_name, RCUBE_CHARSET, 'UTF7-IMAP'); if (!strlen($old_name) || !strlen($new_name) || $old_name === $new_name) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if ($this->storage->folder_exists($new_name)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $type = kolab_storage::folder_type($old_name); if ($type === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // don't use kolab_storage for moving mail folders if (preg_match('/^mail/', $type)) { $result = $this->storage->rename_folder($old_name, $new_name); } else { $result = kolab_storage::folder_rename($old_name, $new_name); } if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Deletes folder * * @param string $uid Folder UID * * @return bool True on success, False on failure * @throws kolab_api_exception */ public function folder_delete($uid) { $folder = $this->folder_uid2name($uid); $type = $this->folder_type($uid); // don't use kolab_storage for mail folders if ($type === 'mail') { $status = $this->storage->delete_folder($folder); } else { $status = kolab_storage::folder_delete($folder); } if (!$status) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Folder info * * @param string $uid Folder UID * * @return array Folder information * @throws kolab_api_exception */ public function folder_info($uid) { $folder = $this->folder_uid2name($uid); // get IMAP folder info $info = $this->storage->folder_info($folder); // get IMAP folder data $data = $this->storage->folder_data($folder); $info['exists'] = $data['EXISTS']; $info['unseen'] = $data['UNSEEN']; $info['modseq'] = $data['HIGHESTMODSEQ']; // add some more parameters (used in folders list response) $path = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); $path = explode($this->delimiter, $path); $info['name'] = $path[count($path)-1]; $info['fullpath'] = implode($this->delimiter, $path); $info['uid'] = $uid; $info['type'] = kolab_storage::folder_type($folder, true) ?: 'mail'; if (count($path) > 1) { array_pop($path); $parent = implode($this->delimiter, $path); $parent = $this->folder_name2uid(rcube_charset::convert($parent, RCUBE_CHARSET, 'UTF7-IMAP')); $info['parent'] = $parent; } // convert some info to be more compact if (!empty($info['rights'])) { $info['rights'] = implode('', $info['rights']); } // @TODO: subscription status, active state // some info is not very interesting here ;) unset($info['attributes']); return $info; } /** * Returns IMAP folder name with full path * * @param string $uid Folder identifier * * @return string Folder full path (UTF-8) */ public function folder_uid2path($uid) { $folder = $this->folder_uid2name($uid); return strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); } /** * Returns IMAP folder name * * @param string $uid Folder identifier * * @return string Folder name (UTF7-IMAP) */ protected function folder_uid2name($uid) { if ($uid === null || $uid === '') { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // we store last folder in-memory if (isset($this->icache["folder:$uid"])) { return $this->icache["folder:$uid"]; } $uids = $this->folder_uids(); foreach ($uids as $folder => $_uid) { if ($uid === $_uid) { return $this->icache["folder:$uid"] = $folder; } } // slowest method, but we need to try it, the full folders list // might contain non-existing folder (not in folder_uids() result) foreach ($this->folders_list() as $folder) { if ($folder['uid'] === $uid) { return rcube_charset::convert($folder['fullpath'], RCUBE_CHARSET, 'UTF7-IMAP'); } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Helper method to get folder UID * * @param string $folder Folder name (UTF7-IMAP) * * @return string Folder's UID */ protected function folder_name2uid($folder) { $uid_keys = array(kolab_storage::UID_KEY_CYRUS); // get folder identifiers $metadata = $this->storage->get_metadata($folder, $uid_keys); if (!is_array($metadata) && $this->storage->get_error_code() != rcube_imap_generic::ERROR_NO) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } /* // above we assume that cyrus built-in unique identifiers are available // however, if they aren't we'll try kolab folder UIDs if (empty($metadata)) { $uid_keys = array(kolab_storage::UID_KEY_SHARED); // get folder identifiers $metadata = $this->storage->get_metadata($folder, $uid_keys); if (!is_array($metadata) && $this->storage->get_error_code() != rcube_imap_generic::ERROR_NO) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } */ if (!empty($metadata[$folder])) { foreach ($uid_keys as $key) { if ($uid = $metadata[$folder][$key]) { return $uid; } } } return md5($folder); /* // @TODO: // make sure folder exists // generate a folder UID and set it to IMAP $uid = rtrim(chunk_split(md5($folder . $this->get_owner() . uniqid('-', true)), 12, '-'), '-'); if ($this->storage->set_metadata($folder, array(kolab_storage::UID_KEY_SHARED => $uid))) { return $uid; } // create hash from folder name if we can't write the UID metadata return md5($folder . $this->get_owner()); */ } /** * Callback for uasort() that implements correct * locale-aware case-sensitive sorting */ protected function sort_folder_comparator($str1, $str2) { $path1 = explode($this->delimiter, $str1); $path2 = explode($this->delimiter, $str2); foreach ($path1 as $idx => $folder1) { $folder2 = $path2[$idx]; if ($folder1 === $folder2) { continue; } return strcoll($folder1, $folder2); } } /** * Return UIDs of all folders * * @return array Folder name to UID map */ protected function folder_uids() { $uid_keys = array(kolab_storage::UID_KEY_CYRUS); // get folder identifiers $metadata = $this->storage->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } /* // above we assume that cyrus built-in unique identifiers are available // however, if they aren't we'll try kolab folder UIDs if (empty($metadata)) { $uid_keys = array(kolab_storage::UID_KEY_SHARED); // get folder identifiers $metadata = $this->storage->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } */ $lambda = function(&$item, $key, $keys) { reset($keys); foreach ($keys as $key) { $item = $item[$key]; return; } }; array_walk($metadata, $lambda, $uid_keys); return $metadata; } /** * Get folder by UID (use only for non-mail folders) * * @param string $uid Folder UID * @param string $type Folder type * * @return kolab_storage_folder Folder object * @throws kolab_api_exception */ protected function folder_get_by_uid($uid, $type = null) { $folder = $this->folder_uid2name($uid); $folder = kolab_storage::get_folder($folder, $type); if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // Check the given storage folder instance for validity and throw // the right exceptions according to the error state. if (!$folder->valid || ($error = $folder->get_error())) { if ($error === kolab_storage::ERROR_IMAP_CONN) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } else if ($error === kolab_storage::ERROR_CACHE_DB) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } else if ($error === kolab_storage::ERROR_NO_PERMISSION) { throw new kolab_api_exception(kolab_api_exception::FORBIDDEN); } else if ($error === kolab_storage::ERROR_INVALID_FOLDER) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $folder; } /** * Storage host selection */ protected function select_host($username) { // Get IMAP host $host = $this->api->config->get('default_host', 'localhost'); if (is_array($host)) { list($user, $domain) = explode('@', $username); // try to select host by mail domain if (!empty($domain)) { foreach ($host as $storage_host => $mail_domains) { if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) { $host = $storage_host; break; } else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) { $host = is_numeric($storage_host) ? $mail_domains : $storage_host; break; } } } // take the first entry if $host is not found if (is_array($host)) { list($key, $val) = each($default_host); $host = is_numeric($key) ? $val : $key; } } return rcube_utils::parse_host($host); } /** * Authenticates a user in IMAP and returns Roundcube user ID. */ protected function login($username, $password, $host, &$error = null) { if (empty($username)) { return null; } $login_lc = $this->api->config->get('login_lc'); $default_port = $this->api->config->get('default_port', 143); // parse $host $a_host = parse_url($host); if ($a_host['host']) { $host = $a_host['host']; $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; if (!empty($a_host['port'])) { $port = $a_host['port']; } else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) { $port = 993; } } if (!$port) { $port = $default_port; } // Convert username to lowercase. If storage backend // is case-insensitive we need to store always the same username if ($login_lc) { if ($login_lc == 2 || $login_lc === true) { $username = mb_strtolower($username); } else if (strpos($username, '@')) { // lowercase domain name list($local, $domain) = explode('@', $username); $username = $local . '@' . mb_strtolower($domain); } } // Here we need IDNA ASCII // Only rcube_contacts class is using domain names in Unicode $host = rcube_utils::idn_to_ascii($host); $username = rcube_utils::idn_to_ascii($username); // user already registered? if ($user = rcube_user::query($username, $host)) { $username = $user->data['username']; } // authenticate user in IMAP if (!$this->storage->connect($host, $username, $password, $port, $ssl)) { $error = $this->storage->get_error_code(); return null; } // No user in database, but IMAP auth works if (!is_object($user)) { if ($this->api->config->get('auto_create_user')) { // create a new user record $user = rcube_user::create($username, $host); if (!$user) { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to create a user record", ), true, false); return null; } } else { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Access denied for new user $username. 'auto_create_user' is disabled", ), true, false); return null; } } // overwrite config with user preferences $this->user = $user; $this->api->config->set_user_prefs((array)$this->user->get_prefs()); $_SESSION['user_id'] = $this->user->ID; $_SESSION['username'] = $this->user->data['username']; $_SESSION['storage_host'] = $host; $_SESSION['storage_port'] = $port; $_SESSION['storage_ssl'] = $ssl; $_SESSION['password'] = $this->api->encrypt($password); $_SESSION['login_time'] = time(); setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); return $user->ID; } /** * Returns list of tag-relation names assigned to Kolab object or mail message * * @param array|kolab_api_mail $object Object or message * @param array $categories Old categories to merge with */ public function get_tags($object, $categories = null) { // Kolab object if (is_array($object)) { $ident = $object['uid']; } // Mail message else if (is_object($object)) { // support only messages with message-id $ident = $object->{'message-id'}; $folder = $object->folder; $uid = $object->uid; } if (empty($ident)) { return array(); } $config = kolab_storage_config::get_instance(); $tags = $config->get_tags($ident, 100); $delta = 300; // resolve members if it wasn't done recently if ($uid) { foreach ($tags as $idx => $tag) { $force = empty($this->tag_rts[$tag['uid']]) || $this->tag_rts[$tag['uid']] <= time() - $delta; $members = $config->resolve_members($tag, $force); if (empty($members[$folder]) || !in_array($uid, $members[$folder])) { unset($tags[$idx]); } if ($force) { $this->tag_rts[$tag['uid']] = time(); } } // make sure current folder is set correctly again $this->storage->set_folder($folder); } $tags = array_filter(array_map(function($v) { return $v['name']; }, $tags)); // merge result with old categories if (!empty($categories)) { $tags = array_unique(array_merge($tags, (array) $categories)); } return $tags; } /** * Set tag-relations to kolab object */ public function set_tags($uid, $tags) { $config = kolab_storage_config::get_instance(); $config->save_tags($uid, $tags); } } diff --git a/lib/output/json.php b/lib/output/json.php index 8a12d0f..1fd070b 100644 --- a/lib/output/json.php +++ b/lib/output/json.php @@ -1,279 +1,280 @@ | +--------------------------------------------------------------------------+ | 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 * * @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) { - $format = kolab_format::factory($type, kolab_storage::$version); + $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; } // 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']); // 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']; } else { $node['categories'] = $object['categories']; } } 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/tests/API/Contacts.php b/tests/API/Contacts.php index 25f12d7..4f02f29 100644 --- a/tests/API/Contacts.php +++ b/tests/API/Contacts.php @@ -1,190 +1,249 @@ get('folders/' . kolab_api_tests::folder_uid('Contacts') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); + + // check objects count to make sure distribution-lists are returned + $this->assertCount(3, $body); $this->assertSame('a-b-c-d', $body[0]['uid']); $this->assertSame('displname', $body[0]['fn']); } /** * Test contact existence */ function test_contact_exists() { self::$api->head('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/a-b-c-d'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing contact self::$api->head('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); + + // distribution-list + self::$api->head('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/i-j-k-l'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(200, $code); + $this->assertSame('', $body); } /** * Test contact info */ function test_contact_info() { self::$api->get('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/a-b-c-d'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('a-b-c-d', $body['uid']); $this->assertSame('displname', $body['fn']); $this->assertSame(array('tag1'), $body['categories']); + $this->assertSame('individual', $body['kind']); + + // distribution-list + self::$api->get('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/i-j-k-l'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + $this->assertSame('i-j-k-l', $body['uid']); + $this->assertSame('group', $body['kind']); } /** * Test contact create */ function test_contact_create() { $post = json_encode(array( 'n' => array( 'surname' => 'lastname', ), 'note' => 'Test description', )); self::$api->post('contacts/' . kolab_api_tests::folder_uid('Contacts'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['uid'])); // folder does not exists $post = json_encode(array( 'n' => array( 'surname' => 'lastname', ), 'note' => 'Test description', )); self::$api->post('contacts/' . kolab_api_tests::folder_uid('non-existing'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'test' => 'Test summary 2', )); self::$api->post('contacts/' . kolab_api_tests::folder_uid('Contacts'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); + + // distribution-list + $post = json_encode(array( + 'kind' => 'group', + 'fn' => 'test gr', + 'member' => array('urn:uuid:a-b-c-d'), + )); + self::$api->post('contacts/' . kolab_api_tests::folder_uid('Contacts'), array(), $post); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + $this->assertCount(1, $body); + $this->assertTrue(!empty($body['uid'])); + + self::$api->get('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/' . $body['uid']); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $body = json_decode($body, true); + + $this->assertEquals(200, $code); + $this->assertSame('group', $body['kind']); + $this->assertSame('urn:uuid:a-b-c-d', $body['member'][0]); } /** * Test contact update */ function test_contact_update() { $post = json_encode(array( 'note' => 'note1', 'categories' => array('test'), )); self::$api->put('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/a-b-c-d', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('a-b-c-d', $body['uid']); self::$api->get('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/a-b-c-d'); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('note1', $body['note']); $this->assertSame(array('test'), $body['categories']); } /** * Test contact delete */ function test_contact_delete() { // delete existing contact self::$api->delete('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/a-b-c-d'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing contact self::$api->delete('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); + + // delete existing distribution-list + self::$api->delete('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/i-j-k-l'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + + $this->assertEquals(204, $code); + $this->assertSame('', $body); } /** * Test counting task attachments */ function test_count_attachments() { $this->markTestIncomplete('TODO'); } /** * Test listing task attachments */ function test_list_attachments() { $this->markTestIncomplete('TODO'); } } diff --git a/tests/Unit/Filter/Mapistore/Contact.php b/tests/Unit/Filter/Mapistore/Contact.php index 1cbaede..4e258de 100644 --- a/tests/Unit/Filter/Mapistore/Contact.php +++ b/tests/Unit/Filter/Mapistore/Contact.php @@ -1,434 +1,448 @@ output($data, $context); + $api = new kolab_api_filter_mapistore_contact; + $data = kolab_api_tests::get_data('a-b-c-d', 'Contacts', 'contact', 'json', $context); + $result = $api->output($data, $context); + $this->assertSame('IPM.Contact', $result['PidTagMessageClass']); + $this->assertSame('contacts', $result['collection']); $this->assertSame(kolab_api_tests::mapi_uid('Contacts', false, 'a-b-c-d'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Contacts', false), $result['parent_id']); // $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('20150421T145607Z'), $result['PidTagLastModificationTime']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('20150330', false), $result['PidTagBirthday']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('20150301', false), $result['PidTagWeddingAnniversary']); - $this->assertSame(null, $body['PidTagHasAttachments']); + $this->assertSame(null, $result['PidTagHasAttachments']); $this->assertSame('displname', $result['PidTagDisplayName']); $this->assertSame('last', $result['PidTagSurname']); $this->assertSame('test', $result['PidTagGivenName']); $this->assertSame('middlename', $result['PidTagMiddleName']); $this->assertSame('prefx', $result['PidTagDisplayNamePrefix']); $this->assertSame('suff', $result['PidTagGeneration']); $this->assertSame('dsfsdfsdfsdf sdfsdfsdf sdfsdfsfd', $result['PidTagBody']); $this->assertSame('free-busy url', $result['PidLidFreeBusyLocation']); $this->assertSame('title', $result['PidTagTitle']); $this->assertSame('Org', $result['PidTagCompanyName']); $this->assertSame('dept', $result['PidTagDepartmentName']); $this->assertSame('profeion', $result['PidTagProfession']); $this->assertSame('manager name', $result['PidTagManagerName']); $this->assertSame('assist', $result['PidTagAssistant']); $this->assertSame('website', $result['PidTagPersonalHomePage']); $this->assertSame('office street', $result['PidTagOtherAddressStreet']); $this->assertSame('office city', $result['PidTagOtherAddressCity']); $this->assertSame('office state', $result['PidTagOtherAddressStateOrProvince']); $this->assertSame('office zip', $result['PidTagOtherAddressPostalCode']); $this->assertSame('office country', $result['PidTagOtherAddressCountry']); // $this->assertSame('office pobox', $result['PidTagOtherAddressPostOfficeBox']); $this->assertSame('home street', $result['PidTagHomeAddressStreet']); $this->assertSame('home city', $result['PidTagHomeAddressCity']); $this->assertSame('home state', $result['PidTagHomeAddressStateOrProvince']); $this->assertSame('home zip', $result['PidTagHomeAddressPostalCode']); $this->assertSame('home country', $result['PidTagHomeAddressCountry']); // $this->assertSame('home pobox', $result['PidTagHomeAddressPostOfficeBox']); $this->assertSame('work street', $result['PidLidWorkAddressStreet']); $this->assertSame('work city', $result['PidLidWorkAddressCity']); $this->assertSame('work state', $result['PidLidWorkAddressState']); $this->assertSame('work zip', $result['PidLidWorkAddressPostalCode']); $this->assertSame('work country', $result['PidLidWorkAddressCountry']); // $this->assertSame('work pobox', $result['PidLidWorkAddressPostOfficeBox']); $this->assertSame(2, $result['PidLidPostalAddressId']); $this->assertSame('nick', $result['PidTagNickname']); $this->assertSame(2, $result['PidTagGender']); $this->assertSame('spouse', $result['PidTagSpouseName']); $this->assertSame(array('children', 'children2'), $result['PidTagChildrensNames']); $this->assertSame('home phone', $result['PidTagHomeTelephoneNumber']); $this->assertSame('work phone', $result['PidTagBusinessTelephoneNumber']); $this->assertSame('home fax', $result['PidTagHomeFaxNumber']); $this->assertSame('work fax', $result['PidTagBusinessFaxNumber']); $this->assertSame('mobile', $result['PidTagMobileTelephoneNumber']); $this->assertSame('pager', $result['PidTagPagerTelephoneNumber']); $this->assertSame('car phone', $result['PidTagCarTelephoneNumber']); $this->assertSame('other phone', $result['PidTagOtherTelephoneNumber']); $this->assertSame('im gg', $result['PidLidInstantMessagingAddress']); $this->assertSame('test@mail.ru', $result['PidLidEmail1EmailAddress']); $this->assertSame('work@email.pl', $result['PidLidEmail2EmailAddress']); $this->assertSame('other@email.pl', $result['PidLidEmail3EmailAddress']); $this->assertRegExp('/^cy9.*/', $result['PidTagUserX509Certificate']); $this->assertSame(true, $result['PidLidHasPicture']); $this->assertSame(array('tag1'), $result['PidNameKeywords']); // $this->assertRegExp('|^data:application/pgp-keys;base64,|', $result['key'][0]); // $this->assertRegExp('|^data:image/jpeg;base64,|', $result['photo']); // $this->assertSame('individual', $result['kind']); + + // Distribution List + $api = new kolab_api_filter_mapistore_contact; + $data = kolab_api_tests::get_data('i-j-k-l', 'Contacts', 'contact', 'json', $context); + $result = $api->output($data, $context); + + $this->assertSame('IPM.DistList', $result['PidTagMessageClass']); + $this->assertSame('contacts', $result['collection']); + $this->assertSame(kolab_api_tests::mapi_uid('Contacts', false, 'i-j-k-l'), $result['id']); + $this->assertSame(kolab_api_tests::folder_uid('Contacts', false), $result['parent_id']); + $this->assertSame('test group', $result['PidTagDisplayName']); + // @TODO: list members } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_contact; $data = array( 'id' => kolab_api_tests::mapi_uid('Contacts', false, 'a-b-c-d'), 'parent_id' => kolab_api_tests::folder_uid('Contacts', false), // 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('20150421T145607Z'), 'PidTagBirthday' => kolab_api_filter_mapistore_common::date_php2mapi('20150330', true), 'PidTagWeddingAnniversary' => kolab_api_filter_mapistore_common::date_php2mapi('20150301', true), 'PidTagDisplayName' => 'displname', 'PidTagSurname' => 'last', 'PidTagGivenName' => 'test', 'PidTagMiddleName' => 'middlename', 'PidTagDisplayNamePrefix' => 'prefx', 'PidTagGeneration' => 'suff', 'PidTagBody' => 'dsfsdfsdfsdf sdfsdfsdf sdfsdfsfd', 'PidLidFreeBusyLocation' => 'free-busy url', 'PidTagTitle' => 'title', 'PidTagCompanyName' => 'Org', 'PidTagDepartmentName' => 'dept', 'PidTagProfession' => 'profeion', 'PidTagManagerName' => 'manager name', 'PidTagAssistant' => 'assist', 'PidTagPersonalHomePage' => 'website', 'PidTagOtherAddressStreet' => 'office street', 'PidTagOtherAddressCity' => 'office city', 'PidTagOtherAddressStateOrProvince' => 'office state', 'PidTagOtherAddressPostalCode' => 'office zip', 'PidTagOtherAddressCountry' => 'office country', // 'PidTagOtherAddressPostOfficeBox' => 'office pobox', 'PidTagHomeAddressStreet' => 'home street', 'PidTagHomeAddressCity' => 'home city', 'PidTagHomeAddressStateOrProvince' => 'home state', 'PidTagHomeAddressPostalCode' => 'home zip', 'PidTagHomeAddressCountry' => 'home country', // 'PidTagHomeAddressPostOfficeBox' => 'home pobox', 'PidLidWorkAddressStreet' => 'work street', 'PidLidWorkAddressCity' => 'work city', 'PidLidWorkAddressState' => 'work state', 'PidLidWorkAddressPostalCode' => 'work zip', 'PidLidWorkAddressCountry' => 'work country', // 'PidLidWorkAddressPostOfficeBox' => 'work pobox', 'PidLidPostalAddressId' => 1, 'PidTagNickname' => 'nick', 'PidTagGender' => 2, 'PidTagSpouseName' => 'spouse', 'PidTagChildrensNames' => array('children', 'children2'), 'PidTagHomeTelephoneNumber' => 'home phone', 'PidTagBusinessTelephoneNumber' => 'work phone', 'PidTagHomeFaxNumber' => 'home fax', 'PidTagBusinessFaxNumber' => 'work fax', 'PidTagMobileTelephoneNumber' => 'mobile', 'PidTagPagerTelephoneNumber' => 'pager', 'PidTagCarTelephoneNumber' => 'car phone', 'PidTagOtherTelephoneNumber' => 'other phone', 'PidLidInstantMessagingAddress' => 'im gg', 'PidLidEmail1EmailAddress' => 'test@mail.ru', 'PidLidEmail2EmailAddress' => 'work@email.pl', 'PidLidEmail3EmailAddress' => 'other@email.pl', 'PidTagUserX509Certificate' => '1234567890', 'PidTagInitials' => 'initials', ); $result = $api->input($data); // $this->assertSame('a-b-c-d', $result['uid']); // $this->assertSame('20150420T141533Z', $result['rev']); // $this->assertSame('individual', $result['kind']); $this->assertSame('displname', $result['fn']); $this->assertSame('last', $result['n']['surname']); $this->assertSame('test', $result['n']['given']); $this->assertSame('middlename', $result['n']['additional']); $this->assertSame('prefx', $result['n']['prefix']); $this->assertSame('suff', $result['n']['suffix']); $this->assertSame('dsfsdfsdfsdf sdfsdfsdf sdfsdfsfd', $result['note']); $this->assertSame('free-busy url', $result['fburl']); $this->assertSame('title', $result['title']); $this->assertSame('Org', $result['group']['org'][0]); $this->assertSame('dept', $result['group']['org'][1]); $this->assertSame('profeion', $result['group']['role']); $this->assertSame('x-manager', $result['group']['related'][0]['parameters']['type']); $this->assertSame('manager name', $result['group']['related'][0]['text']); $this->assertSame('x-assistant', $result['group']['related'][1]['parameters']['type']); $this->assertSame('assist', $result['group']['related'][1]['text']); // $this->assertSame('', $result['group']['adr']['pobox']); $this->assertSame('office street', $result['group']['adr']['street']); $this->assertSame('office city', $result['group']['adr']['locality']); $this->assertSame('office state', $result['group']['adr']['region']); $this->assertSame('office zip', $result['group']['adr']['code']); $this->assertSame('office country', $result['group']['adr']['country']); $this->assertSame(array('website'), $result['url']); $this->assertSame('home', $result['adr'][0]['parameters']['type']); $this->assertSame(1, $result['adr'][0]['parameters']['pref']); $this->assertSame('home street', $result['adr'][0]['street']); $this->assertSame('home city', $result['adr'][0]['locality']); $this->assertSame('home state', $result['adr'][0]['region']); $this->assertSame('home zip', $result['adr'][0]['code']); $this->assertSame('home country', $result['adr'][0]['country']); $this->assertSame('work', $result['adr'][1]['parameters']['type']); $this->assertSame('work street', $result['adr'][1]['street']); $this->assertSame('work city', $result['adr'][1]['locality']); $this->assertSame('work state', $result['adr'][1]['region']); $this->assertSame('work zip', $result['adr'][1]['code']); $this->assertSame('work country', $result['adr'][1]['country']); $this->assertSame('nick', $result['nickname']); $this->assertSame('spouse', $result['related'][0]['parameters']['type']); $this->assertSame('spouse', $result['related'][0]['text']); $this->assertSame('child', $result['related'][1]['parameters']['type']); $this->assertSame('children', $result['related'][1]['text']); $this->assertSame('child', $result['related'][2]['parameters']['type']); $this->assertSame('children2', $result['related'][2]['text']); $this->assertSame('2015-03-30', $result['bday']); // ? $this->assertSame('2015-03-01', $result['anniversary']); // ? $this->assertSame('M', $result['gender']); $this->assertSame(array('im gg'), $result['impp']); $this->assertSame('home', $result['email'][0]['parameters']['type']); $this->assertSame('test@mail.ru', $result['email'][0]['text']); $this->assertSame('work', $result['email'][1]['parameters']['type']); $this->assertSame('work@email.pl', $result['email'][1]['text']); $this->assertSame('other', $result['email'][2]['parameters']['type']); $this->assertSame('other@email.pl', $result['email'][2]['text']); $this->assertRegExp('|^data:application/pkcs7-mime;base64,|', $result['key'][0]); // $this->assertRegExp('|^data:application/pgp-keys;base64,|', $result['key'][1]); // $this->assertRegExp('|^data:image/jpeg;base64,|', $result['photo']); $this->assertSame('MAPI:PidTagInitials', $result['x-custom'][0]['identifier']); $this->assertSame('initials', $result['x-custom'][0]['value']); $phones = array( 'home' => 'home phone', 'work' => 'work phone', 'faxhome' => 'home fax', 'faxwork' => 'work fax', 'cell' => 'mobile', 'pager' => 'pager', 'x-car' => 'car phone', 'textphone' => 'other phone', ); foreach ($result['tel'] as $tel) { $type = implode('', (array)$tel['parameters']['type']); $text = $tel['text']; if (!empty($phones[$type]) && $phones[$type] == $text) { unset($phones[$type]); } } $this->assertCount(8, $result['tel']); $this->assertCount(0, $phones); self::$original = $result; } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_contact; $data = array( 'id' => kolab_api_tests::mapi_uid('Contacts', false, 'a-b-c-d'), 'parent_id' => kolab_api_tests::folder_uid('Contacts', false), // 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('20150421T145607Z'), 'PidTagBirthday' => kolab_api_filter_mapistore_common::date_php2mapi('20150430', true), 'PidTagWeddingAnniversary' => kolab_api_filter_mapistore_common::date_php2mapi('20150401', true), 'PidTagDisplayName' => 'displname1', 'PidTagSurname' => 'last1', 'PidTagGivenName' => 'test1', 'PidTagMiddleName' => 'middlename1', 'PidTagDisplayNamePrefix' => 'prefx1', 'PidTagGeneration' => 'suff1', 'PidTagBody' => 'body1', 'PidLidFreeBusyLocation' => 'free-busy url1', 'PidTagTitle' => 'title1', 'PidTagCompanyName' => 'Org1', 'PidTagDepartmentName' => 'dept1', 'PidTagProfession' => 'profeion1', 'PidTagManagerName' => 'manager name1', 'PidTagAssistant' => 'assist1', 'PidTagPersonalHomePage' => 'website1', 'PidTagOtherAddressStreet' => 'office street1', 'PidTagOtherAddressCity' => 'office city1', 'PidTagOtherAddressStateOrProvince' => 'office state1', 'PidTagOtherAddressPostalCode' => 'office zip1', 'PidTagOtherAddressCountry' => 'office country1', 'PidTagHomeAddressStreet' => 'home street1', 'PidTagHomeAddressCity' => 'home city1', 'PidTagHomeAddressStateOrProvince' => 'home state1', 'PidTagHomeAddressPostalCode' => 'home zip1', 'PidTagHomeAddressCountry' => 'home country1', 'PidLidWorkAddressStreet' => 'work street1', 'PidLidWorkAddressCity' => 'work city1', 'PidLidWorkAddressState' => 'work state1', 'PidLidWorkAddressPostalCode' => 'work zip1', 'PidLidWorkAddressCountry' => 'work country1', 'PidTagNickname' => 'nick1', 'PidTagGender' => 1, 'PidTagSpouseName' => 'spouse1', 'PidTagChildrensNames' => array('children10', 'children20'), 'PidTagHomeTelephoneNumber' => 'home phone1', 'PidTagBusinessTelephoneNumber' => null, 'PidTagHomeFaxNumber' => 'home fax1', 'PidTagBusinessFaxNumber' => 'work fax1', 'PidTagMobileTelephoneNumber' => 'mobile1', 'PidTagPagerTelephoneNumber' => 'pager1', 'PidTagOtherTelephoneNumber' => 'other phone1', 'PidLidInstantMessagingAddress' => 'im gg1', 'PidLidEmail1EmailAddress' => 'test@mail.ru', 'PidLidEmail2EmailAddress' => 'work@email.pl', 'PidTagUserX509Certificate' => '12345678901', 'PidTagInitials' => 'initials1', 'PidNameKeywords' => array('work1'), ); $result = $api->input($data, self::$original); // $this->assertSame('a-b-c-d', $result['uid']); // $this->assertSame('20150420T141533Z', $result['rev']); // $this->assertSame('individual', $result['kind']); $this->assertSame('displname1', $result['fn']); $this->assertSame('last1', $result['n']['surname']); $this->assertSame('test1', $result['n']['given']); $this->assertSame('middlename1', $result['n']['additional']); $this->assertSame('prefx1', $result['n']['prefix']); $this->assertSame('suff1', $result['n']['suffix']); $this->assertSame('body1', $result['note']); $this->assertSame('free-busy url1', $result['fburl']); $this->assertSame('title1', $result['title']); $this->assertSame('Org1', $result['group']['org'][0]); $this->assertSame('dept1', $result['group']['org'][1]); $this->assertSame('profeion1', $result['group']['role']); $this->assertSame('x-manager', $result['group']['related'][0]['parameters']['type']); $this->assertSame('manager name1', $result['group']['related'][0]['text']); $this->assertSame('x-assistant', $result['group']['related'][1]['parameters']['type']); $this->assertSame('assist1', $result['group']['related'][1]['text']); $this->assertSame('office street1', $result['group']['adr']['street']); $this->assertSame('office city1', $result['group']['adr']['locality']); $this->assertSame('office state1', $result['group']['adr']['region']); $this->assertSame('office zip1', $result['group']['adr']['code']); $this->assertSame('office country1', $result['group']['adr']['country']); $this->assertSame(array('website1'), $result['url']); $this->assertSame('home', $result['adr'][0]['parameters']['type']); $this->assertSame('home street1', $result['adr'][0]['street']); $this->assertSame('home city1', $result['adr'][0]['locality']); $this->assertSame('home state1', $result['adr'][0]['region']); $this->assertSame('home zip1', $result['adr'][0]['code']); $this->assertSame('home country1', $result['adr'][0]['country']); $this->assertSame('work', $result['adr'][1]['parameters']['type']); $this->assertSame('work street1', $result['adr'][1]['street']); $this->assertSame('work city1', $result['adr'][1]['locality']); $this->assertSame('work state1', $result['adr'][1]['region']); $this->assertSame('work zip1', $result['adr'][1]['code']); $this->assertSame('work country1', $result['adr'][1]['country']); $this->assertSame('nick1', $result['nickname']); $this->assertSame('spouse', $result['related'][0]['parameters']['type']); $this->assertSame('spouse1', $result['related'][0]['text']); $this->assertSame('child', $result['related'][1]['parameters']['type']); $this->assertSame('children10', $result['related'][1]['text']); $this->assertSame('child', $result['related'][2]['parameters']['type']); $this->assertSame('children20', $result['related'][2]['text']); $this->assertSame('2015-04-30', $result['bday']); // ? $this->assertSame('2015-04-01', $result['anniversary']); // ? $this->assertSame('F', $result['gender']); $this->assertSame(array('im gg1'), $result['impp']); $this->assertSame('home', $result['email'][0]['parameters']['type']); $this->assertSame('test@mail.ru', $result['email'][0]['text']); $this->assertSame('work', $result['email'][1]['parameters']['type']); $this->assertSame('work@email.pl', $result['email'][1]['text']); $this->assertSame(null, $result['email'][2]); $this->assertRegExp('|^data:application/pkcs7-mime;base64,|', $result['key'][0]); // $this->assertRegExp('|^data:application/pgp-keys;base64,|', $result['key'][1]); // $this->assertRegExp('|^data:image/jpeg;base64,|', $result['photo']); $this->assertSame('MAPI:PidTagInitials', $result['x-custom'][0]['identifier']); $this->assertSame('initials1', $result['x-custom'][0]['value']); $this->assertSame(array('work1'), $result['categories']); $phones = array( 'home' => 'home phone1', 'faxhome' => 'home fax1', 'faxwork' => 'work fax1', 'cell' => 'mobile1', 'pager' => 'pager1', 'x-car' => 'car phone', 'textphone' => 'other phone1', ); foreach ($result['tel'] as $tel) { $type = implode('', (array)$tel['parameters']['type']); $text = $tel['text']; if (!empty($phones[$type]) && $phones[$type] == $text) { unset($phones[$type]); } } $this->assertCount(7, $result['tel']); $this->assertCount(0, $phones); // @TODO: updating some deep items (e.g. adr); } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_contact; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } /** * Test photo_attachment method */ function test_photo_attachment() { $contact = array(); $result = kolab_api_filter_mapistore_contact::photo_attachment($contact); $this->assertSame(null, $result); $contact['photo'] = base64_decode('R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7'); $result = kolab_api_filter_mapistore_contact::photo_attachment($contact); $this->assertSame(true, $result['is_photo']); $this->assertSame(54, $result['size']); $this->assertSame('ContactPicture.gif', $result['filename']); $this->assertSame('image/gif', $result['mimetype']); $this->assertSame($contact['photo'], $result['content']); $this->assertSame(kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID, $result['id']); } } diff --git a/tests/Unit/Input/Json/Contact.php b/tests/Unit/Input/Json/Contact.php index 96f8f76..0c12f03 100644 --- a/tests/Unit/Input/Json/Contact.php +++ b/tests/Unit/Input/Json/Contact.php @@ -1,384 +1,414 @@ input($data); } /** * Test expected exception in input method * * @expectedException kolab_api_exception * @expectedExceptionCode 422 */ function test_input_exception2() { $input = new kolab_api_input_json_contact; $data = 'test'; $input->input($data); } /** * Test input method (convert JSON to internal format) */ function test_input() { $input = new kolab_api_input_json_contact; $data = array( 'note' => 'note', 'categories' => array('test'), 'kind' => 'individual', 'rev' => '2015-04-21T00:00:00Z', 'bday' => '2014-01-01', 'anniversary' => '2014-02-01', 'fn' => 'name', 'nickname' => 'nick', 'n' => array( 'surname' => 'surname', 'given' => 'given', 'additional' => 'middle', 'prefix' => 'prefix', 'suffix' => 'suffix', ), 'title' => array('title'), 'group' => array( 'org' => array('Org', 'dept'), 'role' => 'profession', 'related' => array( array( 'text' => 'manager name', 'parameters' => array('type' => 'x-manager'), ), array( 'text' => 'assist', 'parameters' => array('type' => 'x-assistant'), ), ), 'adr' => array( 'street' => 'office street', 'locality' => 'office city', 'region' => 'office state', 'code' => 'office zip', 'country' => 'office country', // 'parameters' => '', // 'pobox' => '', // 'ext' => '', ), ), 'adr' => array( array( 'parameters' => array('type' => 'home'), 'street' => 'home street', 'locality' => 'home city', 'region' => 'home state', 'code' => 'home zip', 'country' => 'home country', ), array( 'parameters' => array('type' => 'work'), 'street' => 'work street', 'locality' => 'work city', 'region' => 'work state', 'code' => 'work zip', 'country' => 'work country', ), ), 'related' => array( array('text' => 'spouse', 'parameters' => array('type' => 'spouse')), array('text' => 'child1', 'parameters' => array('type' => 'child')), array('text' => 'child2', 'parameters' => array('type' => 'child')), ), 'photo' => 'data:image/jpeg;base64,cGhvdG8x', 'gender' => 'M', 'lang' => array('lang'), 'tel' => array( array( 'text' => 'home phone', 'parameters' => array('type' => 'home'), ), array( 'text' => 'work phone', 'parameters' => array('type' => 'work'), ), array( 'text' => 'home fax', 'parameters' => array('type' => array('fax', 'home')), ), array( 'text' => 'work fax', 'parameters' => array('type' => array('fax', 'work')), ), array( 'text' => 'mobile', 'parameters' => array('type' => 'cell'), ), array( 'text' => 'pager', 'parameters' => array('type' => 'pager'), ), array( 'text' => 'car phone', 'parameters' => array('type' => 'x-car'), ), array( 'text' => 'other phone', 'parameters' => array('type' => 'textphone'), ), ), 'impp' => array('impp'), 'email' => array( array( 'text' => 'test@mail.ru', 'parameters' => array('type' => 'home'), ), array( 'text' => 'work@email.pl', 'parameters' => array('type' => 'work'), ), 'other@email.pl', ), // 'geo' => array(), 'key' => array( 'data:application/pgp-keys;base64,' . base64_encode('1'), 'data:application/pkcs7-mime;base64,' . base64_encode('2'), ), // 'x-crypto' => array(), 'fburl' => 'freebusyurl', 'url' => array('url'), ); $input->input($data); $this->assertSame('note', $data['notes']); $this->assertSame(array('test'), $data['categories']); $this->assertSame('individual', $data['kind']); $this->assertSame('name', $data['name']); $this->assertSame('nick', $data['nickname']); $this->assertSame(array('title'), $data['jobtitle']); $this->assertSame('freebusyurl', $data['freebusyurl']); $this->assertSame('male', $data['gender']); $this->assertSame('photo1', $data['photo']); $this->assertSame(array('lang'), $data['lang']); $this->assertSame(array('impp'), $data['im']); $this->assertSame(array(array('url' => 'url')), $data['website']); // 'n' $this->assertSame('surname', $data['surname']); $this->assertSame('given', $data['firstname']); $this->assertSame('middle', $data['middlename']); $this->assertSame('prefix', $data['prefix']); $this->assertSame('suffix', $data['suffix']); // 'email' $this->assertSame('home', $data['email'][0]['type']); $this->assertSame('test@mail.ru', $data['email'][0]['address']); $this->assertSame('work', $data['email'][1]['type']); $this->assertSame('work@email.pl', $data['email'][1]['address']); $this->assertSame('other@email.pl', $data['email'][2]['address']); // 'tel' $this->assertSame('home', $data['phone'][0]['type']); $this->assertSame('home phone', $data['phone'][0]['number']); $this->assertSame('work', $data['phone'][1]['type']); $this->assertSame('work phone', $data['phone'][1]['number']); $this->assertSame('homefax', $data['phone'][2]['type']); $this->assertSame('home fax', $data['phone'][2]['number']); $this->assertSame('workfax', $data['phone'][3]['type']); $this->assertSame('work fax', $data['phone'][3]['number']); $this->assertSame('mobile', $data['phone'][4]['type']); $this->assertSame('mobile', $data['phone'][4]['number']); $this->assertSame('pager', $data['phone'][5]['type']); $this->assertSame('pager', $data['phone'][5]['number']); $this->assertSame('car', $data['phone'][6]['type']); $this->assertSame('car phone', $data['phone'][6]['number']); $this->assertSame('other', $data['phone'][7]['type']); $this->assertSame('other phone', $data['phone'][7]['number']); // 'related' $this->assertSame('spouse', $data['spouse']); $this->assertSame(array('child1', 'child2'), $data['children']); // 'group' $this->assertSame('Org', $data['organization']); $this->assertSame('dept', $data['department']); $this->assertSame('profession', $data['profession']); $this->assertSame('manager name', $data['manager']); $this->assertSame('assist', $data['assistant']); $this->assertSame('office street', $data['address'][0]['street']); $this->assertSame('office city', $data['address'][0]['locality']); $this->assertSame('office state', $data['address'][0]['region']); $this->assertSame('office zip', $data['address'][0]['code']); $this->assertSame('office country', $data['address'][0]['country']); $this->assertSame('office', $data['address'][0]['type']); // $this->assertSame('', $data['address'][0]['parameters']); // $this->assertSame('', $data['address'][0]['pobox']); // $this->assertSame('', $data['address'][0]['ext']); // 'adr' $this->assertSame('home', $data['address'][1]['type']); $this->assertSame('home street', $data['address'][1]['street']); $this->assertSame('home city', $data['address'][1]['locality']); $this->assertSame('home state', $data['address'][1]['region']); $this->assertSame('home zip', $data['address'][1]['code']); $this->assertSame('home country', $data['address'][1]['country']); $this->assertSame('work', $data['address'][2]['type']); $this->assertSame('work street', $data['address'][2]['street']); $this->assertSame('work city', $data['address'][2]['locality']); $this->assertSame('work state', $data['address'][2]['region']); $this->assertSame('work zip', $data['address'][2]['code']); $this->assertSame('work country', $data['address'][2]['country']); // 'key' $this->assertSame('1', $data['pgppublickey']); $this->assertSame('2', $data['pkcs7publickey']); // $this->assertSame(kolab_api_input_json::to_datetime('2015-04-20T14:22:18Z')->format('c'), $data['created']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2015-04-21T00:00:00Z')->format('c'), $data['changed']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-01-01')->format('c'), $data['birthday']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-02-01')->format('c'), $data['anniversary']->format('c')); // for test_input2() below self::$original = $data; } /** * Test input method with merge */ function test_input2() { $input = new kolab_api_input_json_contact; $data = array( 'note' => 'note1', 'categories' => array('test1'), 'kind' => 'individual', // 'rev' => '2015-04-21T00:00:00Z', 'bday' => '2014-01-11', 'anniversary' => '2014-02-11', 'fn' => 'name1', 'nickname' => 'nick1', 'n' => array( 'surname' => 'surname1', 'given' => 'given1', 'additional' => 'middle1', 'prefix' => null, 'suffix' => 'suffix1', ), 'title' => array('title1'), 'group' => array( 'org' => array('Org1', 'dept1'), 'role' => 'profession1', 'adr' => array( 'street' => 'office street1', 'locality' => 'office city1', 'region' => null, 'code' => 'office zip1', 'country' => 'office country1', ), ), 'adr' => array( array( // @TODO: libkolab plugin does not support 'pref' yet 'parameters' => array('type' => 'home', 'pref' => 1), 'street' => 'home street1', 'locality' => 'home city1', 'region' => 'home state1', 'code' => null, 'country' => 'home country1', ), ), 'related' => array( array('text' => 'spouse1', 'parameters' => array('type' => 'spouse')), array('text' => 'child11', 'parameters' => array('type' => 'child')), array('text' => 'child21', 'parameters' => array('type' => 'child')), ), 'photo' => 'data:image/jpeg;base64,cGhvdG8x', 'gender' => 'F', 'lang' => array('lang1'), 'tel' => array( array( 'text' => 'home phone1', 'parameters' => array('type' => 'home'), ), ), 'impp' => array('impp1'), 'email' => array('other@email.pl'), // 'geo' => array(), 'key' => array( 'data:application/pgp-keys;base64,' . base64_encode('1'), ), // 'x-crypto' => array(), 'fburl' => 'freebusyurl1', 'url' => array('url1'), ); $input->input($data, self::$original); $this->assertSame('note1', $data['notes']); $this->assertSame(array('test1'), $data['categories']); $this->assertSame('individual', $data['kind']); $this->assertSame('name1', $data['name']); $this->assertSame('nick1', $data['nickname']); $this->assertSame(array('title1'), $data['jobtitle']); $this->assertSame('freebusyurl1', $data['freebusyurl']); $this->assertSame('female', $data['gender']); $this->assertSame('photo1', $data['photo']); $this->assertSame(array('lang1'), $data['lang']); $this->assertSame(array('impp1'), $data['im']); $this->assertSame(array(array('url' => 'url1')), $data['website']); // 'n' $this->assertSame('surname1', $data['surname']); $this->assertSame('given1', $data['firstname']); $this->assertSame('middle1', $data['middlename']); $this->assertSame(null, $data['prefix']); $this->assertSame('suffix1', $data['suffix']); // 'email' $this->assertSame('other@email.pl', $data['email'][0]['address']); $this->assertCount(1, $data['email']); // 'tel' $this->assertSame('home', $data['phone'][0]['type']); $this->assertSame('home phone1', $data['phone'][0]['number']); $this->assertCount(1, $data['phone']); // 'related' $this->assertSame('spouse1', $data['spouse']); $this->assertSame(array('child11', 'child21'), $data['children']); // 'group' $this->assertSame('Org1', $data['organization']); $this->assertSame('dept1', $data['department']); $this->assertSame('profession1', $data['profession']); $this->assertSame(null, $data['manager']); $this->assertSame(null, $data['assistant']); $this->assertSame('office', $data['address'][0]['type']); $this->assertSame('office street1', $data['address'][0]['street']); $this->assertSame('office city1', $data['address'][0]['locality']); $this->assertSame(null, $data['address'][0]['region']); $this->assertSame('office zip1', $data['address'][0]['code']); $this->assertSame('office country1', $data['address'][0]['country']); // 'adr' $this->assertSame('home', $data['address'][1]['type']); $this->assertSame('home street1', $data['address'][1]['street']); $this->assertSame('home city1', $data['address'][1]['locality']); $this->assertSame('home state1', $data['address'][1]['region']); $this->assertSame(null, $data['address'][1]['code']); $this->assertSame('home country1', $data['address'][1]['country']); $this->assertCount(2, $data['address']); // 'key' $this->assertSame('1', $data['pgppublickey']); $this->assertSame(null, $data['pkcs7publickey']); $this->assertSame(kolab_api_input_json::to_datetime('2014-01-11')->format('c'), $data['birthday']->format('c')); $this->assertSame(kolab_api_input_json::to_datetime('2014-02-11')->format('c'), $data['anniversary']->format('c')); } + + /** + * Test input method (convert JSON to internal format) for distribution-lists + */ + function test_input_distlist() + { + $input = new kolab_api_input_json_contact; + $data = array( + 'uid' => 'i-j-k-l', + 'kind' => 'group', + 'fn' => 'test group', + 'member' => array( + 'urn:uuid:a-b-c-d', + 'urn:uuid:e-f-g-h', + 'mailto:John%20Doe%3Cjdoe%40example.com%3E', + ), + ); + + $input->input($data); + + $this->assertSame('group', $data['kind']); + $this->assertSame('test group', $data['name']); + $this->assertSame('a-b-c-d', $data['member'][0]['uid']); + $this->assertSame(null, $data['member'][0]['email']); + $this->assertSame('e-f-g-h', $data['member'][1]['uid']); + $this->assertSame(null, $data['member'][1]['email']); + $this->assertSame(null, $data['member'][2]['uid']); + $this->assertSame('jdoe@example.com', $data['member'][2]['email']); + $this->assertSame('John Doe', $data['member'][2]['name']); + } } diff --git a/tests/Unit/Output/Json/Contact.php b/tests/Unit/Output/Json/Contact.php index e2d03ea..589d0d4 100644 --- a/tests/Unit/Output/Json/Contact.php +++ b/tests/Unit/Output/Json/Contact.php @@ -1,100 +1,119 @@ element($object); $this->assertSame('a-b-c-d', $result['uid']); // $this->assertSame('3.1.0', $result['x-kolab-version']); // $this->assertSame('20150420T141533Z', $result['rev']); $this->assertSame('individual', $result['kind']); $this->assertSame('displname', $result['fn']); $this->assertSame('last', $result['n']['surname']); $this->assertSame('test', $result['n']['given']); $this->assertSame('middlename', $result['n']['additional']); $this->assertSame('prefx', $result['n']['prefix']); $this->assertSame('suff', $result['n']['suffix']); $this->assertSame('dsfsdfsdfsdf sdfsdfsdf sdfsdfsfd', $result['note']); $this->assertSame('free-busy url', $result['fburl']); $this->assertSame(array('title'), $result['title']); $this->assertSame('Org', $result['group']['org'][0]); $this->assertSame('dept', $result['group']['org'][1]); $this->assertSame('profeion', $result['group']['role']); $this->assertSame('x-manager', $result['group']['related'][0]['parameters']['type']); $this->assertSame('manager name', $result['group']['related'][0]['text']); $this->assertSame('x-assistant', $result['group']['related'][1]['parameters']['type']); $this->assertSame('assist', $result['group']['related'][1]['text']); // $this->assertSame('', $result['group']['adr']['parameters']); // $this->assertSame('', $result['group']['adr']['pobox']); // $this->assertSame('', $result['group']['adr']['ext']); $this->assertSame('office street', $result['group']['adr']['street']); $this->assertSame('office city', $result['group']['adr']['locality']); $this->assertSame('office state', $result['group']['adr']['region']); $this->assertSame('office zip', $result['group']['adr']['code']); $this->assertSame('office country', $result['group']['adr']['country']); $this->assertSame('website', $result['url'][0]); $this->assertSame('home', $result['adr'][0]['parameters']['type']); $this->assertSame('home street', $result['adr'][0]['street']); $this->assertSame('home city', $result['adr'][0]['locality']); $this->assertSame('home state', $result['adr'][0]['region']); $this->assertSame('home zip', $result['adr'][0]['code']); $this->assertSame('home country', $result['adr'][0]['country']); $this->assertSame('work', $result['adr'][1]['parameters']['type']); $this->assertSame(1, $result['adr'][1]['parameters']['pref']); $this->assertSame('work street', $result['adr'][1]['street']); $this->assertSame('work city', $result['adr'][1]['locality']); $this->assertSame('work state', $result['adr'][1]['region']); $this->assertSame('work zip', $result['adr'][1]['code']); $this->assertSame('work country', $result['adr'][1]['country']); $this->assertSame('nick', $result['nickname']); $this->assertSame('spouse', $result['related'][0]['parameters']['type']); $this->assertSame('spouse', $result['related'][0]['text']); $this->assertSame('child', $result['related'][1]['parameters']['type']); $this->assertSame('children', $result['related'][1]['text']); $this->assertSame('child', $result['related'][2]['parameters']['type']); $this->assertSame('children2', $result['related'][2]['text']); $this->assertSame('20150330', $result['bday']); // ? $this->assertSame('20150301', $result['anniversary']); // ? $this->assertRegExp('|^data:image/jpeg;base64,|', $result['photo']); $this->assertSame('M', $result['gender']); $this->assertSame('home', $result['tel'][0]['parameters']['type']); $this->assertSame('home phone', $result['tel'][0]['text']); $this->assertSame('work', $result['tel'][1]['parameters']['type']); $this->assertSame('work phone', $result['tel'][1]['text']); $this->assertSame('fax', $result['tel'][2]['parameters']['type'][0]); $this->assertSame('home', $result['tel'][2]['parameters']['type'][1]); $this->assertSame('home fax', $result['tel'][2]['text']); $this->assertSame('fax', $result['tel'][3]['parameters']['type'][0]); $this->assertSame('work', $result['tel'][3]['parameters']['type'][1]); $this->assertSame('work fax', $result['tel'][3]['text']); $this->assertSame('cell', $result['tel'][4]['parameters']['type']); $this->assertSame('mobile', $result['tel'][4]['text']); $this->assertSame('pager', $result['tel'][5]['parameters']['type']); $this->assertSame('pager', $result['tel'][5]['text']); $this->assertSame('x-car', $result['tel'][6]['parameters']['type']); $this->assertSame('car phone', $result['tel'][6]['text']); $this->assertSame('textphone', $result['tel'][7]['parameters']['type']); $this->assertSame('other phone', $result['tel'][7]['text']); $this->assertSame('im gg', $result['impp'][0]); $this->assertSame('home', $result['email'][0]['parameters']['type']); $this->assertSame('test@mail.ru', $result['email'][0]['text']); $this->assertSame('work', $result['email'][1]['parameters']['type']); $this->assertSame('work@email.pl', $result['email'][1]['text']); $this->assertSame('other@email.pl', $result['email'][2]); $this->assertRegExp('|^data:application/pgp-keys;base64,|', $result['key'][0]); $this->assertRegExp('|^data:application/pkcs7-mime;base64,|', $result['key'][1]); } + + /** + * Test element method (Internal to JSON conversion) for distribution-lists + */ + function test_element_distlist() + { + $output = kolab_api_tests::get_output_class('json', 'contact'); + $object = kolab_api_tests::get_data('i-j-k-l', 'Contacts', 'contact', null, $context); + $result = $output->element($object); + + $this->assertSame('i-j-k-l', $result['uid']); +// $this->assertSame('3.1.0', $result['x-kolab-version']); +// $this->assertSame('20150420T141533Z', $result['rev']); + $this->assertSame('group', $result['kind']); + $this->assertSame('test group', $result['fn']); + $this->assertSame('urn:uuid:a-b-c-d', $result['member'][0]); + $this->assertSame('urn:uuid:e-f-g-h', $result['member'][1]); + $this->assertSame('mailto:John%20Doe%3Cjdoe%40example.com%3E', $result['member'][2]); + } } diff --git a/tests/data/contact/i-j-k-l b/tests/data/contact/i-j-k-l new file mode 100644 index 0000000..6a1c637 --- /dev/null +++ b/tests/data/contact/i-j-k-l @@ -0,0 +1,63 @@ +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_1c09d772eb24d828039d1466d151855c" +From: mark.german@example.org +To: mark.german@example.org +Date: Thu, 27 Aug 2015 10:51:01 +0200 +X-Kolab-Type: application/x-vnd.kolab.distribution-list +X-Kolab-Mime-Version: 3.0 +Subject: i-j-k-l +User-Agent: Kolab 3.1/Roundcube 1.2-git + +--=_1c09d772eb24d828039d1466d151855c +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=ISO-8859-1 + +This is a Kolab Groupware object. To view this object you will need an emai= +l client that understands the Kolab Groupware format. For a list of such em= +ail clients please visit http://www.kolab.org/ + + +--=_1c09d772eb24d828039d1466d151855c +Content-Transfer-Encoding: 8bit +Content-Type: application/vcard+xml; charset=UTF-8; + name=kolab.xml +Content-Disposition: attachment; + filename=kolab.xml; + + + + + + + urn:uuid:i-j-k-l + + + 3.1.0 + + + Roundcube-libkolab-1.1 Libkolabxml-1.1 + + + 20150827T085101Z + + + group + + + test group + + + urn:uuid:a-b-c-d + + + urn:uuid:e-f-g-h + + + mailto:John%20Doe%3cjdoe%40example.com%3e + + + + + +--=_1c09d772eb24d828039d1466d151855c-- diff --git a/tests/data/data.json b/tests/data/data.json index 22fbdd6..801a9f4 100644 --- a/tests/data/data.json +++ b/tests/data/data.json @@ -1,72 +1,72 @@ { "folders": { "INBOX": { "type": "mail.inbox", "items": ["1","2","5","6","7"] }, "Trash": { "type": "mail.wastebasket", "items": [] }, "Drafts": { "type": "mail.drafts" }, "Sent": { "type": "mail.sentitems" }, "Junk": { "type": "mail.junkemail" }, "Calendar": { "type": "event.default", "items": ["100-100-100-100","101-101-101-101"] }, "Calendar/Personal Calendar": { "type": "event" }, "Contacts": { "type": "contact.default", - "items": ["a-b-c-d","e-f-g-h"] + "items": ["a-b-c-d","e-f-g-h","i-j-k-l"] }, "Files": { "type": "file.default" }, "Files2": { "type": "file" }, "Notes": { "type": "note.default", "items": ["1-1-1-1","2-2-2-2"] }, "Tasks": { "type": "task.default", "items":["10-10-10-10","20-20-20-20"] }, "Configuration": { "type": "configuration.default", "items": ["98-98-98-98","99-99-99-99"] }, "Mail-Test": { "type": "mail" }, "Mail-Test2": { "type": "mail" } }, "tags": { "tag1": { "members": ["1", "10-10-10-10", "1-1-1-1", "a-b-c-d", "100-100-100-100"] }, "tag2": { } } } diff --git a/tests/lib/kolab_api_backend.php b/tests/lib/kolab_api_backend.php index 8f1c756..b417b3d 100644 --- a/tests/lib/kolab_api_backend.php +++ b/tests/lib/kolab_api_backend.php @@ -1,1034 +1,1034 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_backend { /** * Singleton instace of kolab_api_backend * * @var kolab_api_backend */ static protected $instance; public $delimiter = '/'; public $username = 'user@example.org'; public $storage; public $user; public $db = array(); protected $folder = array(); protected $data = array(); /** * This implements the 'singleton' design pattern * * @return kolab_api_backend The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new kolab_api_backend; self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $api = kolab_api::get_instance(); $db_file = $api->config->get('temp_dir') . '/tests.db'; if (file_exists($db_file)) { $db = file_get_contents($db_file); $this->db = unserialize($db); } $json = file_get_contents(__DIR__ . '/../data/data.json'); $this->data = json_decode($json, true); $this->folders = $this->parse_folders_list($this->data['folders']); if (!array_key_exists('tags', $this->db)) { $this->db['tags'] = $this->data['tags']; } $this->user = new kolab_api_user; $this->storage = $this; } /** * Authenticate a user * * @param string Username * @param string Password * * @return bool */ public function authenticate($username, $password) { return true; } /** * Get list of folders * * @param string $type Folder type * * @return array|bool List of folders, False on backend failure */ public function folders_list($type = null) { return array_values($this->folders); } /** * Returns folder type * * @param string $uid Folder unique identifier * @param string $with_suffix Enable to not remove the subtype * * @return string Folder type */ public function folder_type($uid, $with_suffix = false) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $type = $folder['type'] ?: 'mail'; if (!$with_suffix) { list($type, ) = explode('.', $type); } return $type; } /** * Returns objects in a folder * * @param string $uid Folder unique identifier * * @return array Objects (of type kolab_api_mail or array) * @throws kolab_api_exception */ public function objects_list($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $result = array(); $is_mail = empty($folder['type']) || preg_match('/^mail/', $folder['type']); foreach ((array) $folder['items'] as $id) { $object = $this->object_get($uid, $id); if ($is_mail) { $object = new kolab_api_message($object->headers, array('is_header' => true)); } $result[] = $object; } return $result; } /** * Counts objects in a folder * * @param string $uid Folder unique identifier * * @return int Objects count * @throws kolab_api_exception */ public function objects_count($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } return count($folder['items']); } /** * Delete objects in a folder * * @param string $uid Folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_delete($uid, $set) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($set === '*') { foreach ((array) $this->folders[$uid]['items'] as $i) { unset($this->db['messages'][$i]); } $this->folders[$uid]['items'] = array(); $this->db['items'][$uid] = array(); } else { $this->folders[$uid]['items'] = array_values(array_diff($this->folders[$uid]['items'], $set)); foreach ($set as $i) { unset($this->db['items'][$uid][$i]); unset($this->db['messages'][$i]); } } $this->db['folders'][$uid]['items'] = $this->folders[$uid]['items']; $this->save_db(); } /** * Move objects into another folder * * @param string $uid Folder unique identifier * @param string $target_uid Target folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_move($uid, $target_uid, $set) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $target = $this->folders[$target_uid]; if (!$target) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($set === "*") { $set = $this->folders[$uid]['items']; } // @TODO: we should check if all objects from the set exist $diff = array_values(array_diff($this->folders[$uid]['items'], $set)); $this->folders[$uid]['items'] = $diff; $this->db['folders'][$uid]['items'] = $diff; $diff = array_values(array_merge((array) $this->folders[$target_uid]['items'], $set)); $this->folders[$target_uid]['items'] = $diff; $this->db['folders'][$target_uid]['items'] = $diff; foreach ($set as $i) { if ($this->db['items'][$uid][$i]) { $this->db['items'][$target_uid][$i] = $this->db['items'][$uid][$i]; unset($this->db['items'][$uid][$i]); } } $this->save_db(); } /** * Get object data * * @param string $folder_uid Folder unique identifier * @param string $uid Object identifier * * @return kolab_api_mail|array Object data * @throws kolab_api_exception */ public function object_get($folder_uid, $uid) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if (!in_array($uid, (array) $folder['items'])) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($data = $this->db['items'][$folder_uid][$uid]) { return $data; } list($type,) = explode('.', $folder['type']); $file = $this->get_file_content($uid, $type); if (empty($file)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // get message content and parse it $file = str_replace("\r?\n", "\r\n", $file); $params = array('uid' => $uid, 'folder' => $folder_uid); $object = new kolab_api_message($file, $params); // get assigned tag-relations $tags = array(); foreach ($this->db['tags'] as $tag_name => $tag) { if (in_array($uid, (array) $tag['members'])) { $tags[] = $tag_name; } } if ($type != 'mail') { $object = $object->to_array($type); $object['categories'] = array_unique(array_merge($tags, (array) $object['categories'])); } else { $object = new kolab_api_message($object); $object->set_categories($tags); } return $object; } /** * Create an object * * @param string $folder_uid Folder unique identifier * @param string $data Object data * @param string $type Object type * * @throws kolab_api_exception */ public function object_create($folder_uid, $data, $type) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /* if (strpos($folder['type'], $type) !== 0) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } */ - $uid = microtime(true); + $uid = str_replace('.', '', microtime(true)); if (is_array($data)) { $categories = $data['categories']; $data['uid'] = $uid; $this->db['items'][$folder_uid][$uid] = $data; } else { $categories = $data->categories; $uid = $data->save($folder['fullpath']); } if (!empty($categories)) { foreach ($categories as $cat) { if (!$this->db['tags'][$cat]) { $this->db['tags'][$cat] = array(); } if (!in_array($uid, (array) $this->db['tags'][$cat]['members'])) { $this->db['tags'][$cat]['members'][] = $uid; } } } $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); return $uid; } /** * Update an object * * @param string $folder_uid Folder unique identifier * @param string $data Object data * @param string $type Object type * * @throws kolab_api_exception */ public function object_update($folder_uid, $data, $type) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /* if (strpos($folder['type'], $type) !== 0) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } */ // kolab object if (is_array($data)) { $uid = $data['uid']; $categories = $data['categories']; // remove _formatobj which is problematic in serialize/unserialize unset($data['_formatobj']); $this->db['items'][$folder_uid][$uid] = $data; } // email message else { $old_uid = $data->uid; $categories = $data->categories; $uid = $data->save($folder['fullpath']); $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; } // remove old tag assignments foreach ($this->db['tags'] as $tag_name => $tag) { if (($idx = array_search($old_uid ?: $uid, (array) $this->db['tags'][$tag_name]['members'])) !== false) { unset($this->db['tags'][$tag_name]['members'][$idx]); } } // assign new tags foreach ((array) $categories as $tag) { if (!$this->db['tags'][$tag]) { $this->db['tags'][$tag] = array(); } $this->db['tags'][$tag]['members'][] = $uid; } $this->save_db(); return $uid; } /** * Get attachment body * * @param mixed $object Object data (from self::object_get()) * @param string $part_id Attachment part identifier * @param mixed $mode NULL to return a string, -1 to print body * or file pointer to save the body into * * @return string Attachment body if $mode=null * @throws kolab_api_exception */ public function attachment_get($object, $part_id, $mode = null) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } $body = $object->get_part_body($part_id); if (!$mode) { return $body; } else if ($mode === -1) { echo $body; } } /** * Delete an attachment from the message * * @param mixed $object Object data (from self::object_get()) * @param string $id Attachment identifier * * @return string Message/object UID * @throws kolab_api_exception */ public function attachment_delete($object, $id) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; $key = $msg_uid . ":" . $part_id; // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } if ($object->get_part_body($id) === null) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $uid = $object->attachment_delete($id); // change UID only for mail messages if (!is_numeric($msg_uid)) { $this->db['messages'][$msg_uid] = $this->db['messages'][$uid]; unset($this->db['messages'][$uid]); $this->save_db(); $uid = $msg_uid; } if ($msg_uid != $uid) { $folder_uid = $object->folder; $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); } return $uid; } /** * Create an attachment and add to a message/object * * @param mixed $object Object data (from self::object_get()) * @param rcube_message_part $attachment Attachment data * * @return string Message/object UID * @throws kolab_api_exception */ public function attachment_create($object, $attachment) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } $uid = $object->attachment_add($attachment); $folder_uid = $object->folder; // change UID only for mail messages if (!is_numeric($msg_uid)) { $this->db['messages'][$msg_uid] = $this->db['messages'][$uid]; unset($this->db['messages'][$uid]); $params = array('uid' => $msg_uid, 'folder' => $folder_uid); $object = new kolab_api_message(base64_decode($this->db['messages'][$msg_uid]), $params); $object = $object->to_array($this->folders[$folder_uid]['type']); // $object['categories'] = $tags; unset($object['_formatobj']); $this->db['items'][$folder_uid][$msg_uid] = $object; $this->save_db(); $uid = $msg_uid; } if ($msg_uid != $uid) { $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); } return $uid; } /** * Update an attachment in a message/object * * @param mixed $object Object data (from self::object_get()) * @param rcube_message_part $attachment Attachment data * * @return string Message/object UID * @throws kolab_api_exception */ public function attachment_update($object, $attachment) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } $uid = $object->attachment_update($attachment); $folder_uid = $object->folder; // change UID only for mail messages if (!is_numeric($msg_uid)) { $this->db['messages'][$msg_uid] = $this->db['messages'][$uid]; unset($this->db['messages'][$uid]); $params = array('uid' => $msg_uid, 'folder' => $folder_uid); $object = new kolab_api_message(base64_decode($this->db['messages'][$msg_uid]), $params); $object = $object->to_array($this->folders[$folder_uid]['type']); // $object['categories'] = $tags; unset($object['_formatobj']); $this->db['items'][$folder_uid][$msg_uid] = $object; $this->save_db(); $uid = $msg_uid; } if ($msg_uid != $uid) { $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); } return $uid; } /** * Creates a folder * * @param string $name Folder name (UTF-8) * @param string $parent Parent folder identifier * @param string $type Folder type * * @return bool Folder identifier on success */ public function folder_create($name, $parent = null, $type = null) { $folder = $name; if ($parent) { $parent_folder = $this->folders[$parent]; if (!$parent_folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $folder = $parent_folder['fullpath'] . $this->delimiter . $folder; } $uid = kolab_api_tests::folder_uid($folder, false); // check if folder exists if ($this->folders[$uid]) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $this->folders[$uid] = array( 'name' => $name, 'fullpath' => $folder, 'parent' => $parent ? kolab_api_tests::folder_uid($parent, false) : null, 'uid' => $uid, 'type' => $type ? $type : 'mail', ); $this->db['folders'][$uid] = $this->folders[$uid]; $this->save_db(); return $uid; } /** * Updates a folder * * @param string $uid Folder identifier * @param array $updates Updates (array with keys type, subscribed, active) * * @throws kolab_api_exception */ public function folder_update($uid, $updates) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } foreach ($updates as $idx => $value) { $this->db['folders'][$uid][$idx] = $value; $this->folders[$uid][$idx] = $value; } $this->save_db(); } /** * Renames/moves a folder * * @param string $old_name Folder name (UTF8) * @param string $new_name New folder name (UTF8) * * @throws kolab_api_exception */ public function folder_rename($old_name, $new_name) { $old_uid = kolab_api_tests::folder_uid($old_name, false); $new_uid = kolab_api_tests::folder_uid($new_name, false); $folder = $this->folders[$old_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($this->folders[$new_uid]) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $path = explode($this->delimiter, $new_name); $folder['fullpath'] = $new_name; $folder['name'] = array_pop($path); unset($this->folders[$old_uid]); $this->folders[$new_uid] = $folder; $this->db['folders'][$new_uid] = $folder; $this->db['deleted'][] = $old_uid; $this->save_db(); } /** * Deletes folder * * @param string $uid Folder UID * * @return bool True on success, False on failure * @throws kolab_api_exception */ public function folder_delete($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } unset($this->folders[$uid]); $this->db['deleted'][] = $uid; $this->save_db(); } /** * Folder info * * @param string $uid Folder UID * * @return array Folder information * @throws kolab_api_exception */ public function folder_info($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // some info is not very interesting here ;) unset($folder['items']); return $folder; } /** * Returns IMAP folder name with full path * * @param string $uid Folder identifier * * @return string Folder full path (UTF-8) */ public function folder_uid2path($uid) { if ($uid === null || $uid === '') { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } return $folder['fullpath']; } /** * Parse folders list into API format */ protected function parse_folders_list($list) { $folders = array(); foreach ($list as $path => $folder) { $uid = kolab_api_tests::folder_uid($path, false); if (!empty($this->db['deleted']) && in_array($uid, $this->db['deleted'])) { continue; } if (strpos($path, $this->delimiter)) { $list = explode($this->delimiter, $path); $name = array_pop($list); $parent = implode($this->delimiter, $list); $parent_id = kolab_api_tests::folder_uid($parent, false); } else { $parent_id = null; $name = $path; } $data = array( 'name' => $name, 'fullpath' => $path, 'parent' => $parent_id, 'uid' => $uid, ); if (!empty($this->db['folders']) && !empty($this->db['folders'][$uid])) { $data = array_merge($data, $this->db['folders'][$uid]); } $folders[$uid] = array_merge($folder, $data); } foreach ((array) $this->db['folders'] as $uid => $folder) { if (!$folders[$uid]) { $folders[$uid] = $folder; } } // sort folders uasort($folders, array($this, 'sort_folder_comparator')); return $folders; } /** * Callback for uasort() that implements correct * locale-aware case-sensitive sorting */ protected function sort_folder_comparator($str1, $str2) { $path1 = explode($this->delimiter, $str1['fullpath']); $path2 = explode($this->delimiter, $str2['fullpath']); foreach ($path1 as $idx => $folder1) { $folder2 = $path2[$idx]; if ($folder1 === $folder2) { continue; } return strcoll($folder1, $folder2); } } /** * Save current database state */ public function save_db() { $api = kolab_api::get_instance(); $db_file = $api->config->get('temp_dir') . '/tests.db'; $db = serialize($this->db); file_put_contents($db_file, $db); } /** * Wrapper for rcube_imap::set_flag() */ public function set_flag($uid, $flag) { $flag = strtoupper($flag); $folder_uid = $this->folder_uid($folder); $flags = (array) $this->db['flags'][$uid]; if (strpos($flag, 'UN') === 0) { $flag = substr($flag, 3); $flags = array_values(array_diff($flags, array($flag))); } else { $flags[] = $flag; $flags = array_unique($flags); } $this->db['flags'][$uid] = $flags; $this->save_db(); return true; } /** * Wrapper for rcube_imap::save_message() */ public function save_message($folder, $streams) { $folder_uid = $this->folder_uid($folder); $uid = '3' . count($this->db['messages']) . preg_replace('/^[0-9]+\./', '', microtime(true)); $content = ''; foreach ($streams as $stream) { rewind($stream); $content .= stream_get_contents($stream); } $this->db['messages'][$uid] = base64_encode($content); $this->save_db(); return $uid; } /** * Wrapper for rcube_imap::delete_message() */ public function delete_message($uid, $folder) { $folder_uid = $this->folder_uid($folder); $this->folders[$folder_uid]['items'] = array_values(array_diff((array)$this->folders[$folder_uid]['items'], array($uid))); unset($this->db['items'][$folder_uid][$uid]); unset($this->db['messages'][$uid]); $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); return true; } /** * Wrapper for rcube_imap::get_raw_body */ public function get_raw_body($uid, $fp = null, $part = null) { $file = $this->get_file_content($uid); $file = explode("\r\n\r\n", $file, 2); if (stripos($part, 'TEXT') !== false) { $body = $file[1]; if (preg_match('/^([0-9]+)/', $part, $m)) { if (preg_match('/boundary="?([^"]+)"?/', $file[0], $mm)) { $parts = explode('--' . $mm[1], $body); $parts = explode("\r\n\r\n", $parts[$m[1]], 2); $body = $parts[1]; } else { $body = ''; } } } if ($fp) { fwrite($fp, $body); return true; } else { return $body; } } /** * Wrapper for rcube_imap::get_raw_headers */ public function get_raw_headers($uid) { $file = $this->get_file_content($uid); $file = explode("\r\n\r\n", $file, 2); return $file[0]; } /** * Wrapper for rcube_imap::set_folder */ public function set_folder($folder) { // do nothing } /** * Find folder UID by its name */ protected function folder_uid($name) { foreach ($this->folders as $uid => $folder) { if ($folder['fullpath'] == $name) { return $uid; } } } /** * Get sample message from tests/data dir */ protected function get_file_content($uid, $type = null) { if ($file = $this->db['messages'][$uid]) { $file = base64_decode($file); } else { if (empty($type)) { foreach (array('mail', 'event', 'task', 'note', 'contact') as $t) { $file = __DIR__ . '/../data/' . $t . '/' . $uid; if (file_exists($file)) { $type = $t; break; } } } $file = file_get_contents(__DIR__ . '/../data/' . $type . '/' . $uid); } return $file; } } /** * Dummy class imitating rcube_user */ class kolab_api_user { public function get_username($type) { $api = kolab_api_backend::get_instance(); list($local, $domain) = explode('@', $api->username); if ($type == 'domain') { return $domain; } else if ($type == 'local') { return $local; } return $api->username; } public function get_user_id() { return 10; } public function get_identity() { return array( 'email' => 'user@example.org', 'name' => 'Test User', ); } } diff --git a/tests/lib/kolab_api_tests.php b/tests/lib/kolab_api_tests.php index 01d4041..4325287 100644 --- a/tests/lib/kolab_api_tests.php +++ b/tests/lib/kolab_api_tests.php @@ -1,420 +1,420 @@ | | | | 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 | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_tests { static $items_map; static $folders_map; static $db; /** * Reset backend state */ public static function reset_backend() { $rcube = rcube::get_instance(); $temp_dir = $rcube->config->get('temp_dir'); $filename = $temp_dir . '/tests.db'; if (file_exists($filename)) { unlink($filename); } $username = $rcube->config->get('tests_username'); $password = $rcube->config->get('tests_password'); if (!$username) { return; } $authenticated = self::login($username, $password); if (!$authenticated) { throw new Exception("IMAP login failed for user $username"); } // get all existing folders $imap = $rcube->get_storage(); $old_folders = $imap->list_folders('', '*'); $old_subscribed = $imap->list_folders_subscribed('', '*'); // get configured folders $json = file_get_contents(__DIR__ . '/../data/data.json'); $data = json_decode($json, true); $items = array(); $uids = array(); // initialize/update content in existing folders // create configured folders if they do not exists foreach ($data['folders'] as $folder_name => $folder) { if (($idx = array_search($folder_name, $old_folders)) !== false) { // cleanup messages in the folder $imap->delete_message('*', $folder_name); unset($old_folders[$idx]); // make sure it's subscribed if (!in_array($folder_name, $old_subscribed)) { $imap->subscribe($folder_name); } } else { // create the folder $imap->create_folder($folder_name, true); } // set folder type kolab_storage::set_folder_type($folder_name, $folder['type']); list($type, ) = explode('.', $folder['type']); // append messages foreach ((array) $folder['items'] as $uid) { $file = file_get_contents(__DIR__ . "/../data/$type/$uid"); // replace member message references if ($uid == '99-99-99-99') { $repl = urlencode($username) . '/INBOX/' . $items[1]; $file = str_replace('mark.german%40example.org/INBOX/1', $repl, $file); } $res = $imap->save_message($folder_name, $file); if (is_numeric($uid)) { $items[$uid] = $res; } } } // remove extra folders $deleted = array(); foreach ($old_folders as $folder) { // ...but only personal if ($imap->folder_namespace($folder) == 'personal') { $path = explode('/', $folder); while (array_pop($path) !== null) { if (in_array(implode('/', $path), $deleted)) { $deleted[] = $folder; continue 2; } } if (!$imap->delete_folder($folder)) { throw new Exception("Failed removing '$folder'"); } $deleted[] = $folder; } else { } } // get folder UIDs map $uid_keys = array(kolab_storage::UID_KEY_CYRUS); // get folder identifiers $metadata = $imap->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new Exception("Failed to get folders metadata"); } foreach ($metadata as $folder => $meta) { $uids[$folder] = $meta[kolab_storage::UID_KEY_CYRUS]; } self::$items_map = $items; self::$folders_map = $uids; } /** * Initialize testing environment */ public static function init() { $rcube = rcube::get_instance(); // If tests_username is set we use real Kolab server // otherwise use dummy backend class which emulates a real server if (!$rcube->config->get('tests_username')) { // Load backend wrappers for tests // @TODO: maybe we could replace kolab_storage and rcube_imap instead? require_once __DIR__ . '/kolab_api_backend.php'; // Message wrapper for unit tests require_once __DIR__ . '/kolab_api_message.php'; + } - // extend include path with kolab_format/kolab_storage classes - $include_path = __DIR__ . '/../../lib/ext/plugins/libkolab/lib' . PATH_SEPARATOR . ini_get('include_path'); - set_include_path($include_path); + // extend include path with kolab_format/kolab_storage classes + $include_path = __DIR__ . '/../../lib/ext/plugins/libkolab/lib' . PATH_SEPARATOR . ini_get('include_path'); + set_include_path($include_path); - require_once __DIR__ . '/../../lib/ext/plugins/libcalendaring/libcalendaring.php'; - } + require_once __DIR__ . '/../../lib/ext/plugins/libcalendaring/libcalendaring.php'; // load HTTP_Request2 wrapper for functional/integration tests require_once __DIR__ . '/kolab_api_request.php'; } /** * Initializes kolab_api_request object * * @param string Accepted response type (xml|json) * * @return kolab_api_request Request object */ public static function get_request($type, $suffix = '') { $rcube = rcube::get_instance(); $base_uri = $rcube->config->get('tests_uri', 'http://localhost/copenhagen-tests'); $username = $rcube->config->get('tests_username', 'test@example.org'); $password = $rcube->config->get('tests_password', 'test@example.org'); if ($suffix) { $base_uri .= $suffix; } $request = new kolab_api_request($base_uri, $username, $password); // set expected response type $request->set_header('Accept', $type == 'xml' ? 'application/xml' : 'application/json'); return $request; } /** * Get data object */ public static function get_data($uid, $folder_name, $type, $format = '', &$context = null) { require_once __DIR__ . '/kolab_api_message.php'; $file = file_get_contents(__DIR__ . "/../data/$type/$uid"); $folder_uid = self::folder_uid($folder_name, false); // get message content and parse it $file = str_replace("\r?\n", "\r\n", $file); $params = array('uid' => $uid, 'folder' => $folder_uid); $object = new kolab_api_message($file, $params); if (empty(self::$db)) { $json = file_get_contents(__DIR__ . '/../data/data.json'); self::$db = json_decode($json, true); } // get assigned tag-relations $tags = array(); foreach (self::$db['tags'] as $tag_name => $tag) { if (in_array($uid, (array) $tag['members'])) { $tags[] = $tag_name; } } if ($type != 'mail') { $object = $object->to_array($type); $object['categories'] = $tags; } else { $object = new kolab_api_message($object); $object->set_categories($tags); } $context = array( 'object' => $object, 'folder_uid' => $folder_uid, 'object_uid' => $uid, ); if ($format) { $model = self::get_output_class($format, $type); $object = $model->element($object); } return $object; } public static function get_output_class($format, $type) { // fake GET request to have proper API class in kolab_api::get_instance $_GET['request'] = "{$type}s"; $output = "kolab_api_output_{$format}"; $class = "{$output}_{$type}"; $output = new $output(kolab_api::get_instance()); $model = new $class($output); return $model; } /** * Get folder UID by name */ public static function folder_uid($name, $api_test = true) { if ($api_test && !empty(self::$folders_map)) { if (self::$folders_map[$name]) { return self::$folders_map[$name]; } // it maybe is a newly created folder? check the metadata again $rcube = rcube::get_instance(); $imap = $rcube->get_storage(); $uid_keys = array(kolab_storage::UID_KEY_CYRUS); $metadata = $imap->get_metadata($name, $uid_keys); if ($uid = $metadata[$name][kolab_storage::UID_KEY_CYRUS]) { return self::$folders_map[$name] = $uid; } } return md5($name); } /** * Get message UID */ public static function msg_uid($uid, $api_test = true) { if ($uid && $api_test && !empty(self::$items_map)) { if (self::$items_map[$uid]) { return self::$items_map[$uid]; } } return $uid; } /** * Build MAPI object identifier */ public static function mapi_uid($folder_name, $api_test, $msg_uid, $attachment_uid = null) { $folder_uid = self::folder_uid($folder_name, $api_test); $msg_uid = self::msg_uid($msg_uid, $api_test); return kolab_api_filter_mapistore::uid_encode($folder_uid, $msg_uid, $attachment_uid); } protected static function login($username, $password) { $rcube = rcube::get_instance(); $login_lc = $rcube->config->get('login_lc'); $host = $rcube->config->get('default_host'); $default_port = $rcube->config->get('default_port', 143); $rcube->storage = null; $storage = $rcube->get_storage(); // parse $host $a_host = parse_url($host); if ($a_host['host']) { $host = $a_host['host']; $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; if (!empty($a_host['port'])) { $port = $a_host['port']; } else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) { $port = 993; } } if (!$port) { $port = $default_port; } // Convert username to lowercase. If storage backend // is case-insensitive we need to store always the same username if ($login_lc) { if ($login_lc == 2 || $login_lc === true) { $username = mb_strtolower($username); } else if (strpos($username, '@')) { // lowercase domain name list($local, $domain) = explode('@', $username); $username = $local . '@' . mb_strtolower($domain); } } // Here we need IDNA ASCII // Only rcube_contacts class is using domain names in Unicode $host = rcube_utils::idn_to_ascii($host); $username = rcube_utils::idn_to_ascii($username); // user already registered? if ($user = rcube_user::query($username, $host)) { $username = $user->data['username']; } // authenticate user in IMAP if (!$storage->connect($host, $username, $password, $port, $ssl)) { throw new Exception("Unable to connect to IMAP"); } // No user in database, but IMAP auth works if (!is_object($user)) { if ($rcube->config->get('auto_create_user')) { // create a new user record $user = rcube_user::create($username, $host); if (!$user) { throw new Exception("Failed to create a user record"); } } else { throw new Exception("Access denied for new user $username. 'auto_create_user' is disabled"); } } // overwrite config with user preferences $rcube->user = $user; $rcube->config->set_user_prefs((array)$user->get_prefs()); /* $_SESSION['user_id'] = $user->ID; $_SESSION['username'] = $user->data['username']; $_SESSION['storage_host'] = $host; $_SESSION['storage_port'] = $port; $_SESSION['storage_ssl'] = $ssl; $_SESSION['password'] = $rcube->encrypt($password); $_SESSION['login_time'] = time(); */ setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); // clear the cache $storage->clear_cache('mailboxes', true); // to clear correctly the cache index in testing environments // (where we call self::reset_backend() many times in one go) // we need to also close() the cache if ($ctype = $rcube->config->get('imap_cache')) { $cache = $rcube->get_cache('IMAP', $ctype, $rcube->config->get('imap_cache_ttl', '10d')); $cache->close(); } // clear also libkolab cache $db = $rcube->get_dbh(); $db->query('DELETE FROM `kolab_folders`'); return true; } }