diff --git a/ajax/addressbook/add.php b/ajax/addressbook/add.php deleted file mode 100644 index 8b8bdbe7..00000000 --- a/ajax/addressbook/add.php +++ /dev/null @@ -1,37 +0,0 @@ - - * Copyright (c) 2011 Bart Visscher - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - - -// Check if we are a user -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); -OCP\JSON::callCheck(); -require_once __DIR__.'/../loghandler.php'; - -debug('name: '.$_POST['name']); - -$userid = OCP\USER::getUser(); -$name = isset($_POST['name'])?trim(strip_tags($_POST['name'])):null; -$description = isset($_POST['description']) - ? trim(strip_tags($_POST['description'])) - : null; - -if(is_null($name)) { - bailOut('Cannot add addressbook with an empty name.'); -} -$bookid = OCA\Contacts\Addressbook::add($userid, $name, $description); -if(!$bookid) { - bailOut('Error adding addressbook: '.$name); -} - -if(!OCA\Contacts\Addressbook::setActive($bookid, 1)) { - bailOut('Error activating addressbook.'); -} -$addressbook = OCA\Contacts\Addressbook::find($bookid); -OCP\JSON::success(array('data' => array('addressbook' => $addressbook))); diff --git a/ajax/categories/add.php b/ajax/categories/add.php deleted file mode 100644 index 4ef861e8..00000000 --- a/ajax/categories/add.php +++ /dev/null @@ -1,29 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - - -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); -OCP\JSON::callCheck(); - -require_once __DIR__.'/../loghandler.php'; - -$category = isset($_POST['category']) ? trim(strip_tags($_POST['category'])) : null; - -if(is_null($category) || $category === "") { - bailOut(OCA\Contacts\App::$l10n->t('No category name given.')); -} - -$catman = new OC_VCategories('contact'); -$id = $catman->add($category); - -if($id !== false) { - OCP\JSON::success(array('data' => array('id'=>$id, 'name' => $category))); -} else { - bailOut(OCA\Contacts\App::$l10n->t('Error adding group.')); -} diff --git a/ajax/categories/addto.php b/ajax/categories/addto.php deleted file mode 100644 index 34ec82d4..00000000 --- a/ajax/categories/addto.php +++ /dev/null @@ -1,34 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - - -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); -OCP\JSON::callCheck(); - -require_once __DIR__.'/../loghandler.php'; - -$categoryid = isset($_POST['categoryid']) ? $_POST['categoryid'] : null; -$contactids = isset($_POST['contactids']) ? $_POST['contactids'] : null; - -if(is_null($categoryid)) { - bailOut(OCA\Contacts\App::$l10n->t('Group ID missing from request.')); -} - -if(is_null($contactids)) { - bailOut(OCA\Contacts\App::$l10n->t('Contact ID missing from request.')); -} - -$catmgr = OCA\Contacts\App::getVCategories(); - -foreach($contactids as $contactid) { - debug('contactid: ' . $contactid . ', categoryid: ' . $categoryid); - $catmgr->addToCategory($contactid, $categoryid); -} - -OCP\JSON::success(); diff --git a/ajax/categories/delete.php b/ajax/categories/delete.php deleted file mode 100644 index b00c935e..00000000 --- a/ajax/categories/delete.php +++ /dev/null @@ -1,51 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - - -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); -OCP\JSON::callCheck(); - -require_once __DIR__.'/../loghandler.php'; - -$categories = isset($_POST['categories']) ? $_POST['categories'] : null; -$fromobjects = (isset($_POST['fromobjects']) - && ($_POST['fromobjects'] === 'true' || $_POST['fromobjects'] === '1')) ? true : false; - -if(is_null($categories)) { - bailOut(OCA\Contacts\App::$l10n->t('No categories selected for deletion.')); -} - -debug(print_r($categories, true)); -if($fromobjects) { - $addressbooks = OCA\Contacts\Addressbook::all(OCP\USER::getUser()); - if(count($addressbooks) == 0) { - bailOut(OCA\Contacts\App::$l10n->t('No address books found.')); - } - $addressbookids = array(); - foreach($addressbooks as $addressbook) { - $addressbookids[] = $addressbook['id']; - } - $contacts = OCA\Contacts\VCard::all($addressbookids); - if(count($contacts) == 0) { - bailOut(OCA\Contacts\App::$l10n->t('No contacts found.')); - } - - $cards = array(); - foreach($contacts as $contact) { - $cards[] = array($contact['id'], $contact['carddata']); - } -} - -$catman = new OC_VCategories('contact'); -$catman->delete($categories, $cards); - -if($fromobjects) { - OCA\Contacts\VCard::updateDataByID($cards); -} -OCP\JSON::success(); diff --git a/ajax/categories/list.php b/ajax/categories/list.php deleted file mode 100644 index 077fa99e..00000000 --- a/ajax/categories/list.php +++ /dev/null @@ -1,46 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - - -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); - -$catmgr = OCA\Contacts\App::getVCategories(); -$categories = $catmgr->categories(OC_VCategories::FORMAT_MAP); -foreach($categories as &$category) { - $ids = array(); - $contacts = $catmgr->itemsForCategory( - $category['name'], - array( - 'tablename' => '*PREFIX*contacts_cards', - 'fields' => array('id',), - )); - foreach($contacts as $contact) { - $ids[] = $contact['id']; - } - $category['contacts'] = $ids; -} - -$favorites = $catmgr->getFavorites(); - -OCP\JSON::success(array( - 'data' => array( - 'categories' => $categories, - 'favorites' => $favorites, - 'shared' => OCP\Share::getItemsSharedWith('addressbook', OCA\Contacts\Share_Backend_Addressbook::FORMAT_ADDRESSBOOKS), - 'lastgroup' => OCP\Config::getUserValue( - OCP\User::getUser(), - 'contacts', - 'lastgroup', 'all'), - 'sortorder' => OCP\Config::getUserValue( - OCP\User::getUser(), - 'contacts', - 'groupsort', ''), - ) - ) -); diff --git a/ajax/categories/removefrom.php b/ajax/categories/removefrom.php deleted file mode 100644 index 5c6b092b..00000000 --- a/ajax/categories/removefrom.php +++ /dev/null @@ -1,34 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - - -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); -OCP\JSON::callCheck(); - -require_once __DIR__.'/../loghandler.php'; - -$categoryid = isset($_POST['categoryid']) ? $_POST['categoryid'] : null; -$contactids = isset($_POST['contactids']) ? $_POST['contactids'] : null; - -if(is_null($categoryid)) { - bailOut(OCA\Contacts\App::$l10n->t('Group ID missing from request.')); -} - -if(is_null($contactids)) { - bailOut(OCA\Contacts\App::$l10n->t('Contact ID missing from request.')); -} - -$catmgr = OCA\Contacts\App::getVCategories(); - -foreach($contactids as $contactid) { - debug('id: ' . $contactid .', categoryid: ' . $categoryid); - $catmgr->removeFromCategory($contactid, $categoryid); -} - -OCP\JSON::success(); diff --git a/ajax/contact/add.php b/ajax/contact/add.php deleted file mode 100644 index fecd2fb4..00000000 --- a/ajax/contact/add.php +++ /dev/null @@ -1,73 +0,0 @@ - - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 library. If not, see . - * - */ - -// Check if we are a user -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); -OCP\JSON::callCheck(); - -require_once __DIR__.'/../loghandler.php'; - -$aid = isset($_POST['aid']) ? $_POST['aid'] : null; -if(!$aid) { - $ids = OCA\Contacts\Addressbook::activeIds(); - if(count($ids) > 0) { - $aid = min($ids); // first active addressbook. - } else { - $addressbooks = OCA\Contacts\Addressbook::all(OCP\User::getUser()); - if(count($addressbooks) === 0) { - bailOut(OCA\Contacts\App::$l10n->t('You have no addressbooks.')); - } else { - $aid = $addressbooks[0]['id']; - } - } -} - -$isnew = isset($_POST['isnew']) ? $_POST['isnew'] : false; - -$vcard = Sabre\VObject\Component::create('VCARD'); -$uid = substr(md5(rand().time()), 0, 10); -$vcard->add('UID', $uid); - -$id = null; -try { - $id = OCA\Contacts\VCard::add($aid, $vcard, null, $isnew); -} catch(Exception $e) { - bailOut($e->getMessage()); -} - -if(!$id) { - bailOut('There was an error adding the contact.'); -} - -$lastmodified = OCA\Contacts\App::lastModified($vcard); -if(!$lastmodified) { - $lastmodified = new DateTime(); -} -OCP\JSON::success(array( - 'data' => array( - 'id' => $id, - 'aid' => $aid, - 'details' => OCA\Contacts\VCard::structureContact($vcard), - 'lastmodified' => $lastmodified->format('U') - ) -)); diff --git a/ajax/contact/delete.php b/ajax/contact/delete.php deleted file mode 100644 index e19f7cfb..00000000 --- a/ajax/contact/delete.php +++ /dev/null @@ -1,41 +0,0 @@ -. - * - */ -// Check if we are a user -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); -OCP\JSON::callCheck(); - -require_once __DIR__.'/../loghandler.php'; - -$id = isset($_POST['id']) ? $_POST['id'] : null; -if(!$id) { - bailOut(OCA\Contacts\App::$l10n->t('id is not set.')); -} - -try { - OCA\Contacts\VCard::delete($id); -} catch(Exception $e) { - bailOut($e->getMessage()); -} - -OCP\JSON::success(array('data' => array( 'id' => $id ))); diff --git a/ajax/contact/deleteproperty.php b/ajax/contact/deleteproperty.php deleted file mode 100644 index e1674469..00000000 --- a/ajax/contact/deleteproperty.php +++ /dev/null @@ -1,73 +0,0 @@ -. - * - */ - -// Check if we are a user -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); -OCP\JSON::callCheck(); - -require_once __DIR__.'/../loghandler.php'; - -$id = isset($_POST['id']) ? $_POST['id'] : null; -$name = isset($_POST['name']) ? $_POST['name'] : null; -$checksum = isset($_POST['checksum']) ? $_POST['checksum'] : null; -$l10n = OCA\Contacts\App::$l10n; - -$multi_properties = array('EMAIL', 'TEL', 'IMPP', 'ADR', 'URL'); - -if(!$id) { - bailOut(OCA\Contacts\App::$l10n->t('id is not set.')); -} - -if(!$name) { - bailOut(OCA\Contacts\App::$l10n->t('element name is not set.')); -} - -if(!$checksum && in_array($name, $multi_properties)) { - bailOut(OCA\Contacts\App::$l10n->t('checksum is not set.')); -} - -$vcard = OCA\Contacts\App::getContactVCard( $id ); - -if(!is_null($checksum)) { - $line = OCA\Contacts\App::getPropertyLineByChecksum($vcard, $checksum); - if(is_null($line)) { - bailOut($l10n->t('Information about vCard is incorrect. Please reload the page.')); - exit(); - } - unset($vcard->children[$line]); -} else { - unset($vcard->{$name}); -} - -try { - OCA\Contacts\VCard::edit($id, $vcard); -} catch(Exception $e) { - bailOut($e->getMessage()); -} - -OCP\JSON::success(array( - 'data' => array( - 'id' => $id, - 'lastmodified' => OCA\Contacts\App::lastModified($vcard)->format('U'), - ) -)); diff --git a/ajax/contact/list.php b/ajax/contact/list.php index 4251ee5b..8fc3c204 100644 --- a/ajax/contact/list.php +++ b/ajax/contact/list.php @@ -23,10 +23,10 @@ $aid = isset($_GET['aid'])?$_GET['aid']:null; $active_addressbooks = array(); if(is_null($aid)) { // Called initially to get the active addressbooks. - $active_addressbooks = OCA\Contacts\Addressbook::active(OCP\USER::getUser()); + $active_addressbooks = OCA\Contacts\AddressbookLegacy::active(OCP\USER::getUser()); } else { // called each time more contacts has to be shown. - $active_addressbooks = array(OCA\Contacts\Addressbook::find($aid)); + $active_addressbooks = array(OCA\Contacts\AddressbookLegacy::find($aid)); } $lastModified = OCA\Contacts\App::lastModified(); @@ -42,18 +42,6 @@ $contacts_addressbook = array(); $ids = array(); foreach($active_addressbooks as $addressbook) { $ids[] = $addressbook['id']; - /*if(!isset($contacts_addressbook[$addressbook['id']])) { - $contacts_addressbook[$addressbook['id']] - = array('contacts' => array('type' => 'book',)); - $contacts_addressbook[$addressbook['id']]['displayname'] - = $addressbook['displayname']; - $contacts_addressbook[$addressbook['id']]['description'] - = $addressbook['description']; - $contacts_addressbook[$addressbook['id']]['permissions'] - = $addressbook['permissions']; - $contacts_addressbook[$addressbook['id']]['owner'] - = $addressbook['userid']; - }*/ } $contacts_alphabet = array(); @@ -63,14 +51,6 @@ $contacts_alphabet = array_merge( $contacts_alphabet, OCA\Contacts\VCard::all($ids) ); -/*foreach($ids as $id) { - if($id) { - $contacts_alphabet = array_merge( - $contacts_alphabet, - OCA\Contacts\VCard::all($id, $offset, 50) - ); - } -}*/ uasort($contacts_alphabet, 'cmp'); @@ -89,38 +69,11 @@ if($contacts_alphabet) { 'data' => $details, ); } catch (Exception $e) { + \OCP\Util::writeLog('contacts', 'Exception: ' . $e->getMessage(), \OCP\Util::DEBUG); continue; } - // This should never execute. - /*if(!isset($contacts_addressbook[$contact['addressbookid']])) { - $contacts_addressbook[$contact['addressbookid']] = array( - 'contacts' => array('type' => 'book',) - ); - } - $display = trim($contact['fullname']); - if(!$display) { - $vcard = OCA\Contacts\App::getContactVCard($contact['id']); - if(!is_null($vcard)) { - $struct = OCA\Contacts\VCard::structureContact($vcard); - $display = isset($struct['EMAIL'][0]) - ? $struct['EMAIL'][0]['value'] - : '[UNKNOWN]'; - } - } - $contacts_addressbook[$contact['addressbookid']]['contacts'][] = array( - 'type' => 'contact', - 'id' => $contact['id'], - 'addressbookid' => $contact['addressbookid'], - 'displayname' => htmlspecialchars($display), - 'permissions' => - isset($contacts_addressbook[$contact['addressbookid']]['permissions']) - ? $contacts_addressbook[$contact['addressbookid']]['permissions'] - : '0', - );*/ } } -//unset($contacts_alphabet); -//uasort($contacts, 'cmp'); OCP\JSON::success(array( 'data' => array( diff --git a/ajax/contact/saveproperty.php b/ajax/contact/saveproperty.php deleted file mode 100644 index bc99229f..00000000 --- a/ajax/contact/saveproperty.php +++ /dev/null @@ -1,260 +0,0 @@ - - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library 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 library. If not, see . - * - */ - -namespace OCA\Contacts; -use Sabre\VObject as VObject; - -require_once __DIR__.'/../loghandler.php'; - -function setParameters($property, $parameters, $reset = false) { - if(!$parameters) { - return; - } - - if($reset) { - $property->parameters = array(); - } - debug('Setting parameters: ' . print_r($parameters, true)); - foreach($parameters as $key => $parameter) { - debug('Adding parameter: ' . $key); - if(is_array($parameter)) { - foreach($parameter as $val) { - if(is_array($val)) { - foreach($val as $val2) { - if(trim($key) && trim($val2)) { - debug('Adding parameter: '.$key.'=>'.print_r($val2, true)); - $property->add($key, strip_tags($val2)); - } - } - } else { - if(trim($key) && trim($val)) { - debug('Adding parameter: '.$key.'=>'.print_r($val, true)); - $property->add($key, strip_tags($val)); - } - } - } - } else { - if(trim($key) && trim($val)) { - debug('Adding parameter: '.$key.'=>'.print_r($val, true)); - $property->add($key, strip_tags($parameter)); - } - } - } -} - -// Check if we are a user -\OCP\JSON::checkLoggedIn(); -\OCP\JSON::checkAppEnabled('contacts'); -\OCP\JSON::callCheck(); -$id = isset($_POST['id'])?$_POST['id']:null; -$name = isset($_POST['name'])?$_POST['name']:null; -$value = isset($_POST['value'])?$_POST['value']:null; -$parameters = isset($_POST['parameters'])?$_POST['parameters']:null; -$checksum = isset($_POST['checksum'])?$_POST['checksum']:null; - -debug('value: ' . print_r($value, 1)); - -$multi_properties = array('EMAIL', 'TEL', 'IMPP', 'ADR', 'URL'); - -if(!$name) { - bailOut(App::$l10n->t('element name is not set.')); -} -if(!$id) { - bailOut(App::$l10n->t('id is not set.')); -} -if(!$checksum && in_array($name, $multi_properties)) { - bailOut(App::$l10n->t('checksum is not set.')); -} -if(is_array($value)) { - $value = array_map('strip_tags', $value); - // NOTE: Important, otherwise the compound value will be - // set in the order the fields appear in the form! - ksort($value); - //if($name == 'CATEGORIES') { - // $value = VCard::escapeDelimiters($value, ','); - //} else { - // $value = VCard::escapeDelimiters($value, ';'); - //} -} else { - $value = trim(strip_tags($value)); -} - -$vcard = App::getContactVCard($id); -if(!$vcard) { - bailOut(App::$l10n->t('Couldn\'t find vCard for %d.', array($id))); -} - -$property = null; - -if(in_array($name, $multi_properties)) { - if($checksum !== 'new') { - $line = App::getPropertyLineByChecksum($vcard, $checksum); - if(is_null($line)) { - bailOut(App::$l10n->t( - 'Information about vCard is incorrect. Please reload the page: ').$checksum - ); - } - $property = $vcard->children[$line]; - $element = $property->name; - - if($element != $name) { - bailOut(App::$l10n->t( - 'Something went FUBAR. ').$name.' != '.$element - ); - } - } else { - // Add new property - $element = $name; - if (!is_scalar($value)) { - $property = VObject\Property::create($name); - if(in_array($name, array('ADR',))) { - $property->setParts($value); - } else { - bailOut(App::$l10n->t( - 'Cannot save property of type "%s" as array', array($name,) - )); - } - setParameters($property, $parameters); - } else { - $property = VObject\Property::create($name, $value, $parameters); - } - $vcard->add($property); - $checksum = substr(md5($property->serialize()), 0, 8); - try { - VCard::edit($id, $vcard); - } catch(Exception $e) { - bailOut($e->getMessage()); - } - \OCP\JSON::success(array('data' => array( - 'checksum' => $checksum, - 'oldchecksum' => $_POST['checksum'], - ))); - exit(); - } -} else { - $element = $name; - $property = $vcard->select($name); - if(count($property) === 0) { - $property = VObject\Property::create($name); - $vcard->add($property); - } else { - $property = array_shift($property); - } -} - -/* preprocessing value */ -switch($element) { - case 'BDAY': - $date = New \DateTime($value); - $value = $date->format('Y-m-d'); - break; - case 'FN': - if(!$value) { - // create a method thats returns an alternative for FN. - //$value = getOtherValue(); - } - break; - case 'NOTE': - $value = str_replace('\n', '\\n', $value); - break; - case 'EMAIL': - $value = strtolower($value); - break; - case 'IMPP': - if(is_null($parameters) || !isset($parameters['X-SERVICE-TYPE'])) { - bailOut(App::$l10n->t('Missing IM parameter.')); - } - $impp = App::getIMOptions($parameters['X-SERVICE-TYPE']); - if(is_null($impp)) { - bailOut(App::$l10n->t('Unknown IM: '.$parameters['X-SERVICE-TYPE'])); - } - $value = $impp['protocol'] . ':' . $value; - break; -} - -// If empty remove the property -if(!$value) { - if(in_array($name, $multi_properties)) { - unset($vcard->children[$line]); - $checksum = ''; - } else { - unset($vcard->{$name}); - } -} else { - /* setting value */ - switch($element) { - case 'BDAY': - $vcard->BDAY = $value; - - if(!isset($vcard->BDAY['VALUE'])) { - $vcard->BDAY->add('VALUE', 'DATE'); - } else { - $vcard->BDAY->VALUE = 'DATE'; - } - break; - case 'ADR': - case 'N': - if(is_array($value)) { - $property->setParts($value); - } else { - debug('Saving N ' . $value); - $vcard->N = $value; - } - break; - case 'EMAIL': - case 'TEL': - case 'IMPP': - case 'URL': - debug('Setting element: (EMAIL/TEL/ADR)'.$element); - $property->setValue($value); - break; - default: - $vcard->{$name} = $value; - break; - } - setParameters($property, $parameters, true); - - // Do checksum and be happy - if(in_array($name, $multi_properties)) { - $checksum = substr(md5($property->serialize()), 0, 8); - } -} -//debug('New checksum: '.$checksum); -//$vcard->children[$line] = $property; ??? -try { - VCard::edit($id, $vcard); -} catch(Exception $e) { - bailOut($e->getMessage()); -} - -if(in_array($name, $multi_properties)) { - \OCP\JSON::success(array('data' => array( - 'line' => $line, - 'checksum' => $checksum, - 'oldchecksum' => $_POST['checksum'], - 'lastmodified' => App::lastModified($vcard)->format('U'), - ))); -} else { - \OCP\JSON::success(array('data' => array( - 'lastmodified' => App::lastModified($vcard)->format('U'), - ))); -} diff --git a/ajax/currentphoto.php b/ajax/currentphoto.php index b6b10770..605ec0d0 100644 --- a/ajax/currentphoto.php +++ b/ajax/currentphoto.php @@ -20,22 +20,39 @@ * */ +namespace OCA\Contacts; + // Firefox and Konqueror tries to download application/json for me. --Arthur -OCP\JSON::setContentTypeHeader('text/plain'); -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); +\OCP\JSON::setContentTypeHeader('text/plain'); +\OCP\JSON::checkLoggedIn(); +\OCP\JSON::checkAppEnabled('contacts'); require_once 'loghandler.php'; -if (!isset($_GET['id'])) { - bailOut(OCA\Contacts\App::$l10n->t('No contact ID was submitted.')); +$contactid = isset($_GET['contactid']) ? $_GET['contactid'] : ''; +$addressbookid = isset($_GET['addressbookid']) ? $_GET['addressbookid'] : ''; +$backend = isset($_GET['backend']) ? $_GET['backend'] : ''; + +if(!$contactid) { + bailOut('Missing contact id.'); } -$contact = OCA\Contacts\App::getContactVCard($_GET['id']); +if(!$addressbookid) { + bailOut('Missing address book id.'); +} + +$app = new App(); +// FIXME: Get backend and addressbookid +$contact = $app->getContact($backend, $addressbookid, $contactid); +if(!$contact) { + \OC_Cache::remove($tmpkey); + bailOut(App::$l10n + ->t('Error getting contact object.')); +} // invalid vcard -if( is_null($contact)) { - bailOut(OCA\Contacts\App::$l10n->t('Error reading contact photo.')); +if(!$contact) { + bailOut(App::$l10n->t('Error reading contact photo.')); } else { - $image = new OC_Image(); + $image = new \OC_Image(); if(!isset($contact->PHOTO) || !$image->loadFromBase64((string)$contact->PHOTO)) { if(isset($contact->LOGO)) { $image->loadFromBase64((string)$contact->LOGO); @@ -43,13 +60,13 @@ if( is_null($contact)) { } if($image->valid()) { $tmpkey = 'contact-photo-'.$contact->UID; - if(OC_Cache::set($tmpkey, $image->data(), 600)) { - OCP\JSON::success(array('data' => array('id'=>$_GET['id'], 'tmp'=>$tmpkey))); + if(\OC_Cache::set($tmpkey, $image->data(), 600)) { + \OCP\JSON::success(array('data' => array('id'=>$_GET['id'], 'tmp'=>$tmpkey))); exit(); } else { - bailOut(OCA\Contacts\App::$l10n->t('Error saving temporary file.')); + bailOut(App::$l10n->t('Error saving temporary file.')); } } else { - bailOut(OCA\Contacts\App::$l10n->t('The loading photo is not valid.')); + bailOut(App::$l10n->t('The loading photo is not valid.')); } } diff --git a/ajax/editaddress.php b/ajax/editaddress.php deleted file mode 100644 index de36b02c..00000000 --- a/ajax/editaddress.php +++ /dev/null @@ -1,41 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - - -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); - -$id = $_GET['id']; -$checksum = isset($_GET['checksum'])?$_GET['checksum']:''; -$vcard = OCA\Contacts\App::getContactVCard($id); -$adr_types = OCA\Contacts\App::getTypesOfProperty('ADR'); - -$tmpl = new OCP\Template("contacts", "part.edit_address_dialog"); -if($checksum) { - $line = OCA\Contacts\App::getPropertyLineByChecksum($vcard, $checksum); - $element = $vcard->children[$line]; - $adr = OCA\Contacts\VCard::structureProperty($element); - $types = array(); - if(isset($adr['parameters']['TYPE'])) { - if(is_array($adr['parameters']['TYPE'])) { - $types = array_map('htmlspecialchars', $adr['parameters']['TYPE']); - $types = array_map('strtoupper', $types); - } else { - $types = array(strtoupper(htmlspecialchars($adr['parameters']['TYPE']))); - } - } - $tmpl->assign('types', $types, false); - $adr = array_map('htmlspecialchars', $adr['value']); - $tmpl->assign('adr', $adr, false); -} - -$tmpl->assign('id', $id); -$tmpl->assign('adr_types', $adr_types); - -$page = $tmpl->fetchPage(); -OCP\JSON::success(array('data' => array('page'=>$page, 'checksum'=>$checksum))); diff --git a/ajax/editname.php b/ajax/editname.php deleted file mode 100644 index bac9717c..00000000 --- a/ajax/editname.php +++ /dev/null @@ -1,34 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - - -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); -require_once 'loghandler.php'; - -$tmpl = new OCP\Template("contacts", "part.edit_name_dialog"); - -$id = isset($_GET['id'])?$_GET['id']:''; - -if($id) { - $vcard = OCA\Contacts\App::getContactVCard($id); - $name = array('', '', '', '', ''); - if($vcard->__isset('N')) { - $property = $vcard->__get('N'); - if($property) { - $name = OCA\Contacts\VCard::structureProperty($property); - } - } - $name = array_map('htmlspecialchars', $name['value']); - $tmpl->assign('name', $name, false); - $tmpl->assign('id', $id, false); -} else { - bailOut(OCA\Contacts\App::$l10n->t('Contact ID is missing.')); -} -$page = $tmpl->fetchPage(); -OCP\JSON::success(array('data' => array('page'=>$page))); diff --git a/ajax/importdialog.php b/ajax/importdialog.php deleted file mode 100644 index f44539cf..00000000 --- a/ajax/importdialog.php +++ /dev/null @@ -1,15 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - - -OCP\JSON::checkLoggedIn(); -OCP\App::checkAppEnabled('contacts'); -$tmpl = new OCP\Template('contacts', 'part.import'); -$tmpl->assign('path', $_POST['path']); -$tmpl->assign('filename', $_POST['filename']); -$tmpl->printpage(); diff --git a/ajax/oc_photo.php b/ajax/oc_photo.php index 6e184d4b..0c8d2e3f 100644 --- a/ajax/oc_photo.php +++ b/ajax/oc_photo.php @@ -24,8 +24,8 @@ OCP\JSON::checkLoggedIn(); OCP\JSON::checkAppEnabled('contacts'); require_once 'loghandler.php'; -if(!isset($_GET['id'])) { - bailOut(OCA\Contacts\App::$l10n->t('No contact ID was submitted.')); +if(!isset($_GET['contact'])) { + bailOut(OCA\Contacts\App::$l10n->t('No contact info was submitted.')); } if(!isset($_GET['path'])) { @@ -33,7 +33,7 @@ if(!isset($_GET['path'])) { } $localpath = \OC\Files\Filesystem::getLocalFile($_GET['path']); -$tmpkey = 'contact-photo-'.$_GET['id']; +$tmpkey = 'contact-photo-'.$_GET['contact']['contactid']; if(!file_exists($localpath)) { bailOut(OCA\Contacts\App::$l10n->t('File doesn\'t exist:').$localpath); @@ -55,7 +55,7 @@ if(!$image->fixOrientation()) { // No fatal error so we don't bail out. OCP\Util::DEBUG); } if(OC_Cache::set($tmpkey, $image->data(), 600)) { - OCP\JSON::success(array('data' => array('id'=>$_GET['id'], 'tmp'=>$tmpkey))); + OCP\JSON::success(array('data' => array('id'=>$_GET['contact']['contactid'], 'tmp'=>$tmpkey))); exit(); } else { bailOut('Couldn\'t save temporary image: '.$tmpkey); diff --git a/ajax/savecrop.php b/ajax/savecrop.php index a9ab6834..68350d4b 100644 --- a/ajax/savecrop.php +++ b/ajax/savecrop.php @@ -3,7 +3,7 @@ * ownCloud - Addressbook * * @author Thomas Tanghus - * @copyright 2012 Thomas Tanghus + * @copyright 2012-13 Thomas Tanghus * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE @@ -19,13 +19,13 @@ * License along with this library. If not, see . * */ -// Check if we are a user -OCP\JSON::checkLoggedIn(); -OCP\JSON::checkAppEnabled('contacts'); -OCP\JSON::callCheck(); -// Firefox and Konqueror tries to download application/json for me. --Arthur -OCP\JSON::setContentTypeHeader('text/plain; charset=utf-8'); +namespace OCA\Contacts; + +// Check if we are a user +\OCP\JSON::checkLoggedIn(); +\OCP\JSON::checkAppEnabled('contacts'); +\OCP\JSON::callCheck(); require_once 'loghandler.php'; @@ -38,92 +38,101 @@ $y1 = (isset($_POST['y1']) && $_POST['y1']) ? $_POST['y1'] : 0; $w = (isset($_POST['w']) && $_POST['w']) ? $_POST['w'] : -1; $h = (isset($_POST['h']) && $_POST['h']) ? $_POST['h'] : -1; $tmpkey = isset($_POST['tmpkey']) ? $_POST['tmpkey'] : ''; -$id = isset($_POST['id']) ? $_POST['id'] : ''; + +$contactid = isset($_POST['contactid']) ? $_POST['contactid'] : ''; +$addressbookid = isset($_POST['addressbookid']) ? $_POST['addressbookid'] : ''; +$backend = isset($_POST['backend']) ? $_POST['backend'] : ''; if($tmpkey == '') { bailOut('Missing key to temporary file.'); } -if($id == '') { +if($contactid == '') { bailOut('Missing contact id.'); } -OCP\Util::writeLog('contacts', 'savecrop.php: key: '.$tmpkey, OCP\Util::DEBUG); +if($addressbookid == '') { + bailOut('Missing address book id.'); +} -$data = OC_Cache::get($tmpkey); +\OCP\Util::writeLog('contacts', 'savecrop.php: key: '.$tmpkey, \OCP\Util::DEBUG); + +$app = new App(); +// FIXME: Get backend and addressbookid +$contact = $app->getContact($backend, $addressbookid, $contactid); +if(!$contact) { + \OC_Cache::remove($tmpkey); + bailOut(App::$l10n + ->t('Error getting contact object.')); +} + +$data = \OC_Cache::get($tmpkey); if($data) { - $image = new OC_Image(); + $image = new \OC_Image(); if($image->loadFromData($data)) { $w = ($w != -1 ? $w : $image->width()); $h = ($h != -1 ? $h : $image->height()); - OCP\Util::writeLog('contacts', + \OCP\Util::writeLog('contacts', 'savecrop.php, x: '.$x1.' y: '.$y1.' w: '.$w.' h: '.$h, - OCP\Util::DEBUG); + \OCP\Util::DEBUG); if($image->crop($x1, $y1, $w, $h)) { if(($image->width() <= 200 && $image->height() <= 200) || $image->resize(200)) { - $vcard = OCA\Contacts\App::getContactVCard($id); - if(!$vcard) { - OC_Cache::remove($tmpkey); - bailOut(OCA\Contacts\App::$l10n - ->t('Error getting contact object.')); - } - if($vcard->__isset('PHOTO')) { - OCP\Util::writeLog('contacts', + // For vCard 3.0 the type must be e.g. JPEG or PNG + // For version 4.0 the full mimetype should be used. + // https://tools.ietf.org/html/rfc2426#section-3.1.4 + $type = strval($contact->VERSION) === '4.0' + ? $image->mimeType() + : strtoupper(array_pop(explode('/', $image->mimeType()))); + if(isset($contact->PHOTO)) { + \OCP\Util::writeLog('contacts', 'savecrop.php: PHOTO property exists.', - OCP\Util::DEBUG); - $property = $vcard->__get('PHOTO'); + \OCP\Util::DEBUG); + $property = $contact->PHOTO; if(!$property) { - OC_Cache::remove($tmpkey); - bailOut(OCA\Contacts\App::$l10n + \OC_Cache::remove($tmpkey); + bailOut(App::$l10n ->t('Error getting PHOTO property.')); } - $property->setValue($image->__toString()); + $property->setValue(strval($image)); + $property->parameters = []; + /*$property->ENCODING = 'b'; + $property->TYPE = $type;*/ $property->parameters[] - = new Sabre\VObject\Parameter('ENCODING', 'b'); + = new \Sabre\VObject\Parameter('ENCODING', 'b'); $property->parameters[] - = new Sabre\VObject\Parameter('TYPE', $image->mimeType()); - $vcard->__set('PHOTO', $property); + = new \Sabre\VObject\Parameter('TYPE', $image->mimeType()); + $contact->PHOTO = $property; } else { - OCP\Util::writeLog('contacts', + \OCP\Util::writeLog('contacts', 'savecrop.php: files: Adding PHOTO property.', - OCP\Util::DEBUG); - // For vCard 3.0 the type must be e.g. JPEG or PNG - // For version 4.0 the full mimetype should be used. - // https://tools.ietf.org/html/rfc2426#section-3.1.4 - $type = $vcard->VERSION == '4.0' - ? $image->mimeType() - : strtoupper(array_pop(explode('/', $image->mimeType()))); - $vcard->add('PHOTO', - $image->__toString(), array('ENCODING' => 'b', + \OCP\Util::DEBUG); + $contact->add('PHOTO', + strval($image), array('ENCODING' => 'b', 'TYPE' => $type)); } - $now = new DateTime; - $vcard->{'REV'} = $now->format(DateTime::W3C); - if(!OCA\Contacts\VCard::edit($id, $vcard)) { - bailOut(OCA\Contacts\App::$l10n->t('Error saving contact.')); + if(!$contact->save()) { + bailOut(App::$l10n->t('Error saving contact.')); } - OCA\Contacts\App::cacheThumbnail($id, $image); - OCP\JSON::success(array( + $thumbnail = $contact->cacheThumbnail($image); + \OCP\JSON::success(array( 'data' => array( - 'id' => $id, - 'width' => $image->width(), - 'height' => $image->height(), - 'lastmodified' => OCA\Contacts\App::lastModified($vcard)->format('U') + 'id' => $contactid, + 'thumbnail' => $thumbnail, ) )); } else { - bailOut(OCA\Contacts\App::$l10n->t('Error resizing image')); + bailOut(App::$l10n->t('Error resizing image')); } } else { - bailOut(OCA\Contacts\App::$l10n->t('Error cropping image')); + bailOut(App::$l10n->t('Error cropping image')); } } else { - bailOut(OCA\Contacts\App::$l10n->t('Error creating temporary image')); + bailOut(App::$l10n->t('Error creating temporary image')); } } else { - bailOut(OCA\Contacts\App::$l10n->t('Error finding image: ').$tmpkey); + bailOut(App::$l10n->t('Error finding image: ').$tmpkey); } -OC_Cache::remove($tmpkey); +\OC_Cache::remove($tmpkey); diff --git a/ajax/uploadimport.php b/ajax/uploadimport.php index bd1aedf6..d52e538a 100644 --- a/ajax/uploadimport.php +++ b/ajax/uploadimport.php @@ -32,57 +32,48 @@ $view = OCP\Files::getStorage('contacts'); if(!$view->file_exists('imports')) { $view->mkdir('imports'); } -$tmpfile = md5(rand()); - -// If it is a Drag'n'Drop transfer it's handled here. -$fn = (isset($_SERVER['HTTP_X_FILE_NAME']) ? $_SERVER['HTTP_X_FILE_NAME'] : false); -$fn = strtr($fn, array('/' => '', "\\" => '')); -if($fn) { - if(OC\Files\Filesystem::isFileBlacklisted($fn)) { - bailOut($l10n->t('Upload of blacklisted file:') . $fn); - } - if($view->file_put_contents('/imports/'.$fn, file_get_contents('php://input'))) { - OCP\JSON::success(array('data' => array('file'=>$tmpfile, 'name'=>$fn))); - exit(); - } else { - bailOut($l10n->t('Error uploading contacts to storage.')); - } -} // File input transfers are handled here -if (!isset($_FILES['importfile'])) { - OCP\Util::writeLog('contacts', - 'ajax/uploadphoto.php: No file was uploaded. Unknown error.', - OCP\Util::DEBUG); - OCP\JSON::error(array(' - data' => array( - 'message' => 'No file was uploaded. Unknown error' ))); - exit(); +if (!isset($_FILES['file'])) { + bailOut($l10n->t('No file was uploaded. Unknown error')); } -$error = $_FILES['importfile']['error']; -if($error !== UPLOAD_ERR_OK) { + +$file=$_FILES['file']; + +if($file['error'] !== UPLOAD_ERR_OK) { $errors = array( - 0=>$l10n->t("There is no error, the file uploaded with success"), - 1=>$l10n->t("The uploaded file exceeds the upload_max_filesize directive in php.ini").ini_get('upload_max_filesize'), - 2=>$l10n->t("The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form"), - 3=>$l10n->t("The uploaded file was only partially uploaded"), - 4=>$l10n->t("No file was uploaded"), - 6=>$l10n->t("Missing a temporary folder") + UPLOAD_ERR_OK => $l10n->t("There is no error, the file uploaded with success"), + UPLOAD_ERR_INI_SIZE => $l10n->t("The uploaded file exceeds the upload_max_filesize directive in php.ini") + .ini_get('upload_max_filesize'), + UPLOAD_ERR_FORM_SIZE => $l10n->t("The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form"), + UPLOAD_ERR_PARTIAL => $l10n->t("The uploaded file was only partially uploaded"), + UPLOAD_ERR_NO_FILE => $l10n->t("No file was uploaded"), + UPLOAD_ERR_NO_TMP_DIR => $l10n->t('Missing a temporary folder'), + UPLOAD_ERR_CANT_WRITE => $l10n->t('Failed to write to disk'), ); bailOut($errors[$error]); } -$file=$_FILES['importfile']; -if(file_exists($file['tmp_name'])) { - $filename = strtr($file['name'], array('/' => '', "\\" => '')); +$maxUploadFilesize = OCP\Util::maxUploadFilesize('/'); +$maxHumanFilesize = OCP\Util::humanFileSize($maxUploadFilesize); + +$totalSize = $file['size']; +if ($maxUploadFilesize >= 0 and $totalSize > $maxUploadFilesize) { + bailOut($l10n->t('Not enough storage available')); +} + +$tmpname = $file['tmp_name']; +$filename = strtr($file['name'], array('/' => '', "\\" => '')); +if(is_uploaded_file($tmpname)) { if(OC\Files\Filesystem::isFileBlacklisted($filename)) { bailOut($l10n->t('Upload of blacklisted file:') . $filename); } - if($view->file_put_contents('/imports/'.$filename, file_get_contents($file['tmp_name']))) { - OCP\JSON::success(array('data' => array('file'=>$filename, 'name'=>$filename))); + if($view->file_put_contents('/imports/'.$filename, file_get_contents($tmpname))) { + OCP\JSON::success(array('file'=>$filename)); } else { bailOut($l10n->t('Error uploading contacts to storage.')); } } else { - bailOut('Temporary file: \''.$file['tmp_name'].'\' has gone AWOL?'); + bailOut('Temporary file: \''.$tmpname.'\' has gone AWOL?'); } + diff --git a/ajax/uploadphoto.php b/ajax/uploadphoto.php index a26560ba..ab76fa04 100644 --- a/ajax/uploadphoto.php +++ b/ajax/uploadphoto.php @@ -29,13 +29,22 @@ OCP\JSON::callCheck(); OCP\JSON::setContentTypeHeader('text/plain; charset=utf-8'); require_once 'loghandler.php'; $l10n = OCA\Contacts\App::$l10n; + +$contactid = isset($_POST['contactid']) ? $_POST['contactid'] : ''; +$addressbookid = isset($_POST['addressbookid']) ? $_POST['addressbookid'] : ''; +$backend = isset($_POST['backend']) ? $_POST['backend'] : ''; + +if($contactid == '') { + bailOut('Missing contact id.'); +} + +if($addressbookid == '') { + bailOut('Missing address book id.'); +} + // If it is a Drag'n'Drop transfer it's handled here. $fn = (isset($_SERVER['HTTP_X_FILE_NAME']) ? $_SERVER['HTTP_X_FILE_NAME'] : false); if ($fn) { - if (!isset($_GET['id'])) { - bailOut($l10n->t('No contact ID was submitted.')); - } - $id = $_GET['id']; $tmpkey = 'contact-photo-'.md5($fn); $data = file_get_contents('php://input'); $image = new OC_Image(); @@ -50,10 +59,14 @@ if ($fn) { if(OC_Cache::set($tmpkey, $image->data(), 600)) { OCP\JSON::success(array( 'data' => array( - 'mime'=>$_SERVER['CONTENT_TYPE'], - 'name'=>$fn, - 'id'=>$id, - 'tmp'=>$tmpkey))); + 'mime'=> $_SERVER['CONTENT_TYPE'], + 'name'=> $fn, + 'contactid'=> $id, + 'addressbookid'=> addressbookid, + 'backend'=> $backend, + 'tmp'=>$tmpkey + )) + ); exit(); } else { bailOut($l10n->t('Couldn\'t save temporary image: ').$tmpkey); @@ -63,10 +76,6 @@ if ($fn) { } } -// Uploads from file dialog are handled here. -if (!isset($_POST['id'])) { - bailOut($l10n->t('No contact ID was submitted.')); -} if (!isset($_FILES['imagefile'])) { bailOut($l10n->t('No file was uploaded. Unknown error')); } @@ -101,9 +110,14 @@ if(file_exists($file['tmp_name'])) { 'mime'=>$file['type'], 'size'=>$file['size'], 'name'=>$file['name'], - 'id'=>$_POST['id'], 'tmp'=>$tmpkey, - ))); + ), + 'metadata' => array( + 'contactid'=> $contactid, + 'addressbookid'=> $addressbookid, + 'backend'=> $backend, + ), + )); exit(); } else { bailOut($l10n->t('Couldn\'t save temporary image: ').$tmpkey); diff --git a/appinfo/app.php b/appinfo/app.php index 7a2407c2..509a9b81 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -1,26 +1,32 @@ 'contacts_index', 'order' => 10, - 'href' => OCP\Util::linkTo( 'contacts', 'index.php' ), + 'href' => \OC_Helper::linkToRoute('contacts_index'), 'icon' => OCP\Util::imagePath( 'contacts', 'contacts.svg' ), 'name' => OC_L10N::get('contacts')->t('Contacts') )); @@ -30,8 +36,9 @@ OC_Search::registerProvider('OCA\Contacts\SearchProvider'); OCP\Share::registerBackend('contact', 'OCA\Contacts\Share_Backend_Contact'); OCP\Share::registerBackend('addressbook', 'OCA\Contacts\Share_Backend_Addressbook', 'contact'); +/* if(OCP\User::isLoggedIn()) { foreach(OCA\Contacts\Addressbook::all(OCP\USER::getUser()) as $addressbook) { OCP\Contacts::registerAddressBook(new OCA\Contacts\AddressbookProvider($addressbook['id'])); } -} +}*/ diff --git a/appinfo/classpath.php b/appinfo/classpath.php new file mode 100644 index 00000000..3dee4b79 --- /dev/null +++ b/appinfo/classpath.php @@ -0,0 +1,28 @@ +*dbname* true false - utf8 + *dbprefix*contacts_addressbooks diff --git a/appinfo/migrate.php b/appinfo/migrate.php index 8fe84d52..aa505633 100644 --- a/appinfo/migrate.php +++ b/appinfo/migrate.php @@ -1,5 +1,7 @@ content->copyRows( $options ); // If both returned some ids then they worked - if(is_array($ids) && is_array($ids2)) { - return true; - } else { - return false; - } + return (is_array($ids) && is_array($ids2)); } // Import function for contacts - function import( ) { - switch( $this->appinfo->version ) { + function import() { + switch($this->appinfo->version) { default: // All versions of the app have had the same db structure, so all can use the same import function - $query = $this->content->prepare( 'SELECT * FROM contacts_addressbooks WHERE userid = ?' ); - $results = $query->execute( array( $this->olduid ) ); + $query = $this->content->prepare('SELECT * FROM `contacts_addressbooks` WHERE `userid` = ?'); + $results = $query->execute(array($this->olduid)); $idmap = array(); - while( $row = $results->fetchRow() ) { + while($row = $results->fetchRow()) { // Import each addressbook - $addressbookquery = OCP\DB::prepare( 'INSERT INTO `*PREFIX*contacts_addressbooks` (`userid`, `displayname`, `uri`, `description`, `ctag`) VALUES (?, ?, ?, ?, ?)' ); - $addressbookquery->execute( array( $this->uid, $row['displayname'], $row['uri'], $row['description'], $row['ctag'] ) ); + $addressbookquery = OCP\DB::prepare('INSERT INTO `*PREFIX*contacts_addressbooks` ' + . '(`userid`, `displayname`, `uri`, `description`, `ctag`) VALUES (?, ?, ?, ?, ?)'); + $addressbookquery->execute( + array( + $this->uid, + $row['displayname'], + $row['uri'], + $row['description'], + $row['ctag'] + ) + ); // Map the id $idmap[$row['id']] = OCP\DB::insertid('*PREFIX*contacts_addressbooks'); // Make the addressbook active @@ -49,12 +56,21 @@ class OC_Migration_Provider_Contacts extends OC_Migration_Provider{ // Now tags foreach($idmap as $oldid => $newid) { - $query = $this->content->prepare( 'SELECT * FROM contacts_cards WHERE addressbookid = ?' ); - $results = $query->execute( array( $oldid ) ); - while( $row = $results->fetchRow() ) { + $query = $this->content->prepare('SELECT * FROM `contacts_cards` WHERE `addressbookid` = ?'); + $results = $query->execute(array($oldid)); + while($row = $results->fetchRow()) { // Import the contacts - $contactquery = OCP\DB::prepare( 'INSERT INTO `*PREFIX*contacts_cards` (`addressbookid`, `fullname`, `carddata`, `uri`, `lastmodified`) VALUES (?, ?, ?, ?, ?)' ); - $contactquery->execute( array( $newid, $row['fullname'], $row['carddata'], $row['uri'], $row['lastmodified'] ) ); + $contactquery = OCP\DB::prepare('INSERT INTO `*PREFIX*contacts_cards` ' + . '(`addressbookid`, `fullname`, `carddata`, `uri`, `lastmodified`) VALUES (?, ?, ?, ?, ?)'); + $contactquery->execute( + array( + $newid, + $row['fullname'], + $row['carddata'], + $row['uri'], + $row['lastmodified'] + ) + ); } } // All done! @@ -67,4 +83,4 @@ class OC_Migration_Provider_Contacts extends OC_Migration_Provider{ } // Load the provider -new OC_Migration_Provider_Contacts( 'contacts' ); \ No newline at end of file +new MigrationProvider('contacts'); diff --git a/appinfo/remote.php b/appinfo/remote.php index 13914096..0702c5e7 100644 --- a/appinfo/remote.php +++ b/appinfo/remote.php @@ -21,6 +21,7 @@ */ OCP\App::checkAppEnabled('contacts'); +require_once __DIR__ . '/classpath.php'; if(substr(OCP\Util::getRequestUri(), 0, strlen(OC_App::getAppWebPath('contacts').'/carddav.php')) == OC_App::getAppWebPath('contacts').'/carddav.php') { $baseuri = OC_App::getAppWebPath('contacts').'/carddav.php'; @@ -33,15 +34,19 @@ OC_App::loadApps($RUNTIME_APPTYPES); // Backends $authBackend = new OC_Connector_Sabre_Auth(); $principalBackend = new OC_Connector_Sabre_Principal(); -$carddavBackend = new OC_Connector_Sabre_CardDAV(); + +$addressbookbackends = array(); +$addressbookbackends[] = new OCA\Contacts\Backend\Shared(); +$addressbookbackends[] = new OCA\Contacts\Backend\Database(); +$carddavBackend = new OCA\Contacts\CardDAV\Backend($addressbookbackends); $requestBackend = new OC_Connector_Sabre_Request(); // Root nodes $principalCollection = new Sabre_CalDAV_Principal_Collection($principalBackend); -$principalCollection->disableListing = true; // Disable listening +$principalCollection->disableListing = true; // Disable listing -$addressBookRoot = new OC_Connector_Sabre_CardDAV_AddressBookRoot($principalBackend, $carddavBackend); -$addressBookRoot->disableListing = true; // Disable listening +$addressBookRoot = new OCA\Contacts\CardDAV\AddressBookRoot($principalBackend, $carddavBackend); +$addressBookRoot->disableListing = true; // Disable listing $nodes = array( $principalCollection, @@ -54,10 +59,13 @@ $server->httpRequest = $requestBackend; $server->setBaseUri($baseuri); // Add plugins $server->addPlugin(new Sabre_DAV_Auth_Plugin($authBackend, 'ownCloud')); -$server->addPlugin(new Sabre_CardDAV_Plugin()); +$server->addPlugin(new OCA\Contacts\CardDAV\Plugin()); $server->addPlugin(new Sabre_DAVACL_Plugin()); $server->addPlugin(new Sabre_DAV_Browser_Plugin(false)); // Show something in the Browser, but no upload $server->addPlugin(new Sabre_CardDAV_VCFExportPlugin()); +if(defined('DEBUG') && DEBUG) { + $server->debugExceptions = true; +} // And off we go! $server->exec(); diff --git a/appinfo/routes.php b/appinfo/routes.php new file mode 100644 index 00000000..bdb4a18a --- /dev/null +++ b/appinfo/routes.php @@ -0,0 +1,532 @@ +create('contacts_index', '/') + ->actionInclude('contacts/index.php'); +// ->action( +// function($params){ +// // +// } +// ); + +$this->create('contacts_jsconfig', 'ajax/config.js') + ->actionInclude('contacts/js/config.php'); + +/* TODO: + - Check what it requires to be a RESTful API. I think maybe {user} + shouldn't be in the URI but be authenticated in headers or elsewhere. + - Do security checks: logged in, csrf + - Move the actual code to controllers. +*/ +$this->create('contacts_address_books_for_user', 'addressbooks/{user}/') + ->get() + ->action( + function($params) { + session_write_close(); + $app = new App($params['user']); + $addressBooks = $app->getAddressBooksForUser(); + $response = array(); + foreach($addressBooks as $addressBook) { + $response[] = $addressBook->getMetaData(); + } + \OCP\JSON::success(array( + 'data' => array( + 'addressbooks' => $response, + ) + )); + } + ) + ->requirements(array('user')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_address_book_collection', 'addressbook/{user}/{backend}/{addressbookid}/contacts') + ->get() + ->action( + function($params) { + session_write_close(); + $app = new App($params['user']); + $addressBook = $app->getAddressBook($params['backend'], $params['addressbookid']); + $lastModified = $addressBook->lastModified(); + if(!is_null($lastModified)) { + \OCP\Response::enableCaching(); + \OCP\Response::setLastModifiedHeader($lastModified); + \OCP\Response::setETagHeader(md5($lastModified)); + } + $contacts = array(); + foreach($addressBook->getChildren() as $contact) { + //$contact->retrieve(); + //error_log(__METHOD__.' jsondata: '.print_r($contact, true)); + $response = Utils\JSONSerializer::serializeContact($contact); + if($response !== null) { + $contacts[] = $response; + } + } + \OCP\JSON::success(array( + 'data' => array( + 'contacts' => $contacts, + ) + )); + } + ) + ->requirements(array('user', 'backend', 'addressbookid')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_address_book_add', 'addressbook/{user}/{backend}/add') + ->post() + ->action( + function($params) { + session_write_close(); + $app = new App($params['user']); + $backend = App::getBackend('local', $params['user']); + $id = $backend->createAddressBook($_POST); + if($id === false) { + bailOut(App::$l10n->t('Error creating address book')); + } + \OCP\JSON::success(array( + 'data' => $backend->getAddressBook($id) + )); + } + ) + ->requirements(array('user', 'backend', 'addressbookid')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_address_book_delete', 'addressbook/{user}/{backend}/{addressbookid}/delete') + ->post() + ->action( + function($params) { + session_write_close(); + $app = new App($params['user']); + $backend = App::getBackend('local', $params['user']); + if(!$backend->deleteAddressBook($params['addressbookid'])) { + bailOut(App::$l10n->t('Error deleting address book')); + } + \OCP\JSON::success(); + } + ) + ->requirements(array('user', 'backend', 'addressbookid')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_address_book_add_contact', 'addressbook/{user}/{backend}/{addressbookid}/contact/add') + ->post() + ->action( + function($params) { + session_write_close(); + $app = new App($params['user']); + $addressBook = $app->getAddressBook($params['backend'], $params['addressbookid']); + $id = $addressBook->addChild(); + if($id === false) { + bailOut(App::$l10n->t('Error creating contact.')); + } + $contact = $addressBook->getChild($id); + \OCP\JSON::success(array( + 'data' => Utils\JSONSerializer::serializeContact($contact), + )); + } + ) + ->requirements(array('user', 'backend', 'addressbookid')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_address_book_delete_contact', 'addressbook/{user}/{backend}/{addressbookid}/contact/{contactid}/delete') + ->post() + ->action( + function($params) { + session_write_close(); + $app = new App($params['user']); + $addressBook = $app->getAddressBook($params['backend'], $params['addressbookid']); + $response = $addressBook->deleteChild($params['contactid']); + if($response === false) { + bailOut(App::$l10n->t('Error deleting contact.')); + } + \OCP\JSON::success(); + } + ) + ->requirements(array('user', 'backend', 'addressbookid', 'contactid')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_contact_photo', 'addressbook/{user}/{backend}/{addressbookid}/contact/{contactid}/photo') + ->get() + ->action( + function($params) { + // TODO: Cache resized photo + session_write_close(); + $etag = null; + $caching = null; + $max_size = 170; + $app = new App(); + $contact = $app->getContact($params['backend'], $params['addressbookid'], $params['contactid']); + $image = new \OC_Image(); + if (isset($contact->PHOTO) && $image->loadFromBase64((string)$contact->PHOTO)) { + // OK + $etag = md5($contact->PHOTO); + } + else + // Logo :-/ + if (isset($contact->LOGO) && $image->loadFromBase64((string)$contact->LOGO)) { + // OK + $etag = md5($contact->LOGO); + } + if ($image->valid()) { + $modified = $contact->lastModified(); + // Force refresh if modified within the last minute. + if(!is_null($modified)) { + $caching = (time() - $modified > 60) ? null : 0; + } + \OCP\Response::enableCaching($caching); + if(!is_null($modified)) { + \OCP\Response::setLastModifiedHeader($modified); + } + if($etag) { + \OCP\Response::setETagHeader($etag); + } + if ($image->width() > $max_size || $image->height() > $max_size) { + $image->resize($max_size); + } + header('Content-Type: ' . $image->mimeType()); + $image->show(); + } + } + ) + ->requirements(array('user', 'backend', 'addressbook', 'contactid')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_contact_delete_property', 'addressbook/{user}/{backend}/{addressbookid}/contact/{contactid}/property/delete') + ->post() + ->action( + function($params) { + session_write_close(); + $request = Request::getRequest($params); + $name = $request->post['name']; + $checksum = isset($request->post['checksum']) ? $request->post['checksum'] : null; + + debug('contacts_contact_delete_property, name: ' . print_r($name, true)); + debug('contacts_contact_delete_property, checksum: ' . print_r($checksum, true)); + + $app = new App($request->parameters['user']); + $contact = $app->getContact( + $request->parameters['backend'], + $request->parameters['addressbookid'], + $request->parameters['contactid'] + ); + + if(!$contact) { + bailOut(App::$l10n->t('Couldn\'t find contact.')); + } + if(!$name) { + bailOut(App::$l10n->t('Property name is not set.')); + } + if(!$checksum && in_array($name, Utils\Properties::$multi_properties)) { + bailOut(App::$l10n->t('Property checksum is not set.')); + } + if(!is_null($checksum)) { + try { + $contact->unsetPropertyByChecksum($checksum); + } catch(Exception $e) { + bailOut(App::$l10n->t('Information about vCard is incorrect. Please reload the page.')); + } + } else { + unset($contact->{$name}); + } + if(!$contact->save()) { + bailOut(App::$l10n->t('Error saving contact to backend.')); + } + \OCP\JSON::success(array( + 'data' => array( + 'backend' => $request->parameters['backend'], + 'addressbookid' => $request->parameters['addressbookid'], + 'contactid' => $request->parameters['contactid'], + 'lastmodified' => $contact->lastModified(), + ) + )); + } + ) + ->requirements(array('user', 'backend', 'addressbook', 'contactid')) + ->defaults(array('user' => \OCP\User::getUser())); + +// Save a single property. +$this->create('contacts_contact_save_property', 'addressbook/{user}/{backend}/{addressbookid}/contact/{contactid}/property/save') + ->post() + ->action( + function($params) { + session_write_close(); + $request = Request::getRequest($params); + // TODO: When value is empty unset the property and return a checksum of 'new' if multi_property + $name = $request->post['name']; + $value = $request->post['value']; + $parameters = isset($request->post['parameters']) ? $request->post['parameters'] : null; + $checksum = isset($request->post['checksum']) ? $request->post['checksum'] : null; + + debug('contacts_contact_save_property, name: ' . print_r($name, true)); + debug('contacts_contact_save_property, value: ' . print_r($value, true)); + debug('contacts_contact_save_property, parameters: ' . print_r($parameters, true)); + debug('contacts_contact_save_property, checksum: ' . print_r($checksum, true)); + + $app = new App($params['user']); + $contact = $app->getContact($params['backend'], $params['addressbookid'], $params['contactid']); + + $response = array('contactid' => $params['contactid']); + + if(!$contact) { + bailOut(App::$l10n->t('Couldn\'t find contact.')); + } + if(!$name) { + bailOut(App::$l10n->t('Property name is not set.')); + } + if(is_array($value)) { + // NOTE: Important, otherwise the compound value will be + // set in the order the fields appear in the form! + ksort($value); + } + if(!$checksum && in_array($name, Utils\Properties::$multi_properties)) { + bailOut(App::$l10n->t('Property checksum is not set.')); + } elseif($checksum && in_array($name, Utils\Properties::$multi_properties)) { + try { + $checksum = $contact->setPropertyByChecksum($checksum, $name, $value, $parameters); + $response['checksum'] = $checksum; + } catch(Exception $e) { + bailOut(App::$l10n->t('Information about vCard is incorrect. Please reload the page.')); + } + } elseif(!in_array($name, Utils\Properties::$multi_properties)) { + if(!$contact->setPropertyByName($name, $value, $parameters)) { + bailOut(App::$l10n->t('Error setting property')); + } + } + if(!$contact->save()) { + bailOut(App::$l10n->t('Error saving property to backend')); + } + $response['lastmodified'] = $contact->lastModified(); + $contact->save(); + \OCP\JSON::success(array('data' => $response)); + } + ) + ->requirements(array('user', 'backend', 'addressbook', 'contactid')) + ->defaults(array('user' => \OCP\User::getUser())); + +// Save all properties. Used for merging contacts. +$this->create('contacts_contact_save_all', 'addressbook/{user}/{backend}/{addressbookid}/contact/{contactid}/save') + ->post() + ->action( + function($params) { + session_write_close(); + $request = Request::getRequest($params); + \OCP\Util::writeLog('contacts', __METHOD__.' params: '.print_r($request->parameters, true), \OCP\Util::DEBUG); + + $app = new App($params['user']); + $contact = $app->getContact($params['backend'], $params['addressbookid'], $params['contactid']); + + $response = array('contactid' => $params['contactid']); + + if(!$contact) { + bailOut(App::$l10n->t('Couldn\'t find contact.')); + } + if(!$contact->mergeFromArray($request->params)) { + bailOut(App::$l10n->t('Error merging into contact.')); + } + if(!$contact->save()) { + bailOut(App::$l10n->t('Error saving contact to backend.')); + } + $data = Utils\JSONSerializer::serializeContact($contact); + \OCP\JSON::success(array('data' => $data)); + } + ) + ->requirements(array('user', 'backend', 'addressbook', 'contactid')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_categories_list', 'groups/{user}/') + ->get() + ->action( + function($params) { + session_write_close(); + $catmgr = new \OC_VCategories('contact', $params['user']); + $categories = $catmgr->categories(\OC_VCategories::FORMAT_MAP); + foreach($categories as &$category) { + $ids = $catmgr->idsForCategory($category['name']); + $category['contacts'] = $ids; + } + + $favorites = $catmgr->getFavorites(); + + \OCP\JSON::success(array( + 'data' => array( + 'categories' => $categories, + 'favorites' => $favorites, + 'shared' => \OCP\Share::getItemsSharedWith('addressbook', Share_Backend_Addressbook::FORMAT_ADDRESSBOOKS), + 'lastgroup' => \OCP\Config::getUserValue( + $params['user'], + 'contacts', + 'lastgroup', 'all'), + 'sortorder' => \OCP\Config::getUserValue( + $params['user'], + 'contacts', + 'groupsort', ''), + ) + ) + ); + } + ) + ->requirements(array('user')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_categories_add', 'groups/{user}/add') + ->post() + ->action( + function($params) { + session_write_close(); + $request = Request::getRequest($params); + $name = $request->post['name']; + + if(is_null($name) || $name === "") { + bailOut(App::$l10n->t('No group name given.')); + } + + $catman = new \OC_VCategories('contact', $params['user']); + $id = $catman->add($name); + + if($id !== false) { + \OCP\JSON::success(array('data' => array('id'=>$id, 'name' => $name))); + } else { + bailOut(App::$l10n->t('Error adding group.')); + } + } + ) + ->requirements(array('user')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_categories_delete', 'groups/{user}/delete') + ->post() + ->action( + function($params) { + session_write_close(); + $request = Request::getRequest($params); + $name = $request->post['name']; + + if(is_null($name) || $name === "") { + bailOut(App::$l10n->t('No group name given.')); + } + + $catman = new \OC_VCategories('contact', $params['user']); + $catman->delete($name); + + \OCP\JSON::success(); + } + ) + ->requirements(array('user')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_categories_addto', 'groups/{user}/addto/{categoryid}') + ->post() + ->action( + function($params) { + session_write_close(); + $request = Request::getRequest($params); + $categoryid = $request['categoryid']; + $ids = $request['contactids']; + debug('request: '.print_r($request->post, true)); + + if(is_null($categoryid) || $categoryid === '') { + bailOut(App::$l10n->t('Group ID missing from request.')); + } + + if(is_null($ids)) { + bailOut(App::$l10n->t('Contact ID missing from request.')); + } + + $catman = new \OC_VCategories('contact', $params['user']); + foreach($ids as $contactid) { + debug('contactid: ' . $contactid . ', categoryid: ' . $categoryid); + $catman->addToCategory($contactid, $categoryid); + } + + \OCP\JSON::success(); + } + ) + ->requirements(array('user', 'categoryid')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_categories_removefrom', 'groups/{user}/removefrom/{categoryid}') + ->post() + ->action( + function($params) { + session_write_close(); + $request = Request::getRequest($params); + $categoryid = $request['categoryid']; + $ids = $request['contactids']; + debug('request: '.print_r($request->post, true)); + + if(is_null($categoryid) || $categoryid === '') { + bailOut(App::$l10n->t('Group ID missing from request.')); + } + + if(is_null($ids)) { + bailOut(App::$l10n->t('Contact ID missing from request.')); + } + + $catman = new \OC_VCategories('contact', $params['user']); + foreach($ids as $contactid) { + debug('contactid: ' . $contactid . ', categoryid: ' . $categoryid); + $catman->removeFromCategory($contactid, $categoryid); + } + + \OCP\JSON::success(); + } + ) + ->requirements(array('user', 'categoryid')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_setpreference', 'preference/{user}/set') + ->post() + ->action( + function($params) { + session_write_close(); + $request = Request::getRequest($params); + $key = $request->post['key']; + $value = $request->post['value']; + + if(is_null($key) || $key === "") { + bailOut(App::$l10n->t('No key is given.')); + } + + if(is_null($value) || $value === "") { + bailOut(App::$l10n->t('No value is given.')); + } + + if(\OCP\Config::setUserValue($params['user'], 'contacts', $key, $value)) { + \OCP\JSON::success(array( + 'data' => array( + 'key' => $key, + 'value' => $value) + ) + ); + } else { + bailOut(App::$l10n->t( + 'Could not set preference: ' . $key . ':' . $value) + ); + } + } + ) + ->requirements(array('user')) + ->defaults(array('user' => \OCP\User::getUser())); + +$this->create('contacts_index_properties', 'indexproperties/{user}/') + ->post() + ->action( + function($params) { + session_write_close(); + \OC_Hook::emit('OCA\Contacts', 'indexProperties', array()); + + \OCP\Config::setUserValue($params['user'], 'contacts', 'contacts_properties_indexed', 'yes'); + \OCP\JSON::success(array('isIndexed' => true)); + } + ) + ->requirements(array('user')) + ->defaults(array('user' => \OCP\User::getUser())); diff --git a/css/contacts.css b/css/contacts.css index c1b1745a..7fe0ccc6 100644 --- a/css/contacts.css +++ b/css/contacts.css @@ -30,14 +30,14 @@ top: -8px; left: -6px; } -#content textarea { font-family: inherit; } +#contact textarea { font-family: inherit; } -#content ::-moz-placeholder, #content input:-moz-placeholder, #content input[placeholder], #content input:placeholder, #content input:-ms-input-placeholder, #content input::-webkit-input-placeholder, #content input:-moz-placeholder { +#contact ::-moz-placeholder, #contact input:-moz-placeholder, #contact input[placeholder], #contact input:placeholder, #contact input:-ms-input-placeholder, #contact input::-webkit-input-placeholder, #contact input:-moz-placeholder { color: #bbb; text-overflow: ellipsis; } -#content input:not([type="checkbox"]), #content select:not(.button), #content textarea { +#contact input:not([type="checkbox"]), #contact select:not(.button), #contact textarea { background-color: #fefefe; border: 1px solid #fff !important; -moz-appearance:none !important; -webkit-appearance: none !important; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; @@ -45,17 +45,17 @@ -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; float: left; } -#content input[type="button"]:hover, #content select:hover, #content select:focus, #content select:active, #content input[type="button"]:focus, #content .button:hover, button:hover { background-color:#fff; color:#333; } +#contact input[type="button"]:hover, #contact select:hover, #contact select:focus, #contact select:active, #contact input[type="button"]:focus, #contact .button:hover, button:hover { background-color:#fff; color:#333; } -#content fieldset, #content div, #content span, #content section { +#contact fieldset, #contact div, #contact span, #contact section { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } -#content fieldset.editor { +#contact fieldset.editor { padding:4px; border: 1px solid #1d2d44; -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; outline:none; } -#content input:invalid, #content input:hover:not([type="checkbox"]), #content input:active:not([type="checkbox"]), #content input:focus:not([type="checkbox"]), #content input.active, #content textarea:focus, #content textarea:hover { +#contact input:invalid, #contact input:hover:not([type="checkbox"]), #contact input:active:not([type="checkbox"]), #contact input:focus:not([type="checkbox"]), #contact input.active, #contact textarea:focus, #contact textarea:hover { border: 1px solid silver !important; -moz-border-radius:.3em; -webkit-border-radius:.3em; border-radius:.3em; outline:none; float: left; @@ -137,6 +137,7 @@ #grouplist { z-index: 100; } #grouplist h3 .action:not(.starred):not(.checked):not(.favorite) { float: right; display: none; padding: 0; margin: auto; } #grouplist h3:not([data-type="shared"]):not(.editing):hover .action.numcontacts, #grouplist h3:not([data-type="shared"]):not(.editing) .active.action.numcontacts { display: inline-block; } +.action.numcontacts { width: 6em; } #grouplist h3.active[data-type="category"]:not(.editing):hover .action.delete { display: inline-block; } #grouplist h3:not(.editing) .action.delete { width: 20px; height: 20px; } @@ -201,7 +202,6 @@ .no-svg .mail { background-image:url('%webroot%/core/img/actions/mail.png'); } .no-svg .import, .no-svg .upload { background-image:url('%webroot%/core/img/actions/upload.png'); } .no-svg .export, .no-svg .download { background-image:url('%webroot%/core/img/actions/download.png'); } -/*.no-svg .cloud { background-image:url('%webroot%/core/img/places/picture.png'); }*/ .no-svg .globe { background-image:url('%webroot%/core/img/actions/public.png'); } .no-svg .settings { background-image:url('%webroot%/core/img/actions/settings.svg'); } .no-svg .starred { background-image:url('%appswebroot%/contacts/img/starred.png'); background-size: contain; } @@ -222,7 +222,6 @@ .svg .mail { background-image:url('%webroot%/core/img/actions/mail.svg'); } .svg .import,.svg .upload { background-image:url('%webroot%/core/img/actions/upload.svg'); } .svg .export,.svg .download { background-image:url('%webroot%/core/img/actions/download.svg'); } -/*.svg .cloud { background-image:url('%webroot%/core/img/places/picture.svg'); }*/ .svg .globe { background-image:url('%webroot%/core/img/actions/public.svg'); } .svg .settings { background-image:url('%webroot%/core/img/actions/settings.svg'); } .svg .starred { background-image:url('%appswebroot%/contacts/img/starred.svg'); background-size: contain; } @@ -246,11 +245,16 @@ } .control > * { background: none repeat scroll 0 0 #F8F8F8; color: #555 !important; font-size: 100%; margin: 0px; } +.dim { + opacity: .50;filter:Alpha(Opacity=50); + z-index: 0; +} + .ui-draggable { height: 3em; z-index: 1000; } -.ui-draggable { height: 3em; z-index: 1000; } -.ui-draggable-dragging { +.dragContact { cursor: move; background-repeat: no-repeat !important; + background-position: .3em .3em !important; background-color: #fff; z-index: 5; width: 200px; height: 30px; @@ -258,6 +262,8 @@ font-weight: bold; border: thin solid silver; border-radius: 3px; padding: 3px; + z-index: 1000; + transition: background-image 500ms ease 0s; } .ui-state-hover { border: 1px solid dashed; z-index: 1; } @@ -318,7 +324,7 @@ ul.propertylist { width: 450px; } .propertylist li > input[type="checkbox"].impp { clear: none; } .propertylist li > label.xab { display: block; color: #bbb; float:left; clear: both; padding: 0.5em 0 0 2.5em; } .propertylist li > label.xab:hover { color: #777; } -#rightcontent label, #rightcontent dt, #rightcontent th, #rightcontent .label { +#contact label, #contact dt, #contact th, #contact .label { float: left; font-size: 0.7em; font-weight: bold; color: #bbb !important; @@ -327,9 +333,9 @@ ul.propertylist { width: 450px; } box-sizing: border-box; -moz-box-sizing: border-box; } -#rightcontent label:hover, #rightcontent dt:hover, #rightcontent input.label:hover { color: #777 !important; } -#rightcontent input.label, #rightcontent input.label { margin:-2px; } -#rightcontent input.label:hover, #rightcontent input.label:active { border: 0 none !important; border-radius: 0; cursor: pointer; } +#contact label:hover, #contact dt:hover, #contact input.label:hover { color: #777 !important; } +#contact input.label, #contact input.label { margin:-2px; } +#contact input.label:hover, #contact input.label:active { border: 0 none !important; border-radius: 0; cursor: pointer; } .typelist[type="button"] { float: left; max-width: 8em; border: 0; background-color: #fff; color: #bbb; box-shadow: none; } /* for multiselect */ .typelist[type="button"]:hover { color: #777; } /* for multiselect */ .addresslist { clear: both; font-weight: bold; } @@ -407,7 +413,7 @@ ul.propertylist { width: 450px; } } /* Single elements */ #file_upload_target, #import_upload_target, #crop_target { display:none; } -#import_fileupload { +#import_upload_start { height: 2.29em; /*width: 2.5em;*/ width: 95%; @@ -417,6 +423,12 @@ ul.propertylist { width: 450px; } cursor: pointer; z-index: 1001; } +/* Those dang Germans and their long sentences ;) */ +label[for="import_upload_start"] { + display: inline-block; + max-width: 16em; + white-space: pre-line; +} .import-upload-button { background-image: url("%webroot%/core/img/actions/upload.svg"); background-position: center center; @@ -438,6 +450,10 @@ input:not([type="checkbox"]).propertytype { text-align: right; margin: 0; } + +.thumbnail { + background-image:url('%appswebroot%/contacts/img/person.png'); +} input[type="checkbox"].propertytype { width: 10px; } .contactphoto { float: left; display: inline-block; @@ -466,8 +482,27 @@ input[type="checkbox"].propertytype { width: 10px; } #phototools li { display: inline; } #phototools li a { float:left; opacity: 0.6; } #phototools li a:hover { opacity: 0.8; } -#contactphoto_fileupload, #import_fileupload { -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; filter:alpha(opacity=0); opacity:0; z-index:1001; } +#contactphoto_fileupload, #import_upload_start { -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; filter:alpha(opacity=0); opacity:0; z-index:1001; } +#dialog-merge-contacts .mergelist { + margin: 10px; +} +#dialog-merge-contacts .mergelist > li { + height: 30px; + -webkit-transition:background-image 500ms; -moz-transition:background-image 500ms; -o-transition:background-image 500ms; transition:background-image 500ms; + position:relative; + background-position: 0 .2em !important; background-repeat:no-repeat !important; +} +#dialog-merge-contacts .mergelist > li > input { + opacity: 0; +} +#dialog-merge-contacts .mergelist > li > input:hover, #dialog-merge-contacts .mergelist > li > input:checked { + opacity: 1; +} +#dialog-merge-contacts .mergelist > li > label { + padding-left: 20px; + font-weight: normal; +} .no-svg .favorite { display: inline-block; float: left; height: 20px; width: 20px; background-image:url('%appswebroot%/contacts/img/inactive_star.png'); } .no-svg .favorite.active, .favorite:hover { background-image:url('%appswebroot%/contacts/img/active_star.png'); } .no-svg .favorite.inactive { background-image:url('%appswebroot%/contacts/img/inactive_star.png'); } @@ -558,7 +593,6 @@ input[type="checkbox"].propertytype { width: 10px; } #contactlist tr > td.name { font-weight: bold; text-indent: 1.6em; -webkit-transition:background-image 500ms; -moz-transition:background-image 500ms; -o-transition:background-image 500ms; transition:background-image 500ms; position:relative; background-position:1em .5em !important; background-repeat:no-repeat !important; } #contactlist tr > td a.mailto { position: absolute; float: right; clear: none; cursor:pointer; width:16px; height:16px; margin: 2px; z-index: 200; opacity: 0.6; background-image:url('%webroot%/core/img/actions/mail.svg'); } #contactlist tr > td a.mailto:hover { opacity: 0.8; } -#contactlist.dim { background-image: #ddd; opacity: .50;filter:Alpha(Opacity=50); } #contact { background-color: white; color: #333333; diff --git a/css/jquery.ocdialog.css b/css/jquery.ocdialog.css new file mode 100644 index 00000000..f7a6e24e --- /dev/null +++ b/css/jquery.ocdialog.css @@ -0,0 +1,36 @@ +.oc-dialog { + background: white; + color: #333333; + border-radius: 3px; box-shadow: 0 0 7px #888888; + padding: 15px; + z-index: 200; + font-size: 100%; +} +.oc-dialog-title { + background: white; + font-weight: bold; + font-size: 110%; + margin-bottom: 10px; +} +.oc-dialog-content { + z-index: 200; + background: white; + overflow-y: auto; +} +.oc-dialog-separator { +} +.oc-dialog-buttonrow { + background: white; + float: right; + position: relative; + bottom: 0; + display: block; + margin-top: 10px; +} + +.oc-dialog-close { + position:absolute; + top:7px; right:7px; + height:20px; width:20px; + background:url('%webroot%/core/img/actions/delete.svg') no-repeat center; +} diff --git a/export.php b/export.php index 588a6df7..38b5cd4c 100644 --- a/export.php +++ b/export.php @@ -43,7 +43,9 @@ if(!is_null($bookid)) { } } elseif(!is_null($contactid)) { try { - $data = OCA\Contacts\VCard::find($contactid); + $app = new OCA\Contacts\App(); + $contact = $app->getContact($_GET['backend'], $_GET['addressbookid'], $_GET['contactid']); + $data = $contact->serialize(); } catch(Exception $e) { OCP\JSON::error( array( @@ -56,8 +58,8 @@ if(!is_null($bookid)) { } header('Content-Type: text/vcard'); header('Content-Disposition: inline; filename=' - . str_replace(' ', '_', $data['fullname']) . '.vcf'); - echo $data['carddata']; + . str_replace(' ', '_', $contact->FN) . '.vcf'); + echo $data; } elseif(!is_null($selectedids)) { $selectedids = explode(',', $selectedids); $l10n = \OC_L10N::get('contacts'); diff --git a/import.php b/import.php index c2a74869..6190686b 100644 --- a/import.php +++ b/import.php @@ -5,12 +5,17 @@ * later. * See the COPYING-README file. */ + +namespace OCA\Contacts; + +use Sabre\VObject; + //check for addressbooks rights or create new one ob_start(); -OCP\JSON::checkLoggedIn(); -OCP\App::checkAppEnabled('contacts'); -OCP\JSON::callCheck(); +\OCP\JSON::checkLoggedIn(); +\OCP\App::checkAppEnabled('contacts'); +\OCP\JSON::callCheck(); session_write_close(); $nl = "\n"; @@ -19,70 +24,49 @@ global $progresskey; $progresskey = 'contacts.import-' . (isset($_GET['progresskey'])?$_GET['progresskey']:''); if (isset($_GET['progress']) && $_GET['progress']) { - echo OC_Cache::get($progresskey); + echo \OC_Cache::get($progresskey); die; } function writeProgress($pct) { global $progresskey; - OC_Cache::set($progresskey, $pct, 300); + \OC_Cache::set($progresskey, $pct, 300); } writeProgress('10'); $view = null; $inputfile = strtr($_POST['file'], array('/' => '', "\\" => '')); -if(OC\Files\Filesystem::isFileBlacklisted($inputfile)) { - OCP\JSON::error(array('data' => array('message' => 'Upload of blacklisted file: ' . $inputfile))); +if(\OC\Files\Filesystem::isFileBlacklisted($inputfile)) { + \OCP\JSON::error(array('data' => array('message' => 'Upload of blacklisted file: ' . $inputfile))); exit(); } if(isset($_POST['fstype']) && $_POST['fstype'] == 'OC_FilesystemView') { - $view = OCP\Files::getStorage('contacts'); + $view = \OCP\Files::getStorage('contacts'); $file = $view->file_get_contents('/imports/' . $inputfile); } else { $file = \OC\Files\Filesystem::file_get_contents($_POST['path'] . '/' . $inputfile); } if(!$file) { - OCP\JSON::error(array('data' => array('message' => 'Import file was empty.'))); + \OCP\JSON::error(array('data' => array('message' => 'Import file was empty.'))); exit(); } -if(isset($_POST['method']) && $_POST['method'] == 'new') { - $id = OCA\Contacts\Addressbook::add(OCP\USER::getUser(), - $_POST['addressbookname']); - if(!$id) { - OCP\JSON::error( - array( - 'data' => array('message' => 'Error creating address book.') + +$id = $_POST['id']; + +if(!$id) { + \OCP\JSON::error( + array( + 'data' => array( + 'message' => 'Error getting the ID of the address book.', + 'file'=>\OCP\Util::sanitizeHTML($inputfile) ) - ); - exit(); - } - OCA\Contacts\Addressbook::setActive($id, 1); -}else{ - $id = $_POST['id']; - if(!$id) { - OCP\JSON::error( - array( - 'data' => array( - 'message' => 'Error getting the ID of the address book.', - 'file'=>OCP\Util::sanitizeHTML($inputfile) - ) - ) - ); - exit(); - } - try { - OCA\Contacts\Addressbook::find($id); // is owner access check - } catch(Exception $e) { - OCP\JSON::error( - array( - 'data' => array( - 'message' => $e->getMessage(), - 'file'=>OCP\Util::sanitizeHTML($inputfile) - ) - ) - ); - exit(); - } + ) + ); + exit(); } + +$app = new App(); +$addressBook = $app->getAddressBook('local', $id); + //analyse the contacts file writeProgress('40'); $file = str_replace(array("\r","\n\n"), array("\n","\n"), $file); @@ -110,69 +94,72 @@ $imported = 0; $failed = 0; $partial = 0; if(!count($parts) > 0) { - OCP\JSON::error( + \OCP\JSON::error( array( 'data' => array( 'message' => 'No contacts to import in ' - . OCP\Util::sanitizeHTML($inputfile).'. Please check if the file is corrupted.', + . \OCP\Util::sanitizeHTML($inputfile).'. Please check if the file is corrupted.', 'file'=>OCP\Util::sanitizeHTML($inputfile) ) ) ); if(isset($_POST['fstype']) && $_POST['fstype'] == 'OC_FilesystemView') { if(!$view->unlink('/imports/' . $inputfile)) { - OCP\Util::writeLog('contacts', - 'Import: Error unlinking OC_FilesystemView ' . '/' . OCP\Util::sanitizeHTML($inputfile), - OCP\Util::ERROR); + \OCP\Util::writeLog('contacts', + 'Import: Error unlinking OC_FilesystemView ' . '/' . \OCP\Util::sanitizeHTML($inputfile), + \OCP\Util::ERROR); } } exit(); } foreach($parts as $part) { try { - $vcard = Sabre\VObject\Reader::read($part); - } catch (Sabre\VObject\ParseException $e) { + $vcard = VObject\Reader::read($part); + } catch (VObject\ParseException $e) { try { - $vcard = Sabre\VObject\Reader::read($part, Sabre\VObject\Reader::OPTION_IGNORE_INVALID_LINES); + $vcard = VObject\Reader::read($part, VObject\Reader::OPTION_IGNORE_INVALID_LINES); $partial += 1; - OCP\Util::writeLog('contacts', + \OCP\Util::writeLog('contacts', 'Import: Retrying reading card. Error parsing VCard: ' . $e->getMessage(), - OCP\Util::ERROR); - } catch (Exception $e) { + \OCP\Util::ERROR); + } catch (\Exception $e) { $failed += 1; - OCP\Util::writeLog('contacts', + \OCP\Util::writeLog('contacts', 'Import: skipping card. Error parsing VCard: ' . $e->getMessage(), - OCP\Util::ERROR); + \OCP\Util::ERROR); continue; // Ditch cards that can't be parsed by Sabre. } } try { - OCA\Contacts\VCard::add($id, $vcard); - $imported += 1; - } catch (Exception $e) { - OCP\Util::writeLog('contacts', - 'Error importing vcard: ' . $e->getMessage() . $nl . $vcard, - OCP\Util::ERROR); + if($addressBook->addChild($vcard)) { + $imported += 1; + } else { + $failed += 1; + } + } catch (\Exception $e) { + \OCP\Util::writeLog('contacts', __LINE__ . ' ' . + 'Error importing vcard: ' . $e->getMessage() . $nl . $vcard->serialize(), + \OCP\Util::ERROR); $failed += 1; } } //done the import writeProgress('100'); sleep(3); -OC_Cache::remove($progresskey); +\OC_Cache::remove($progresskey); if(isset($_POST['fstype']) && $_POST['fstype'] == 'OC_FilesystemView') { if(!$view->unlink('/imports/' . $inputfile)) { - OCP\Util::writeLog('contacts', + \OCP\Util::writeLog('contacts', 'Import: Error unlinking OC_FilesystemView ' . '/' . $inputfile, - OCP\Util::ERROR); + \OCP\Util::ERROR); } } -OCP\JSON::success( +\OCP\JSON::success( array( 'data' => array( 'imported'=>$imported, 'failed'=>$failed, - 'file'=>OCP\Util::sanitizeHTML($inputfile), + 'file'=>\OCP\Util::sanitizeHTML($inputfile), ) ) ); diff --git a/index.php b/index.php index 99b87bc1..b77aed01 100644 --- a/index.php +++ b/index.php @@ -1,64 +1,67 @@ + * Copyright (c) 2012, 2013 Thomas Tanghus * Copyright (c) 2011 Jakob Sack mail@jakobsack.de * This file is licensed under the Affero General Public License version 3 or * later. * See the COPYING-README file. */ +namespace OCA\Contacts; // Check if we are a user -OCP\User::checkLoggedIn(); -OCP\App::checkAppEnabled('contacts'); +\OCP\User::checkLoggedIn(); +\OCP\App::checkAppEnabled('contacts'); // Get active address books. This creates a default one if none exists. -$ids = OCA\Contacts\Addressbook::activeIds(OCP\USER::getUser()); +//$ids = OCA\Contacts\Addressbook::activeIds(OCP\USER::getUser()); // Load the files we need -OCP\App::setActiveNavigationEntry('contacts_index'); +\OCP\App::setActiveNavigationEntry('contacts_index'); -$impp_types = OCA\Contacts\App::getTypesOfProperty('IMPP'); -$adr_types = OCA\Contacts\App::getTypesOfProperty('ADR'); -$phone_types = OCA\Contacts\App::getTypesOfProperty('TEL'); -$email_types = OCA\Contacts\App::getTypesOfProperty('EMAIL'); -$ims = OCA\Contacts\App::getIMOptions(); +$impp_types = Utils\Properties::getTypesForProperty('IMPP'); +$adr_types = Utils\Properties::getTypesForProperty('ADR'); +$phone_types = Utils\Properties::getTypesForProperty('TEL'); +$email_types = Utils\Properties::getTypesForProperty('EMAIL'); +$ims = Utils\Properties::getIMOptions(); $im_protocols = array(); foreach($ims as $name => $values) { $im_protocols[$name] = $values['displayname']; } -$categories = OCA\Contacts\App::getCategories(); -$maxUploadFilesize = OCP\Util::maxUploadFilesize('/'); +$maxUploadFilesize = \OCP\Util::maxUploadFilesize('/'); -OCP\Util::addscript('', 'multiselect'); -OCP\Util::addscript('', 'jquery.multiselect'); -OCP\Util::addscript('', 'oc-vcategories'); -OCP\Util::addscript('contacts', 'modernizr.custom'); -OCP\Util::addscript('contacts', 'app'); -OCP\Util::addscript('contacts', 'contacts'); -OCP\Util::addscript('contacts', 'groups'); -OCP\Util::addscript('contacts', 'expanding'); -OCP\Util::addscript('contacts', 'jquery.combobox'); -OCP\Util::addscript('files', 'jquery.fileupload'); -OCP\Util::addscript('contacts', 'jquery.Jcrop'); -OCP\Util::addStyle('3rdparty/fontawesome', 'font-awesome'); -OCP\Util::addStyle('contacts', 'font-awesome'); -OCP\Util::addStyle('', 'multiselect'); -OCP\Util::addStyle('', 'jquery.multiselect'); -OCP\Util::addStyle('contacts', 'jquery.combobox'); -OCP\Util::addStyle('contacts', 'jquery.Jcrop'); -OCP\Util::addStyle('contacts', 'contacts'); +\OCP\Util::addscript('', 'multiselect'); +\OCP\Util::addscript('', 'jquery.multiselect'); +\OCP\Util::addscript('', 'oc-vcategories'); +\OCP\Util::addscript('', 'octemplate'); +\OCP\Util::addscript('contacts', 'modernizr.custom'); +\OCP\Util::addscript('contacts', 'app'); +\OCP\Util::addscript('contacts', 'contacts'); +\OCP\Util::addscript('contacts', 'storage'); +\OCP\Util::addscript('contacts', 'groups'); +//\OCP\Util::addscript('contacts', 'expanding'); +\OCP\Util::addscript('contacts', 'jquery.combobox'); +\OCP\Util::addscript('contacts', 'jquery.ocdialog'); +\OCP\Util::addscript('files', 'jquery.fileupload'); +\OCP\Util::addscript('contacts', 'jquery.Jcrop'); +\OCP\Util::addStyle('3rdparty/fontawesome', 'font-awesome'); +\OCP\Util::addStyle('contacts', 'font-awesome'); +\OCP\Util::addStyle('', 'multiselect'); +\OCP\Util::addStyle('', 'jquery.multiselect'); +\OCP\Util::addStyle('contacts', 'jquery.combobox'); +\OCP\Util::addStyle('contacts', 'jquery.ocdialog'); +\OCP\Util::addStyle('contacts', 'jquery.Jcrop'); +\OCP\Util::addStyle('contacts', 'contacts'); -$tmpl = new OCP\Template( "contacts", "contacts", "user" ); +$tmpl = new \OCP\Template( "contacts", "contacts", "user" ); $tmpl->assign('uploadMaxFilesize', $maxUploadFilesize); $tmpl->assign('uploadMaxHumanFilesize', - OCP\Util::humanFileSize($maxUploadFilesize), false); -$tmpl->assign('addressbooks', OCA\Contacts\Addressbook::all(OCP\USER::getUser())); + \OCP\Util::humanFileSize($maxUploadFilesize), false); +//$tmpl->assign('addressbooks', OCA\Contacts\Addressbook::all(OCP\USER::getUser())); $tmpl->assign('phone_types', $phone_types); $tmpl->assign('email_types', $email_types); $tmpl->assign('adr_types', $adr_types); $tmpl->assign('impp_types', $impp_types); -$tmpl->assign('categories', $categories); $tmpl->assign('im_protocols', $im_protocols); $tmpl->printPage(); diff --git a/js/app.js b/js/app.js index 5101dbbd..89e22d93 100644 --- a/js/app.js +++ b/js/app.js @@ -74,14 +74,6 @@ utils.moveCursorToEnd = function(el) { } }; -if (typeof Object.create !== 'function') { - Object.create = function (o) { - function F() {} - F.prototype = o; - return new F(); - }; -} - Array.prototype.clone = function() { return this.slice(0); }; @@ -157,25 +149,25 @@ OC.notify = function(params) { OC.Contacts = OC.Contacts || { init:function() { if(oc_debug === true) { - $(document).ajaxError(function(e, xhr, settings, exception) { - // Don't try to get translation because it's likely a network error. - OC.notify({ - message: 'error in: ' + settings.url + ', '+'error: ' + xhr.responseText - }); + $.error = console.error; + $(document).ajaxError(function(e, xhr, settings, exception) { + console.error('Error in: ', settings.url, ' : ', xhr.responseText, exception); }); } this.scrollTimeoutMiliSecs = 100; this.isScrolling = false; this.cacheElements(); + this.storage = new OC.Contacts.Storage(); this.contacts = new OC.Contacts.ContactList( + this.storage, this.$contactList, this.$contactListItemTemplate, this.$contactDragItemTemplate, this.$contactFullTemplate, this.detailTemplates ); - this.groups = new OC.Contacts.GroupList(this.$groupList, this.$groupListItemTemplate); + this.groups = new OC.Contacts.GroupList(this.storage, this.$groupList, this.$groupListItemTemplate); OCCategories.changed = this.groups.categoriesChanged; OCCategories.app = 'contacts'; OCCategories.type = 'contact'; @@ -233,7 +225,7 @@ OC.Contacts = OC.Contacts || { this.$ninjahelp = $('#ninjahelp'); this.$firstRun = $('#firstrun'); this.$settings = $('#contacts-settings'); - this.$importFileInput = $('#import_fileupload'); + this.$importFileInput = $('#import_upload_start'); this.$importIntoSelect = $('#import_into'); }, // Build the select to add/remove from groups. @@ -287,13 +279,18 @@ OC.Contacts = OC.Contacts || { $(window).trigger('beforeunload'); }); - $(window).bind('hashchange', function() { - console.log('hashchange', window.location.hash) + this.hashChange = function() { + //console.log('hashchange', window.location.hash) var id = parseInt(window.location.hash.substr(1)); - if(id) { + if(id && id !== self.currentid) { self.openContact(id); + } else if(!id && self.currentid) { + self.closeContact(self.currentid); } - }); + } + + $(window).bind('popstate', this.hashChange); + $(window).bind('hashchange', this.hashChange); // App specific events $(document).bind('status.contact.deleted', function(e, data) { @@ -312,7 +309,10 @@ OC.Contacts = OC.Contacts || { self.hideActions(); }); + // Keep error messaging at one place to be able to replace it. $(document).bind('status.contact.error', function(e, data) { + console.warn(data.message); + console.trace(); OC.notify({message:data.message}); }); @@ -344,11 +344,15 @@ OC.Contacts = OC.Contacts || { self.openContact(self.currentid); } }); - if(!result.is_indexed) { + if(!contacts_properties_indexed) { // Wait a couple of mins then check if contacts are indexed. setTimeout(function() { - OC.notify({message:t('contacts', 'Indexing contacts'), timeout:20}); - $.post(OC.filePath('contacts', 'ajax', 'indexproperties.php')); + $.when($.post(OC.Router.generate('contacts_index_properties'))) + .then(function(response) { + if(!response.isIndexed) { + OC.notify({message:t('contacts', 'Indexing contacts'), timeout:20}); + } + }); }, 10000); } else { console.log('contacts are indexed.'); @@ -418,41 +422,79 @@ OC.Contacts = OC.Contacts || { }); $(document).bind('request.contact.export', function(e, data) { - var id = parseInt(data.id); + var id = String(data.id); console.log('contact', data.id, 'request.contact.export'); - document.location.href = OC.linkTo('contacts', 'export.php') + '?contactid=' + self.currentid; + document.location.href = OC.linkTo('contacts', 'export.php') + '?' + $.param(data); }); $(document).bind('request.contact.close', function(e, data) { - var id = parseInt(data.id); + var id = String(data.id); console.log('contact', data.id, 'request.contact.close'); self.closeContact(id); }); $(document).bind('request.contact.delete', function(e, data) { - var id = parseInt(data.id); - console.log('contact', data.id, 'request.contact.delete'); + var id = String(data.contactid); + console.log('contact', data, 'request.contact.delete'); self.closeContact(id); - self.contacts.delayedDelete(id); + self.contacts.delayedDelete(data); self.$contactList.removeClass('dim'); self.showActions(['add']); }); - $(document).bind('request.select.contactphoto.fromlocal', function(e, result) { - console.log('request.select.contactphoto.fromlocal', result); - $('#contactphoto_fileupload').trigger('click'); + $(document).bind('request.contact.merge', function(e, data) { + console.log('contact','request.contact.merge', data); + var merger = self.contacts.findById(data.merger); + var mergees = []; + if(!merger) { + $(document).trigger('status.contact.error', { + message: t('contacts', 'Merge failed. Cannot find contact: {id}', {id:data.merger}) + }); + return; + } + $.each(data.mergees, function(idx, id) { + var contact = self.contacts.findById(id); + if(!contact) { + console.warn('cannot find', id, 'by id'); + } + mergees.push(contact); + }); + if(!merger.merge(mergees)) { + $(document).trigger('status.contact.error', { + message: t('contacts', 'Merge failed.') + }); + return; + } + merger.saveAll(function(response) { + if(response.error) { + $(document).trigger('status.contact.error', { + message: t('contacts', 'Merge failed. Error saving contact.') + }); + return; + } else { + if(data.deleteOther) { + self.contacts.delayedDelete(mergees); + } + self.openContact(merger.getId()); + } + }); }); - $(document).bind('request.select.contactphoto.fromcloud', function(e, result) { - console.log('request.select.contactphoto.fromcloud', result); + $(document).bind('request.select.contactphoto.fromlocal', function(e, metadata) { + console.log('request.select.contactphoto.fromlocal', metadata); + $('#contactphoto_fileupload').trigger('click', metadata); + }); + + $(document).bind('request.select.contactphoto.fromcloud', function(e, metadata) { + console.log('request.select.contactphoto.fromcloud', metadata); OC.dialogs.filepicker(t('contacts', 'Select photo'), function(path) { - self.cloudPhotoSelected(self.currentid, path); + self.cloudPhotoSelected(metadata, path); }, false, 'image', true); }); - $(document).bind('request.edit.contactphoto', function(e, result) { - console.log('request.edit.contactphoto', result); - self.editCurrentPhoto(result.id); + $(document).bind('request.edit.contactphoto', function(e, metadata) { + console.log('request.edit.contactphoto', metadata); + self.editCurrentPhoto(metadata); }); $(document).bind('request.addressbook.activate', function(e, result) { @@ -476,9 +518,8 @@ OC.Contacts = OC.Contacts || { } $.each(result.contacts, function(idx, contactid) { var contact = self.contacts.findById(contactid); - console.log('contactid', contactid, contact); - self.contacts.findById(contactid).removeFromGroup(result.groupname); + contact.removeFromGroup(result.groupname); }); }); @@ -494,10 +535,17 @@ OC.Contacts = OC.Contacts || { // Group sorted, save the sort order $(document).bind('status.groups.sorted', function(e, result) { console.log('status.groups.sorted', result); - $.post(OC.filePath('contacts', 'ajax', 'setpreference.php'), {'key':'groupsort', 'value':result.sortorder}, function(jsondata) { - if(jsondata.status !== 'success') { - OC.notify({message: jsondata ? jsondata.data.message : t('contacts', 'Network or server error. Please inform administrator.')}); + $.when(self.storage.setPreference('groupsort', result.sortorder)).then(function(response) { + if(response.error) { + OC.notify({message: response ? response.message : t('contacts', 'Network or server error. Please inform administrator.')}); } + }) + .fail(function(jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.log( "Request Failed: " + err); + $(document).trigger('status.contact.error', { + message: t('contacts', 'Failed saving sort order: {error}', {error:err}) + }); }); }); // Group selected, only show contacts from that group @@ -520,10 +568,17 @@ OC.Contacts = OC.Contacts || { } else { self.contacts.showContacts(self.currentgroup); } - $.post(OC.filePath('contacts', 'ajax', 'setpreference.php'), {'key':'lastgroup', 'value':self.currentgroup}, function(jsondata) { - if(!jsondata || jsondata.status !== 'success') { - OC.notify({message: (jsondata && jsondata.data) ? jsondata.data.message : t('contacts', 'Network or server error. Please inform administrator.')}); + $.when(self.storage.setPreference('lastgroup', self.currentgroup)).then(function(response) { + if(response.error) { + OC.notify({message: response.message}); } + }) + .fail(function(jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.log( "Request Failed: " + err); + $(document).trigger('status.contact.error', { + message: t('contacts', 'Failed saving last group: {error}', {error:err}) + }); }); self.$rightContent.scrollTop(0); }); @@ -567,6 +622,13 @@ OC.Contacts = OC.Contacts || { $('body').bind('click', bodyListener); } }); + $('#contactphoto_fileupload').on('click', function(event, metadata) { + var form = $('#file_upload_form'); + form.find('input[name="contactid"]').val(metadata.contactid); + form.find('input[name="addressbookid"]').val(metadata.addressbookid); + form.find('input[name="backend"]').val(metadata.backend); + }); + $('#contactphoto_fileupload').on('change', function() { self.uploadPhoto(this.files); }); @@ -593,38 +655,35 @@ OC.Contacts = OC.Contacts || { self.buildGroupSelect(); } if(isChecked) { - self.showActions(['add', 'download', 'groups', 'delete', 'favorite']); + self.showActions(['add', 'download', 'groups', 'delete', 'favorite', 'merge']); } else { self.showActions(['add']); } }); this.$contactList.on('change', 'input:checkbox', function(event) { - if($(this).is(':checked')) { - if(self.$groups.find('option').length === 1) { - self.buildGroupSelect(); - } - self.showActions(['add', 'download', 'groups', 'delete', 'favorite']); - } else if(self.contacts.getSelectedContacts().length === 0) { + var selected = self.contacts.getSelectedContacts(); + console.log('selected', selected.length); + if(selected.length > 0 && self.$groups.find('option').length === 1) { + self.buildGroupSelect(); + } + if(selected.length === 0) { self.showActions(['add']); + } else if(selected.length === 1) { + self.showActions(['add', 'download', 'groups', 'delete', 'favorite']); + } else { + self.showActions(['add', 'download', 'groups', 'delete', 'favorite', 'merge']); } }); // Add to/remove from group multiple contacts. - // FIXME: Refactor this to be usable for favoriting also. this.$groups.on('change', function() { var $opt = $(this).find('option:selected'); var action = $opt.parent().data('action'); - var ids, groupName, groupId, buildnow = false; + var groupName, groupId, buildnow = false; - // If a contact is open the action is only applied to that, - // otherwise on all selected items. - if(self.currentid) { - ids = [self.currentid]; - buildnow = true; - } else { - ids = self.contacts.getSelectedContacts(); - } + var contacts = self.contacts.getSelectedContacts(); + var ids = $.map(contacts, function(c) {return c.getId();}); self.setAllChecked(false); self.$toggleAll.prop('checked', false); @@ -637,7 +696,7 @@ OC.Contacts = OC.Contacts || { console.log('add group...'); self.$groups.val(-1); self.addGroup(function(response) { - if(response.status === 'success') { + if(!response.error) { groupId = response.id; groupName = response.name; self.groups.addTo(ids, groupId, function(result) { @@ -676,7 +735,6 @@ OC.Contacts = OC.Contacts || { groupName = $opt.text(), groupId = $opt.val(); - console.log('trut', groupName, groupId); if(action === 'add') { self.groups.addTo(ids, $opt.val(), function(result) { console.log('after add', result); @@ -751,14 +809,12 @@ OC.Contacts = OC.Contacts || { if(event.ctrlKey || event.metaKey) { event.stopPropagation(); event.preventDefault(); - console.log('select', event); self.dontScroll = true; self.contacts.select($(this).data('id'), true); return; } if($(event.target).is('a.mailto')) { var mailto = 'mailto:' + $.trim($(this).find('.email').text()); - console.log('mailto', mailto); try { window.location.href=mailto; } catch(e) { @@ -774,6 +830,7 @@ OC.Contacts = OC.Contacts || { * * @param object $list A jquery object of an unordered list * @param object book An object with the properties 'id', 'name' and 'permissions'. + * @param bool add Whether the address book list should be updated. */ var appendAddressBook = function($list, book, add) { if(add) { @@ -791,34 +848,25 @@ OC.Contacts = OC.Contacts || { var id = parseInt($(this).parents('li').first().data('id')); console.log('delete', id); var $li = $(this).parents('li').first(); - $.ajax({ - type:'POST', - url:OC.filePath('contacts', 'ajax', 'addressbook/delete.php'), - data:{ id: id }, - success:function(jsondata) { - console.log(jsondata); - if(jsondata.status == 'success') { - self.contacts.unsetAddressbook(id); - $li.remove(); - OC.notify({ - message:t('contacts','Deleting done. Click here to cancel reloading.'), - timeout:5, - timeouthandler:function() { - console.log('reloading'); - window.location.href = OC.linkTo('contacts', 'index.php'); - }, - clickhandler:function() { - console.log('reloading cancelled'); - OC.notify({cancel:true}); - } - }); - } else { - OC.notify({message:jsondata.data.message}); - } - }, - error:function(jqXHR, textStatus, errorThrown) { - OC.notify({message:textStatus + ': ' + errorThrown}); - id = false; + $.when(self.storage.deleteAddressBook('local',id)) + .then(function(response) { + if(!response.error) { + self.contacts.unsetAddressbook(id); + $li.remove(); + OC.notify({ + message:t('contacts','Deleting done. Click here to cancel reloading.'), + timeout:5, + timeouthandler:function() { + console.log('reloading'); + window.location.href = OC.Router.generate('contacts_index'); + }, + clickhandler:function() { + console.log('reloading cancelled'); + OC.notify({cancel:true}); + } + }); + } else { + OC.notify({message:response.message}); } }); }); @@ -827,7 +875,7 @@ OC.Contacts = OC.Contacts || { var book = self.contacts.addressbooks[id]; var uri = (book.owner === oc_current_user ) ? book.uri : book.uri + '_shared_by_' + book.owner; var link = OC.linkToRemote('carddav')+'/addressbooks/'+encodeURIComponent(oc_current_user)+'/'+encodeURIComponent(uri); - var $dropdown = $(''); + var $dropdown = $(''); $dropdown.appendTo($(this).parents('li').first()); var $input = $dropdown.find('input'); $input.focus().get(0).select(); @@ -840,60 +888,51 @@ OC.Contacts = OC.Contacts || { $list.append($li); }; - var $addAddressBookNew = this.$settings.find('.addaddressbook'); - var $addAddressBookPart = $addAddressBookNew.next('ul'); - var $addInput = $addAddressBookPart.find('input.addaddressbookinput').focus(); - $addInput.on('keydown', function(event) { - if(event.keyCode === 13) { - event.stopPropagation(); - $addAddressBookPart.find('.addaddressbookok').trigger('click'); - } - }); - $addAddressBookPart.on('click keydown', 'button', function(event) { - if(wrongKey(event)) { - return; - } - if($(this).is('.addaddressbookok')) { - if($addInput.val().trim() === '') { - return false; - } else { - var name = $addInput.val().trim(); - $addInput.addClass('loading'); - $addAddressBookPart.find('button input').prop('disabled', true); - console.log('adding', name); - self.addAddressbook({ - name: name, - description: '' - }, function(response) { - if(!response || !response.status) { - OC.notify({ - message:t('contacts', 'Network or server error. Please inform administrator.') - }); - return false; - } else if(response.status === 'error') { - OC.notify({message: response.message}); - return false; - } else if(response.status === 'success') { - var book = response.addressbook; - var $list = self.$settings.find('[data-id="addressbooks"]').next('ul'); - appendAddressBook($list, book, true); - } - $addInput.removeClass('loading'); - $addAddressBookPart.find('button input').prop('disabled', false); - $addAddressBookPart.hide().prev('button').show(); - }); - } - } else if($(this).is('.addaddressbookcancel')) { - $addAddressBookPart.hide().prev('button').show(); - } - }); - this.$settings.find('.addaddressbook').on('click keydown', function(event) { if(wrongKey(event)) { return; } $(this).hide(); - $addAddressBookPart.show(); + var $addAddressbookPart = $(this).next('ul').show(); + var $addinput = $addAddressbookPart.find('input.addaddressbookinput').focus(); + $addAddressbookPart.on('click keydown', 'button', function(event) { + if(wrongKey(event)) { + return; + } + if($(this).is('.addaddressbookok')) { + if($addinput.val().trim() === '') { + return false; + } else { + var name = $addinput.val().trim(); + $addinput.addClass('loading'); + $addAddressbookPart.find('button input').prop('disabled', true); + //console.log('adding', name); + $.when(self.storage.addAddressBook('local', + {displayname: name, description: ''})).then(function(response) { + if(response.error) { + OC.notify({message: response.message}); + return false; + } else { + var book = response.data; + var $list = self.$settings.find('[data-id="addressbooks"]').next('ul'); + appendAddressBook($list, book, true); + } + $addinput.removeClass('loading'); + $addAddressbookPart.find('button input').prop('disabled', false); + $addAddressbookPart.hide().prev('button').show(); + }) + .fail(function(jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.log( "Request Failed: " + err); + $(document).trigger('status.contact.error', { + message: t('contacts', 'Failed adding address book: {error}', {error:err}) + }); + }); + } + } else if($(this).is('.addaddressbookcancel')) { + $addAddressbookPart.hide().prev('button').show(); + } + }); }); this.$settings.find('h2').on('click keydown', function(event) { @@ -903,10 +942,9 @@ OC.Contacts = OC.Contacts || { if($(this).next('ul').is(':visible')) { return; } - console.log('settings'); + //console.log('settings'); var $list = $(this).next('ul'); if($(this).data('id') === 'addressbooks') { - console.log('addressbooks'); if(!self.$addressbookTmpl) { self.$addressbookTmpl = $('#addressbookTemplate'); @@ -922,27 +960,26 @@ OC.Contacts = OC.Contacts || { $list.find('a.action.share').css('display', 'none'); } } else if($(this).data('id') === 'import') { - console.log('import'); $('.import-upload').show(); $('.import-select').hide(); var addAddressbookCallback = function(select, name) { var id = false; - self.addAddressbook({ - name: name, - description: '' - }, function(response) { - if(!response || !response.status) { - OC.notify({ - message:t('contacts', 'Network or server error. Please inform administrator.') - }); - return false; - } else if(response.status === 'error') { + $.when(this.storage.addAddressBook('local', + {name: name, description: ''})).then(function(response) { + if(response.error) { OC.notify({message: response.message}); return false; - } else if(response.status === 'success') { - id = response.addressbook.id; + } else { + id = response.data.id; } + }) + .fail(function(jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.log( "Request Failed: " + err); + $(document).trigger('status.contact.error', { + message: t('contacts', 'Failed adding addres books: {error}', {error:err}) + }); }); return id; }; @@ -970,6 +1007,7 @@ OC.Contacts = OC.Contacts || { self.currentid = 'new'; // Properties that the contact doesn't know console.log('addContact, groupid', self.currentgroup); + self.$contactList.addClass('dim'); var groupprops = { favorite: false, groups: self.groups.categories, @@ -1009,8 +1047,9 @@ OC.Contacts = OC.Contacts || { } console.log('delete'); if(self.currentid) { - console.assert(utils.isUInt(self.currentid), 'self.currentid is not an integer'); - self.contacts.delayedDelete(self.currentid); + console.assert(typeof self.currentid === 'string', 'self.currentid is not a string'); + contactInfo = self.contacts[self.currentid].metaData(); + self.contacts.delayedDelete(contactInfo); } else { self.contacts.delayedDelete(self.contacts.getSelectedContacts()); } @@ -1022,31 +1061,48 @@ OC.Contacts = OC.Contacts || { return; } console.log('download'); + var contacts = self.contacts.getSelectedContacts(); + var ids = $.map(contacts, function(c) {return c.getId();}); document.location.href = OC.linkTo('contacts', 'export.php') - + '?selectedids=' + self.contacts.getSelectedContacts().join(','); + + '?selectedids=' + ids.join(','); + }); + + this.$header.on('click keydown', '.merge', function(event) { + if(wrongKey(event)) { + return; + } + console.log('merge'); + self.mergeSelectedContacts(); }); this.$header.on('click keydown', '.favorite', function(event) { if(wrongKey(event)) { return; } - if(!utils.isUInt(self.currentid)) { - return; + + var contacts = self.contacts.getSelectedContacts(); + + self.setAllChecked(false); + self.$toggleAll.prop('checked', false); + if(!self.currentid) { + self.showActions(['add']); } - // FIXME: This should only apply for contacts list. - var state = self.groups.isFavorite(self.currentid); - console.log('Favorite?', this, state); - self.groups.setAsFavorite(self.currentid, !state, function(jsondata) { - if(jsondata.status === 'success') { - if(state) { - self.$header.find('.favorite').switchClass('active', ''); - } else { - self.$header.find('.favorite').switchClass('', 'active'); - } - } else { - OC.notify({message:t('contacts', jsondata.data.message)}); + + $.each(contacts, function(idx, contact) { + if(!self.groups.isFavorite(contact.getId())) { + self.groups.setAsFavorite(contact.getId(), true, function(result) { + if(result.status !== 'success') { + OC.notify({message: + t('contacts', + 'Error setting {name} as favorite.', + {name:contact.getDisplayName()}) + }); + } + }); } }); + + self.showActions(['add']); }); this.$contactList.on('mouseenter', 'td.email', function(event) { @@ -1060,14 +1116,28 @@ OC.Contacts = OC.Contacts || { // Import using jquery.fileupload $(function() { - var uploadingFiles = {}, numfiles = 0, uploadedfiles = 0, retries = 0; + var uploadingFiles = {}, + numfiles = 0, + uploadedfiles = 0, + importedfiles = 0, + retries = 0, + succeded = 0 + failed = 0; var aid, importError = false; var $progressbar = $('#import-progress'); var $status = $('#import-status-text'); + self.$importFileInput.on('fileuploaddone', function(e, data) { + console.log('fileuploaddone', data); + var file = data.result.file; + console.log('fileuploaddone, file', file); + uploadedfiles += 1; + }); + var waitForImport = function() { - if(numfiles == 0 && uploadedfiles == 0) { - $progressbar.progressbar('value',100); + console.log('waitForImport', numfiles, uploadedfiles, importedfiles); + if(uploadedfiles === importedfiles && importedfiles === numfiles) { + $progressbar.progressbar('value', 100); if(!importError) { OC.notify({ message:t('contacts','Import done. Click here to cancel reloading.'), @@ -1082,7 +1152,9 @@ OC.Contacts = OC.Contacts || { } }); } - retries = aid = 0; + $status.text(t('contacts', '{success} imported, {failed} failed.', + {success:succeded, failed:failed})).fadeIn(); + numfiles = uploadedfiles = importedfiles = retries = aid = failed = succeded = 0; $progressbar.fadeOut(); setTimeout(function() { $status.fadeOut('slow'); @@ -1110,11 +1182,11 @@ OC.Contacts = OC.Contacts || { var importFiles = function(aid, uploadingFiles) { console.log('importFiles', aid, uploadingFiles); - if(numfiles != uploadedfiles) { + if(numfiles !== uploadedfiles) { OC.notify({message:t('contacts', 'Not all files uploaded. Retrying...')}); retries += 1; if(retries > 3) { - numfiles = uploadedfiles = retries = aid = 0; + numfiles = uploadedfiles = importedfiles = retries = failed = succeded = aid = 0; uploadingFiles = {}; $progressbar.fadeOut(); OC.dialogs.alert(t('contacts', 'Something went wrong with the upload, please retry.'), t('contacts', 'Error')); @@ -1130,14 +1202,19 @@ OC.Contacts = OC.Contacts || { $status.text(t('contacts', 'Importing from {filename}...', {filename:fileName})).fadeIn(); doImport(fileName, aid, function(response) { if(response.status === 'success') { - $status.text(t('contacts', '{success} imported, {failed} failed.', - {success:response.data.imported, failed:response.data.failed})).fadeIn(); + importedfiles += 1; + succeded += response.data.imported; + failed += response.data.failed; + OC.notify({ + message:t('contacts', '{success} imported, {failed} failed from {file}', + {success:response.data.imported, failed:response.data.failed, file:response.data.failed} + ) + }); } else { $('.import-upload').show(); $('.import-select').hide(); } delete uploadingFiles[fileName]; - numfiles -= 1; uploadedfiles -= 1; $progressbar.progressbar('value',50+(50/(todo-uploadedfiles))); }); }); @@ -1155,80 +1232,34 @@ OC.Contacts = OC.Contacts || { importFiles(aid, uploadingFiles); }); - $('#import_fileupload').fileupload({ + self.$importFileInput.fileupload({ acceptFileTypes: /^text\/(directory|vcard|x-vcard)$/i, add: function(e, data) { - var files = data.files; + var file = data.files[0]; + console.log('add', file.name); var totalSize=0; - if(files) { - numfiles += files.length; uploadedfiles = 0; - for(var i=0;i$('#max_upload').val()) { OC.dialogs.alert(t('contacts','The file you are trying to upload exceed the maximum size for file uploads on this server.'), t('contacts','Upload too large')); - numfiles = uploadedfiles = retries = aid = 0; + numfiles = uploadedfiles = importedfiles = retries = failed = succeded = aid = 0; uploadingFiles = {}; return; - } else { - if($.support.xhrFileUpload) { - $.each(files, function(i, file) { - var fileName = file.name; - console.log('file.name', file.name); - var jqXHR = $('#import_fileupload').fileupload('send', - { - files: file, - formData: function(form) { - var formArray = form.serializeArray(); - formArray['aid'] = aid; - return formArray; - }}) - .success(function(response, textStatus, jqXHR) { - if(response.status == 'success') { - // import the file - uploadedfiles += 1; - } else { - OC.notify({message:response.data.message}); - $('.import-upload').show(); - $('.import-select').hide(); - $('#import-progress').hide(); - $('#import-status-text').hide(); - } - return false; - }) - .error(function(jqXHR, textStatus, errorThrown) { - console.log(textStatus); - OC.notify({message:errorThrown + ': ' + textStatus}); - }); - uploadingFiles[fileName] = jqXHR; - }); - } else { - data.submit().success(function(data, status) { - response = jQuery.parseJSON(data[0].body.innerText); - if(response[0] != undefined && response[0].status == 'success') { - var file=response[0]; - delete uploadingFiles[file.name]; - $('tr').filterAttr('data-file',file.name).data('mime',file.mime); - var size = $('tr').filterAttr('data-file',file.name).find('td.filesize').text(); - if(size==t('files','Pending')){ - $('tr').filterAttr('data-file',file.name).find('td.filesize').text(file.size); - } - FileList.loadingDone(file.name); - } else { - OC.notify({message:response.data.message}); - } - }); - } } + var jqXHR = data.submit(); + uploadingFiles[file.name] = jqXHR; + }, fail: function(e, data) { console.log('fail'); OC.notify({message:data.errorThrown + ': ' + data.textStatus}); + numfiles = uploadedfiles = importedfiles = retries = failed = succeded = aid = 0; $('.import-upload').show(); $('.import-select').hide(); // TODO: Remove file from upload queue. @@ -1240,19 +1271,12 @@ OC.Contacts = OC.Contacts || { start: function(e, data) { $progressbar.progressbar({value:0}); $progressbar.fadeIn(); - if(data.dataType != 'iframe ') { - $('#upload input.stop').show(); - } }, stop: function(e, data) { console.log('stop, data', data); // stop only gets fired once so we collect uploaded items here. $('.import-upload').hide(); $('.import-select').show(); - - if(data.dataType != 'iframe ') { - $('#upload input.stop').hide(); - } } }); }); @@ -1261,8 +1285,8 @@ OC.Contacts = OC.Contacts || { event.preventDefault(); }); - $(document).on('keypress', function(event) { - if(!$(event.target).is('body')) { + $(document).on('keyup', function(event) { + if(!$(event.target).is('body') || event.isPropagationStopped()) { return; } var keyCode = Math.max(event.keyCode, event.which); @@ -1350,41 +1374,123 @@ OC.Contacts = OC.Contacts || { $('.tooltipped.downwards.onfocus').tipsy({trigger: 'focus', gravity: 'n'}); $('.tooltipped.rightwards.onfocus').tipsy({trigger: 'focus', gravity: 'w'}); }, + mergeSelectedContacts: function() { + var contacts = this.contacts.getSelectedContacts(); + var self = this; + this.$rightContent.append('
'); + if(!this.$mergeContactsTmpl) { + this.$mergeContactsTmpl = $('#mergeContactsTemplate'); + } + var $dlg = this.$mergeContactsTmpl.octemplate(); + var $liTmpl = $dlg.find('li').detach(); + var $mergeList = $dlg.find('.mergelist'); + $.each(contacts, function(idx, contact) { + var $li = $liTmpl + .octemplate({idx: idx, id: contact.getId(), displayname: contact.getDisplayName()}); + if(!contact.data.thumbnail) { + $li.addClass('thumbnail'); + } else { + $li.css('background-image', 'url(data:image/png;base64,' + contact.data.thumbnail + ')'); + } + if(idx === 0) { + $li.find('input:radio').prop('checked', true); + } + $mergeList.append($li); + }); + this.$contactList.addClass('dim'); + $('#merge_contacts_dialog').html($dlg).ocdialog({ + closeOnEscape: true, + title: t('contacts', 'Merge contacts'), + height: 'auto', width: 'auto', + buttons: [ + { + text: t('contacts', 'Merge contacts'), + click:function() { + // Do the merging, use $(this) to get dialog + var contactid = $(this).find('input:radio:checked').val(); + var others = []; + var deleteOther = $(this).find('#delete_other').prop('checked'); + console.log('Selected contact', contactid, 'Delete others', deleteOther); + $.each($(this).find('input:radio:not(:checked)'), function(idx, item) { + others.push($(item).val()); + }); + console.log('others', others); + $(document).trigger('request.contact.merge', { + merger: contactid, + mergees: others, + deleteOther: deleteOther + }); + + $(this).ocdialog('close'); + }, + defaultButton: true + }, + { + text: t('contacts', 'Cancel'), + click:function(dlg) { + $(this).ocdialog('close'); + return false; + } + } + ], + close: function(event, ui) { + $(this).ocdialog('destroy').remove(); + $('#add_group_dialog').remove(); + self.$contactList.removeClass('dim'); + }, + open: function(event, ui) { + $dlg.find('input').focus(); + } + }); + }, addGroup: function(cb) { var self = this; - $('body').append('
'); + this.$rightContent.append('
'); if(!this.$addGroupTmpl) { this.$addGroupTmpl = $('#addGroupTemplate'); } + this.$contactList.addClass('dim'); var $dlg = this.$addGroupTmpl.octemplate(); - $('#add_group_dialog').html($dlg).dialog({ + $('#add_group_dialog').html($dlg).ocdialog({ modal: true, closeOnEscape: true, title: t('contacts', 'Add group'), height: 'auto', width: 'auto', - buttons: { - 'Ok':function() { - self.groups.addGroup( - {name:$dlg.find('input:text').val()}, - function(response) { - if(typeof cb === 'function') { - cb(response); - } else { - if(response.status !== 'success') { - OC.notify({message: response.message}); + buttons: [ + { + text: t('contacts', 'OK'), + click:function() { + var name = $(this).find('input').val(); + if(name.trim() === '') { + return false; + } + self.groups.addGroup( + {name:$dlg.find('input:text').val()}, + function(response) { + if(typeof cb === 'function') { + cb(response); + } else { + if(response.error) { + OC.notify({message: response.message}); + } } - } - }); - $(this).dialog('close'); + }); + $(this).ocdialog('close'); + }, + defaultButton: true }, - 'Cancel':function() { - $(this).dialog('close'); - return false; + { + text: t('contacts', 'Cancel'), + click:function(dlg) { + $(this).ocdialog('close'); + return false; + } } - }, + ], close: function(event, ui) { - $(this).dialog('destroy').remove(); + $(this).ocdialog('destroy').remove(); $('#add_group_dialog').remove(); + self.$contactList.removeClass('dim'); }, open: function(event, ui) { $dlg.find('input').focus(); @@ -1401,6 +1507,7 @@ OC.Contacts = OC.Contacts || { this.$rightContent.scrollTop(this.contacts.contactPos(id)-30); }, closeContact: function(id) { + $(window).unbind('hashchange', this.hashChange); if(typeof this.currentid === 'number') { var contact = this.contacts.findById(id); if(contact && contact.close()) { @@ -1419,12 +1526,16 @@ OC.Contacts = OC.Contacts || { $(document).trigger('status.nomorecontacts'); } //$('body').unbind('click', this.bodyListener); + window.location.hash = ''; + $(window).bind('hashchange', this.hashChange); }, openContact: function(id) { + this.hideActions(); console.log('Contacts.openContact', id); if(this.currentid) { this.closeContact(this.currentid); } + $(window).unbind('hashchange', this.hashChange); this.currentid = parseInt(id); console.log('Contacts.openContact, Favorite', this.currentid, this.groups.isFavorite(this.currentid), this.groups); this.setAllChecked(false); @@ -1458,10 +1569,12 @@ OC.Contacts = OC.Contacts || { if($contactelem.find($(e.target)).length === 0) { self.closeContact(self.currentid); } - }; + };*/ + window.location.hash = this.currentid.toString(); setTimeout(function() { - $('body').bind('click', self.bodyListener); - }, 500);*/ + //$('body').bind('click', self.bodyListener); + $(window).bind('hashchange', this.hashChange); + }, 500); }, update: function() { console.log('update'); @@ -1475,7 +1588,6 @@ OC.Contacts = OC.Contacts || { var file = filelist[0]; var target = $('#file_upload_target'); var form = $('#file_upload_form'); - form.find('input[name="id"]').val(this.currentid); var totalSize=0; if(file.size > $('#max_upload').val()){ OC.notify({ @@ -1489,7 +1601,10 @@ OC.Contacts = OC.Contacts || { var response=jQuery.parseJSON(target.contents().text()); if(response != undefined && response.status == 'success') { console.log('response', response); - self.editPhoto(self.currentid, response.data.tmp); + self.editPhoto( + response.metadata, + response.data.tmp + ); //alert('File: ' + file.tmp + ' ' + file.name + ' ' + file.mime); } else { OC.notify({message:response.data.message}); @@ -1498,28 +1613,28 @@ OC.Contacts = OC.Contacts || { form.submit(); } }, - cloudPhotoSelected:function(id, path) { + cloudPhotoSelected:function(metadata, path) { var self = this; - console.log('cloudPhotoSelected, id', id); + console.log('cloudPhotoSelected', metadata); $.getJSON(OC.filePath('contacts', 'ajax', 'oc_photo.php'), - {path: path, id: id},function(jsondata) { + {path: path, contact: metadata},function(jsondata) { if(jsondata.status == 'success') { //alert(jsondata.data.page); - self.editPhoto(jsondata.data.id, jsondata.data.tmp); - $('#edit_photo_dialog_img').html(jsondata.data.page); + self.editPhoto(metadata, jsondata.data.tmp); + //$('#edit_photo_dialog_img').html(jsondata.data.page); } else{ OC.notify({message: jsondata.data.message}); } }); }, - editCurrentPhoto:function(id) { + editCurrentPhoto:function(metadata) { var self = this; - $.getJSON(OC.filePath('contacts', 'ajax', 'currentphoto.php'), - {id: id}, function(jsondata) { + $.getJSON(OC.filePath('contacts', 'ajax', 'currentphoto.php'), metadata, + function(jsondata) { if(jsondata.status == 'success') { //alert(jsondata.data.page); - self.editPhoto(jsondata.data.id, jsondata.data.tmp); + self.editPhoto(metadata, jsondata.data.tmp); $('#edit_photo_dialog_img').html(jsondata.data.page); } else{ @@ -1527,8 +1642,8 @@ OC.Contacts = OC.Contacts || { } }); }, - editPhoto:function(id, tmpkey) { - console.log('editPhoto', id, tmpkey); + editPhoto:function(metadata, tmpkey) { + console.log('editPhoto', metadata, tmpkey); $('.tipsy').remove(); // Simple event handler, called from onChange and onSelect // event handlers, as per the Jcrop invocation above @@ -1550,7 +1665,13 @@ OC.Contacts = OC.Contacts || { this.$cropBoxTmpl = $('#cropBoxTemplate'); } $('body').append('
'); - var $dlg = this.$cropBoxTmpl.octemplate({id: id, tmpkey: tmpkey}); + var $dlg = this.$cropBoxTmpl.octemplate( + { + backend: metadata.backend, + addressbookid: metadata.addressbookid, + contactid: metadata.contactid, + tmpkey: tmpkey + }); var cropphoto = new Image(); $(cropphoto).load(function () { @@ -1590,208 +1711,38 @@ OC.Contacts = OC.Contacts || { }); }).error(function () { OC.notify({message:t('contacts','Error loading profile picture.')}); - }).attr('src', OC.linkTo('contacts', 'tmpphoto.php')+'?tmpkey='+tmpkey); + }).attr('src', OC.linkTo('contacts', 'tmpphoto.php')+'?tmpkey='+tmpkey+'&refresh='+Math.random()); }, savePhoto:function($dlg) { var form = $dlg.find('#cropform'); q = form.serialize(); console.log('savePhoto', q); $.post(OC.filePath('contacts', 'ajax', 'savecrop.php'), q, function(response) { - var jsondata = $.parseJSON(response); - console.log('savePhoto, jsondata', typeof jsondata); - if(jsondata && jsondata.status === 'success') { + //var jsondata = $.parseJSON(response); + console.log('savePhoto, response', typeof response); + if(response && response.status === 'success') { // load cropped photo. $(document).trigger('status.contact.photoupdated', { - id: jsondata.data.id + id: response.data.id, + thumbnail: response.data.thumbnail }); } else { - if(!jsondata) { + if(!response) { OC.notify({message:t('contacts', 'Network or server error. Please inform administrator.')}); } else { - OC.notify({message: jsondata.data.message}); + OC.notify({message: response.data.message}); } } }); }, - addAddressbook:function(data, cb) { - $.ajax({ - type:'POST', - async:false, - url:OC.filePath('contacts', 'ajax', 'addressbook/add.php'), - data:{ name: data.name, description: data.description }, - success:function(jsondata) { - if(jsondata.status == 'success') { - if(typeof cb === 'function') { - cb({ - status:'success', - addressbook: jsondata.data.addressbook - }); - } - } else { - if(typeof cb === 'function') { - cb({status:'error', message:jsondata.data.message}); - } else { - OC.notify({message:textStatus + ': ' + errorThrown}); - } - } - }, - error:function(jqXHR, textStatus, errorThrown) { - if(typeof cb === 'function') { - cb({ - status:'success', - message: textStatus + ': ' + errorThrown - }); - } else { - OC.notify({message:textStatus + ': ' + errorThrown}); - } - } - }); - }, - // NOTE: Deprecated - selectAddressbook:function(cb) { - var self = this; - var jqxhr = $.get(OC.filePath('contacts', 'templates', 'selectaddressbook.html'), function(data) { - $('body').append('
'); - var $dlg = $('#addressbook_dialog').html(data).octemplate({ - nameplaceholder: t('contacts', 'Enter name'), - descplaceholder: t('contacts', 'Enter description') - }).dialog({ - modal: true, height: 'auto', width: 'auto', - title: t('contacts', 'Select addressbook'), - buttons: { - 'Ok':function() { - aid = $(this).find('input:checked').val(); - if(aid == 'new') { - var displayname = $(this).find('input.name').val(); - var description = $(this).find('input.desc').val(); - if(!$.trim(displayname)) { - OC.dialogs.alert(t('contacts', 'The address book name cannot be empty.'), t('contacts', 'Error')); - return false; - } - console.log('ID, name and desc', aid, displayname, description); - if(typeof cb === 'function') { - // TODO: Create addressbook - var data = {name:displayname, description:description}; - self.addAddressbook(data, function(data) { - if(data.status === 'success') { - cb({ - status:'success', - addressbook:data.addressbook - }); - } else { - cb({status:'error'}); - } - }); - } - $(this).dialog('close'); - } else { - console.log('aid ' + aid); - if(typeof cb === 'function') { - cb({ - status:'success', - addressbook:self.contacts.addressbooks[parseInt(aid)] - }); - } - $(this).dialog('close'); - } - }, - 'Cancel':function() { - $(this).dialog('close'); - } - }, - close: function(event, ui) { - $(this).dialog('destroy').remove(); - $('#addressbook_dialog').remove(); - }, - open: function(event, ui) { - console.log('open', $(this)); - var $lastrow = $(this).find('tr.new'); - $.each(self.contacts.addressbooks, function(i, book) { - console.log('book', i, book); - if(book.owner === OC.currentUser - || (book.permissions & OC.PERMISSION_UPDATE - || book.permissions & OC.PERMISSION_CREATE - || book.permissions & OC.PERMISSION_DELETE)) { - var row = '
' - + ''; - var $row = $(row).octemplate({ - id:book.id, - displayname:book.displayname, - description:book.description - }); - $lastrow.before($row); - } - }); - $(this).find('input[type="radio"]').first().prop('checked', true); - $lastrow.find('input.name,input.desc').on('focus', function(e) { - $lastrow.find('input[type="radio"]').prop('checked', true); - }); - } - }); - }).error(function() { - OC.notify({message: t('contacts', 'Network or server error. Please inform administrator.')}); - }); - } }; -(function( $ ) { - /** - * Object Template - * Inspired by micro templating done by e.g. underscore.js - */ - var Template = { - init: function(vars, options, elem) { - // Mix in the passed in options with the default options - this.vars = vars; - this.options = $.extend({},this.options,options); - - this.elem = elem; - var self = this; - - if(typeof this.options.escapeFunction === 'function') { - $.each(this.vars, function(key, val) { - if(typeof val === 'string') { - self.vars[key] = self.options.escapeFunction(val); - } - }); - } - - var _html = this._build(this.vars); - return $(_html); - }, - // From stackoverflow.com/questions/1408289/best-way-to-do-variable-interpolation-in-javascript - _build: function(o){ - var data = this.elem.attr('type') === 'text/template' ? this.elem.html() : this.elem.get(0).outerHTML; - try { - return data.replace(/{([^{}]*)}/g, - function (a, b) { - var r = o[b]; - return typeof r === 'string' || typeof r === 'number' ? r : a; - } - ); - } catch(e) { - console.error(e, 'data:', data) - } - }, - options: { - escapeFunction: function(str) {return $('').text(str).html();} - } - }; - - $.fn.octemplate = function(vars, options) { - var vars = vars ? vars : {}; - if(this.length) { - var _template = Object.create(Template); - return _template.init(vars, options, this); - } - }; - -})( jQuery ); - - $(document).ready(function() { - OC.Contacts.init(); + OC.Router.registerLoadedCallback(function() { + $.getScript(OC.Router.generate('contacts_jsconfig'), function() { + OC.Contacts.init(); + }); + }); }); diff --git a/ajax/addressbook/delete.php b/js/config.php similarity index 53% rename from ajax/addressbook/delete.php rename to js/config.php index 9f04d605..838f4767 100644 --- a/ajax/addressbook/delete.php +++ b/js/config.php @@ -2,8 +2,8 @@ /** * ownCloud - Addressbook * - * @author Jakob Sack - * @copyright 2011 Jakob Sack mail@jakobsack.de + * @author Thomas Tanghus + * @copyright 2012 Thomas Tanghus * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE @@ -20,21 +20,17 @@ * */ -// Check if we are a user +OCP\JSON::setContentTypeHeader('text/javascript'); OCP\JSON::checkLoggedIn(); OCP\JSON::checkAppEnabled('contacts'); -OCP\JSON::callCheck(); -require_once __DIR__.'/../loghandler.php'; -$id = $_POST['id']; -if(!$id) { - bailOut(OCA\Contacts\App::$l10n->t('id is not set.')); -} +$user = OCP\User::getUser(); -try { - OCA\Contacts\Addressbook::delete($id); -} catch(Exception $e) { - bailOut($e->getMessage()); -} - -OCP\JSON::success(array('data' => array( 'id' => $id ))); +echo 'var contacts_groups_sortorder=[' . OCP\Config::getUserValue($user, 'contacts', 'groupsort', '') . '],'; +echo 'contacts_properties_indexed = ' + . (OCP\Config::getUserValue($user, 'contacts', 'contacts_properties_indexed', 'no') === 'no' + ? 'false' : 'true') . ','; +echo 'contacts_categories_indexed = ' + . (OCP\Config::getUserValue($user, 'contacts', 'contacts_categories_indexed', 'no') === 'no' + ? 'false' : 'true') . ','; +echo 'lang=\'' . OCP\Config::getUserValue($user, 'core', 'lang', 'en') . '\';'; diff --git a/js/contacts.js b/js/contacts.js index 10cbdd3f..632cc2d2 100644 --- a/js/contacts.js +++ b/js/contacts.js @@ -7,17 +7,18 @@ OC.Contacts = OC.Contacts || {}; * An item which binds the appropriate html and event handlers * @param parent the parent ContactList * @param id The integer contact id. - * @param access An access object containing and 'owner' string variable and an integer 'permissions' variable. + * @param metadata An metadata object containing and 'owner' string variable and an integer 'permissions' variable. * @param data the data used to populate the contact * @param listtemplate the jquery object used to render the contact list item * @param fulltemplate the jquery object used to render the entire contact * @param detailtemplates A map of jquery objects used to render the contact parts e.g. EMAIL, TEL etc. */ - var Contact = function(parent, id, access, data, listtemplate, dragtemplate, fulltemplate, detailtemplates) { - //console.log('contact:', id, access); //parent, id, data, listtemplate, fulltemplate); + var Contact = function(parent, id, metadata, data, listtemplate, dragtemplate, fulltemplate, detailtemplates) { + //console.log('contact:', id, metadata, data); //parent, id, data, listtemplate, fulltemplate); this.parent = parent, + this.storage = parent.storage, this.id = id, - this.access = access, + this.metadata = metadata, this.data = data, this.$dragTemplate = dragtemplate, this.$listTemplate = listtemplate, @@ -27,6 +28,98 @@ OC.Contacts = OC.Contacts || {}; this.multi_properties = ['EMAIL', 'TEL', 'IMPP', 'ADR', 'URL']; }; + Contact.prototype.metaData = function() { + return { + contactid: this.id, + addressbookid: this.metadata.parent, + backend: this.metadata.backend + } + }; + + Contact.prototype.getDisplayName = function() { + return this.getPreferredValue('FN'); + }; + + Contact.prototype.getId = function() { + return this.id; + }; + + Contact.prototype.getParent = function() { + return this.metadata.parent; + }; + + Contact.prototype.getBackend = function() { + return this.metadata.backend; + }; + + Contact.prototype.merge = function(mergees) { + console.log('Contact.merge, mergees', mergees); + if(!mergees instanceof Array && !mergees instanceof Contact) { + throw new TypeError('BadArgument: Contact.merge() only takes Contacts'); + } else { + if(mergees instanceof Contact) { + mergees = [mergees]; + } + } + + // For multi_properties + var addIfNotExists = function(name, newproperty) { + // If the property isn't set at all just add it and return. + if(!self.data[name]) { + self.data[name] = [newproperty]; + return; + } + var found = false; + $.each(self.data[name], function(idx, property) { + if(name === 'ADR') { + // Do a simple string comparison + if(property.value.join(';').toLowerCase() === newproperty.value.join(';').toLowerCase()) { + found = true; + return false; // break loop + } + } else { + if(property.value.toLowerCase() === newproperty.value.toLowerCase()) { + found = true; + return false; // break loop + } + } + }); + if(found) { + return; + } + // Not found, so adding it. + self.data[name].push(newproperty); + } + + var self = this; + $.each(mergees, function(idx, mergee) { + console.log('Contact.merge, mergee', mergee); + if(!mergee instanceof Contact) { + throw new TypeError('BadArgument: Contact.merge() only takes Contacts'); + } + if(mergee === self) { + throw new Error('BadArgument: Why should I merge with myself?'); + } + $.each(mergee.data, function(name, properties) { + if(self.multi_properties.indexOf(name) === -1) { + if(self.data[name] && self.data[name].length > 0) { + // If the property exists don't touch it. + return true; // continue + } else { + // Otherwise add it. + self.data[name] = properties; + } + } else { + $.each(properties, function(idx, property) { + addIfNotExists(name, property); + }); + } + }); + console.log('Merged', self.data); + }); + return true; + }; + Contact.prototype.showActions = function(act) { this.$footer.children().hide(); if(act && act.length > 0) { @@ -68,7 +161,7 @@ OC.Contacts = OC.Contacts || {}; newvalue: params.newvalue, oldvalue: params.oldvalue }); - console.log('undoQueue', this.undoQueue); + //console.log('undoQueue', this.undoQueue); } Contact.prototype.addProperty = function($option, name) { @@ -137,6 +230,8 @@ OC.Contacts = OC.Contacts || {}; console.log('Contact.deleteProperty, element', element, $container); var params = { name: element, + backend: this.metadata.backend, + addressbookid: this.metadata.parent, id: this.id }; if(this.multi_properties.indexOf(element) !== -1) { @@ -153,16 +248,9 @@ OC.Contacts = OC.Contacts || {}; } this.setAsSaving(obj, true); var self = this; - $.post(OC.filePath('contacts', 'ajax', 'contact/deleteproperty.php'), params, function(jsondata) { - if(!jsondata) { - $(document).trigger('status.contact.error', { - status: 'error', - message: t('contacts', 'Network or server error. Please inform administrator.') - }); - self.setAsSaving(obj, false); - return false; - } - if(jsondata.status == 'success') { + $.when(this.storage.deleteProperty(this.metadata.backend, this.metadata.parent, this.id, params)) + .then(function(response) { + if(!response.error) { // TODO: Test if removing from internal data structure works if(self.multi_properties.indexOf(element) !== -1) { // First find out if an existing element by looking for checksum @@ -206,19 +294,68 @@ OC.Contacts = OC.Contacts || {}; return true; } else { $(document).trigger('status.contact.error', { - status: 'error', - message: jsondata.data.message + message: response.message }); self.setAsSaving(obj, false); return false; } - },'json'); + }) + .fail(function(jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.warn( "Request Failed: " + err); + $(document).trigger('status.contact.error', { + message: t('contacts', 'Failed deleting property: {error}', {error:err}) + }); + }); +; }; + /** + * @brief Save all properties. Used for merging contacts. + * If this is a new contact it will first be saved to the datastore and a + * new datastructure will be added to the object. + */ + Contact.prototype.saveAll = function(cb) { + console.log('Contact.saveAll'); + if(!this.id) { + var self = this; + this.add({isnew:true}, function(response) { + if(response.error) { + console.warn('No response object'); + return false; + } + self.saveAll(); + }); + return; + } + var self = this; + this.setAsSaving(this.$fullelem, true); + var data = JSON.stringify(this.data); + //console.log('stringified', data); + $.when(this.storage.saveAllProperties(this.metadata.backend, this.metadata.parent, this.id, data)) + .then(function(response) { + if(!response.error) { + self.data = response.data.data; + self.metadata = response.data.metadata; + if(typeof cb === 'function') { + cb({error:false}); + } + } else { + $(document).trigger('status.contact.error', { + message: response.message + }); + if(typeof cb === 'function') { + cb({error:true, message:response.message}); + } + } + self.setAsSaving(self.$fullelem, false); + }); + } + /** * @brief Act on change of a property. * If this is a new contact it will first be saved to the datastore and a - * new datastructure will be added to the object. FIXME: Not implemented yet. + * new datastructure will be added to the object. * If the obj argument is not provided 'name' and 'value' MUST be provided * and this is only allowed for single elements like N, FN, CATEGORIES. * @param obj. The form form field that has changed. @@ -241,32 +378,25 @@ OC.Contacts = OC.Contacts || {}; } var obj = null; var element = null; - var q = ''; + var args = []; if(params.obj) { obj = params.obj; - q = this.queryStringFor(obj); + args = this.argumentsFor(obj); + args['parameters'] = this.parametersFor(obj); element = this.propertyTypeFor(obj); } else { + args = params; element = params.name; var value = utils.isArray(params.value) ? $.param(params.value) : encodeURIComponent(params.value); - q = 'id=' + this.id + '&value=' + value + '&name=' + element; } - console.log('q', q); + console.log('args', args); var self = this; this.setAsSaving(obj, true); - $.post(OC.filePath('contacts', 'ajax', 'contact/saveproperty.php'), q, function(jsondata){ - if(!jsondata) { - $(document).trigger('status.contact.error', { - status: 'error', - message: t('contacts', 'Network or server error. Please inform administrator.') - }); - $(obj).addClass('error'); - self.setAsSaving(obj, false); - return false; - } - if(jsondata.status == 'success') { + $.when(this.storage.saveProperty(this.metadata.backend, this.metadata.parent, this.id, args)) + .then(function(response) { + if(!response.error) { if(!self.data[element]) { self.data[element] = []; } @@ -283,7 +413,7 @@ OC.Contacts = OC.Contacts || {}; self.pushToUndo({ action:'save', name: element, - newchecksum: jsondata.data.checksum, + newchecksum: response.data.checksum, oldchecksum: checksum, newvalue: value, oldvalue: obj.defaultValue @@ -294,7 +424,7 @@ OC.Contacts = OC.Contacts || {}; name: element, value: value, parameters: parameters, - checksum: jsondata.data.checksum + checksum: response.data.checksum }; return false; } @@ -304,17 +434,17 @@ OC.Contacts = OC.Contacts || {}; self.pushToUndo({ action:'add', name: element, - newchecksum: jsondata.data.checksum, + newchecksum: response.data.checksum, newvalue: value, }); self.data[element].push({ name: element, value: value, parameters: parameters, - checksum: jsondata.data.checksum, + checksum: response.data.checksum, }); } - self.propertyContainerFor(obj).data('checksum', jsondata.data.checksum); + self.propertyContainerFor(obj).data('checksum', response.data.checksum); } else { // Save value and parameters internally var value = obj ? self.valueFor(obj) : params.value; @@ -327,6 +457,16 @@ OC.Contacts = OC.Contacts || {}; case 'CATEGORIES': // We deal with this in addToGroup() break; + case 'BDAY': + // reverse order again. + value = $.datepicker.formatDate('yy-mm-dd', $.datepicker.parseDate('dd-mm-yy', value)); + self.data[element][0] = { + name: element, + value: value, + parameters: self.parametersFor(obj), + checksum: response.data.checksum + }; + break; case 'FN': if(!self.data.FN || !self.data.FN.length) { self.data.FN = [{name:'FN', value:'', parameters:[]}]; @@ -380,7 +520,6 @@ OC.Contacts = OC.Contacts || {}; $fullname.val(self.data.FN[0]['value']); update_fn = true; } else if($fullname.val() == value[1] + ' ') { - console.log('change', value); self.data.FN[0]['value'] = value[1] + ' ' + value[0]; $fullname.val(self.data.FN[0]['value']); update_fn = true; @@ -395,7 +534,6 @@ OC.Contacts = OC.Contacts || {}; }, 1000); } case 'NICKNAME': - case 'BDAY': case 'ORG': case 'TITLE': case 'NOTE': @@ -403,7 +541,7 @@ OC.Contacts = OC.Contacts || {}; name: element, value: value, parameters: self.parametersFor(obj), - checksum: jsondata.data.checksum + checksum: response.data.checksum }; break; default: @@ -418,13 +556,12 @@ OC.Contacts = OC.Contacts || {}; return true; } else { $(document).trigger('status.contact.error', { - status: 'error', - message: jsondata.data.message + message: response.message }); self.setAsSaving(obj, false); return false; } - },'json'); + }); }; /** @@ -499,22 +636,16 @@ OC.Contacts = OC.Contacts || {}; * @returns The callback gets an object as argument with a variable 'status' of either 'success' * or 'error'. On success the 'data' property of that object contains the contact id as 'id', the * addressbook id as 'aid' and the contact data structure as 'details'. + * TODO: Use Storage for adding and make sure to get all metadata. */ Contact.prototype.add = function(params, cb) { var self = this; - $.post(OC.filePath('contacts', 'ajax', 'contact/add.php'), - params, function(jsondata) { - if(!jsondata) { - $(document).trigger('status.contact.error', { - status: 'error', - message: t('contacts', 'Network or server error. Please inform administrator.') - }); - return false; - } - if(jsondata.status === 'success') { - self.id = parseInt(jsondata.data.id); - self.access.id = parseInt(jsondata.data.aid); - self.data = jsondata.data.details; + $.when(this.storage.addContact(this.metadata.backend, this.metadata.parent)) + .then(function(response) { + if(!response.error) { + self.id = String(response.data.metadata.id); + self.metadata = response.data.metadata; + self.data = response.data.data; self.$groupSelect.multiselect('enable'); // Add contact to current group if(self.groupprops && self.groupprops.currentgroup.id !== 'all' @@ -534,9 +665,14 @@ OC.Contacts = OC.Contacts || {}; id: self.id, contact: self }); + } else { + $(document).trigger('status.contact.error', { + message: response.message + }); + return false; } if(typeof cb == 'function') { - cb(jsondata); + cb(response); } }); }; @@ -548,9 +684,14 @@ OC.Contacts = OC.Contacts || {}; */ Contact.prototype.destroy = function(cb) { var self = this; - $.post(OC.filePath('contacts', 'ajax', 'contact/delete.php'), - {id: this.id}, function(jsondata) { - if(jsondata && jsondata.status === 'success') { + $.when(this.storage.deleteContact( + this.metadata.backend, + this.metadata.parent, + this.id) + ).then(function(response) { + //$.post(OC.filePath('contacts', 'ajax', 'contact/delete.php'), + // {id: this.id}, function(response) { + if(!response.error) { if(self.$listelem) { self.$listelem.remove(); } @@ -559,22 +700,49 @@ OC.Contacts = OC.Contacts || {}; } } if(typeof cb == 'function') { - var retval = {status: jsondata ? jsondata.status : 'error'}; - if(jsondata) { - if(jsondata.status === 'success') { - retval['id'] = jsondata.id; - } else { - retval['message'] = jsondata.message; - } + if(response.error) { + cb(response); } else { - retval['message'] = t('contacts', 'There was an unknown error when trying to delete this contact'); - retval['id'] = self.id; + cb({id:self.id}); } - cb(retval); } }); }; + Contact.prototype.argumentsFor = function(obj) { + var args = {}; + var ptype = this.propertyTypeFor(obj); + args['name'] = ptype; + + if(this.multi_properties.indexOf(ptype) !== -1) { + args['checksum'] = this.checksumFor(obj); + } + + if($(obj).hasClass('propertycontainer')) { + if($(obj).is('select[data-element="categories"]')) { + args['value'] = []; + $.each($(obj).find(':selected'), function(idx, e) { + args['value'].push($(e).text()); + }); + } else { + args['value'] = $(obj).val(); + } + } else { + var $elements = this.propertyContainerFor(obj) + .find('input.value,select.value,textarea.value'); + if($elements.length > 1) { + args['value'] = []; + $.each($elements, function(idx, e) { + args['value'][parseInt($(e).attr('name').substr(6,1))] = $(e).val(); + //args['value'].push($(e).val()); + }); + } else { + args['value'] = $elements.val(); + } + } + return args; + }; + Contact.prototype.queryStringFor = function(obj) { var q = 'id=' + this.id; var ptype = this.propertyTypeFor(obj); @@ -617,7 +785,7 @@ OC.Contacts = OC.Contacts || {}; Contact.prototype.valueFor = function(obj) { var $container = this.propertyContainerFor(obj); console.assert($container.length > 0, 'Couldn\'t find container for ' + $(obj)); - return $container.is('input') + return $container.is('input.value') ? $container.val() : (function() { var $elem = $container.find('textarea.value,input.value:not(:checkbox)'); @@ -636,23 +804,29 @@ OC.Contacts = OC.Contacts || {}; Contact.prototype.parametersFor = function(obj, asText) { var parameters = []; - $.each(this.propertyContainerFor(obj).find('select.parameter,input:checkbox:checked.parameter,textarea'), function(i, elem) { + $.each(this.propertyContainerFor(obj) + .find('select.parameter,input:checkbox:checked.parameter,textarea'), // Why do I look for textarea? + function(i, elem) { var $elem = $(elem); var paramname = $elem.data('parameter'); if(!parameters[paramname]) { parameters[paramname] = []; } - var val; - if(asText) { - if($elem.is(':checkbox')) { - val = $elem.attr('title'); - } else if($elem.is('select')) { - val = $elem.find(':selected').text(); + if($elem.is(':checkbox')) { + if(asText) { + parameters[paramname].push($elem.attr('title')); + } else { + parameters[paramname].push($elem.attr('value')); } - } else { - val = $elem.val(); + } else if($elem.is('select')) { + $.each($elem.find(':selected'), function(idx, e) { + if(asText) { + parameters[paramname].push($(e).text()); + } else { + parameters[paramname].push($(e).val()); + } + }); } - parameters[paramname].push(val); }); return parameters; }; @@ -673,6 +847,7 @@ OC.Contacts = OC.Contacts || {}; name: this.getPreferredValue('FN', '') }); } + this.setThumbnail(this.$dragelem); return this.$dragelem; } @@ -683,18 +858,23 @@ OC.Contacts = OC.Contacts || {}; Contact.prototype.renderListItem = function(isnew) { this.$listelem = this.$listTemplate.octemplate({ id: this.id, - name: isnew ? this.getPreferredValue('FN', '') : this.getPreferredValue('FN', ''), - email: isnew ? this.getPreferredValue('EMAIL', '') : this.getPreferredValue('EMAIL', ''), - tel: isnew ? this.getPreferredValue('TEL', '') : this.getPreferredValue('TEL', ''), - adr: isnew ? this.getPreferredValue('ADR', []).clean('').join(', ') : this.getPreferredValue('ADR', []).clean('').join(', '), + parent: this.metadata.parent, + backend: this.metadata.backend, + name: this.getPreferredValue('FN', ''), + email: this.getPreferredValue('EMAIL', ''), + tel: this.getPreferredValue('TEL', ''), + adr: this.getPreferredValue('ADR', []).clean('').join(', '), categories: this.getPreferredValue('CATEGORIES', []) .clean('').join(' / ') }); - if(this.access.owner !== OC.currentUser - && !(this.access.permissions & OC.PERMISSION_UPDATE - || this.access.permissions & OC.PERMISSION_DELETE)) { + if(this.metadata.owner !== OC.currentUser + && !(this.metadata.permissions & OC.PERMISSION_UPDATE + || this.metadata.permissions & OC.PERMISSION_DELETE)) { this.$listelem.find('input:checkbox').prop('disabled', true).css('opacity', '0'); } + if(isnew) { + this.setThumbnail(); + } return this.$listelem; }; @@ -723,7 +903,7 @@ OC.Contacts = OC.Contacts || {}; }); self.$groupSelect.bind('multiselectclick', function(event, ui) { var action = ui.checked ? 'addtogroup' : 'removefromgroup'; - console.assert(typeof self.id === 'number', 'ID is not a number') + console.assert(typeof self.id === 'string', 'ID is not a string') $(document).trigger('request.contact.' + action, { id: self.id, groupid: parseInt(ui.value) @@ -739,6 +919,34 @@ OC.Contacts = OC.Contacts || {}; } }; + var buildAddressBookSelect = function(availableAddressBooks) { + console.log('address books', availableAddressBooks.length, availableAddressBooks); + /* TODO: + * - Check address books permissions. + * - Add method to change address book. + */ + $.each(availableAddressBooks, function(idx, addressBook) { + //console.log('addressBook', idx, addressBook); + var $option = $(''); + if(self.metadata.parent === addressBook.id + && self.metadata.backend === addressBook.backend) { + $option.attr('selected', 'selected'); + } + self.$addressBookSelect.append($option); + }); + self.$addressBookSelect.multiselect({ + header: false, + selectedList: 3, + noneSelectedText: self.$addressBookSelect.attr('title'), + selectedText: t('contacts', '# groups') + }); + if(self.id) { + self.$addressBookSelect.multiselect('disable'); + } + }; + var n = this.getPreferredValue('N', ['', '', '', '', '']); //console.log('Contact.renderContact', this.data); var values = this.data @@ -770,6 +978,11 @@ OC.Contacts = OC.Contacts || {}; this.$groupSelect = this.$fullelem.find('#contactgroups'); buildGroupSelect(groupprops.groups); + if(Object.keys(this.parent.addressbooks).length > 1) { + this.$addressBookSelect = this.$fullelem.find('#contactaddressbooks'); + buildAddressBookSelect(this.parent.addressbooks); + } + this.$addMenu = this.$fullelem.find('#addproperty'); this.$addMenu.on('change', function(event) { //console.log('add', $(this).val()); @@ -820,13 +1033,9 @@ OC.Contacts = OC.Contacts || {}; id: self.id }); } else if($(this).is('.export')) { - $(document).trigger('request.contact.export', { - id: self.id - }); + $(document).trigger('request.contact.export', self.metaData()); } else if($(this).is('.delete')) { - $(document).trigger('request.contact.delete', { - id: self.id - }); + $(document).trigger('request.contact.delete', self.metaData()); } return false; }); @@ -847,7 +1056,8 @@ OC.Contacts = OC.Contacts || {}; if($(this).hasClass('value') && this.value === this.defaultValue) { return; } - console.log('change', this.defaultValue, this.value); + //console.log('change', this.defaultValue, this.value); + this.defaultValue = this.value; self.saveProperty({obj:event.target}); }); @@ -971,9 +1181,9 @@ OC.Contacts = OC.Contacts || {}; if($meta.length) { $meta.html(meta.join('/')); } - if(self.access.owner === OC.currentUser - || self.access.permissions & OC.PERMISSION_UPDATE - || self.access.permissions & OC.PERMISSION_DELETE) { + if(self.metadata.owner === OC.currentUser + || self.metadata.permissions & OC.PERMISSION_UPDATE + || self.metadata.permissions & OC.PERMISSION_DELETE) { $property.find('select.type[name="parameters[TYPE][]"]') .combobox({ singleclick: true, @@ -985,9 +1195,9 @@ OC.Contacts = OC.Contacts || {}; } } }); - if(this.access.owner !== OC.currentUser - && !(this.access.permissions & OC.PERMISSION_UPDATE - || this.access.permissions & OC.PERMISSION_DELETE)) { + if(this.metadata.owner !== OC.currentUser + && !(this.metadata.permissions & OC.PERMISSION_UPDATE + || this.metadata.permissions & OC.PERMISSION_DELETE)) { this.setEnabled(false); this.showActions(['close', 'export']); } else { @@ -998,9 +1208,9 @@ OC.Contacts = OC.Contacts || {}; }; Contact.prototype.isEditable = function() { - return ((this.access.owner === OC.currentUser) - || (this.access.permissions & OC.PERMISSION_UPDATE - || this.access.permissions & OC.PERMISSION_DELETE)); + return ((this.metadata.owner === OC.currentUser) + || (this.metadata.permissions & OC.PERMISSION_UPDATE + || this.metadata.permissions & OC.PERMISSION_DELETE)); }; /** @@ -1063,7 +1273,7 @@ OC.Contacts = OC.Contacts || {}; var val = self.valueFor(input); var params = self.parametersFor(input, true); $(this).find('.meta').html(params['TYPE'].join('/')); - $(this).find('.adr').html(escapeHTML(self.valueFor($editor.find('input').first()).clean('').join(', '))); + $(this).find('.adr').html(self.valueFor($editor.find('input').first()).clean('').join(', ')); $(this).next('.listactions').css('display', 'inline-block'); $('body').unbind('click', bodyListener); }); @@ -1151,27 +1361,68 @@ OC.Contacts = OC.Contacts || {}; return this.detailTemplates['impp'].octemplate(values); }; + /** + * Set a thumbnail for the contact if a PHOTO property exists + */ + Contact.prototype.setThumbnail = function($elem, refresh) { + if(!this.data.thumbnail && !refresh) { + return; + } + if(!$elem) { + $elem = this.getListItemElement().find('td.name'); + } + if(!$elem.hasClass('thumbnail') && !refresh) { + return; + } + if(this.data.thumbnail) { + $elem.removeClass('thumbnail'); + $elem.css('background-image', 'url(data:image/png;base64,' + this.data.thumbnail + ')'); + } else { + $elem.addClass('thumbnail'); + } + } + /** * Render the PHOTO property. */ Contact.prototype.loadPhoto = function(dontloadhandlers) { var self = this; - var id = this.id || 'new'; - var refreshstr = '&refresh='+Math.random(); - this.$photowrapper = this.$fullelem.find('#photowrapper'); - this.$photowrapper.addClass('loading').addClass('wait'); + var id = this.id || 'new', + backend = this.metadata.backend, + parent = this.metadata.parent, + src; + var $phototools = this.$fullelem.find('#phototools'); - delete this.photo; - $('img.contactphoto').remove(); - this.photo = new Image(); - $(this.photo).load(function () { - $(this).addClass('contactphoto'); - self.$photowrapper.css({width: $(this).get(0).width + 10, height: $(this).get(0).height + 10}); + if(!this.$photowrapper) { + this.$photowrapper = this.$fullelem.find('#photowrapper'); + } + + var finishLoad = function(image) { + console.log('finishLoad', self.getDisplayName(), image.width, image.height); + $(image).addClass('contactphoto'); + self.$photowrapper.css({width: image.width + 10, height: image.height + 10}); self.$photowrapper.removeClass('loading').removeClass('wait'); - $(this).insertAfter($phototools).fadeIn(); - }).error(function () { - OC.notify({message:t('contacts','Error loading profile picture.')}); - }).attr('src', OC.linkTo('contacts', 'photo.php')+'?id='+id+refreshstr); + $(image).insertAfter($phototools).fadeIn(); + }; + + this.$photowrapper.addClass('loading').addClass('wait'); + if(this.getPreferredValue('PHOTO', null) === null) { + $.when(this.storage.getDefaultPhoto()) + .then(function(image) { + $('img.contactphoto').detach(); + finishLoad(image); + }); + } else { + $.when(this.storage.getContactPhoto(backend, parent, id)) + .then(function(image) { + $('img.contactphoto').remove(); + finishLoad(image); + }) + .fail(function(defaultImage) { + $('img.contactphoto').remove(); + finishLoad(defaultImage); + }); + } if(!dontloadhandlers && this.isEditable()) { this.$photowrapper.on('mouseenter', function(event) { @@ -1190,19 +1441,13 @@ OC.Contacts = OC.Contacts || {}; $phototools.find('li a').tipsy(); $phototools.find('.edit').on('click', function() { - $(document).trigger('request.edit.contactphoto', { - id: self.id - }); + $(document).trigger('request.edit.contactphoto', self.metaData()); }); $phototools.find('.cloud').on('click', function() { - $(document).trigger('request.select.contactphoto.fromcloud', { - id: self.id - }); + $(document).trigger('request.select.contactphoto.fromcloud', self.metaData()); }); $phototools.find('.upload').on('click', function() { - $(document).trigger('request.select.contactphoto.fromlocal', { - id: self.id - }); + $(document).trigger('request.select.contactphoto.fromlocal', self.metaData()); }); if(this.data && this.data.PHOTO) { $phototools.find('.delete').show(); @@ -1211,11 +1456,11 @@ OC.Contacts = OC.Contacts || {}; $phototools.find('.delete').hide(); $phototools.find('.edit').hide(); } - $(document).bind('status.contact.photoupdated', function(e, result) { + $(document).bind('status.contact.photoupdated', function(e, data) { + console.log('status.contact.photoupdated', data); self.loadPhoto(true); - var refreshstr = '&refresh='+Math.random(); - self.getListItemElement().find('td.name') - .css('background', 'url(' + OC.filePath('', '', 'remote.php')+'/contactthumbnail?id='+self.id+refreshstr + ')'); + self.data.thumbnail = data.thumbnail; + self.setThumbnail(null, true); }); } }; @@ -1346,12 +1591,14 @@ OC.Contacts = OC.Contacts || {}; }; Contact.prototype.next = function() { - var $next = this.$listelem.next('tr:visible'); + // This used to work..? + //var $next = this.$listelem.next('tr:visible'); + var $next = this.$listelem.nextAll('tr').filter(':visible').first(); if($next.length > 0) { this.$listelem.removeClass('active'); $next.addClass('active'); $(document).trigger('status.contact.currentlistitem', { - id: parseInt($next.data('id')), + id: String($next.data('id')), pos: Math.round($next.position().top), height: Math.round($next.height()) }); @@ -1359,12 +1606,13 @@ OC.Contacts = OC.Contacts || {}; }; Contact.prototype.prev = function() { - var $prev = this.$listelem.prev('tr:visible'); + //var $prev = this.$listelem.prev('tr:visible'); + var $prev = this.$listelem.prevAll('tr').filter(':visible').first(); if($prev.length > 0) { this.$listelem.removeClass('active'); $prev.addClass('active'); $(document).trigger('status.contact.currentlistitem', { - id: parseInt($prev.data('id')), + id: String($prev.data('id')), pos: Math.round($prev.position().top), height: Math.round($prev.height()) }); @@ -1372,6 +1620,7 @@ OC.Contacts = OC.Contacts || {}; }; var ContactList = function( + storage, contactlist, contactlistitemtemplate, contactdragitemtemplate, @@ -1382,17 +1631,19 @@ OC.Contacts = OC.Contacts || {}; var self = this; this.length = 0; this.contacts = {}; + this.addressbooks = {}; this.deletionQueue = []; + this.storage = storage; this.$contactList = contactlist; this.$contactDragItemTemplate = contactdragitemtemplate; this.$contactListItemTemplate = contactlistitemtemplate; this.$contactFullTemplate = contactfulltemplate; this.contactDetailTemplates = contactdetailtemplates; this.$contactList.scrollTop(0); - this.loadContacts(0); + this.getAddressBooks(); $(document).bind('status.contact.added', function(e, data) { self.length += 1; - self.contacts[parseInt(data.id)] = data.contact; + self.contacts[String(data.id)] = data.contact; self.insertContact(data.contact.renderListItem(true)); }); @@ -1404,6 +1655,14 @@ OC.Contacts = OC.Contacts || {}; }); }; + /** + * Get the number of contacts in the list + * @return integer + */ + ContactList.prototype.count = function() { + return Object.keys(this.contacts.contacts).length + } + /** * Show/hide contacts belonging to an addressbook. * @param int aid. Addressbook id. @@ -1412,9 +1671,9 @@ OC.Contacts = OC.Contacts || {}; */ ContactList.prototype.showFromAddressbook = function(aid, show, hideothers) { console.log('ContactList.showFromAddressbook', aid, show); - aid = parseInt(aid); + aid = String(aid); for(var contact in this.contacts) { - if(this.contacts[contact].access.id === aid) { + if(this.contacts[contact].metadata.parent === aid) { this.contacts[contact].getListItemElement().toggle(show); } else if(hideothers) { this.contacts[contact].getListItemElement().hide(); @@ -1429,7 +1688,7 @@ OC.Contacts = OC.Contacts || {}; ContactList.prototype.showSharedAddressbooks = function(show) { console.log('ContactList.showSharedAddressbooks', show); for(var contact in this.contacts) { - if(this.contacts[contact].access.owner !== OC.currentUser) { + if(this.contacts[contact].metadata.owner !== OC.currentUser) { if(show) { this.contacts[contact].getListItemElement().show(); } else { @@ -1444,6 +1703,8 @@ OC.Contacts = OC.Contacts || {}; * @param Array contacts. A list of contact ids. */ ContactList.prototype.showContacts = function(contacts) { + console.log('showContacts', contacts); + var self = this; if(contacts.length === 0) { // ~5 times faster $('tr:visible.contact').hide(); @@ -1451,7 +1712,16 @@ OC.Contacts = OC.Contacts || {}; } if(contacts === 'all') { // ~2 times faster - $('tr.contact:not(:visible)').show(); + var $elems = $('tr.contact:not(:visible)'); + $elems.show(); + $.each($elems, function(idx, elem) { + try { + var id = $(elem).data('id'); + self.contacts[id].setThumbnail(); + } catch(e) { + console.warn('Failed getting id from', $elem, e); + } + }); return; } for(var id in this.contacts) { @@ -1459,10 +1729,11 @@ OC.Contacts = OC.Contacts || {}; if(contact === null) { continue; } - if(contacts.indexOf(parseInt(id)) === -1) { + if(contacts.indexOf(String(id)) === -1) { contact.getListItemElement().hide(); } else { contact.getListItemElement().show(); + contact.setThumbnail(); } } }; @@ -1503,34 +1774,54 @@ OC.Contacts = OC.Contacts || {}; */ ContactList.prototype.findById = function(id) { if(!id) { - console.warn('id missing'); + console.warn('ContactList.findById: id missing'); console.trace(); return false; } - id = parseInt(id); + id = String(id); if(typeof this.contacts[id] === 'undefined') { console.warn('Could not find contact with id', id); console.trace(); return null; } - return this.contacts[parseInt(id)]; + return this.contacts[String(id)]; }; - ContactList.prototype.delayedDelete = function(id) { + /** + * @param object data An object or array of objects containing contact identification + * { + * contactid: '1234', + * addressbookid: '4321', + * backend: 'local' + * } + */ + ContactList.prototype.delayedDelete = function(data) { + console.log('delayedDelete, data:', typeof data, data); var self = this; - if(utils.isUInt(id)) { + if(!utils.isArray(data)) { this.currentContact = null; - self.$contactList.show(); - this.deletionQueue.push(id); - } else if(utils.isArray(id)) { - $.extend(this.deletionQueue, id); + //self.$contactList.show(); + if(data instanceof Contact) { + this.deletionQueue.push(data); + } else { + var contact = this.findById(data.contactid); + this.deletionQueue.push(contact); + } + } else if(utils.isArray(data)) { + $.each(data, function(idx, contact) { + //console.log('delayedDelete, meta:', contact); + self.deletionQueue.push(contact); + }); + //$.extend(this.deletionQueue, data); } else { - throw { name: 'WrongParameterType', message: 'ContactList.delayedDelete only accept integers or arrays.'}; + throw { name: 'WrongParameterType', message: 'ContactList.delayedDelete only accept objects or arrays.'}; } - $.each(this.deletionQueue, function(idx, id) { - self.contacts[id].detach().setChecked(false); + //console.log('delayedDelete, deletionQueue', this.deletionQueue); + $.each(this.deletionQueue, function(idx, contact) { + //console.log('delayedDelete', contact); + contact.detach().setChecked(false); }); - console.log('deletionQueue', this.deletionQueue); + //console.log('deletionQueue', this.deletionQueue); if(!window.onbeforeunload) { window.onbeforeunload = function(e) { e = e || window.event; @@ -1548,16 +1839,16 @@ OC.Contacts = OC.Contacts || {}; message:t('contacts','Click to undo deletion of {num} contacts', {num: self.deletionQueue.length}), //timeout:5, timeouthandler:function() { - console.log('timeout'); + //console.log('timeout'); // Don't fire all deletes at once self.deletionTimer = setInterval(function() { self.deleteContacts(); }, 500); }, clickhandler:function() { - console.log('clickhandler'); - $.each(self.deletionQueue, function(idx, id) { - self.insertContact(self.contacts[id].getListItemElement()); + //console.log('clickhandler'); + $.each(self.deletionQueue, function(idx, contact) { + self.insertContact(contact.getListItemElement()); }); OC.notify({cancel:true}); OC.notify({message:t('contacts', 'Cancelled deletion of {num}', {num: self.deletionQueue.length})}); @@ -1568,20 +1859,19 @@ OC.Contacts = OC.Contacts || {}; }; /** - * Delete a contact with this id - * @param id the id of the contact + * Delete contacts in the queue */ ContactList.prototype.deleteContacts = function() { var self = this; - console.log('ContactList.deleteContacts, deletionQueue', this.deletionQueue); + //console.log('ContactList.deleteContacts, deletionQueue', this.deletionQueue); if(typeof this.deletionTimer === 'undefined') { console.log('No deletion timer!'); window.onbeforeunload = null; return; } - var id = this.deletionQueue.shift(); - if(typeof id === 'undefined') { + var contact = this.deletionQueue.shift(); + if(typeof contact === 'undefined') { clearInterval(this.deletionTimer); delete this.deletionTimer; window.onbeforeunload = null; @@ -1589,13 +1879,10 @@ OC.Contacts = OC.Contacts || {}; } // Let contact remove itself. - var contact = this.findById(id); - if(contact === null) { - return false; - } + var id = contact.getId(); contact.destroy(function(response) { console.log('deleteContact', response, self.length); - if(response.status === 'success') { + if(!response.error) { delete self.contacts[id]; $(document).trigger('status.contact.deleted', { id: id @@ -1661,10 +1948,17 @@ OC.Contacts = OC.Contacts || {}; * @param object props */ ContactList.prototype.addContact = function(props) { + var addressBook = this.addressbooks[Object.keys(this.addressbooks)[0]] + var metadata = { + parent: addressBook.id, + backend: addressBook.backend, + permissions: addressBook.permissions, + owner: addressBook.owner + }; var contact = new Contact( this, null, - {owner:OC.currentUser, permissions: 31}, + metadata, null, this.$contactListItemTemplate, this.$contactDragItemTemplate, @@ -1672,7 +1966,6 @@ OC.Contacts = OC.Contacts || {}; this.contactDetailTemplates ); if(utils.isUInt(this.currentContact)) { - console.assert(typeof this.currentContact == 'number', 'this.currentContact is not a number'); this.contacts[this.currentContact].close(); } return contact.renderContact(props); @@ -1681,13 +1974,15 @@ OC.Contacts = OC.Contacts || {}; /** * Get contacts selected in list * - * @returns array of integer contact ids. + * @returns array of contact ids. */ ContactList.prototype.getSelectedContacts = function() { var contacts = []; + var self = this; $.each(this.$contactList.find('tr > td > input:checkbox:visible:checked'), function(a, b) { - contacts.push(parseInt($(b).parents('tr').first().data('id'))); + var id = String($(b).parents('tr').first().data('id')); + contacts.push(self.contacts[id]); }); return contacts; }; @@ -1703,7 +1998,7 @@ OC.Contacts = OC.Contacts || {}; self.contacts[contact].setCurrent(false); }); } - this.contacts[parseInt(id)].setCurrent(true); + this.contacts[String(id)].setCurrent(true); }; // Should only be neccesary with progressive loading, but it's damn fast, so... ;) @@ -1715,6 +2010,7 @@ OC.Contacts = OC.Contacts || {}; return $(a).find('td.name').text().toUpperCase().localeCompare($(b).find('td.name').text().toUpperCase()); }); + // TODO: Test if I couldn't just append rows. var items = []; $.each(rows, function(index, row) { items.push(row); @@ -1741,40 +2037,88 @@ OC.Contacts = OC.Contacts || {}; * @param object book */ ContactList.prototype.setAddressbook = function(book) { - this.addressbooks[parseInt(book.id)] = { - owner: book.userid, - uri: book.uri, - permissions: parseInt(book.permissions), - id: parseInt(book.id), - displayname: book.displayname, - description: book.description, - active: Boolean(parseInt(book.active)) - }; + console.log('setAddressbook', book.id, this.addressbooks); + var id = String(book.id); + this.addressbooks[id] = book; }; + /** * Load contacts * @param int offset */ - ContactList.prototype.loadContacts = function(offset, cb) { + ContactList.prototype.getAddressBooks = function() { var self = this; - // Should the actual ajax call be in the controller? - $.getJSON(OC.filePath('contacts', 'ajax', 'contact/list.php'), {offset: offset}, function(jsondata) { - if (jsondata && jsondata.status == 'success') { - //console.log('ContactList.loadContacts', jsondata.data); - self.addressbooks = {}; - $.each(jsondata.data.addressbooks, function(i, book) { - self.setAddressbook(book); + $.when(this.storage.getAddressBooksForUser()).then(function(response) { + console.log('ContactList.getAddressBooks', response); + if(!response.error) { + var num = response.data.addressbooks.length; + $.each(response.data.addressbooks, function(idx, addressBook) { + self.setAddressbook(addressBook); + self.loadContacts( + addressBook['backend'], + addressBook['id'], + function(cbresponse) { + //console.log('loaded', idx, cbresponse); + num -= 1; + if(num === 0) { + if(self.length > 0) { + setTimeout(function() { + self.doSort(); // TODO: Test this + self.setCurrent(self.$contactList.find('tr:visible').first().data('id'), false); + } + , 2000); + } + $(document).trigger('status.contacts.loaded', { + status: true, + numcontacts: self.length + }); + if(self.length === 0) { + $(document).trigger('status.nomorecontacts'); + } + } + if(cbresponse.error) { + $(document).trigger('status.contact.error', { + message: + t('contacts', 'Failed loading contacts from {addressbook}: {error}', + {addressbook:addressBook['displayname'], error:err}) + }); + } + }); }); + } else { + $(document).trigger('status.contact.error', { + message: response.message + }); + return false; + } + }) + .fail(function(jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.warn( "Request Failed: " + err); + $(document).trigger('status.contact.error', { + message: t('contacts', 'Failed loading address books: {error}', {error:err}) + }); + }); + }; + + /** + * Load contacts + * @param int offset + */ + ContactList.prototype.loadContacts = function(backend, addressBookId, cb) { + var self = this; + $.when(this.storage.getContacts(backend, addressBookId)).then(function(response) { + //console.log('ContactList.loadContacts', response); + if(!response.error) { var items = []; - if(jsondata.data.contacts.length === 0) { - $(document).trigger('status.nomorecontacts'); - } - $.each(jsondata.data.contacts, function(c, contact) { - self.contacts[parseInt(contact.id)] + $.each(response.data.contacts, function(c, contact) { + var id = String(contact.metadata.id); + contact.metadata.backend = backend; + self.contacts[id] = new Contact( self, - parseInt(contact.id), - self.addressbooks[parseInt(contact.aid)], + id, + contact.metadata, contact.data, self.$contactListItemTemplate, self.$contactDragItemTemplate, @@ -1782,14 +2126,14 @@ OC.Contacts = OC.Contacts || {}; self.contactDetailTemplates ); self.length +=1; - var $item = self.contacts[parseInt(contact.id)].renderListItem(); + var $item = self.contacts[id].renderListItem(); items.push($item.get(0)); $item.find('td.name').draggable({ cursor: 'move', distance: 10, revert: 'invalid', helper: function (e,ui) { - return self.contacts[parseInt(contact.id)].renderDragItem().appendTo('body'); + return self.contacts[id].renderDragItem().appendTo('body'); }, opacity: 1, scope: 'contacts' @@ -1802,22 +2146,24 @@ OC.Contacts = OC.Contacts || {}; if(items.length > 0) { self.$contactList.append(items); } - setTimeout(function() { - self.doSort(); - self.setCurrent(self.$contactList.find('tr:visible:first-child').data('id'), false); - } - , 2000); - $(document).trigger('status.contacts.loaded', { - status: true, - numcontacts: jsondata.data.contacts.length, - is_indexed: jsondata.data.is_indexed + cb({error:false}); + } else { + $(document).trigger('status.contact.error', { + message: response.message + }); + cb({ + error:true, + message: response.message }); } - if(typeof cb === 'function') { - cb(); - } + }) + .fail(function(jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.warn( "Request Failed: " + err); + cb({error: true, message: err}); }); - } + }; + OC.Contacts.ContactList = ContactList; })(window, jQuery, OC); diff --git a/js/groups.js b/js/groups.js index d0055104..696bf615 100644 --- a/js/groups.js +++ b/js/groups.js @@ -16,7 +16,8 @@ OC.Contacts = OC.Contacts || {}; * 'contacts': An array of contact ids belonging to that group * 'obj': A reference to the groupList object. */ - var GroupList = function(groupList, listItemTmpl) { + var GroupList = function(storage, groupList, listItemTmpl) { + this.storage = storage; this.$groupList = groupList; var self = this; var numtypes = ['category', 'fav', 'all']; @@ -29,7 +30,7 @@ OC.Contacts = OC.Contacts || {}; if($(event.target).is('.action.delete')) { var id = $(event.target).parents('h3').first().data('id'); self.deleteGroup(id, function(response) { - if(response.status !== 'success') { + if(response.error) { OC.notify({message:response.data.message}); } }); @@ -129,7 +130,6 @@ OC.Contacts = OC.Contacts || {}; * @param function cb. Optional callback function. */ GroupList.prototype.setAsFavorite = function(contactid, state, cb) { - contactid = parseInt(contactid); var $groupelem = this.findById('fav'); var contacts = $groupelem.data('contacts'); if(state) { @@ -185,7 +185,7 @@ OC.Contacts = OC.Contacts || {}; } var self = this; var doPost = false; - if(typeof contactid === 'number') { + if(typeof contactid === 'string') { if(contacts.indexOf(contactid) === -1) { ids.push(contactid); doPost = true; @@ -211,14 +211,8 @@ OC.Contacts = OC.Contacts || {}; console.warn('Invalid data type: ' + typeof contactid); } if(doPost) { - $.post(OC.filePath('contacts', 'ajax', 'categories/addto.php'), {contactids: ids, categoryid: groupid},function(jsondata) { - if(!jsondata) { - if(typeof cb === 'function') { - cb({status:'error', message:'Network or server error. Please inform administrator.'}); - } - return; - } - if(jsondata.status === 'success') { + $.when(this.storage.addToGroup(ids, groupid)).then(function(response) { + if(!response.error) { contacts = contacts.concat(ids).sort(); $groupelem.data('contacts', contacts); var $numelem = $groupelem.find('.numcontacts'); @@ -237,7 +231,7 @@ OC.Contacts = OC.Contacts || {}; } } else { if(typeof cb == 'function') { - cb({status:'error', message:jsondata.data.message}); + cb({status:'error', message:response.message}); } } }); @@ -300,14 +294,8 @@ OC.Contacts = OC.Contacts || {}; } } if(doPost) { - $.post(OC.filePath('contacts', 'ajax', 'categories/removefrom.php'), {contactids: ids, categoryid: groupid},function(jsondata) { - if(!jsondata) { - if(typeof cb === 'function') { - cb({status:'error', message:'Network or server error. Please inform administrator.'}); - } - return; - } - if(jsondata.status === 'success') { + $.when(this.storage.removeFromGroup(ids, groupid)).then(function(response) { + if(!response.error) { $.each(ids, function(idx, id) { contacts.splice(contacts.indexOf(id), 1); }); @@ -323,7 +311,7 @@ OC.Contacts = OC.Contacts || {}; } } else { if(typeof cb == 'function') { - cb({status:'error', message:jsondata.data.message}); + cb({status:'error', message:response.message}); } } }); @@ -363,7 +351,7 @@ OC.Contacts = OC.Contacts || {}; var dragitem = ui.draggable, droptarget = $(this); console.log('dropped', dragitem); if(dragitem.is('.name')) { - var id = dragitem.parent().data('id'); + var id = String(dragitem.parent().data('id')); console.log('contact dropped', id, 'on', $(this).data('id')); if($(this).data('type') === 'fav') { $(this).data('obj').setAsFavorite(id, true); @@ -397,22 +385,35 @@ OC.Contacts = OC.Contacts || {}; var contacts = $elem.data('contacts'); var self = this; console.log('delete group', groupid, contacts); - $.post(OC.filePath('contacts', 'ajax', 'categories/delete.php'), {categories: name}, function(jsondata) { - if (jsondata && jsondata.status == 'success') { + $.when(this.storage.deleteGroup(name)).then(function(response) { + if (!response.error) { + $.each(self.categories, function(idx, category) { + if(category.id === groupid) { + self.categories.splice(self.categories.indexOf(category), 1); + return false; // Break loop + } + }); $(document).trigger('status.group.groupremoved', { groupid: groupid, newgroupid: parseInt($newelem.data('id')), - groupname: self.nameById(groupid), + groupname: name, contacts: contacts }); $elem.remove(); self.selectGroup({element:$newelem}); } else { - // + console.log('Error', response); } if(typeof cb === 'function') { - cb(jsondata); + cb(response); } + }) + .fail(function(jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.log( "Request Failed: " + err); + $(document).trigger('status.contact.error', { + message: t('contacts', 'Failed deleting group: {error}', {error:err}) + }); }); }; @@ -441,7 +442,7 @@ OC.Contacts = OC.Contacts || {}; $input.prop('disabled', true); $elem.data('rawname', ''); self.addGroup({name:name, element: $elem}, function(response) { - if(response.status === 'success') { + if(!response.error) { $elem.prepend(escapeHTML(response.name)).removeClass('editing').attr('data-id', response.id); $input.next('.checked').remove(); $input.remove(); @@ -545,10 +546,10 @@ OC.Contacts = OC.Contacts || {}; } return; } - $.post(OC.filePath('contacts', 'ajax', 'categories/add.php'), {category: name}, function(jsondata) { - if (jsondata && jsondata.status == 'success') { - name = jsondata.data.name; - var id = jsondata.data.id; + $.when(this.storage.addGroup(name)).then(function(response) { + if (!response.error) { + name = response.data.name; + var id = response.data.id; var tmpl = self.$groupListItemTemplate; var $elem = params.element ? params.element @@ -578,13 +579,20 @@ OC.Contacts = OC.Contacts || {}; $elem.tipsy({trigger:'manual', gravity:'w', fallback: t('contacts', 'You can drag groups to\narrange them as you like.')}); $elem.tipsy('show'); if(typeof cb === 'function') { - cb({status:'success', id:parseInt(id), name:name}); + cb({id:parseInt(id), name:name}); } } else { if(typeof cb === 'function') { - cb({status:'error', message:jsondata.data.message}); + cb({error:true, message:response.data.message}); } } + }) + .fail(function(jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.log( "Request Failed: " + err); + $(document).trigger('status.contact.error', { + message: t('contacts', 'Failed adding group: {error}', {error:err}) + }); }); }; @@ -595,15 +603,14 @@ OC.Contacts = OC.Contacts || {}; var tmpl = this.$groupListItemTemplate; tmpl.octemplate({id: 'all', type: 'all', num: numcontacts, name: t('contacts', 'All')}).appendTo($groupList); - $.getJSON(OC.filePath('contacts', 'ajax', 'categories/list.php'), {}, function(jsondata) { - if (jsondata && jsondata.status == 'success') { - self.lastgroup = jsondata.data.lastgroup; - self.sortorder = jsondata.data.sortorder.length > 0 - ? $.map(jsondata.data.sortorder.split(','), function(c) {return parseInt(c);}) - : []; + $.when(this.storage.getGroupsForUser()).then(function(response) { + if (response && !response.error) { + self.lastgroup = response.data.lastgroup; + self.sortorder = contacts_groups_sortorder; console.log('sortorder', self.sortorder); // Favorites - var contacts = $.map(jsondata.data.favorites, function(c) {return parseInt(c);}); + // Map to strings easier lookup an contacts list. + var contacts = $.map(response.data.favorites, function(c) {return String(c);}); var $elem = tmpl.octemplate({ id: 'fav', type: 'fav', @@ -627,8 +634,8 @@ OC.Contacts = OC.Contacts || {}; } console.log('favorites', $elem.data('contacts')); // Normal groups - $.each(jsondata.data.categories, function(c, category) { - var contacts = $.map(category.contacts, function(c) {return parseInt(c);}); + $.each(response.data.categories, function(c, category) { + var contacts = $.map(category.contacts, function(c) {return String(c);}); var $elem = (tmpl).octemplate({ id: category.id, type: 'category', @@ -663,13 +670,13 @@ OC.Contacts = OC.Contacts || {}; }); // Shared addressbook - $.each(jsondata.data.shared, function(c, shared) { + $.each(response.data.shared, function(c, shared) { var sharedindicator = ''; var $elem = (tmpl).octemplate({ id: shared.id, type: 'shared', - num: '', //jsondata.data.shared.length, + num: response.data.shared.length, name: shared.displayname }); $elem.find('.numcontacts').after(sharedindicator); @@ -703,6 +710,10 @@ OC.Contacts = OC.Contacts || {}; if(typeof cb === 'function') { cb(); } + }) + .fail(function(jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.log( "Request Failed: " + err); }); }; diff --git a/js/jquery.ocdialog.js b/js/jquery.ocdialog.js new file mode 100644 index 00000000..852d0559 --- /dev/null +++ b/js/jquery.ocdialog.js @@ -0,0 +1,194 @@ +(function($) { + $.widget('oc.ocdialog', { + options: { + width: 'auto', + height: 'auto', + closeButton: true, + closeOnEscape: true + }, + _create: function() { + console.log('ocdialog._create'); + var self = this; + + this.originalCss = { + display: this.element[0].style.display, + width: this.element[0].style.width, + height: this.element[0].style.height, + }; + + this.originalTitle = this.element.attr('title'); + this.options.title = this.options.title || this.originalTitle; + + this.$dialog = $('
') + .attr({ + // Setting tabIndex makes the div focusable + tabIndex: -1, + role: 'dialog' + }) + .insertBefore(this.element); + this.$dialog.append(this.element.detach()); + this.element.removeAttr('title').addClass('oc-dialog-content').appendTo(this.$dialog); + + this.$dialog.css({ + display: 'inline-block', + position: 'fixed' + }); + + $(document).on('keydown keyup', function(event) { + if(event.target !== self.$dialog.get(0) && self.$dialog.find($(event.target)).length === 0) { + return; + } + // Escape + if(event.keyCode === 27 && self.options.closeOnEscape) { + self.close(); + return false; + } + // Enter + if(event.keyCode === 13) { + event.stopImmediatePropagation(); + if(event.type === 'keyup') { + event.preventDefault(); + return false; + } + // If no button is selected we trigger the primary + if(self.$buttonrow && self.$buttonrow.find($(event.target)).length === 0) { + var $button = self.$buttonrow.find('button.primary'); + if($button) { + $button.trigger('click'); + } + } else if(self.$buttonrow) { + $(event.target).trigger('click'); + } + return false; + } + }); + $(window).resize(function() { + self.parent = self.$dialog.parent().length > 0 ? self.$dialog.parent() : $('body'); + var pos = self.parent.position(); + self.$dialog.css({ + left: pos.left + (self.parent.width() - self.$dialog.outerWidth())/2, + top: pos.top + (self.parent.height() - self.$dialog.outerHeight())/2 + }); + }); + + this._setOptions(this.options); + $(window).trigger('resize'); + }, + _init: function() { + console.log('ocdialog._init'); + this.$dialog.focus(); + this._trigger('open'); + }, + _setOption: function(key, value) { + console.log('_setOption', key, value); + var self = this; + switch(key) { + case 'title': + var $title = $('

' + this.options.title + + '

'); //
'); + if(this.$title) { + this.$title.replaceWith($title); + } else { + this.$title = $title.prependTo(this.$dialog); + } + this._setSizes(); + break; + case 'buttons': + var $buttonrow = $('
'); + if(this.$buttonrow) { + this.$buttonrow.replaceWith($buttonrow); + } else { + this.$buttonrow = $buttonrow.appendTo(this.$dialog); + } + $.each(value, function(idx, val) { + var $button = $(''); + if(val.defaultButton) { + $button.addClass('primary'); + self.$defaultButton = $button; + } + self.$buttonrow.append($button); + $button.click(function() { + val.click.apply(self.element[0], arguments); + }); + }); + this.$buttonrow.find('button') + .on('focus', function(event) { + self.$buttonrow.find('button').removeClass('primary'); + $(this).addClass('primary'); + }); + this._setSizes(); + break; + case 'closeButton': + console.log('closeButton', value); + if(value) { + var $closeButton = $(''); + console.log('closeButton', $closeButton); + this.$dialog.prepend($closeButton); + $closeButton.on('click', function() { + self.close(); + }); + } + break; + case 'width': + this.$dialog.css('width', value); + break; + case 'height': + this.$dialog.css('height', value); + break; + case 'close': + this.closeCB = value; + break; + } + //this._super(key, value); + $.Widget.prototype._setOption.apply(this, arguments ); + }, + _setOptions: function(options) { + console.log('_setOptions', options); + //this._super(options); + $.Widget.prototype._setOptions.apply(this, arguments); + }, + _setSizes: function() { + var content_height = this.$dialog.height(); + if(this.$title) { + content_height -= this.$title.outerHeight(true); + } + if(this.$buttonrow) { + content_height -= this.$buttonrow.outerHeight(true); + } + this.parent = this.$dialog.parent().length > 0 ? this.$dialog.parent() : $('body'); + content_height = Math.min(content_height, this.parent.height()-20) + this.element.css({ + height: content_height + 'px', + width: this.$dialog.innerWidth() + 'px' + }); + }, + widget: function() { + return this.$dialog + }, + close: function() { + console.log('close'); + var self = this; + // Ugly hack to catch remaining keyup events. + setTimeout(function() { + self._trigger('close', self); + self.$dialog.hide(); + }, 200); + }, + destroy: function() { + console.log('destroy'); + if(this.$title) { + this.$title.remove() + } + if(this.$buttonrow) { + this.$buttonrow.remove() + } + + if(this.originalTitle) { + this.element.attr('title', this.originalTitle); + } + this.element.removeClass('oc-dialog-content') + .css(this.originalCss).detach().insertBefore(this.$dialog); + this.$dialog.remove(); + } + }); +}(jQuery)); diff --git a/js/storage.js b/js/storage.js new file mode 100644 index 00000000..e7d41628 --- /dev/null +++ b/js/storage.js @@ -0,0 +1,444 @@ +OC.Contacts = OC.Contacts || {}; + +/** + * TODO: Use $.Deferred. + */ + +(function(window, $, OC) { + 'use strict'; + + var JSONResponse = function(response) { + if(!response || !response.status || response.status === 'error') { + this.error = true; + this.message = response.data.message || 'Unknown error.'; + } else { + this.error = false; + if(response.data) { + this.data = response.data; + } else { + this.data = response; + } + } + } + + /** + * An object for saving contact data to backends + * + * All methods returns a jQuery.Deferred object which resolves + * to either the requested response or an error object: + * { + * status: 'error', + * message: The error message + * } + * + * @param string user The user to query for. Defaults to current user + */ + var Storage = function(user) { + this.user = user ? user : OC.currentUser; + } + + /** + * Get all address books registered for this user. + * + * @return An array containing object of address book metadata e.g.: + * { + * backend:'local', + * id:'1234' + * permissions:31, + * displayname:'Contacts' + * } + */ + Storage.prototype.getAddressBooksForUser = function() { + return this.requestRoute( + 'contacts_address_books_for_user', + 'GET', + {user: this.user} + ); + } + + /** + * Add an address book to a specific backend + * + * @param string backend - currently defaults to 'local' + * @param object params An object {displayname:"My contacts", description:""} + * @return An array containing contact data e.g.: + * { + * metadata: + * { + * id:'1234' + * permissions:31, + * displayname:'My contacts', + * lastmodified: (unix timestamp), + * owner: 'joye', + * } + */ + Storage.prototype.addAddressBook = function(backend, parameters) { + console.log('Storage.addAddressBook', backend); + return this.requestRoute( + 'contacts_address_book_add', + 'POST', + {user: this.user, backend: 'local'}, + parameters + ); + } + + /** + * Delete an address book from a specific backend + * + * @param string backend + * @param string addressbookid Address book ID + */ + Storage.prototype.deleteAddressBook = function(backend, addressbookid) { + console.log('Storage.deleteAddressBook', backend, addressbookid); + return this.requestRoute( + 'contacts_address_book_delete', + 'POST', + {user: this.user, backend: 'local', addressbookid: addressbookid} + ); + } + + /** + * Get contacts from an address book from a specific backend + * + * @param string backend + * @param string addressbookid Address book ID + * @return An array containing contact data e.g.: + * { + * metadata: + * { + * id:'1234' + * permissions:31, + * displayname:'John Q. Public', + * lastmodified: (unix timestamp), + * owner: 'joye', + * parent: (id of the parent address book) + * data: //array of VCard data + * } + */ + Storage.prototype.getContacts = function(backend, addressbookid) { + return this.requestRoute( + 'contacts_address_book_collection', + 'GET', + {user: this.user, backend: backend, addressbookid: addressbookid} + ); + } + + /** + * Add a contact to an address book from a specific backend + * + * @param string backend + * @param string addressbookid Address book ID + * @return An array containing contact data e.g.: + * { + * metadata: + * { + * id:'1234' + * permissions:31, + * displayname:'John Q. Public', + * lastmodified: (unix timestamp), + * owner: 'joye', + * parent: (id of the parent address book) + * data: //array of VCard data + * } + */ + Storage.prototype.addContact = function(backend, addressbookid) { + console.log('Storage.addContact', backend, addressbookid); + return this.requestRoute( + 'contacts_address_book_add_contact', + 'POST', + {user: this.user, backend: backend, addressbookid: addressbookid} + ); + } + + /** + * Delete a contact from an address book from a specific backend + * + * @param string backend + * @param string addressbookid Address book ID + * @param string contactid Address book ID + */ + Storage.prototype.deleteContact = function(backend, addressbookid, contactid) { + console.log('Storage.deleteContact', backend, addressbookid, contactid); + return this.requestRoute( + 'contacts_address_book_delete_contact', + 'POST', + {user: this.user, backend: backend, addressbookid: addressbookid, contactid: contactid} + ); + } + + /** + * Get Image instance for a contacts profile picture + * + * @param string backend + * @param string addressbookid Address book ID + * @param string contactid Address book ID + * @return Image + */ + Storage.prototype.getContactPhoto = function(backend, addressbookid, contactid) { + var photo = new Image(); + var url = OC.Router.generate( + 'contacts_contact_photo', + {user: this.user, backend: backend, addressbookid: addressbookid, contactid: contactid} + ); + var defer = $.Deferred(); + $.when( + $(photo).load(function() { + defer.resolve(photo); + }) + .error(function() { + console.log('Error loading default photo', arguments) + }) + .attr('src', url + '?refresh=' + Math.random()) + ) + .fail(function(jqxhr, textStatus, error) { + defer.reject(); + var err = textStatus + ', ' + error; + console.log( "Request Failed: " + err); + $(document).trigger('status.contact.error', { + message: t('contacts', 'Failed loading photo: {error}', {error:err}) + }); + }); + return defer.promise(); + } + + /** + * Get Image instance for default profile picture + * + * This method loads the default picture only once and caches it. + * + * @return Image + */ + Storage.prototype.getDefaultPhoto = function() { + console.log('Storage.getDefaultPhoto'); + if(!this._defaultPhoto) { + var url = OC.imagePath('contacts', 'person_large.png'); + this._defaultPhoto = new Image(); + $.when( + $(this._defaultPhoto) + .load(function() { + console.log('Default photo loaded', arguments); + }).error(function() { + console.log('Error loading default photo', arguments) + }).attr('src', url) + ) + .then(function(response) { + console.log('Storage.defaultPhoto', response); + }) + .fail(function(jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.log( "Request Failed: " + err); + $(document).trigger('status.contact.error', { + message: t('contacts', 'Failed loading default photo: {error}', {error:err}) + }); + }); + }; + return this._defaultPhoto; + } + + /** + * Delete a single property. + * + * @param string backend + * @param string addressbookid Address book ID + * @param string contactid Contact ID + * @param object params An object with the following properties: + * @param string name The name of the property e.g. EMAIL. + * @param string checksum For non-singular properties such as email this must contain + * an 8 character md5 checksum of the serialized \Sabre\Property + */ + Storage.prototype.deleteProperty = function(backend, addressbookid, contactid, params) { + return this.requestRoute( + 'contacts_contact_delete_property', + 'POST', + {user: this.user, backend: backend, addressbookid: addressbookid, contactid: contactid}, + params + ); + } + + /** + * Save a property. + * + * @param string backend + * @param string addressbookid Address book ID + * @param string contactid Contact ID + * @param object params An object with the following properties: + * @param string name The name of the property e.g. EMAIL. + * @param string|array value The of the property + * @param array parameters Optional parameters for the property + * @param string checksum For non-singular properties such as email this must contain + * an 8 character md5 checksum of the serialized \Sabre\Property + */ + Storage.prototype.saveProperty = function(backend, addressbookid, contactid, params) { + return this.requestRoute( + 'contacts_contact_save_property', + 'POST', + {user: this.user, backend: backend, addressbookid: addressbookid, contactid: contactid}, + params + ); + } + + /** + * Save all properties. Used when merging contacts. + * + * @param string backend + * @param string addressbookid Address book ID + * @param string contactid Contact ID + * @param object params An object with the all properties: + */ + Storage.prototype.saveAllProperties = function(backend, addressbookid, contactid, params) { + console.log('Storage.saveAllProperties', params); + return this.requestRoute( + 'contacts_contact_save_all', + 'POST', + {user: this.user, backend: backend, addressbookid: addressbookid, contactid: contactid}, + params + ); + } + + /** + * Get all groups for this user. + * + * @return An array containing the groups, the favorites, any shared + * address books, the last selected group and the sort order of the groups. + * { + * 'categories': [{'id':1',Family'}, {...}], + * 'favorites': [123,456], + * 'shared': [], + * 'lastgroup':'1', + * 'sortorder':'3,2,4' + * } + */ + Storage.prototype.getGroupsForUser = function() { + console.log('getGroupsForUser'); + return this.requestRoute( + 'contacts_categories_list', + 'GET', + {user: this.user} + ); + } + + /** + * Add a group + * + * @param string name + * @return A JSON object containing the (maybe sanitized) group name and its ID: + * { + * 'id':1234, + * 'name':'My group' + * } + */ + Storage.prototype.addGroup = function(name) { + console.log('Storage.addGroup', name); + return this.requestRoute( + 'contacts_categories_add', + 'POST', + {user: this.user}, + {name: name} + ); + } + + /** + * Delete a group + * + * @param string name + */ + Storage.prototype.deleteGroup = function(name) { + return this.requestRoute( + 'contacts_categories_delete', + 'POST', + {user: this.user}, + {name: name} + ); + } + + /** + * Add contacts to a group + * + * @param array contactids + */ + Storage.prototype.addToGroup = function(contactids, categoryid) { + console.log('Storage.addToGroup', contactids, categoryid); + return this.requestRoute( + 'contacts_categories_addto', + 'POST', + {user: this.user, categoryid: categoryid}, + {contactids: contactids} + ); + } + + /** + * Remove contacts from a group + * + * @param array contactids + */ + Storage.prototype.removeFromGroup = function(contactids, categoryid) { + console.log('Storage.addToGroup', contactids, categoryid); + return this.requestRoute( + 'contacts_categories_removefrom', + 'POST', + {user: this.user, categoryid: categoryid}, + {contactids: contactids} + ); + } + + /** + * Set a user preference + * + * @param string key + * @param string value + */ + Storage.prototype.setPreference = function(key, value) { + return this.requestRoute( + 'contacts_setpreference', + 'POST', + {user: this.user}, + {key: key, value:value} + ); + } + + Storage.prototype.requestRoute = function(route, type, routeParams, params) { + var isJSON = (typeof params === 'string'); + var contentType = isJSON ? 'application/json' : 'application/x-www-form-urlencoded'; + var processData = !isJSON; + contentType += '; charset=UTF-8'; + var self = this; + var url = OC.Router.generate(route, routeParams); + var ajaxParams = { + type: type, + url: url, + dataType: 'json', + contentType: contentType, + processData: processData, + data: params + }; + var defer = $.Deferred(); + /*$.when($.ajax(ajaxParams)).then(function(response) { + defer.resolve(new JSONResponse(response)); + }).fail(function(jqxhr, textStatus, error) { + defer.reject( + new JSONResponse({ + status:'error', + data:{message:t('contacts', 'Request failed: {error}', {error:textStatus + ', ' + error})} + }) + ); + });*/ + + var jqxhr = $.ajax(ajaxParams) + .done(function(response) { + defer.resolve(new JSONResponse(response)); + }) + .fail(function(jqxhr, textStatus, error) { + defer.reject( + new JSONResponse({ + status:'error', + data:{message:t('contacts', 'Request failed: {error}', {error:textStatus + ', ' + error})} + }) + ); + }); + + return defer.promise(); + } + + OC.Contacts.Storage = Storage; + +})(window, jQuery, OC); diff --git a/lib/abstractpimcollection.php b/lib/abstractpimcollection.php new file mode 100644 index 00000000..2373da82 --- /dev/null +++ b/lib/abstractpimcollection.php @@ -0,0 +1,168 @@ +. + * + */ + +namespace OCA\Contacts; + +/** + * Subclass this for PIM collections + */ + +abstract class PIMCollectionAbstract extends PIMObjectAbstract implements \Iterator, \Countable, \ArrayAccess { + + // Iterator properties + + protected $objects = array(); + + protected $counter = 0; + + /** + * This is a collection so return null. + * @return null + */ + function getParent() { + null; + } + + /** + * Returns a specific child node, referenced by its id + * TODO: Maybe implement it here? + * + * @param string $id + * @return IPIMObject + */ + abstract function getChild($id); + + /** + * Returns an array with all the child nodes + * + * @return IPIMObject[] + */ + abstract function getChildren($limit = null, $offset = null); + + /** + * Checks if a child-node with the specified id exists + * + * @param string $id + * @return bool + */ + abstract function childExists($id); + + /** + * Add a child to the collection + * + * It's up to the implementations to "keep track" of the children. + * + * @param mixed $data + * @return string ID of the newly added child + */ + abstract public function addChild($data); + + /** + * Delete a child from the collection + * + * @param string $id + * @return bool + */ + abstract public function deleteChild($id); + + // Iterator methods + + public function rewind() { + $this->counter = 0; + } + + public function next() { + $this->counter++; + } + + public function valid() { + return array_key_exists($this->counter, $this->objects); + } + + public function current() { + return $this->objects[$this->counter]; + } + + /** Implementations can choose to return the current objects ID/UUID + * to be able to iterate over the collection with ID => Object pairs: + * foreach($collection as $id => $object) {} + */ + public function key() { + return $this->counter; + } + + // Countable method. + + /** + * For implementations using a backend where fetching all object at once + * would give too much overhead, they can maintain an internal count value + * and fetch objects progressively. Simply watch the diffence between + * $this->counter, the value of count($this->objects) and the internal + * value, and fetch more objects when needed. + */ + public function count() { + return count($this->objects); + } + + // ArrayAccess methods + + public function offsetSet($offset, $value) { + if (is_null($offset)) { + $this->objects[] = $value; + } else { + $this->objects[$offset] = $value; + } + } + + public function offsetExists($offset) { + return isset($this->objects[$offset]); + } + + public function offsetUnset($offset) { + unset($this->objects[$offset]); + } + + public function offsetGet($offset) { + return isset($this->objects[$offset]) ? $this->objects[$offset] : null; + } + + // Magic property accessors + // NOTE: They should go in the implementations(?) + /* + public function __set($id, $value) { + $this->objects[$id] = $value; + } + + public function __get($id) { + return $this->objects[$id]; + } + + public function __isset($id) { + return isset($this->objects[$id]); + } + + public function __unset($id) { + unset($this->objects[$id]); + } + */ + +} diff --git a/lib/abstractpimobject.php b/lib/abstractpimobject.php new file mode 100644 index 00000000..42636822 --- /dev/null +++ b/lib/abstractpimobject.php @@ -0,0 +1,137 @@ +. + * + */ + +namespace OCA\Contacts; + +/** + * Subclass this class or implement IPIMObject interface for PIM objects + */ + +abstract class PIMObjectAbstract implements IPIMObject { + + /** + * This variable holds the ID of this object. + * Depending on the backend, this can be either a string + * or an integer, so we treat them all as strings. + * + * @var string + */ + protected $id; + + /** + * This variable holds the owner of this object. + * + * @var string + */ + protected $owner; + + /** + * This variable holds the parent of this object if any. + * + * @var string|null + */ + protected $parent; + + /** + * This variable holds the permissions of this object. + * + * @var integer + */ + protected $permissions; + + /** + * @return string + */ + public function getId() { + return $this->id; + } + + /** + * @return string|null + */ + function getDisplayName() { + return $this->displayName; + } + + /** + * @return string|null + */ + public function getOwner() { + return $this->owner; + } + + /** + * If this object is part of a collection return a reference + * to the parent object, otherwise return null. + * @return IPIMObject|null + */ + function getParent() { + return $this->parent; + } + + /** CRUDS permissions (Create, Read, Update, Delete, Share) using a bitmask of + * + * \OCP\PERMISSION_CREATE + * \OCP\PERMISSION_READ + * \OCP\PERMISSION_UPDATE + * \OCP\PERMISSION_DELETE + * \OCP\PERMISSION_SHARE + * or + * \OCP\PERMISSION_ALL + * + * @return integer + */ + function getPermissions() { + return $this->permissions; + } + + /** + * @return AbstractBackend + */ + function getBackend() { + return $this->backend; + } + + /** + * @param integer $permission + * @return boolean + */ + function hasPermission($permission) { + return $this->getPermissions() & $permission; + } + + /** + * Save the data to backend + * + * @param array $data + * @return bool + */ + abstract public function update(array $data); + + /** + * Delete the data from backend + * + * @return bool + */ + abstract public function delete(); + +} \ No newline at end of file diff --git a/lib/addressbook.php b/lib/addressbook.php index 1913898a..37b43c2f 100644 --- a/lib/addressbook.php +++ b/lib/addressbook.php @@ -2,8 +2,8 @@ /** * ownCloud - Addressbook * - * @author Jakob Sack - * @copyright 2011 Jakob Sack mail@jakobsack.de + * @author Thomas Tanghus + * @copyright 2013 Thomas Tanghus (thomas@tanghus.net) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE @@ -19,416 +19,262 @@ * License along with this library. If not, see . * */ -/* - * - * The following SQL statement is just a help for developers and will not be - * executed! - * - * CREATE TABLE contacts_addressbooks ( - * id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, - * userid VARCHAR(255) NOT NULL, - * displayname VARCHAR(255), - * uri VARCHAR(100), - * description TEXT, - * ctag INT(11) UNSIGNED NOT NULL DEFAULT '1' - * ); - * - */ namespace OCA\Contacts; /** * This class manages our addressbooks. */ -class Addressbook { +class Addressbook extends PIMCollectionAbstract { + + protected $_count; /** - * @brief Returns the list of addressbooks for a specific user. - * @param string $uid - * @param boolean $active Only return addressbooks with this $active state, default(=false) is don't care - * @param boolean $shared Whether to also return shared addressbook. Defaults to true. - * @return array or false. + * @var Backend\AbstractBackend */ - public static function all($uid, $active = false, $shared = true) { - $values = array($uid); - $active_where = ''; - if ($active) { - $active_where = ' AND `active` = ?'; - $values[] = 1; - } - try { - $stmt = \OCP\DB::prepare( 'SELECT * FROM `*PREFIX*contacts_addressbooks` WHERE `userid` = ? ' . $active_where . ' ORDER BY `displayname`' ); - $result = $stmt->execute($values); - if (\OC_DB::isError($result)) { - \OCP\Util::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); - return false; + protected $backend; + + /** + * An array containing the mandatory: + * 'displayname' + * 'discription' + * 'permissions' + * + * And the optional: + * 'Etag' + * 'lastModified' + * + * @var array + */ + protected $addressBookInfo; + + /** + * @param AbstractBackend $backend The storage backend + * @param array $addressBookInfo + */ + public function __construct(Backend\AbstractBackend $backend, array $addressBookInfo) { + $this->backend = $backend; + $this->addressBookInfo = $addressBookInfo; + if(is_null($this->getId())) { + $id = $this->backend->createAddressBook($addressBookInfo); + if($id === false) { + throw new \Exception('Error creating address book.'); } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.' exception: '.$e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.' uid: '.$uid, \OCP\Util::DEBUG); + $this->addressBookInfo = $this->backend->getAddressBook($id); + //print(__METHOD__. ' '. __LINE__ . ' addressBookInfo: ' . print_r($this->backend->getAddressBook($id), true)); + } + //\OCP\Util::writeLog('contacts', __METHOD__.' backend: ' . print_r($this->backend, true), \OCP\Util::DEBUG); + } + + /** + * @return string|null + */ + public function getId() { + return isset($this->addressBookInfo['id']) + ? $this->addressBookInfo['id'] + : null; + } + + /** + * @return array + */ + public function getMetaData() { + $metadata = $this->addressBookInfo; + $metadata['lastmodified'] = $this->lastModified(); + $metadata['backend'] = $this->getBackend()->name; + return $metadata; + } + + /** + * @return string + */ + public function getDisplayName() { + return $this->addressBookInfo['displayname']; + } + + /** + * @return string + */ + public function getURI() { + return $this->addressBookInfo['uri']; + } + + /** + * @return string + */ + public function getOwner() { + return $this->addressBookInfo['owner']; + } + + /** + * @return string + */ + public function getPermissions() { + return $this->addressBookInfo['permissions']; + } + + function getBackend() { + return $this->backend; + } + + /** + * Returns a specific child node, referenced by its id + * + * @param string $id + * @return Contact|null + */ + function getChild($id) { + //\OCP\Util::writeLog('contacts', __METHOD__.' id: '.$id, \OCP\Util::DEBUG); + if(!isset($this->objects[$id])) { + $contact = $this->backend->getContact($this->getId(), $id); + if($contact) { + $this->objects[$id] = new Contact($this, $this->backend, $contact); + } + } + // When requesting a single contact we preparse it + if(isset($this->objects[$id])) { + $this->objects[$id]->retrieve(); + return $this->objects[$id]; + } + } + + /** + * Checks if a child-node with the specified id exists + * + * @param string $id + * @return bool + */ + function childExists($id) { + return ($this->getChild($id) !== null); + } + + /** + * Returns an array with all the child nodes + * + * @return Contact[] + */ + function getChildren($limit = null, $offset = null, $omitdata = false) { + //\OCP\Util::writeLog('contacts', __METHOD__.' backend: ' . print_r($this->backend, true), \OCP\Util::DEBUG); + $contacts = array(); + + foreach($this->backend->getContacts($this->getId(), $limit, $offset, $omitdata) as $contact) { + //\OCP\Util::writeLog('contacts', __METHOD__.' id: '.$contact['id'], \OCP\Util::DEBUG); + if(!isset($this->objects[$contact['id']])) { + $this->objects[$contact['id']] = new Contact($this, $this->backend, $contact); + } + $contacts[] = $this->objects[$contact['id']]; + } + return $contacts; + } + + /** + * Add a contact to the address book + * This takes an array or a VCard|Contact and return + * the ID or false. + * + * @param array|VObject\VCard $data + * @return int|bool + */ + public function addChild($data = null) { + $contact = new Contact($this, $this->backend, $data); + if($contact->save() === false) { return false; } - - $addressbooks = array(); - while( $row = $result->fetchRow()) { - $row['permissions'] = \OCP\PERMISSION_ALL; - $addressbooks[] = $row; - } - - if($shared === true) { - $addressbooks = array_merge($addressbooks, \OCP\Share::getItemsSharedWith('addressbook', Share_Backend_Addressbook::FORMAT_ADDRESSBOOKS)); - } - if(!$active && !count($addressbooks)) { - $id = self::addDefault($uid); - return array(self::find($id),); - } - return $addressbooks; - } - - /** - * @brief Get active addressbook IDs for a user. - * @param integer $uid User id. If null current user will be used. - * @return array - */ - public static function activeIds($uid = null) { - if(is_null($uid)) { - $uid = \OCP\USER::getUser(); - } - - // query all addressbooks to force creation of default if it desn't exist. - $activeaddressbooks = self::all($uid); - $ids = array(); - foreach($activeaddressbooks as $addressbook) { - if($addressbook['active']) { - $ids[] = $addressbook['id']; - } - } - return $ids; - } - - /** - * @brief Returns the list of active addressbooks for a specific user. - * @param string $uid - * @return array - */ - public static function active($uid) { - return self::all($uid, true); - } - - /** - * @brief Returns the list of addressbooks for a principal (DAV term of user) - * @param string $principaluri - * @return array - */ - public static function allWherePrincipalURIIs($principaluri) { - $uid = self::extractUserID($principaluri); - return self::all($uid); - } - - /** - * @brief Gets the data of one address book - * @param integer $id - * @return associative array or false. - */ - public static function find($id) { - try { - $stmt = \OCP\DB::prepare( 'SELECT * FROM `*PREFIX*contacts_addressbooks` WHERE `id` = ?' ); - $result = $stmt->execute(array($id)); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: ' . $e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', id: ' . $id, \OCP\Util::DEBUG); - return false; - } - $row = $result->fetchRow(); - - if($row['userid'] != \OCP\USER::getUser() && !\OC_Group::inGroup(\OCP\User::getUser(), 'admin')) { - $sharedAddressbook = \OCP\Share::getItemSharedWithBySource('addressbook', $id); - if (!$sharedAddressbook || !($sharedAddressbook['permissions'] & \OCP\PERMISSION_READ)) { - throw new \Exception( - App::$l10n->t( - 'You do not have the permissions to read this addressbook.' - ) - ); - } - $row['permissions'] = $sharedAddressbook['permissions']; - } else { - $row['permissions'] = \OCP\PERMISSION_ALL; - } - return $row; - } - - /** - * @brief Adds default address book - * @return $id ID of the newly created addressbook or false on error. - */ - public static function addDefault($uid = null) { - if(is_null($uid)) { - $uid = \OCP\USER::getUser(); - } - $id = self::add($uid, 'Contacts', 'Default Address Book'); - if($id !== false) { - self::setActive($id, true); + $id = $contact->getId(); + if($this->count() !== null) { + $this->_count += 1; } + \OCP\Util::writeLog('contacts', __METHOD__.' id: '.$id, \OCP\Util::DEBUG); return $id; } /** - * @brief Creates a new address book - * @param string $userid - * @param string $name - * @param string $description - * @return insertid + * Delete a contact from the address book + * + * @param string $id + * @return bool */ - public static function add($uid,$name,$description='') { - try { - $stmt = \OCP\DB::prepare( 'SELECT `uri` FROM `*PREFIX*contacts_addressbooks` WHERE `userid` = ? ' ); - $result = $stmt->execute(array($uid)); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); - return false; + public function deleteChild($id) { + if($this->backend->deleteContact($this->getId(), $id)) { + if(isset($this->objects[$id])) { + unset($this->objects[$id]); } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__ . ' exception: ' . $e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__ . ' uid: ' . $uid, \OCP\Util::DEBUG); - return false; - } - $uris = array(); - while($row = $result->fetchRow()) { - $uris[] = $row['uri']; - } - - $uri = self::createURI($name, $uris ); - try { - $stmt = \OCP\DB::prepare( 'INSERT INTO `*PREFIX*contacts_addressbooks` (`userid`,`displayname`,`uri`,`description`,`ctag`) VALUES(?,?,?,?,?)' ); - $result = $stmt->execute(array($uid,$name,$uri,$description,1)); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); - return false; + if($this->count() !== null) { + $this->_count -= 1; } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', uid: '.$uid, \OCP\Util::DEBUG); - return false; - } - - return \OCP\DB::insertid('*PREFIX*contacts_addressbooks'); - } - - /** - * @brief Creates a new address book from the data sabredav provides - * @param string $principaluri - * @param string $uri - * @param string $name - * @param string $description - * @return insertid or false - */ - public static function addFromDAVData($principaluri, $uri, $name, $description) { - $uid = self::extractUserID($principaluri); - - try { - $stmt = \OCP\DB::prepare('INSERT INTO `*PREFIX*contacts_addressbooks` ' - . '(`userid`,`displayname`,`uri`,`description`,`ctag`) VALUES(?,?,?,?,?)'); - $result = $stmt->execute(array($uid, $name, $uri, $description, 1)); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: ' . $e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', uid: ' . $uid, \OCP\Util::DEBUG); - \OCP\Util::writeLog('contacts', __METHOD__.', uri: ' . $uri, \OCP\Util::DEBUG); - return false; - } - - return \OCP\DB::insertid('*PREFIX*contacts_addressbooks'); - } - - /** - * @brief Edits an addressbook - * @param integer $id - * @param string $name - * @param string $description - * @return boolean - */ - public static function edit($id,$name,$description) { - // Need these ones for checking uri - $addressbook = self::find($id); - if ($addressbook['userid'] != \OCP\User::getUser() && !\OC_Group::inGroup(OCP\User::getUser(), 'admin')) { - $sharedAddressbook = \OCP\Share::getItemSharedWithBySource('addressbook', $id); - if (!$sharedAddressbook || !($sharedAddressbook['permissions'] & \OCP\PERMISSION_UPDATE)) { - throw new \Exception( - App::$l10n->t( - 'You do not have the permissions to update this addressbook.' - ) - ); - } - } - if(is_null($name)) { - $name = $addressbook['name']; - } - if(is_null($description)) { - $description = $addressbook['description']; - } - - try { - $stmt = \OCP\DB::prepare('UPDATE `*PREFIX*contacts_addressbooks` SET `displayname`=?,`description`=?, `ctag`=`ctag`+1 WHERE `id`=?'); - $result = $stmt->execute(array($name,$description,$id)); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); - throw new \Exception( - App::$l10n->t( - 'There was an error updating the addressbook.' - ) - ); - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__ . ', exception: ' . $e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__ . ', id: ' . $id, \OCP\Util::DEBUG); - throw new \Exception( - App::$l10n->t( - 'There was an error updating the addressbook.' - ) - ); - } - - return true; - } - - /** - * @brief Activates an addressbook - * @param integer $id - * @param boolean $active - * @return boolean - */ - public static function setActive($id,$active) { - $sql = 'UPDATE `*PREFIX*contacts_addressbooks` SET `active` = ? WHERE `id` = ?'; - - try { - $stmt = \OCP\DB::prepare($sql); - $stmt->execute(array(intval($active), $id)); return true; - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__ . ', exception for ' . $id.': ' . $e->getMessage(), \OCP\Util::ERROR); + } + return false; + } + + /** + * @internal implements Countable + * @return int|null + */ + public function count() { + if(!isset($this->_count)) { + $this->_count = $this->backend->numContacts($this->getId()); + } + return $this->_count; + } + + /** + * Update and save the address book data to backend + * NOTE: @see IPIMObject::update for consistency considerations. + * + * @param array $data + * @return bool + */ + public function update(array $data) { + if(count($data) === 0) { return false; } - } - - /** - * @brief Checks if an addressbook is active. - * @param integer $id ID of the address book. - * @return boolean - */ - public static function isActive($id) { - $sql = 'SELECT `active` FROM `*PREFIX*contacts_addressbooks` WHERE `id` = ?'; - try { - $stmt = \OCP\DB::prepare( $sql ); - $result = $stmt->execute(array($id)); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); - return false; - } - $row = $result->fetchRow(); - return (bool)$row['active']; - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: ' . $e->getMessage(), \OCP\Util::ERROR); - return false; - } - } - - /** - * @brief removes an address book - * @param integer $id - * @return boolean true on success, otherwise an exception will be thrown - */ - public static function delete($id) { - $addressbook = self::find($id); - - if ($addressbook['userid'] != \OCP\User::getUser() && !\OC_Group::inGroup(\OCP\User::getUser(), 'admin')) { - $sharedAddressbook = \OCP\Share::getItemSharedWithBySource('addressbook', $id); - if (!$sharedAddressbook || !($sharedAddressbook['permissions'] & \OCP\PERMISSION_DELETE)) { - throw new \Exception( - App::$l10n->t( - 'You do not have the permissions to delete this addressbook.' - ) - ); + foreach($data as $key => $value) { + switch($key) { + case 'displayname': + $this->addressBookInfo['displayname'] = $value; + break; + case 'description': + $this->addressBookInfo['description'] = $value; + break; } } - - // First delete cards belonging to this addressbook. - $cards = VCard::all($id); - foreach($cards as $card) { - try { - VCard::delete($card['id']); - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', - __METHOD__.', exception deleting vCard '.$card['id'].': ' - . $e->getMessage(), - \OCP\Util::ERROR); - } - } - - try { - $stmt = \OCP\DB::prepare('DELETE FROM `*PREFIX*contacts_addressbooks` WHERE `id` = ?'); - $stmt->execute(array($id)); - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', - __METHOD__.', exception for ' . $id . ': ' - . $e->getMessage(), - \OCP\Util::ERROR); - throw new \Exception( - App::$l10n->t( - 'There was an error deleting this addressbook.' - ) - ); - } - - \OCP\Share::unshareAll('addressbook', $id); - - if(count(self::all(\OCP\User::getUser())) == 0) { - self::addDefault(); - } - - return true; + return $this->backend->updateAddressBook($this->getId(), $data); } /** - * @brief Updates ctag for addressbook - * @param integer $id - * @return boolean + * Save the address book data to backend + * NOTE: @see IPIMObject::update for consistency considerations. + * + * @return bool */ - public static function touch($id) { - $stmt = \OCP\DB::prepare( 'UPDATE `*PREFIX*contacts_addressbooks` SET `ctag` = `ctag` + 1 WHERE `id` = ?' ); - $stmt->execute(array($id)); - - return true; - } - - /** - * @brief Creates a URI for Addressbook - * @param string $name name of the addressbook - * @param array $existing existing addressbook URIs - * @return string new name - */ - public static function createURI($name,$existing) { - $name = str_replace(' ', '_', strtolower($name)); - $newname = $name; - $i = 1; - while(in_array($newname, $existing)) { - $newname = $name.$i; - $i = $i + 1; + public function save() { + if(!$this->hasPermission(OCP\PERMISSION_UPDATE)) { + throw new Exception('You don\'t have permissions to update the address book.'); } - return $newname; } /** - * @brief gets the userid from a principal path - * @return string + * Delete the address book from backend + * + * @return bool */ - public static function extractUserID($principaluri) { - list($prefix, $userid) = \Sabre_DAV_URLUtil::splitPath($principaluri); - return $userid; + public function delete() { + if(!$this->hasPermission(OCP\PERMISSION_DELETE)) { + throw new Exception('You don\'t have permissions to delete the address book.'); + } + return $this->backend->deleteAddressBook($this->getId()); } + + /** + * @brief Get the last modification time for the object. + * + * Must return a UNIX time stamp or null if the backend + * doesn't support it. + * + * @returns int | null + */ + public function lastModified() { + return $this->backend->lastModifiedAddressBook($this->getId()); + } + } diff --git a/lib/addressbooklegacy.php b/lib/addressbooklegacy.php new file mode 100644 index 00000000..617a803c --- /dev/null +++ b/lib/addressbooklegacy.php @@ -0,0 +1,387 @@ +. + * + */ +/* + * + * The following SQL statement is just a help for developers and will not be + * executed! + * + * CREATE TABLE contacts_addressbooks ( + * id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + * userid VARCHAR(255) NOT NULL, + * displayname VARCHAR(255), + * uri VARCHAR(100), + * description TEXT, + * ctag INT(11) UNSIGNED NOT NULL DEFAULT '1' + * ); + * + */ + +namespace OCA\Contacts; + +/** + * This class manages our addressbooks. + */ +class AddressbookLegacy { + /** + * @brief Returns the list of addressbooks for a specific user. + * @param string $uid + * @param boolean $active Only return addressbooks with this $active state, default(=false) is don't care + * @param boolean $shared Whether to also return shared addressbook. Defaults to true. + * @return array or false. + */ + public static function all($uid, $active = false, $shared = true) { + $values = array($uid); + $active_where = ''; + if ($active) { + $active_where = ' AND `active` = ?'; + $values[] = 1; + } + try { + $stmt = \OCP\DB::prepare( 'SELECT * FROM `*PREFIX*contacts_addressbooks` WHERE `userid` = ? ' . $active_where . ' ORDER BY `displayname`' ); + $result = $stmt->execute($values); + if (\OC_DB::isError($result)) { + \OCP\Util::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + return false; + } + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.' exception: '.$e->getMessage(), \OCP\Util::ERROR); + \OCP\Util::writeLog('contacts', __METHOD__.' uid: '.$uid, \OCP\Util::DEBUG); + return false; + } + + $addressbooks = array(); + while( $row = $result->fetchRow()) { + $row['permissions'] = \OCP\PERMISSION_ALL; + $addressbooks[] = $row; + } + + if($shared === true) { + $addressbooks = array_merge($addressbooks, \OCP\Share::getItemsSharedWith('addressbook', Share_Backend_Addressbook::FORMAT_ADDRESSBOOKS)); + } + if(!$active && !count($addressbooks)) { + $id = self::addDefault($uid); + return array(self::find($id),); + } + return $addressbooks; + } + + /** + * @brief Get active addressbook IDs for a user. + * @param integer $uid User id. If null current user will be used. + * @return array + */ + public static function activeIds($uid = null) { + if(is_null($uid)) { + $uid = \OCP\USER::getUser(); + } + + // query all addressbooks to force creation of default if it desn't exist. + $activeaddressbooks = self::all($uid); + $ids = array(); + foreach($activeaddressbooks as $addressbook) { + if($addressbook['active']) { + $ids[] = $addressbook['id']; + } + } + return $ids; + } + + /** + * @brief Returns the list of active addressbooks for a specific user. + * @param string $uid + * @return array + */ + public static function active($uid) { + return self::all($uid, true); + } + + /** + * @brief Gets the data of one address book + * @param integer $id + * @return associative array or false. + */ + public static function find($id) { + try { + $stmt = \OCP\DB::prepare( 'SELECT * FROM `*PREFIX*contacts_addressbooks` WHERE `id` = ?' ); + $result = $stmt->execute(array($id)); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + return false; + } + } catch(Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.', exception: ' . $e->getMessage(), \OCP\Util::ERROR); + \OCP\Util::writeLog('contacts', __METHOD__.', id: ' . $id, \OCP\Util::DEBUG); + return false; + } + $row = $result->fetchRow(); + + if($row['userid'] != \OCP\USER::getUser() && !\OC_Group::inGroup(\OCP\User::getUser(), 'admin')) { + $sharedAddressbook = \OCP\Share::getItemSharedWithBySource('addressbook', $id); + if (!$sharedAddressbook || !($sharedAddressbook['permissions'] & \OCP\PERMISSION_READ)) { + throw new Exception( + App::$l10n->t( + 'You do not have the permissions to read this addressbook.' + ) + ); + } + $row['permissions'] = $sharedAddressbook['permissions']; + } else { + $row['permissions'] = \OCP\PERMISSION_ALL; + } + return $row; + } + + /** + * @brief Adds default address book + * @return $id ID of the newly created addressbook or false on error. + */ + public static function addDefault($uid = null) { + if(is_null($uid)) { + $uid = \OCP\USER::getUser(); + } + $id = self::add($uid, 'Contacts', 'Default Address Book'); + if($id !== false) { + self::setActive($id, true); + } + return $id; + } + + /** + * @brief Creates a new address book + * @param string $userid + * @param string $name + * @param string $description + * @return insertid + */ + public static function add($uid,$name,$description='') { + try { + $stmt = \OCP\DB::prepare( 'SELECT `uri` FROM `*PREFIX*contacts_addressbooks` WHERE `userid` = ? ' ); + $result = $stmt->execute(array($uid)); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + return false; + } + } catch(Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__ . ' exception: ' . $e->getMessage(), \OCP\Util::ERROR); + \OCP\Util::writeLog('contacts', __METHOD__ . ' uid: ' . $uid, \OCP\Util::DEBUG); + return false; + } + $uris = array(); + while($row = $result->fetchRow()) { + $uris[] = $row['uri']; + } + + $uri = self::createURI($name, $uris ); + try { + $stmt = \OCP\DB::prepare( 'INSERT INTO `*PREFIX*contacts_addressbooks` (`userid`,`displayname`,`uri`,`description`,`ctag`) VALUES(?,?,?,?,?)' ); + $result = $stmt->execute(array($uid,$name,$uri,$description,1)); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + return false; + } + } catch(Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); + \OCP\Util::writeLog('contacts', __METHOD__.', uid: '.$uid, \OCP\Util::DEBUG); + return false; + } + + return \OCP\DB::insertid('*PREFIX*contacts_addressbooks'); + } + + /** + * @brief Edits an addressbook + * @param integer $id + * @param string $name + * @param string $description + * @return boolean + */ + public static function edit($id,$name,$description) { + // Need these ones for checking uri + $addressbook = self::find($id); + if ($addressbook['userid'] != \OCP\User::getUser() && !\OC_Group::inGroup(OCP\User::getUser(), 'admin')) { + $sharedAddressbook = \OCP\Share::getItemSharedWithBySource('addressbook', $id); + if (!$sharedAddressbook || !($sharedAddressbook['permissions'] & \OCP\PERMISSION_UPDATE)) { + throw new \Exception( + App::$l10n->t( + 'You do not have the permissions to update this addressbook.' + ) + ); + } + } + if(is_null($name)) { + $name = $addressbook['name']; + } + if(is_null($description)) { + $description = $addressbook['description']; + } + + try { + $stmt = \OCP\DB::prepare('UPDATE `*PREFIX*contacts_addressbooks` SET `displayname`=?,`description`=?, `ctag`=`ctag`+1 WHERE `id`=?'); + $result = $stmt->execute(array($name,$description,$id)); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + throw new Exception( + App::$l10n->t( + 'There was an error updating the addressbook.' + ) + ); + } + } catch(Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__ . ', exception: ' . $e->getMessage(), \OCP\Util::ERROR); + \OCP\Util::writeLog('contacts', __METHOD__ . ', id: ' . $id, \OCP\Util::DEBUG); + throw new Exception( + App::$l10n->t( + 'There was an error updating the addressbook.' + ) + ); + } + + return true; + } + + /** + * @brief Activates an addressbook + * @param integer $id + * @param boolean $active + * @return boolean + */ + public static function setActive($id,$active) { + $sql = 'UPDATE `*PREFIX*contacts_addressbooks` SET `active` = ? WHERE `id` = ?'; + + try { + $stmt = \OCP\DB::prepare($sql); + $stmt->execute(array(intval($active), $id)); + return true; + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__ . ', exception for ' . $id.': ' . $e->getMessage(), \OCP\Util::ERROR); + return false; + } + } + + /** + * @brief Checks if an addressbook is active. + * @param integer $id ID of the address book. + * @return boolean + */ + public static function isActive($id) { + $sql = 'SELECT `active` FROM `*PREFIX*contacts_addressbooks` WHERE `id` = ?'; + try { + $stmt = \OCP\DB::prepare( $sql ); + $result = $stmt->execute(array($id)); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + return false; + } + $row = $result->fetchRow(); + return (bool)$row['active']; + } catch(Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.', exception: ' . $e->getMessage(), \OCP\Util::ERROR); + return false; + } + } + + /** + * @brief removes an address book + * @param integer $id + * @return boolean true on success, otherwise an exception will be thrown + */ + public static function delete($id) { + $addressbook = self::find($id); + + if ($addressbook['userid'] != \OCP\User::getUser() && !\OC_Group::inGroup(\OCP\User::getUser(), 'admin')) { + $sharedAddressbook = \OCP\Share::getItemSharedWithBySource('addressbook', $id); + if (!$sharedAddressbook || !($sharedAddressbook['permissions'] & \OCP\PERMISSION_DELETE)) { + throw new Exception( + App::$l10n->t( + 'You do not have the permissions to delete this addressbook.' + ) + ); + } + } + + // First delete cards belonging to this addressbook. + $cards = VCard::all($id); + foreach($cards as $card) { + try { + VCard::delete($card['id']); + } catch(Exception $e) { + \OCP\Util::writeLog('contacts', + __METHOD__.', exception deleting vCard '.$card['id'].': ' + . $e->getMessage(), + \OCP\Util::ERROR); + } + } + + try { + $stmt = \OCP\DB::prepare('DELETE FROM `*PREFIX*contacts_addressbooks` WHERE `id` = ?'); + $stmt->execute(array($id)); + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', + __METHOD__.', exception for ' . $id . ': ' + . $e->getMessage(), + \OCP\Util::ERROR); + throw new Exception( + App::$l10n->t( + 'There was an error deleting this addressbook.' + ) + ); + } + + \OCP\Share::unshareAll('addressbook', $id); + + if(count(self::all(\OCP\User::getUser())) == 0) { + self::addDefault(); + } + + return true; + } + + /** + * @brief Updates ctag for addressbook + * @param integer $id + * @return boolean + */ + public static function touch($id) { + $stmt = \OCP\DB::prepare( 'UPDATE `*PREFIX*contacts_addressbooks` SET `ctag` = `ctag` + 1 WHERE `id` = ?' ); + $stmt->execute(array($id)); + + return true; + } + + /** + * @brief Creates a URI for Addressbook + * @param string $name name of the addressbook + * @param array $existing existing addressbook URIs + * @return string new name + */ + public static function createURI($name,$existing) { + $name = str_replace(' ', '_', strtolower($name)); + $newname = $name; + $i = 1; + while(in_array($newname, $existing)) { + $newname = $name.$i; + $i = $i + 1; + } + return $newname; + } + +} diff --git a/lib/addressbookprovider.php b/lib/addressbookprovider.php index dd872e9d..6d80df0c 100644 --- a/lib/addressbookprovider.php +++ b/lib/addressbookprovider.php @@ -24,6 +24,7 @@ namespace OCA\Contacts; /** * This class manages our addressbooks. + * TODO: Port this to use the new backend */ class AddressbookProvider implements \OCP\IAddressBook { @@ -48,7 +49,6 @@ class AddressbookProvider implements \OCP\IAddressBook { */ public function __construct($id) { $this->id = $id; - \Sabre\VObject\Property::$classMap['GEO'] = 'Sabre\\VObject\\Property\\Compound'; } public function getAddressbook() { diff --git a/lib/app.php b/lib/app.php index a48e77f1..66186456 100644 --- a/lib/app.php +++ b/lib/app.php @@ -1,6 +1,6 @@ 'OCA\Contacts\Backend\Database', + 'shared' => 'OCA\Contacts\Backend\Shared', + ); - if(!$card) { - return null; - } - try { - $vcard = \Sabre\VObject\Reader::read($card['carddata']); - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: ' . $e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', id: ' . $id, \OCP\Util::DEBUG); - return null; - } - - if (!is_null($vcard) && !isset($vcard->REV)) { - $rev = new \DateTime('@'.$card['lastmodified']); - $vcard->REV = $rev->format(\DateTime::W3C); - } - return $vcard; + public function __construct( + $user = null, + $addressBooksTableName = '*PREFIX*addressbook', + $backendsTableName = '*PREFIX*addressbooks_backend', + $dbBackend = null + ) { + $this->user = $user ? $user : \OCP\User::getUser(); + $this->addressBooksTableName = $addressBooksTableName; + $this->backendsTableName = $backendsTableName; + $this->dbBackend = $dbBackend + ? $dbBackend + : new Backend\Database($user); } - public static function getPropertyLineByChecksum($vcard, $checksum) { - $line = null; - foreach($vcard->children as $i => $property) { - if(substr(md5($property->serialize()), 0, 8) == $checksum ) { - $line = $i; - break; + /** + * Gets backend by name. + * + * @param string $name + * @return \Backend\AbstractBackend + */ + static public function getBackend($name, $user = null) { + $name = $name ? $name : 'local'; + if (isset(self::$backendClasses[$name])) { + return new self::$backendClasses[$name]($user); + } else { + throw new \Exception('No backend for: ' . $name); + } + } + + /** + * Return all registered address books for current user. + * For now this is hard-coded to using the Database and + * Shared backends, but eventually admins will be able to + * register additional backends, and users will be able to + * subscribe to address books using those backends. + * + * @return AddressBook[] + */ + public function getAddressBooksForUser() { + if(!self::$addressBooks) { + foreach(array_keys(self::$backendClasses) as $backendName) { + $backend = self::getBackend($backendName, $this->user); + $addressBooks = $backend->getAddressBooksForUser(); + if($backendName === 'local' && count($addressBooks) === 0) { + $id = $backend->createAddressBook(array('displayname' => 'Contacts')); + if($id !== false) { + $addressBook = $backend->getAddressBook($id); + $addressBooks = array($addressBook); + } else { + // TODO: Write log + } + } + foreach($addressBooks as $addressBook) { + $addressBook['backend'] = $backendName; + self::$addressBooks[] = new AddressBook($backend, $addressBook); + } } } - return $line; + return self::$addressBooks; } /** - * @return array of vcard prop => label + * Get an address book from a specific backend. + * + * @param string $backendName + * @param string $addressbookid + * @return AddressBook|null */ - public static function getIMOptions($im = null) { - $l10n = self::$l10n; - $ims = array( - 'jabber' => array( - 'displayname' => (string)$l10n->t('Jabber'), - 'xname' => 'X-JABBER', - 'protocol' => 'xmpp', - ), - 'aim' => array( - 'displayname' => (string)$l10n->t('AIM'), - 'xname' => 'X-AIM', - 'protocol' => 'aim', - ), - 'msn' => array( - 'displayname' => (string)$l10n->t('MSN'), - 'xname' => 'X-MSN', - 'protocol' => 'msn', - ), - 'twitter' => array( - 'displayname' => (string)$l10n->t('Twitter'), - 'xname' => 'X-TWITTER', - 'protocol' => 'twitter', - ), - 'googletalk' => array( - 'displayname' => (string)$l10n->t('GoogleTalk'), - 'xname' => null, - 'protocol' => 'xmpp', - ), - 'facebook' => array( - 'displayname' => (string)$l10n->t('Facebook'), - 'xname' => null, - 'protocol' => 'xmpp', - ), - 'xmpp' => array( - 'displayname' => (string)$l10n->t('XMPP'), - 'xname' => null, - 'protocol' => 'xmpp', - ), - 'icq' => array( - 'displayname' => (string)$l10n->t('ICQ'), - 'xname' => 'X-ICQ', - 'protocol' => 'icq', - ), - 'yahoo' => array( - 'displayname' => (string)$l10n->t('Yahoo'), - 'xname' => 'X-YAHOO', - 'protocol' => 'ymsgr', - ), - 'skype' => array( - 'displayname' => (string)$l10n->t('Skype'), - 'xname' => 'X-SKYPE', - 'protocol' => 'skype', - ), - 'qq' => array( - 'displayname' => (string)$l10n->t('QQ'), - 'xname' => 'X-SKYPE', - 'protocol' => 'x-apple', - ), - 'gadugadu' => array( - 'displayname' => (string)$l10n->t('GaduGadu'), - 'xname' => 'X-SKYPE', - 'protocol' => 'x-apple', - ), - ); - if(is_null($im)) { - return $ims; - } else { - $ims['ymsgr'] = $ims['yahoo']; - $ims['gtalk'] = $ims['googletalk']; - return isset($ims[$im]) ? $ims[$im] : null; + public function getAddressBook($backendName, $addressbookid) { + foreach(self::$addressBooks as $addressBook) { + if($addressBook->backend->name === $backendName + && $addressBook->getId() === $addressbookid + ) { + return $addressBook; + } } + // TODO: Check for return values + $backend = self::getBackend($backendName, $this->user); + $info = $backend->getAddressBook($addressbookid); + // FIXME: Backend name should be set by the backend. + $info['backend'] = $backendName; + $addressBook = new AddressBook($backend, $info); + self::$addressBooks[] = $addressBook; + return $addressBook; } /** - * @return types for property $prop + * Get a Contact from an address book from a specific backend. + * + * @param string $backendName + * @param string $addressbookid + * @param string $id - Contact id + * @return Contact|null + * */ - public static function getTypesOfProperty($prop) { - $l = self::$l10n; - switch($prop) { - case 'ADR': - case 'IMPP': - return array( - 'WORK' => $l->t('Work'), - 'HOME' => $l->t('Home'), - 'OTHER' => $l->t('Other'), - ); - case 'TEL': - return array( - 'HOME' => $l->t('Home'), - 'CELL' => $l->t('Mobile'), - 'WORK' => $l->t('Work'), - 'TEXT' => $l->t('Text'), - 'VOICE' => $l->t('Voice'), - 'MSG' => $l->t('Message'), - 'FAX' => $l->t('Fax'), - 'VIDEO' => $l->t('Video'), - 'PAGER' => $l->t('Pager'), - 'OTHER' => $l->t('Other'), - ); - case 'EMAIL': - return array( - 'WORK' => $l->t('Work'), - 'HOME' => $l->t('Home'), - 'INTERNET' => $l->t('Internet'), - 'OTHER' => $l->t('Other'), - ); - } + public function getContact($backendName, $addressbookid, $id) { + $addressBook = $this->getAddressBook($backendName, $addressbookid); + // TODO: Check for return value + return $addressBook->getChild($id); } /** - * @brief returns the vcategories object of the user - * @return (object) $vcategories - */ + * @brief returns the vcategories object of the user + * @return (object) $vcategories + */ public static function getVCategories() { if (is_null(self::$categories)) { if(\OC_VCategories::isEmpty('contact')) { self::scanCategories(); } self::$categories = new \OC_VCategories('contact', - null, - self::getDefaultCategories()); + null, + self::getDefaultCategories()); } return self::$categories; } - /** * @brief returns the categories for the user * @return (Array) $categories @@ -219,19 +171,6 @@ class App { return ($categories ? $categories : self::getDefaultCategories()); } - /** - * @brief returns the default categories of ownCloud - * @return (array) $categories - */ - public static function getDefaultCategories() { - return array( - (string)self::$l10n->t('Friends'), - (string)self::$l10n->t('Family'), - (string)self::$l10n->t('Work'), - (string)self::$l10n->t('Other'), - ); - } - /** * scan vcards for categories. * @param $vccontacts VCards to scan. null to check all vcards for the current user. @@ -269,140 +208,4 @@ class App { } } - /** - * check VCard for new categories. - * @see OC_VCategories::loadFromVObject - */ - public static function loadCategoriesFromVCard($id, $contact) { - if(!$contact instanceof \OC_VObject) { - $contact = new \OC_VObject($contact); - } - self::getVCategories()->loadFromVObject($id, $contact, true); - } - - /** - * @brief Get the last modification time. - * @param OC_VObject|Sabre\VObject\Component|integer|null $contact - * @returns DateTime | null - */ - public static function lastModified($contact = null) { - if(is_null($contact)) { - // FIXME: This doesn't take shared address books into account. - $sql = 'SELECT MAX(`lastmodified`) FROM `*PREFIX*contacts_cards`, `*PREFIX*contacts_addressbooks` ' . - 'WHERE `*PREFIX*contacts_cards`.`addressbookid` = `*PREFIX*contacts_addressbooks`.`id` AND ' . - '`*PREFIX*contacts_addressbooks`.`userid` = ?'; - $stmt = \OCP\DB::prepare($sql); - $result = $stmt->execute(array(\OCP\USER::getUser())); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); - return null; - } - $lastModified = $result->fetchOne(); - if(!is_null($lastModified)) { - return new \DateTime('@' . $lastModified); - } - } else if(is_numeric($contact)) { - $card = VCard::find($contact, array('lastmodified')); - return ($card ? new \DateTime('@' . $card['lastmodified']) : null); - } elseif($contact instanceof \OC_VObject || $contact instanceof VObject\Component) { - return isset($contact->REV) - ? \DateTime::createFromFormat(\DateTime::W3C, $contact->REV) - : null; - } - } - - public static function cacheThumbnail($id, \OC_Image $image = null) { - if(\OC_Cache::hasKey(self::THUMBNAIL_PREFIX . $id) && $image === null) { - return \OC_Cache::get(self::THUMBNAIL_PREFIX . $id); - } - if(is_null($image)) { - $vcard = self::getContactVCard($id); - - // invalid vcard - if(is_null($vcard)) { - \OCP\Util::writeLog('contacts', - __METHOD__.' The VCard for ID ' . $id . ' is not RFC compatible', - \OCP\Util::ERROR); - return false; - } - $image = new \OC_Image(); - if(!isset($vcard->PHOTO)) { - return false; - } - if(!$image->loadFromBase64((string)$vcard->PHOTO)) { - return false; - } - } - if(!$image->centerCrop()) { - \OCP\Util::writeLog('contacts', - 'thumbnail.php. Couldn\'t crop thumbnail for ID ' . $id, - \OCP\Util::ERROR); - return false; - } - if(!$image->resize(self::THUMBNAIL_SIZE)) { - \OCP\Util::writeLog('contacts', - 'thumbnail.php. Couldn\'t resize thumbnail for ID ' . $id, - \OCP\Util::ERROR); - return false; - } - // Cache for around a month - \OC_Cache::set(self::THUMBNAIL_PREFIX . $id, $image->data(), 3000000); - \OCP\Util::writeLog('contacts', 'Caching ' . $id, \OCP\Util::DEBUG); - return \OC_Cache::get(self::THUMBNAIL_PREFIX . $id); - } - - public static function updateDBProperties($contactid, $vcard = null) { - $stmt = \OCP\DB::prepare('DELETE FROM `*PREFIX*contacts_cards_properties` WHERE `contactid` = ?'); - try { - $stmt->execute(array($contactid)); - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__. - ', exception: ' . $e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', id: ' - . $id, \OCP\Util::DEBUG); - throw new \Exception( - App::$l10n->t( - 'There was an error deleting properties for this contact.' - ) - ); - } - - if(is_null($vcard)) { - return; - } - - $stmt = \OCP\DB::prepare( 'INSERT INTO `*PREFIX*contacts_cards_properties` ' - . '(`userid`, `contactid`,`name`,`value`,`preferred`) VALUES(?,?,?,?,?)' ); - foreach($vcard->children as $property) { - if(!in_array($property->name, self::$index_properties)) { - continue; - } - $preferred = 0; - foreach($property->parameters as $parameter) { - if($parameter->name == 'TYPE' && strtoupper($parameter->value) == 'PREF') { - $preferred = 1; - break; - } - } - try { - $result = $stmt->execute( - array( - \OCP\User::getUser(), - $contactid, - $property->name, - $property->value, - $preferred, - ) - ); - if (\OC_DB::isError($result)) { - \OCP\Util::writeLog('contacts', __METHOD__. 'DB error: ' - . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); - return false; - } - } - } } diff --git a/lib/backend/abstractbackend.php b/lib/backend/abstractbackend.php new file mode 100644 index 00000000..b42f5c1b --- /dev/null +++ b/lib/backend/abstractbackend.php @@ -0,0 +1,315 @@ +. + * + */ + +namespace OCA\Contacts\Backend; + +/** + * Subclass this class for address book backends + * + * The following methods MUST be implemented: + * @method array getAddressBooksForUser(string $userid) FIXME: I'm not sure about this one. + * @method array getAddressBook(string $addressbookid) + * @method array getContacts(string $addressbookid) + * @method array getContact(string $addressbookid, mixed $id) + * The following methods MAY be implemented: + * @method bool hasAddressBook(string $addressbookid) + * @method bool updateAddressBook(string $addressbookid, array $updates) + * @method string createAddressBook(string $addressbookid, array $properties) + * @method bool deleteAddressBook(string $addressbookid) + * @method int lastModifiedAddressBook(string $addressbookid) + * @method array numContacts(string $addressbookid) + * @method bool updateContact(string $addressbookid, string $id, array $updates) + * @method string createContact(string $addressbookid, string $id, array $properties) + * @method bool deleteContact(string $addressbookid, string $id) + * @method int lastModifiedContact(string $addressbookid) + */ + +abstract class AbstractBackend { + + /** + * The name of the backend. + * @var string + */ + public $name; + + protected $possibleContactPermissions = array( + \OCP\PERMISSION_CREATE => 'createContact', + \OCP\PERMISSION_READ => 'getContact', + \OCP\PERMISSION_UPDATE => 'updateContact', + \OCP\PERMISSION_DELETE => 'deleteContact', + ); + + protected $possibleAddressBookPermissions = array( + \OCP\PERMISSION_CREATE => 'createAddressBook', + \OCP\PERMISSION_READ => 'getAddressBook', + \OCP\PERMISSION_UPDATE => 'updateAddressBook', + \OCP\PERMISSION_DELETE => 'deleteAddressBook', + ); + + /** + * @brief Get all permissions for contacts + * @returns bitwise-or'ed actions + * + * Returns the supported actions as int to be + * compared with \OCP\PERMISSION_CREATE etc. + * TODO: When getting the permissions we also have to check for + * configured permissions and return min() of the two values. + * A user can for example configure an address book with a backend + * that implements deleteContact() but wants to set it read-only. + */ + public function getContactPermissions() { + $permissions = 0; + foreach($this->possibleContactPermissions AS $permission => $methodName) { + if(method_exists($this, $methodName)) { + $permissions |= $permission; + } + } + + return $permissions; + } + + /** + * @brief Get all permissions for address book. + * @returns bitwise-or'ed actions + * + * Returns the supported actions as int to be + * compared with \OCP\PERMISSION_CREATE etc. + */ + public function getAddressBookPermissions() { + $permissions = 0; + foreach($this->possibleAddressBookPermissions AS $permission => $methodName) { + if(method_exists($this, $methodName)) { + $permissions |= $permission; + } + } + + return $permissions; + } + + /** + * @brief Check if backend implements action for contacts + * @param $actions bitwise-or'ed actions + * @returns boolean + * + * Returns the supported actions as int to be + * compared with \OCP\PERMISSION_CREATE etc. + */ + public function hasContactPermission($permission) { + return (bool)($this->getContactPermissions() & $permission); + } + + /** + * @brief Check if backend implements action for contacts + * @param $actions bitwise-or'ed actions + * @returns boolean + * + * Returns the supported actions as int to be + * compared with \OCP\PERMISSION_CREATE etc. + */ + public function hasAddressBooksPermission($permission) { + return (bool)($this->getAddressBooksPermissions() & $permission); + } + + /** + * Check if the backend has the address book + * + * @param string $addressbookid + * @return bool + */ + public function hasAddressBook($addressbookid) { + return count($this->getAddressBook($addressbookid)) > 0; + } + + /** + * Returns the number of contacts in an address book. + * Implementations can choose to override this method to either + * get the result more effectively or to return null if the backend + * cannot determine the number. + * + * @param string $addressbookid + * @return integer|null + */ + public function numContacts($addressbookid) { + return count($this->getContacts($addressbookid)); + } + + /** + * Returns the list of addressbooks for a specific user. + * + * The returned arrays MUST contain a unique 'id' for the + * backend and a 'displayname', and it MAY contain a + * 'description'. + * + * @param string $principaluri + * @return array + public function getAddressBooksForUser($userid) { + } + */ + + /** + * Get an addressbook's properties + * + * The returned array MUST contain 'displayname' and an integer 'permissions' + * value using there ownCloud CRUDS constants (which MUST be at least + * \OCP\PERMISSION_READ). + * Currently the only ones supported are 'displayname' and + * 'description', but backends can implement additional. + * + * @param string $addressbookid + * @return array $properties + */ + public abstract function getAddressBook($addressbookid); + + /** + * Updates an addressbook's properties + * + * The $properties array contains the changes to be made. + * + * Currently the only ones supported are 'displayname' and + * 'description', but backends can implement additional. + * + * @param string $addressbookid + * @param array $properties + * @return bool + public function updateAddressBook($addressbookid, array $properties) { + } + */ + + /** + * Creates a new address book + * + * Currently the only ones supported are 'displayname' and + * 'description', but backends can implement additional. + * 'displayname' MUST be present. + * + * @param array $properties + * @return string|false The ID if the newly created AddressBook or false on error. + public function createAddressBook(array $properties) { + } + */ + + /** + * Deletes an entire addressbook and all its contents + * + * @param string $addressbookid + * @return bool + public function deleteAddressBook($addressbookid) { + } + */ + + /** + * @brief Get the last modification time for an address book. + * + * Must return a UNIX time stamp or null if the backend + * doesn't support it. + * + * TODO: Implement default methods get/set for backends that + * don't support. + * @param string $addressbookid + * @returns int | null + */ + public function lastModifiedAddressBook($addressbookid) { + } + + /** + * Returns all contacts for a specific addressbook id. + * + * The returned array MUST contain the unique ID of the contact mapped to 'id', a + * displayname mapped to 'displayname' and an integer 'permissions' value using there + * ownCloud CRUDS constants (which MUST be at least \OCP\PERMISSION_READ), and SHOULD + * contain the properties of the contact formatted as a vCard 3.0 + * https://tools.ietf.org/html/rfc2426 mapped to 'carddata' or as an + * \OCA\Contacts\VObject\VCard object mapped to 'vcard'. + * + * Example: + * + * array( + * 0 => array('id' => '4e111fef5df', 'permissions' => 1, 'displayname' => 'John Q. Public', 'vcard' => $object), + * 1 => array('id' => 'bbcca2d1535', 'permissions' => 32, 'displayname' => 'Jane Doe', 'carddata' => $data) + * ); + * + * For contacts that contain loads of data, the 'carddata' or 'vcard' MAY be omitted + * as it can be fetched later. + * + * TODO: Some sort of ETag? + * + * @param string $addressbookid + * @param bool $omitdata Don't fetch the entire carddata or vcard. + * @return array + */ + public abstract function getContacts($addressbookid, $limit = null, $offset = null, $omitdata = false); + + /** + * Returns a specfic contact. + * + * Same as getContacts except that either 'carddata' or 'vcard' is mandatory. + * + * @param string $addressbookid + * @param mixed $id + * @return array|bool + */ + public abstract function getContact($addressbookid, $id); + + /** + * Creates a new contact + * + * @param string $addressbookid + * @param string $carddata + * @return string|bool The identifier for the new contact or false on error. + public function createContact($addressbookid, $carddata) { + } + */ + + /** + * Updates a contact + * + * @param string $addressbookid + * @param mixed $id + * @param string $carddata + * @return bool + public function updateContact($addressbookid, $id, $carddata) { + } + */ + + /** + * Deletes a contact + * + * @param string $addressbookid + * @param mixed $id + * @return bool + public function deleteContact($addressbookid, $id) { + } + */ + + /** + * @brief Get the last modification time for a contact. + * + * Must return a UNIX time stamp or null if the backend + * doesn't support it. + * + * @param string $addressbookid + * @param mixed $id + * @returns int | null + */ + public function lastModifiedContact($addressbookid, $id) { + } +} diff --git a/lib/backend/database.php b/lib/backend/database.php new file mode 100644 index 00000000..72e034f4 --- /dev/null +++ b/lib/backend/database.php @@ -0,0 +1,724 @@ +. + * + */ + +namespace OCA\Contacts\Backend; + +use OCA\Contacts\Contact; +use OCA\Contacts\VObject\VCard; +use Sabre\VObject\Reader; + +/** + * Subclass this class for Cantacts backends + */ + +class Database extends AbstractBackend { + + public $name = 'local'; + static private $preparedQueries = array(); + + /** + * Sets up the backend + * + * @param string $addressBooksTableName + * @param string $cardsTableName + */ + public function __construct( + $userid = null, + $addressBooksTableName = '*PREFIX*contacts_addressbooks', + $cardsTableName = '*PREFIX*contacts_cards', + $indexTableName = '*PREFIX*contacts_cards_properties' + ) { + $this->userid = $userid ? $userid : \OCP\User::getUser(); + $this->addressBooksTableName = $addressBooksTableName; + $this->cardsTableName = $cardsTableName; + $this->indexTableName = $indexTableName; + $this->addressbooks = array(); + } + + /** + * Returns the list of addressbooks for a specific user. + * + * TODO: Create default if none exists. + * + * @param string $principaluri + * @return array + */ + public function getAddressBooksForUser($userid = null) { + $userid = $userid ? $userid : $this->userid; + + try { + if(!isset(self::$preparedQueries['addressbooksforuser'])) { + $sql = 'SELECT `id`, `displayname`, `description`, `ctag` AS `lastmodified`, `userid` AS `owner`, `uri` FROM `' + . $this->addressBooksTableName + . '` WHERE `userid` = ? ORDER BY `displayname`'; + self::$preparedQueries['addressbooksforuser'] = \OCP\DB::prepare($sql); + } + $result = self::$preparedQueries['addressbooksforuser']->execute(array($userid)); + if (\OC_DB::isError($result)) { + \OCP\Util::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + return $this->addressbooks; + } + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.' exception: ' . $e->getMessage(), \OCP\Util::ERROR); + return $this->addressbooks; + } + + while( $row = $result->fetchRow()) { + $row['permissions'] = \OCP\PERMISSION_ALL; + $this->addressbooks[$row['id']] = $row; + } + return $this->addressbooks; + } + + public function getAddressBook($addressbookid) { + //\OCP\Util::writeLog('contacts', __METHOD__.' id: ' + // . $addressbookid, \OCP\Util::DEBUG); + if($this->addressbooks && isset($this->addressbooks[$addressbookid])) { + //print(__METHOD__ . ' ' . __LINE__ .' addressBookInfo: ' . print_r($this->addressbooks[$addressbookid], true)); + return $this->addressbooks[$addressbookid]; + } + // Hmm, not found. Lets query the db. + try { + $query = 'SELECT `id`, `displayname`, `description`, `userid` AS `owner`, `ctag` AS `lastmodified`, `uri` FROM `' + . $this->addressBooksTableName + . '` WHERE `id` = ?'; + if(!isset(self::$preparedQueries['getaddressbook'])) { + self::$preparedQueries['getaddressbook'] = \OCP\DB::prepare($query); + } + $result = self::$preparedQueries['getaddressbook']->execute(array($addressbookid)); + if (\OC_DB::isError($result)) { + \OCP\Util::write('contacts', __METHOD__. 'DB error: ' + . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + return array(); + } + $row = $result->fetchRow(); + $row['permissions'] = \OCP\PERMISSION_ALL; + return $row; + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.' exception: ' + . $e->getMessage(), \OCP\Util::ERROR); + return array(); + } + return array(); + } + + public function hasAddressBook($addressbookid) { + if($this->addressbooks && isset($this->addressbooks[$addressbookid])) { + return true; + } + return count($this->getAddressBook($addressbookid)) > 0; + } + + /** + * Updates an addressbook's properties + * + * @param string $addressbookid + * @param array $changes + * @return bool + */ + public function updateAddressBook($addressbookid, array $changes) { + if(count($changes) === 0) { + return false; + } + + $query = 'UPDATE `' . $this->addressBooksTableName . '` SET '; + + $updates = array(); + + if(isset($changes['displayname'])) { + $query .= '`displayname` = ?, '; + $updates[] = $changes['displayname']; + if($this->addressbooks && isset($this->addressbooks[$addressbookid])) { + $this->addressbooks[$addressbookid]['displayname'] = $changes['displayname']; + } + } + + if(isset($changes['description'])) { + $query .= '`description` = ?, '; + $updates[] = $changes['description']; + if($this->addressbooks && isset($this->addressbooks[$addressbookid])) { + $this->addressbooks[$addressbookid]['description'] = $changes['description']; + } + } + + $query .= '`ctag` = `ctag` + 1 WHERE `id` = ?'; + $updates[] = $addressbookid; + + try { + $stmt = \OCP\DB::prepare($query); + $result = $stmt->execute($updates); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', + __METHOD__. 'DB error: ' + . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + return false; + } + } catch(Exception $e) { + \OCP\Util::writeLog('contacts', + __METHOD__ . ', exception: ' + . $e->getMessage(), \OCP\Util::ERROR); + return false; + } + + return true; + } + + /** + * Creates a new address book + * + * Supported properties are 'displayname', 'description' and 'uri'. + * 'uri' is supported to allow to add from CardDAV requests, and MUST + * be used for the 'uri' database field if present. + * 'displayname' MUST be present. + * + * @param array $properties + * @return string|false The ID if the newly created AddressBook or false on error. + */ + public function createAddressBook(array $properties, $userid = null) { + $userid = $userid ? $userid : $this->userid; + if(count($properties) === 0 || !isset($properties['displayname'])) { + return false; + } + + $query = 'INSERT INTO `' . $this->addressBooksTableName . '` ' + . '(`userid`,`displayname`,`uri`,`description`,`ctag`) VALUES(?,?,?,?,?)'; + + $updates = array($userid, $properties['displayname']); + $updates[] = isset($properties['uri']) + ? $properties['uri'] + : $this->createAddressBookURI($properties['displayname']); + $updates[] = isset($properties['description']) ? $properties['description'] : ''; + $ctag = time(); + $updates[] = $ctag; + + try { + if(!isset(self::$preparedQueries['createaddressbook'])) { + self::$preparedQueries['createaddressbook'] = \OCP\DB::prepare($query); + } + $result = self::$preparedQueries['createaddressbook']->execute($updates); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + return false; + } + } catch(Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__ . ', exception: ' . $e->getMessage(), \OCP\Util::ERROR); + return false; + } + + $newid = \OCP\DB::insertid($this->addressBooksTableName); + if($this->addressbooks) { + $updates['id'] = $newid; + $updates['ctag'] = $ctag; + $updates['lastmodified'] = $ctag; + $this->addressbooks[$addressbookid] = $updates; + } + return $newid; + } + + /** + * Deletes an entire addressbook and all its contents + * + * @param string $addressbookid + * @return bool + */ + public function deleteAddressBook($addressbookid) { + \OC_Hook::emit('OCA\Contacts', 'pre_deleteAddressBook', + array('id' => $addressbookid) + ); + + // Clean up sharing + \OCP\Share::unshareAll('addressbook', $addressbookid); + + // Get all contact ids for this address book + $ids = array(); + $result = null; + $stmt = \OCP\DB::prepare('SELECT `id` FROM `' . $this->cardsTableName . '`' + . ' WHERE `addressbookid` = ?'); + try { + $result = $stmt->execute(array($addressbookid)); + if (\OC_DB::isError($result)) { + \OCP\Util::writeLog('contacts', __METHOD__. 'DB error: ' + . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + return; + } + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__. + ', exception: ' . $e->getMessage(), \OCP\Util::ERROR); + return; + } + + if(!is_null($result)) { + while($id = $result->fetchOne()) { + $ids[] = $id; + } + } + + // Purge contact property indexes + $stmt = \OCP\DB::prepare('DELETE FROM `' . $this->indexTableName + .'` WHERE `contactid` IN ('.str_repeat('?,', count($ids)-1).'?)'); + try { + $stmt->execute($ids); + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__. + ', exception: ' . $e->getMessage(), \OCP\Util::ERROR); + } + + // Purge categories + $catctrl = new \OC_VCategories('contact'); + $catctrl->purgeObjects($ids); + + if(!isset(self::$preparedQueries['deleteaddressbookcontacts'])) { + self::$preparedQueries['deleteaddressbookcontacts'] = + \OCP\DB::prepare('DELETE FROM `' . $this->cardsTableName + . '` WHERE `addressbookid` = ?'); + } + try { + self::$preparedQueries['deleteaddressbookcontacts'] + ->execute(array($addressbookid)); + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__. + ', exception: ' . $e->getMessage(), \OCP\Util::ERROR); + return false; + } + + if(!isset(self::$preparedQueries['deleteaddressbook'])) { + self::$preparedQueries['deleteaddressbook'] = + \OCP\DB::prepare('DELETE FROM `' + . $this->addressBooksTableName . '` WHERE `id` = ?'); + } + try { + self::$preparedQueries['deleteaddressbook'] + ->execute(array($addressbookid)); + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__. + ', exception: ' . $e->getMessage(), \OCP\Util::ERROR); + return false; + } + + if($this->addressbooks && isset($this->addressbooks[$addressbookid])) { + unset($this->addressbooks[$addressbookid]); + } + + return true; + } + + /** + * @brief Updates ctag for addressbook + * @param integer $id + * @return boolean + */ + public function touchAddressBook($id) { + $query = 'UPDATE `' . $this->addressBooksTableName + . '` SET `ctag` = `ctag` + 1 WHERE `id` = ?'; + if(!isset(self::$preparedQueries['touchaddressbook'])) { + self::$preparedQueries['touchaddressbook'] = \OCP\DB::prepare($query); + } + self::$preparedQueries['touchaddressbook']->execute(array($id)); + + return true; + } + + public function lastModifiedAddressBook($addressbookid) { + if($this->addressbooks && isset($this->addressbooks[$addressbookid])) { + return $this->addressbooks[$addressbookid]['lastmodified']; + } + $sql = 'SELECT MAX(`lastmodified`) FROM `' . $this->cardsTableName . '`, `' . $this->addressBooksTableName . '` ' . + 'WHERE `' . $this->cardsTableName . '`.`addressbookid` = `*PREFIX*contacts_addressbooks`.`id` AND ' . + '`' . $this->addressBooksTableName . '`.`userid` = ? AND `' . $this->addressBooksTableName . '`.`id` = ?'; + if(!isset(self::$preparedQueries['lastmodifiedaddressbook'])) { + self::$preparedQueries['lastmodifiedaddressbook'] = \OCP\DB::prepare($sql); + } + $result = self::$preparedQueries['lastmodifiedaddressbook']->execute(array($this->userid, $addressbookid)); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + return null; + } + return $result->fetchOne(); + } + + /** + * Returns the number of contacts in a specific address book. + * + * @param string $addressbookid + * @param bool $omitdata Don't fetch the entire carddata or vcard. + * @return array + */ + public function numContacts($addressbookid) { + $query = 'SELECT COUNT(*) AS `count` FROM `' . $this->cardsTableName . '` WHERE ' + . '`addressbookid` = ?'; + + if(!isset(self::$preparedQueries['count'])) { + self::$preparedQueries['count'] = \OCP\DB::prepare($query); + } + $result = self::$preparedQueries['count']->execute(array($addressbookid)); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + return null; + } + return (int)$result->fetchOne(); + } + + /** + * Returns all contacts for a specific addressbook id. + * + * @param string $addressbookid + * @param bool $omitdata Don't fetch the entire carddata or vcard. + * @return array + */ + public function getContacts($addressbookid, $limit = null, $offset = null, $omitdata = false) { + //\OCP\Util::writeLog('contacts', __METHOD__.' addressbookid: ' . $addressbookid, \OCP\Util::DEBUG); + $cards = array(); + try { + $qfields = $omitdata ? '`id`, `fullname` AS `displayname`' : '*'; + $query = 'SELECT ' . $qfields . ' FROM `' . $this->cardsTableName + . '` WHERE `addressbookid` = ? ORDER BY `fullname`'; + $stmt = \OCP\DB::prepare($query, $limit, $offset); + $result = $stmt->execute(array($addressbookid)); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + return $cards; + } + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); + return $cards; + } + + if(!is_null($result)) { + while($row = $result->fetchRow()) { + $row['permissions'] = \OCP\PERMISSION_ALL; + $cards[] = $row; + } + } + + return $cards; + } + + /** + * Returns a specific contact. + * + * The $id for Database and Shared backends can be an array containing + * either 'id' or 'uri' to be able to play seamlessly with the + * CardDAV backend. + * FIXME: $addressbookid isn't used in the query, so there's no access control. + * OTOH the groups backend - OC_VCategories - doesn't no about parent collections + * only object IDs. Hmm. + * I could make a hack and add an optional, not documented 'nostrict' argument + * so it doesn't look for addressbookid. + * + * @param string $addressbookid + * @param mixed $id Contact ID + * @return array|false + */ + public function getContact($addressbookid, $id, $noCollection = false) { + //\OCP\Util::writeLog('contacts', __METHOD__.' identifier: ' . $addressbookid . ' ' . $id['uri'], \OCP\Util::DEBUG); + + $where_query = '`id` = ?'; + if(is_array($id)) { + $where_query = ''; + if(isset($id['id'])) { + $id = $id['id']; + } elseif(isset($id['uri'])) { + $where_query = '`uri` = ?'; + $id = $id['uri']; + } else { + throw new \Exception( + __METHOD__ . ' If second argument is an array, either \'id\' or \'uri\' has to be set.' + ); + return false; + } + } + $ids = array($id); + + if(!$noCollection) { + $where_query .= ' AND `addressbookid` = ?'; + $ids[] = $addressbookid; + } + + try { + $query = 'SELECT `id`, `carddata`, `lastmodified`, `fullname` AS `displayname` FROM `' + . $this->cardsTableName . '` WHERE ' . $where_query; + $stmt = \OCP\DB::prepare($query); + $result = $stmt->execute($ids); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + return false; + } + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); + \OCP\Util::writeLog('contacts', __METHOD__.', id: '. $id, \OCP\Util::DEBUG); + return false; + } + + $row = $result->fetchRow(); + $row['permissions'] = \OCP\PERMISSION_ALL; + return $row; + } + + public function hasContact($addressbookid, $id) { + return $this->getContact($addressbookid, $id) !== false; + } + + /** + * Creates a new contact + * + * In the Database and Shared backends contact be either a Contact object or a string + * with carddata to be able to play seamlessly with the CardDAV backend. + * If this method is called by the CardDAV backend, the carddata is already validated. + * NOTE: It's assumed that this method is called either from the CardDAV backend, the + * import script, or from the ownCloud web UI in which case either the uri parameter is + * set, or the contact has a UID. If neither is set, it will fail. + * + * @param string $addressbookid + * @param mixed $contact + * @return string|bool The identifier for the new contact or false on error. + */ + public function createContact($addressbookid, $contact, $uri = null) { + + if(!$contact instanceof Contact) { + try { + $contact = Reader::read($contact); + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); + return false; + } + } + + try { + $contact->validate(VCard::REPAIR|VCard::UPGRADE); + } catch (\Exception $e) { + OCP\Util::writeLog('contacts', __METHOD__ . ' ' . + 'Error validating vcard: ' . $e->getMessage(), \OCP\Util::ERROR); + return false; + } + + $uri = is_null($uri) ? $contact->UID . '.vcf' : $uri; + $now = new \DateTime; + $contact->REV = $now->format(\DateTime::W3C); + + $appinfo = \OCP\App::getAppInfo('contacts'); + $appversion = \OCP\App::getAppVersion('contacts'); + $prodid = '-//ownCloud//NONSGML '.$appinfo['name'].' '.$appversion.'//EN'; + $contact->PRODID = $prodid; + + $data = $contact->serialize(); + if(!isset(self::$preparedQueries['createcontact'])) { + self::$preparedQueries['createcontact'] = \OCP\DB::prepare('INSERT INTO `' + . $this->cardsTableName + . '` (`addressbookid`,`fullname`,`carddata`,`uri`,`lastmodified`) VALUES(?,?,?,?,?)' ); + } + try { + $result = self::$preparedQueries['createcontact'] + ->execute( + array( + $addressbookid, + (string)$contact->FN, + $contact->serialize(), + $uri, + time() + ) + ); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + return false; + } + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); + return false; + } + $newid = \OCP\DB::insertid($this->cardsTableName); + + $this->touchAddressBook($addressbookid); + \OC_Hook::emit('OCA\Contacts', 'post_createContact', + array('id' => $newid, 'parent' => $addressbookid, 'contact' => $contact) + ); + return (string)$newid; + } + + /** + * Updates a contact + * + * @param string $addressbookid + * @param mixed $id Contact ID + * @param mixed $contact + * @see getContact + * @return bool + */ + public function updateContact($addressbookid, $id, $contact, $noCollection = false) { + if(!$contact instanceof Contact) { + try { + $contact = Reader::read($contact); + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); + return false; + } + } + $where_query = '`id` = ?'; + if(is_array($id)) { + $where_query = ''; + if(isset($id['id'])) { + $id = $id['id']; + $qname = 'createcontactbyid'; + } elseif(isset($id['uri'])) { + $where_query = '`id` = ?'; + $id = $id['uri']; + $qname = 'createcontactbyuri'; + } else { + throw new Exception( + __METHOD__ . ' If second argument is an array, either \'id\' or \'uri\' has to be set.' + ); + } + } else { + $qname = 'createcontactbyid'; + } + + $now = new \DateTime; + $contact->REV = $now->format(\DateTime::W3C); + + $data = $contact->serialize(); + + $updates = array($contact->FN, $data, time(), $id); + if(!$noCollection) { + $where_query .= ' AND `addressbookid` = ?'; + $updates[] = $addressbookid; + } + + $query = 'UPDATE `' . $this->cardsTableName + . '` SET `fullname` = ?,`carddata` = ?, `lastmodified` = ? WHERE ' . $where_query; + if(!isset(self::$preparedQueries[$qname])) { + self::$preparedQueries[$qname] = \OCP\DB::prepare($query); + } + try { + $result = self::$preparedQueries[$qname]->execute($updates); + if (\OC_DB::isError($result)) { + \OCP\Util::writeLog('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + return false; + } + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.', exception: ' + . $e->getMessage(), \OCP\Util::ERROR); + \OCP\Util::writeLog('contacts', __METHOD__.', id' . $id, \OCP\Util::DEBUG); + return false; + } + + $this->touchAddressBook($addressbookid); + \OC_Hook::emit('OCA\Contacts', 'post_updateContact', + array('id' => $id, 'parent' => $addressbookid, 'contact' => $contact) + ); + return true; + } + + /** + * Deletes a contact + * + * @param string $addressbookid + * @param string $id + * @see getContact + * @return bool + */ + public function deleteContact($addressbookid, $id) { + $where_query = '`id` = ?'; + if(is_array($id)) { + $where_query = ''; + if(isset($id['id'])) { + $id = $id['id']; + $qname = 'deletecontactsbyid'; + } elseif(isset($id['uri'])) { + $where_query = '`id` = ?'; + $id = $id['uri']; + $qname = 'deletecontactsbyuri'; + } else { + throw new Exception( + __METHOD__ . ' If second argument is an array, either \'id\' or \'uri\' has to be set.' + ); + } + } else { + $qname = 'deletecontactsbyid'; + } + \OC_Hook::emit('OCA\Contacts', 'pre_deleteContact', + array('id' => $id) + ); + if(!isset(self::$preparedQueries[$qname])) { + self::$preparedQueries[$qname] = \OCP\DB::prepare('DELETE FROM `' + . $this->cardsTableName + . '` WHERE ' . $where_query . ' AND `addressbookid` = ?'); + } + try { + $result = self::$preparedQueries[$qname]->execute(array($id, $addressbookid)); + if (\OC_DB::isError($result)) { + \OCP\Util::writeLog('contacts', __METHOD__. 'DB error: ' + . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + return false; + } + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__. + ', exception: ' . $e->getMessage(), \OCP\Util::ERROR); + \OCP\Util::writeLog('contacts', __METHOD__.', id: ' + . $id, \OCP\Util::DEBUG); + return false; + } + return true; + } + + /** + * @brief Get the last modification time for a contact. + * + * Must return a UNIX time stamp or null if the backend + * doesn't support it. + * + * @param string $addressbookid + * @param mixed $id + * @returns int | null + */ + public function lastModifiedContact($addressbookid, $id) { + $contact = $this->getContact($addressbookid, $id); + return ($contact ? $contact['lastmodified'] : null); + } + + private function createAddressBookURI($displayname, $userid = null) { + $userid = $userid ? $userid : \OCP\User::getUser(); + $name = str_replace(' ', '_', strtolower($displayname)); + try { + $stmt = \OCP\DB::prepare('SELECT `uri` FROM `' . $this->addressBooksTableName . '` WHERE `userid` = ? '); + $result = $stmt->execute(array($userid)); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + return $name; + } + } catch(Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__ . ' exception: ' . $e->getMessage(), \OCP\Util::ERROR); + return $name; + } + $uris = array(); + while($row = $result->fetchRow()) { + $uris[] = $row['uri']; + } + + $newname = $name; + $i = 1; + while(in_array($newname, $uris)) { + $newname = $name.$i; + $i = $i + 1; + } + return $newname; + } + +} diff --git a/lib/backend/shared.php b/lib/backend/shared.php new file mode 100644 index 00000000..114e149c --- /dev/null +++ b/lib/backend/shared.php @@ -0,0 +1,124 @@ +. + * + */ + +namespace OCA\Contacts\Backend; + +use OCA\Contacts; + +/** + * Subclass this class for Cantacts backends + */ + +class Shared extends Database { + + public $name = 'shared'; + public $addressbooks = array(); + + /** + * Returns the list of addressbooks for a specific user. + * + * @param string $principaluri + * @return array + */ + public function getAddressBooksForUser($userid = null) { + $userid = $userid ? $userid : $this->userid; + + $this->addressbooks = \OCP\Share::getItemsSharedWith( + 'addressbook', + Contacts\Share_Backend_Addressbook::FORMAT_ADDRESSBOOKS + ); + + return $this->addressbooks; + } + + /** + * Returns a specific address book. + * + * @param string $addressbookid + * @param mixed $id Contact ID + * @return mixed + */ + public function getAddressBook($addressbookid) { + $addressbook = \OCP\Share::getItemSharedWithBySource( + 'addressbook', + $addressbookid, + Contacts\Share_Backend_Addressbook::FORMAT_ADDRESSBOOKS + ); + // Not sure if I'm doing it wrongly, or if its supposed to return + // the info in an array? + return (isset($addressbook['permissions']) ? $addressbook : $addressbook[0]); + } + + /** + * Returns all contacts for a specific addressbook id. + * + * TODO: Check for parent permissions + * + * @param string $addressbookid + * @param bool $omitdata Don't fetch the entire carddata or vcard. + * @return array + */ + public function getContacts($addressbookid, $limit = null, $offset = null, $omitdata = false) { + //\OCP\Util::writeLog('contacts', __METHOD__.' addressbookid: ' + // . $addressbookid, \OCP\Util::DEBUG); + $addressbook = \OCP\Share::getItemSharedWithBySource( + 'addressbook', + $addressbookid, + Contacts\Share_Backend_Addressbook::FORMAT_ADDRESSBOOKS, + null, // parameters + true // includeCollection + ); + \OCP\Util::writeLog('contacts', __METHOD__.' shared: ' + . print_r($addressbook, true), \OCP\Util::DEBUG); + + $addressbook = $this->getAddressBook($addressbookid); + $permissions = $addressbook['permissions']; + + $cards = array(); + try { + $qfields = $omitdata ? '`id`, `fullname` AS `displayname`, `lastmodified`' : '*'; + $query = 'SELECT ' . $qfields . ' FROM `' . $this->cardsTableName + . '` WHERE `addressbookid` = ? ORDER BY `fullname`'; + $stmt = \OCP\DB::prepare($query, $limit, $offset); + $result = $stmt->execute(array($addressbookid)); + if (\OC_DB::isError($result)) { + \OC_Log::write('contacts', __METHOD__. 'DB error: ' + . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); + return $cards; + } + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.', exception: ' + . $e->getMessage(), \OCP\Util::ERROR); + return $cards; + } + + if(!is_null($result)) { + while( $row = $result->fetchRow()) { + $row['permissions'] = $permissions; + $cards[] = $row; + } + } + + return $cards; + } + +} diff --git a/lib/sabre/addressbook.php b/lib/carddav/addressbook.php similarity index 74% rename from lib/sabre/addressbook.php rename to lib/carddav/addressbook.php index b6904b6a..0d6f4abb 100644 --- a/lib/sabre/addressbook.php +++ b/lib/carddav/addressbook.php @@ -20,14 +20,18 @@ * */ +namespace OCA\Contacts\CardDAV; + +use OCA\Contacts; + /** * This class overrides __construct to get access to $addressBookInfo and * $carddavBackend, Sabre_CardDAV_AddressBook::getACL() to return read/write * permissions based on user and shared state and it overrides * Sabre_CardDAV_AddressBook::getChild() and Sabre_CardDAV_AddressBook::getChildren() - * to instantiate OC_Connector_Sabre_CardDAV_Cards. + * to instantiate \OCA\Contacts\CardDAV\Cards. */ -class OC_Connector_Sabre_CardDAV_AddressBook extends Sabre_CardDAV_AddressBook { +class AddressBook extends \Sabre_CardDAV_AddressBook { /** * CardDAV backend @@ -43,7 +47,7 @@ class OC_Connector_Sabre_CardDAV_AddressBook extends Sabre_CardDAV_AddressBook { * @param array $addressBookInfo */ public function __construct( - Sabre_CardDAV_Backend_Abstract $carddavBackend, + \Sabre_CardDAV_Backend_Abstract $carddavBackend, array $addressBookInfo) { $this->carddavBackend = $carddavBackend; @@ -70,41 +74,42 @@ class OC_Connector_Sabre_CardDAV_AddressBook extends Sabre_CardDAV_AddressBook { $writeprincipal = $this->getOwner(); $createprincipal = $this->getOwner(); $deleteprincipal = $this->getOwner(); - $uid = OCA\Contacts\Addressbook::extractUserID($this->getOwner()); + $uid = $this->carddavBackend->userIDByPrincipal($this->getOwner()); $readWriteACL = array( array( 'privilege' => '{DAV:}read', - 'principal' => 'principals/' . OCP\User::getUser(), + 'principal' => 'principals/' . \OCP\User::getUser(), 'protected' => true, ), array( 'privilege' => '{DAV:}write', - 'principal' => 'principals/' . OCP\User::getUser(), + 'principal' => 'principals/' . \OCP\User::getUser(), 'protected' => true, ), ); - if($uid != OCP\USER::getUser()) { - $sharedAddressbook = OCP\Share::getItemSharedWithBySource('addressbook', $this->addressBookInfo['id']); + if($uid !== \OCP\User::getUser()) { + list($backendName, $id) = explode('::', $this->addressBookInfo['id']); + $sharedAddressbook = \OCP\Share::getItemSharedWithBySource('addressbook', $id); if($sharedAddressbook) { - if(($sharedAddressbook['permissions'] & OCP\PERMISSION_CREATE) - && ($sharedAddressbook['permissions'] & OCP\PERMISSION_UPDATE) - && ($sharedAddressbook['permissions'] & OCP\PERMISSION_DELETE) + if(($sharedAddressbook['permissions'] & \OCP\PERMISSION_CREATE) + && ($sharedAddressbook['permissions'] & \OCP\PERMISSION_UPDATE) + && ($sharedAddressbook['permissions'] & \OCP\PERMISSION_DELETE) ) { return $readWriteACL; } - if ($sharedAddressbook['permissions'] & OCP\PERMISSION_CREATE) { - $createprincipal = 'principals/' . OCP\USER::getUser(); + if ($sharedAddressbook['permissions'] & \OCP\PERMISSION_CREATE) { + $createprincipal = 'principals/' . \OCP\User::getUser(); } - if ($sharedAddressbook['permissions'] & OCP\PERMISSION_READ) { - $readprincipal = 'principals/' . OCP\USER::getUser(); + if ($sharedAddressbook['permissions'] & \OCP\PERMISSION_READ) { + $readprincipal = 'principals/' . \OCP\User::getUser(); } - if ($sharedAddressbook['permissions'] & OCP\PERMISSION_UPDATE) { - $writeprincipal = 'principals/' . OCP\USER::getUser(); + if ($sharedAddressbook['permissions'] & \OCP\PERMISSION_UPDATE) { + $writeprincipal = 'principals/' . \OCP\User::getUser(); } - if ($sharedAddressbook['permissions'] & OCP\PERMISSION_DELETE) { - $deleteprincipal = 'principals/' . OCP\USER::getUser(); + if ($sharedAddressbook['permissions'] & \OCP\PERMISSION_DELETE) { + $deleteprincipal = 'principals/' . \OCP\User::getUser(); } } } else { @@ -198,8 +203,10 @@ class OC_Connector_Sabre_CardDAV_AddressBook extends Sabre_CardDAV_AddressBook { public function getChild($name) { $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'],$name); - if (!$obj) throw new Sabre_DAV_Exception_NotFound('Card not found'); - return new OC_Connector_Sabre_CardDAV_Card($this->carddavBackend,$this->addressBookInfo,$obj); + if (!$obj) { + throw new \Sabre_DAV_Exception_NotFound('Card not found'); + } + return new Card($this->carddavBackend,$this->addressBookInfo,$obj); } @@ -213,7 +220,7 @@ class OC_Connector_Sabre_CardDAV_AddressBook extends Sabre_CardDAV_AddressBook { $objs = $this->carddavBackend->getCards($this->addressBookInfo['id']); $children = array(); foreach($objs as $obj) { - $children[] = new OC_Connector_Sabre_CardDAV_Card($this->carddavBackend,$this->addressBookInfo,$obj); + $children[] = new Card($this->carddavBackend,$this->addressBookInfo,$obj); } return $children; diff --git a/lib/sabre/addressbookroot.php b/lib/carddav/addressbookroot.php similarity index 87% rename from lib/sabre/addressbookroot.php rename to lib/carddav/addressbookroot.php index 69cd6021..65bad678 100644 --- a/lib/sabre/addressbookroot.php +++ b/lib/carddav/addressbookroot.php @@ -20,11 +20,13 @@ * */ +namespace OCA\Contacts\CardDAV; + /** * This class overrides Sabre_CardDAV_AddressBookRoot::getChildForPrincipal() * to instantiate OC_Connector_CardDAV_UserAddressBooks. */ -class OC_Connector_Sabre_CardDAV_AddressBookRoot extends Sabre_CardDAV_AddressBookRoot { +class AddressBookRoot extends \Sabre_CardDAV_AddressBookRoot { /** * This method returns a node for a principal. @@ -38,7 +40,7 @@ class OC_Connector_Sabre_CardDAV_AddressBookRoot extends Sabre_CardDAV_AddressBo */ public function getChildForPrincipal(array $principal) { - return new OC_Connector_Sabre_CardDAV_UserAddressBooks($this->carddavBackend, $principal['uri']); + return new UserAddressBooks($this->carddavBackend, $principal['uri']); } diff --git a/lib/carddav/backend.php b/lib/carddav/backend.php new file mode 100644 index 00000000..7ca74eef --- /dev/null +++ b/lib/carddav/backend.php @@ -0,0 +1,258 @@ +. + * + */ + +namespace OCA\Contacts\CardDAV; + +use OCA\Contacts; + +class Backend extends \Sabre_CardDAV_Backend_Abstract { + + public function __construct($backends) { + //\OCP\Util::writeLog('contacts', __METHOD__, \OCP\Util::DEBUG); + $this->backends = $backends; + } + + /** + * Returns the list of addressbooks for a specific user. + * + * @param string $principaluri + * @return array + */ + public function getAddressBooksForUser($principaluri) { + $userid = $this->userIDByPrincipal($principaluri); + $userAddressBooks = array(); + foreach($this->backends as $backend) { + $addressBooks = $backend->getAddressBooksForUser($userid); + + foreach($addressBooks as $addressBook) { + if($addressBook['owner'] != \OCP\USER::getUser()) { + $addressBook['uri'] = $addressBook['uri'] . '_shared_by_' . $addressBook['owner']; + $addressBook['displayname'] = $addressBook['displayname']; + } + $userAddressbooks[] = array( + 'id' => $backend->name . '::' . $addressBook['id'], + 'uri' => $addressBook['uri'], + 'principaluri' => 'principals/'.$addressBook['owner'], + '{DAV:}displayname' => $addressBook['displayname'], + '{' . \Sabre_CardDAV_Plugin::NS_CARDDAV . '}addressbook-description' + => $addressBook['description'], + '{http://calendarserver.org/ns/}getctag' => $addressBook['lastmodified'], + ); + } + } + + return $userAddressbooks; + } + + + /** + * Updates an addressbook's properties + * + * See Sabre_DAV_IProperties for a description of the mutations array, as + * well as the return value. + * + * @param mixed $addressbookid + * @param array $mutations + * @see Sabre_DAV_IProperties::updateProperties + * @return bool|array + */ + public function updateAddressBook($addressbookid, array $mutations) { + $name = null; + $description = null; + $changes = array(); + + foreach($mutations as $property=>$newvalue) { + switch($property) { + case '{DAV:}displayname' : + $changes['name'] = $newvalue; + break; + case '{' . \Sabre_CardDAV_Plugin::NS_CARDDAV + . '}addressbook-description' : + $changes['description'] = $newvalue; + break; + default : + // If any unsupported values were being updated, we must + // let the entire request fail. + return false; + } + } + + list($id, $backend) = $this->getBackendForAddressBook($addressbookid); + return $backend->updateAddressBook($id, $changes); + + } + + /** + * Creates a new address book + * + * @param string $principaluri + * @param string $uri Just the 'basename' of the url. + * @param array $properties + * @return void + */ + public function createAddressBook($principaluri, $uri, array $properties) { + + $properties = array(); + $userid = $this->userIDByPrincipal($principaluri); + + foreach($properties as $property=>$newvalue) { + + switch($property) { + case '{DAV:}displayname' : + $properties['displayname'] = $newvalue; + break; + case '{' . \Sabre_CardDAV_Plugin::NS_CARDDAV + . '}addressbook-description' : + $properties['description'] = $newvalue; + break; + default : + throw new \Sabre_DAV_Exception_BadRequest('Unknown property: ' + . $property); + } + + } + + $properties['uri'] = $uri; + + list(,$backend) = $this->getBackendForAddressBook($addressbookid); + $backend->createAddressBook($properties, $userid); + } + + /** + * Deletes an entire addressbook and all its contents + * + * @param int $addressbookid + * @return void + */ + public function deleteAddressBook($addressbookid) { + list($id, $backend) = $this->getBackendForAddressBook($addressbookid); + $backend->deleteAddressBook($id); + } + + /** + * Returns all cards for a specific addressbook id. + * + * @param mixed $addressbookid + * @return array + */ + public function getCards($addressbookid) { + $contacts = array(); + list($id, $backend) = $this->getBackendForAddressBook($addressbookid); + $contacts = $backend->getContacts($id); + + $cards = array(); + foreach($contacts as $contact) { + //OCP\Util::writeLog('contacts', __METHOD__.', uri: ' . $i['uri'], OCP\Util::DEBUG); + $cards[] = array( + 'id' => $contact['id'], + //'carddata' => $i['carddata'], + 'size' => strlen($contact['carddata']), + 'etag' => '"' . md5($contact['carddata']) . '"', + 'uri' => $contact['uri'], + 'lastmodified' => $contact['lastmodified'] ); + } + + return $cards; + } + + /** + * Returns a specfic card + * + * @param mixed $addressbookid + * @param string $carduri + * @return array + */ + public function getCard($addressbookid, $carduri) { + \OCP\Util::writeLog('contacts', __METHOD__.' identifier: ' . $carduri . ' ' . print_r($addressbookid, true), \OCP\Util::DEBUG); + list($id, $backend) = $this->getBackendForAddressBook($addressbookid); + $contact = $backend->getContact($id, array('uri' => $carduri)); + return ($contact ? $contact : false); + + } + + /** + * Creates a new card + * + * We don't return an Etag as the carddata can have been modified + * by Plugin::validate() + * + * @see Plugin::validate() + * @param mixed $addressbookid + * @param string $carduri + * @param string $carddata + * @return string|null + */ + public function createCard($addressbookid, $carduri, $carddata) { + list($id, $backend) = $this->getBackendForAddressBook($addressbookid); + $backend->createContact($id, $carddata, $carduri); + } + + /** + * Updates a card + * + * @param mixed $addressbookid + * @param string $carduri + * @param string $carddata + * @return null + */ + public function updateCard($addressbookid, $carduri, $carddata) { + list($id, $backend) = $this->getBackendForAddressBook($addressbookid); + $backend->updateContact($id, array('uri' => $carduri,), $carddata); + } + + /** + * Deletes a card + * + * @param mixed $addressbookid + * @param string $carduri + * @return bool + */ + public function deleteCard($addressbookid, $carduri) { + list($id, $backend) = $this->getBackendForAddressBook($addressbookid); + return $backend->deleteContact($id); + } + + /** + * @brief gets the userid from a principal path + * @return string + */ + public function userIDByPrincipal($principaluri) { + list(, $userid) = \Sabre_DAV_URLUtil::splitPath($principaluri); + return $userid; + } + + /** + * Get the backend for an address book + * + * @param mixed $addressbookid + * @return array(string, \OCA\Contacts\Backend\AbstractBackend) + */ + public function getBackendForAddressBook($addressbookid) { + list($backendName, $id) = explode('::', $addressbookid); + foreach($this->backends as $backend) { + if($backend->name === $backendName && $backend->hasAddressBook($id)) { + return array($id, $backend); + } + } + throw new \Sabre_DAV_Exception_NotFound('Backend not found: ' . $addressbookid); + } +} diff --git a/lib/sabre/card.php b/lib/carddav/card.php similarity index 77% rename from lib/sabre/card.php rename to lib/carddav/card.php index da141e72..7d27bd34 100644 --- a/lib/sabre/card.php +++ b/lib/carddav/card.php @@ -20,11 +20,15 @@ * */ +namespace OCA\Contacts\CardDAV; + +use OCA\Contacts; + /** * This class overrides Sabre_CardDAV_Card::getACL() * to return read/write permissions based on user and shared state. */ -class OC_Connector_Sabre_CardDAV_Card extends Sabre_CardDAV_Card { +class Card extends \Sabre_CardDAV_Card { /** * Array with information about the containing addressbook @@ -40,7 +44,7 @@ class OC_Connector_Sabre_CardDAV_Card extends Sabre_CardDAV_Card { * @param array $addressBookInfo * @param array $cardData */ - public function __construct(Sabre_CardDAV_Backend_Abstract $carddavBackend, array $addressBookInfo, array $cardData) { + public function __construct(\Sabre_CardDAV_Backend_Abstract $carddavBackend, array $addressBookInfo, array $cardData) { $this->addressBookInfo = $addressBookInfo; parent::__construct($carddavBackend, $addressBookInfo, $cardData); @@ -63,15 +67,16 @@ class OC_Connector_Sabre_CardDAV_Card extends Sabre_CardDAV_Card { $readprincipal = $this->getOwner(); $writeprincipal = $this->getOwner(); - $uid = OCA\Contacts\Addressbook::extractUserID($this->getOwner()); + $uid = $this->carddavBackend->userIDByPrincipal($this->getOwner()); - if($uid != OCP\USER::getUser()) { - $sharedAddressbook = OCP\Share::getItemSharedWithBySource('addressbook', $this->addressBookInfo['id']); - if ($sharedAddressbook && ($sharedAddressbook['permissions'] & OCP\PERMISSION_READ)) { - $readprincipal = 'principals/' . OCP\USER::getUser(); + if($uid != \OCP\USER::getUser()) { + list($backendName, $id) = explode('::', $this->addressBookInfo['id']); + $sharedAddressbook = \OCP\Share::getItemSharedWithBySource('addressbook', $id); + if ($sharedAddressbook && ($sharedAddressbook['permissions'] & \OCP\PERMISSION_READ)) { + $readprincipal = 'principals/' . \OCP\USER::getUser(); } - if ($sharedAddressbook && ($sharedAddressbook['permissions'] & OCP\PERMISSION_UPDATE)) { - $writeprincipal = 'principals/' . OCP\USER::getUser(); + if ($sharedAddressbook && ($sharedAddressbook['permissions'] & \OCP\PERMISSION_UPDATE)) { + $writeprincipal = 'principals/' . \OCP\USER::getUser(); } } diff --git a/lib/carddav/plugin.php b/lib/carddav/plugin.php new file mode 100644 index 00000000..0602e8c9 --- /dev/null +++ b/lib/carddav/plugin.php @@ -0,0 +1,69 @@ +. + * + */ + +namespace OCA\Contacts\CardDAV; + +use Sabre\VObject; +use OCA\Contacts\VObject\VCard; + +/** + * This class overrides Sabre_CardDAV_Plugin::validateVCard() to be able + * to import partially invalid vCards by ignoring invalid lines and to + * validate and upgrade using \OCA\Contacts\VCard. +*/ +class Plugin extends \Sabre_CardDAV_Plugin { + + /** + * Checks if the submitted vCard data is in fact, valid. + * + * An exception is thrown if it's not. + * + * @param resource|string $data + * @return void + */ + protected function validateVCard(&$data) { + \OCP\Util::writeLog('contacts', __METHOD__, \OCP\Util::DEBUG); + + // If it's a stream, we convert it to a string first. + if (is_resource($data)) { + $data = stream_get_contents($data); + } + + // Converting the data to unicode, if needed. + $data = \Sabre_DAV_StringUtil::ensureUTF8($data); + + try { + $vobj = VObject\Reader::read($data, VObject\Reader::OPTION_IGNORE_INVALID_LINES); + } catch (VObject\ParseException $e) { + throw new \Sabre_DAV_Exception_UnsupportedMediaType('This resource only supports valid vcard data. Parse error: ' . $e->getMessage()); + } + + if ($vobj->name !== 'VCARD') { + throw new \Sabre_DAV_Exception_UnsupportedMediaType('This collection can only support vcard objects.'); + } + + $vobj->validate(VCard::REPAIR|VCard::UPGRADE); + $data = $vobj->serialize(); + } +} \ No newline at end of file diff --git a/lib/sabre/useraddressbooks.php b/lib/carddav/useraddressbooks.php similarity index 82% rename from lib/sabre/useraddressbooks.php rename to lib/carddav/useraddressbooks.php index 328b433b..22262d86 100644 --- a/lib/sabre/useraddressbooks.php +++ b/lib/carddav/useraddressbooks.php @@ -20,11 +20,13 @@ * */ +namespace OCA\Contacts\CardDAV; + /** * This class overrides Sabre_CardDAV_UserAddressBooks::getChildren() - * to instantiate OC_Connector_Sabre_CardDAV_AddressBooks. + * to instantiate \OCA\Contacts\CardDAV\AddressBooks. */ -class OC_Connector_Sabre_CardDAV_UserAddressBooks extends Sabre_CardDAV_UserAddressBooks { +class UserAddressBooks extends \Sabre_CardDAV_UserAddressBooks { /** * Returns a list of addressbooks @@ -36,7 +38,7 @@ class OC_Connector_Sabre_CardDAV_UserAddressBooks extends Sabre_CardDAV_UserAddr $addressbooks = $this->carddavBackend->getAddressbooksForUser($this->principalUri); $objs = array(); foreach($addressbooks as $addressbook) { - $objs[] = new OC_Connector_Sabre_CardDAV_AddressBook($this->carddavBackend, $addressbook); + $objs[] = new AddressBook($this->carddavBackend, $addressbook); } return $objs; diff --git a/lib/contact.php b/lib/contact.php new file mode 100644 index 00000000..7da8701b --- /dev/null +++ b/lib/contact.php @@ -0,0 +1,647 @@ +. + * + */ + +namespace OCA\Contacts; + +use Sabre\VObject\Property; + +/** + * Subclass this class or implement IPIMObject interface for PIM objects + */ + +class Contact extends VObject\VCard implements IPIMObject { + + const THUMBNAIL_PREFIX = 'contact-thumbnail-'; + const THUMBNAIL_SIZE = 28; + + /** + * The name of the object type in this case VCARD. + * + * This is used when serializing the object. + * + * @var string + */ + public $name = 'VCARD'; + + protected $props = array(); + + /** + * Create a new Contact object + * + * @param AddressBook $parent + * @param AbstractBackend $backend + * @param mixed $data + */ + public function __construct($parent, $backend, $data = null) { + //\OCP\Util::writeLog('contacts', __METHOD__ . ' ' . print_r($data, true), \OCP\Util::DEBUG); + //\OCP\Util::writeLog('contacts', __METHOD__, \OCP\Util::DEBUG); + $this->props['parent'] = $parent; + $this->props['backend'] = $backend; + $this->props['retrieved'] = false; + $this->props['saved'] = false; + + if(!is_null($data)) { + if($data instanceof VObject\VCard) { + foreach($data->children as $child) { + $this->add($child); + $this->setRetrieved(true); + } + } elseif(is_array($data)) { + foreach($data as $key => $value) { + switch($key) { + case 'id': + $this->props['id'] = $value; + break; + case 'lastmodified': + $this->props['lastmodified'] = $value; + break; + case 'uri': + $this->props['uri'] = $value; + break; + case 'carddata': + $this->props['carddata'] = $value; + $this->retrieve(); + break; + case 'vcard': + $this->props['vcard'] = $value; + $this->retrieve(); + break; + case 'displayname': + case 'fullname': + $this->props['displayname'] = $value; + $this->FN = $value; + break; + } + } + } + } + } + + /** + * @return array|null + */ + public function getMetaData() { + if(!isset($this->props['displayname'])) { + if(!$this->retrieve()) { + \OCP\Util::writeLog('contacts', __METHOD__.' error reading: '.print_r($this->props, true), \OCP\Util::ERROR); + return null; + } + } + return array( + 'id' => $this->getId(), + 'displayname' => $this->getDisplayName(), + 'permissions' => $this->getPermissions(), + 'lastmodified' => $this->lastModified(), + 'owner' => $this->getOwner(), + 'parent' => $this->getParent()->getId(), + 'backend' => $this->getBackend()->name, + ); + } + + /** + * Get a unique key combined of backend name, address book id and contact id. + * + * @return string + */ + public function combinedKey() { + return $this->getBackend()->name . '::' . $this->getParent()->getId() . '::' . $this->getId(); + } + + /** + * @return string|null + */ + public function getOwner() { + return $this->props['parent']->getOwner(); + } + + /** + * @return string|null + */ + public function getId() { + return isset($this->props['id']) ? $this->props['id'] : null; + } + + /** + * @return string|null + */ + function getDisplayName() { + return isset($this->props['displayname']) ? $this->props['displayname'] : null; + } + + /** + * @return string|null + */ + public function getURI() { + return isset($this->props['uri']) ? $this->props['uri'] : null; + } + + /** + * If this object is part of a collection return a reference + * to the parent object, otherwise return null. + * @return IPIMObject|null + */ + function getParent() { + return $this->props['parent']; + } + + function getBackend() { + return $this->props['backend']; + } + + /** CRUDS permissions (Create, Read, Update, Delete, Share) + * + * @return integer + */ + function getPermissions() { + return $this->props['parent']->getPermissions(); + } + + /** + * @param integer $permission + * @return bool + */ + function hasPermission($permission) { + return $this->getPermissions() & $permission; + } + + /** + * Save the address book data to backend + * FIXME + * + * @param array $data + * @return bool + */ + public function update(array $data) { + + foreach($data as $key => $value) { + switch($key) { + case 'displayname': + $this->addressBookInfo['displayname'] = $value; + break; + case 'description': + $this->addressBookInfo['description'] = $value; + break; + } + } + return $this->props['backend']->updateContact( + $this->getParent()->getId(), + $this->getId(), + $this + ); + } + + /** + * Delete the data from backend + * + * @return bool + */ + public function delete() { + return $this->props['backend']->deleteContact( + $this->getParent()->getId(), + $this->getId() + ); + } + + /** + * Save the contact data to backend + * + * @return bool + */ + public function save($force = false) { + if($this->isSaved() && !$force) { + \OCP\Util::writeLog('contacts', __METHOD__.' Already saved: ' . print_r($this->props, true), \OCP\Util::DEBUG); + return true; + } + if(isset($this->FN)) { + $this->props['displayname'] = (string)$this->FN; + } + if($this->getId()) { + if($this->props['backend'] + ->updateContact( + $this->getParent()->getId(), + $this->getId(), + $this->serialize() + ) + ) { + $this->props['lastmodified'] = time(); + $this->setSaved(true); + return true; + } else { + return false; + } + } else { + //print(__METHOD__.' ' . print_r($this->getParent(), true)); + $this->props['id'] = $this->props['backend']->createContact( + $this->getParent()->getId(), $this + ); + $this->setSaved(true); + return $this->getId() !== false; + } + } + + /** + * Get the data from the backend + * FIXME: Clean this up and make sure the logic is OK. + */ + public function retrieve() { + //error_log(__METHOD__); + //\OCP\Util::writeLog('contacts', __METHOD__.' ' . print_r($this->props, true), \OCP\Util::DEBUG); + if($this->isRetrieved()) { + //\OCP\Util::writeLog('contacts', __METHOD__. ' children', \OCP\Util::DEBUG); + return true; + } else { + $data = null; + if(isset($this->props['vcard']) + && $this->props['vcard'] instanceof VObject\VCard) { + foreach($this->props['vcard']->children() as $child) { + $this->add($child); + if($child->name === 'FN') { + $this->props['displayname'] + = strtr($child->value, array('\,' => ',', '\;' => ';', '\\\\' => '\\')); + } + } + $this->setRetrieved(true); + //$this->children = $this->props['vcard']->children(); + unset($this->props['vcard']); + return true; + } elseif(!isset($this->props['carddata'])) { + $result = $this->props['backend']->getContact( + $this->getParent()->getId(), + $this->id + ); + if($result) { + if(isset($result['vcard']) + && $result['vcard'] instanceof VObject\VCard) { + foreach($result['vcard']->children() as $child) { + $this->add($child); + } + $this->setRetrieved(true); + return true; + } elseif(isset($result['carddata'])) { + // Save internal values + $data = $result['carddata']; + $this->props['carddata'] = $result['carddata']; + $this->props['lastmodified'] = $result['lastmodified']; + $this->props['displayname'] = $result['displayname']; + $this->props['permissions'] = $result['permissions']; + } else { + \OCP\Util::writeLog('contacts', __METHOD__ + . ' Could not get vcard or carddata: ' + . $this->getId() + . print_r($result, true), \OCP\Util::DEBUG); + return false; + } + } else { + \OCP\Util::writeLog('contacts', __METHOD__.' Error getting contact: ' . $this->getId(), \OCP\Util::DEBUG); + } + } elseif(isset($this->props['carddata'])) { + $data = $this->props['carddata']; + //error_log(__METHOD__.' data: '.print_r($data, true)); + } + try { + $obj = \Sabre\VObject\Reader::read( + $data, + \Sabre\VObject\Reader::OPTION_IGNORE_INVALID_LINES + ); + if($obj) { + foreach($obj->children as $child) { + $this->add($child); + } + $this->setRetrieved(true); + } else { + \OCP\Util::writeLog('contacts', __METHOD__.' Error reading: ' . print_r($data, true), \OCP\Util::DEBUG); + return false; + } + } catch (\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__ . + ' Error parsing carddata for: ' . $this->getId() . ' ' . $e->getMessage(), + \OCP\Util::ERROR); + return false; + } + } + return true; + } + + /** + * Get a property index in the contact by the checksum of its serialized value + * + * @param string $checksum An 8 char m5d checksum. + * @return \Sabre\VObject\Property Property by reference + * @throws An exception with error code 404 if the property is not found. + */ + public function getPropertyIndexByChecksum($checksum) { + $this->retrieve(); + $idx = 0; + foreach($this->children as $i => &$property) { + if(substr(md5($property->serialize()), 0, 8) == $checksum ) { + return $idx; + } + $idx += 1; + } + throw new Exception('Property not found', 404); + } + + /** + * Get a property by the checksum of its serialized value + * + * @param string $checksum An 8 char m5d checksum. + * @return \Sabre\VObject\Property Property by reference + * @throws An exception with error code 404 if the property is not found. + */ + public function getPropertyByChecksum($checksum) { + $this->retrieve(); + foreach($this->children as $i => &$property) { + if(substr(md5($property->serialize()), 0, 8) == $checksum ) { + return $property; + } + } + throw new \Exception('Property not found', 404); + } + + /** + * Delete a property by the checksum of its serialized value + * It is up to the caller to call ->save() + * + * @param string $checksum An 8 char m5d checksum. + * @throws @see getPropertyByChecksum + */ + public function unsetPropertyByChecksum($checksum) { + $idx = $this->getPropertyIndexByChecksum($checksum); + unset($this->children[$idx]); + $this->setSaved(false); + } + + /** + * Set a property by the checksum of its serialized value + * It is up to the caller to call ->save() + * + * @param string $checksum An 8 char m5d checksum. + * @param string $name Property name + * @param mixed $value + * @param array $parameters + * @throws @see getPropertyByChecksum + * @return string new checksum + */ + public function setPropertyByChecksum($checksum, $name, $value, $parameters=array()) { + // FIXME: Change the debug and bailOut calls + if($checksum === 'new') { + $property = Property::create($name); + $this->add($property); + } else { + $property = $this->getPropertyByChecksum($checksum); + } + switch($name) { + case 'EMAIL': + $value = strtolower($value); + $property->setValue($value); + break; + case 'ADR': + if(is_array($value)) { + $property->setParts($value); + } else { + debug('Saving ADR ' . $value); + $property->setValue($value); + } + break; + case 'IMPP': + if(is_null($parameters) || !isset($parameters['X-SERVICE-TYPE'])) { + bailOut(App::$l10n->t('Missing IM parameter.')); + } + $impp = Utils\Properties::getIMOptions($parameters['X-SERVICE-TYPE']); + if(is_null($impp)) { + bailOut(App::$l10n->t('Unknown IM: '.$parameters['X-SERVICE-TYPE'])); + } + $value = $impp['protocol'] . ':' . $value; + $property->setValue($value); + break; + default: + \OCP\Util::writeLog('contacts', __METHOD__.' adding: '.$name. ' ' . $value, \OCP\Util::DEBUG); + $property->setValue($value); + break; + } + $this->setParameters($property, $parameters); + $this->setSaved(false); + return substr(md5($property->serialize()), 0, 8); + } + + /** + * Set a property by the property name. + * It is up to the caller to call ->save() + * + * @param string $name Property name + * @param mixed $value + * @param array $parameters + * @return bool + */ + public function setPropertyByName($name, $value, $parameters=array()) { + // TODO: parameters are ignored for now. + switch($name) { + case 'BDAY': + try { + $date = New \DateTime($value); + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', + __METHOD__.' DateTime exception: ' . $e->getMessage(), + \OCP\Util::ERROR + ); + return false; + } + $value = $date->format('Y-m-d'); + $this->BDAY = $value; + $this->BDAY->add('VALUE', 'DATE'); + //\OCP\Util::writeLog('contacts', __METHOD__.' BDAY: '.$this->BDAY->serialize(), \OCP\Util::DEBUG); + break; + case 'CATEGORIES': + case 'N': + case 'ORG': + $property = $this->select($name); + if(count($property) === 0) { + $property = \Sabre\VObject\Property::create($name); + $this->add($property); + } else { + // Actually no idea why this works + $property = array_shift($property); + } + if(is_array($value)) { + $property->setParts($value); + } else { + $this->{$name} = $value; + } + break; + default: + \OCP\Util::writeLog('contacts', __METHOD__.' adding: '.$name. ' ' . $value, \OCP\Util::DEBUG); + $this->{$name} = $value; + break; + } + $this->setSaved(false); + return true; + } + + protected function setParameters($property, $parameters, $reset = false) { + if(!$parameters) { + return; + } + + if($reset) { + $property->parameters = array(); + } + debug('Setting parameters: ' . print_r($parameters, true)); + foreach($parameters as $key => $parameter) { + debug('Adding parameter: ' . $key); + if(is_array($parameter)) { + foreach($parameter as $val) { + if(is_array($val)) { + foreach($val as $val2) { + if(trim($key) && trim($val2)) { + debug('Adding parameter: '.$key.'=>'.print_r($val2, true)); + $property->add($key, strip_tags($val2)); + } + } + } else { + if(trim($key) && trim($val)) { + debug('Adding parameter: '.$key.'=>'.print_r($val, true)); + $property->add($key, strip_tags($val)); + } + } + } + } else { + if(trim($key) && trim($parameter)) { + debug('Adding parameter: '.$key.'=>'.print_r($parameter, true)); + $property->add($key, strip_tags($parameter)); + } + } + } + } + + public function lastModified() { + if(!isset($this->props['lastmodified'])) { + $this->retrieve(); + } + return isset($this->props['lastmodified']) + ? $this->props['lastmodified'] + : null; + } + + /** + * Merge in data from a multi-dimentional array + * + * NOTE: This is *NOT* tested! + * The data array has this structure: + * + * array( + * 'EMAIL' => array(array('value' => 'johndoe@example.com', 'parameters' = array('TYPE' => array('HOME','VOICE')))) + * ); + * @param array $data + */ + public function mergeFromArray(array $data) { + foreach($data as $name => $properties) { + if(in_array($name, array('PHOTO', 'UID'))) { + continue; + } + if(!is_array($properties)) { + \OCP\Util::writeLog('contacts', __METHOD__.' not an array?: ' .$name. ' '.print_r($properties, true), \OCP\Util::DEBUG); + } + if(in_array($name, Utils\Properties::$multi_properties)) { + unset($this->{$name}); + } + foreach($properties as $parray) { + //$property = Property::create($name, $parray['value'], $parray['parameters']); + \OCP\Util::writeLog('contacts', __METHOD__.' adding: ' .$name. ' '.print_r($parray['value'], true) . ' ' . print_r($parray['parameters'], true), \OCP\Util::DEBUG); + if(in_array($name, Utils\Properties::$multi_properties)) { + // TODO: wrap in try/catch, check return value + $this->setPropertyByChecksum('new', $name, $parray['value'], $parray['parameters']); + } else { + // TODO: Check return value + if(!isset($this->{$name})) { + $this->setPropertyByName($name, $parray['value'], $parray['parameters']); + } + } + //$this->add($name, $parray['value'], $parray['parameters']); + } + } + $this->setSaved(false); + return true; + } + + public function cacheThumbnail(\OC_Image $image = null) { + $key = self::THUMBNAIL_PREFIX . $this->combinedKey(); + //\OC_Cache::remove($key); + if(\OC_Cache::hasKey($key) && $image === null) { + return \OC_Cache::get($key); + } + if(is_null($image)) { + $this->retrieve(); + $image = new \OC_Image(); + if(!isset($this->PHOTO) && !isset($this->LOGO)) { + return false; + } + if(!$image->loadFromBase64((string)$this->PHOTO)) { + if(!$image->loadFromBase64((string)$this->LOGO)) { + return false; + } + } + } + if(!$image->centerCrop()) { + \OCP\Util::writeLog('contacts', + 'thumbnail.php. Couldn\'t crop thumbnail for ID ' . $key, + \OCP\Util::ERROR); + return false; + } + if(!$image->resize(self::THUMBNAIL_SIZE)) { + \OCP\Util::writeLog('contacts', + 'thumbnail.php. Couldn\'t resize thumbnail for ID ' . $key, + \OCP\Util::ERROR); + return false; + } + // Cache as base64 for around a month + \OC_Cache::set($key, strval($image), 3000000); + \OCP\Util::writeLog('contacts', 'Caching ' . $key, \OCP\Util::DEBUG); + return \OC_Cache::get($key); + } + + public function __set($key, $value) { + parent::__set($key, $value); + $this->setSaved(false); + } + + public function __unset($key) { + parent::__unset($key); + $this->setSaved(false); + } + + protected function setRetrieved($state) { + $this->props['retrieved'] = $state; + } + + protected function isRetrieved() { + return $this->props['retrieved']; + } + + protected function setSaved($state) { + $this->props['saved'] = $state; + } + + protected function isSaved() { + return $this->props['saved']; + } + +} diff --git a/lib/hooks.php b/lib/hooks.php index 184474c7..eb0e54f2 100644 --- a/lib/hooks.php +++ b/lib/hooks.php @@ -42,8 +42,8 @@ class Hooks{ * @param paramters parameters from postCreateUser-Hook * @return array */ - static public function createUser($parameters) { - Addressbook::addDefault($parameters['uid']); + public static function userCreated($parameters) { + //Addressbook::addDefault($parameters['uid']); return true; } @@ -52,17 +52,101 @@ class Hooks{ * @param paramters parameters from postDeleteUser-Hook * @return array */ - static public function deleteUser($parameters) { - $addressbooks = Addressbook::all($parameters['uid']); + public static function userDeleted($parameters) { + $backend = new Backend\Database(); + $addressbook = $backend->getAddressBooksForUser($parameters['uid']); foreach($addressbooks as $addressbook) { - Addressbook::delete($addressbook['id']); + // Purging of contact categories and and properties is done by backend. + $backend->deleteAddressBook($addressbook['id']); } - - return true; } - static public function getCalenderSources($parameters) { + /** + * Delete any registred address books (Future) + */ + public static function addressBookDeletion($parameters) { + } + + public static function contactDeletion($parameters) { + $catctrl = new \OC_VCategories('contact'); + $catctrl->purgeObjects(array($parameters['id'])); + Utils\Properties::updateIndex($parameters['id']); + + // Contact sharing not implemented, but keep for future. + //\OCP\Share::unshareAll('contact', $id); + } + + public static function contactUpdated($parameters) { + //\OCP\Util::writeLog('contacts', __METHOD__.' parameters: '.print_r($parameters, true), \OCP\Util::DEBUG); + $catctrl = new \OC_VCategories('contact'); + $catctrl->loadFromVObject( + $parameters['id'], + new \OC_VObject($parameters['contact']), // OC_VCategories still uses OC_VObject + true // force save + ); + Utils\Properties::updateIndex($parameters['id'], $parameters['contact']); + } + + /** + * Scan vCards for categories. + */ + public static function scanCategories() { + $offset = 0; + $limit = 10; + + $categories = new \OC_VCategories('contact'); + + $app = new App(); + $backend = $app->getBackend('local'); + $addressBookInfos = $backend->getAddressBooksForUser(); + + foreach($addressBookInfos as $addressBookInfo) { + $addressBook = new AddressBook($backend, $addressBookInfo); + while($contacts = $addressBook->getChildren($limit, $offset, false)) { + foreach($contacts as $contact) { + $cards[] = array($contact['id'], $contact['carddata']); + } + \OCP\Util::writeLog('contacts', + __CLASS__.'::'.__METHOD__ + .', scanning: ' . $limit . ' starting from ' . $offset, + \OCP\Util::DEBUG); + // only reset on first batch. + $categories->rescan($cards, true, ($offset === 0 ? true : false)); + $offset += $limit; + } + } + } + + /** + * Scan vCards for categories. + */ + public static function indexProperties() { + $offset = 0; + $limit = 10; + + $app = new App(); + $backend = $app->getBackend('local'); + $addressBookInfos = $backend->getAddressBooksForUser(); + + foreach($addressBookInfos as $addressBookInfo) { + $addressBook = new AddressBook($backend, $addressBookInfo); + while($contacts = $addressBook->getChildren($limit, $offset, false)) { + foreach($contacts as $contact) { + $contact->retrieve(); + } + \OCP\Util::writeLog('contacts', + __CLASS__.'::'.__METHOD__ + .', indexing: ' . $limit . ' starting from ' . $offset, + \OCP\Util::DEBUG); + Utils\Properties::updateIndex($contact->getId(), $contact); + $offset += $limit; + } + } + } + + public static function getCalenderSources($parameters) { + /* $base_url = \OCP\Util::linkTo('calendar', 'ajax/events.php').'?calendar_id='; foreach(Addressbook::all(\OCP\USER::getUser()) as $addressbook) { $parameters['sources'][] @@ -75,9 +159,10 @@ class Hooks{ 'editable' => false, ); } + */ } - static public function getBirthdayEvents($parameters) { + public static function getBirthdayEvents($parameters) { $name = $parameters['calendar_id']; if (strpos($name, 'birthday_') != 0) { return; diff --git a/lib/ipimobject.php b/lib/ipimobject.php new file mode 100644 index 00000000..e0022d67 --- /dev/null +++ b/lib/ipimobject.php @@ -0,0 +1,139 @@ +. + * + */ + +namespace OCA\Contacts; + +/** + * Implement this interface for PIM objects + */ + +interface IPIMObject { + + /** + * If this object is part of a collection return a reference + * to the parent object, otherwise return null. + * @return IPIMObject|null + */ + //function getParent(); + + /** + * Get the identifier for the object. + * @return string + */ + public function getId(); + + /** + * A convenience method for getting all the info about the object. + * + * The returned array MUST contain: + * 'id' @see getId(). + * 'displayname' @see getDisplayName() + * 'owner' @see getOwner() + * 'permissions' @see getPermissions + * 'lastmodified' @see lastModified() + * + * If the object is part of a collection it MUST contain + * 'parent' The identifier for the parent object. @see getParent() + * + * @return array|null + */ + public function getMetaData(); + + /** + * FIXME: This should probably not be in the interface + * as it's *DAV specific. + * @return string + */ + public function getURI(); + + /** + * @return string|null + */ + function getDisplayName(); + + /** + * Get the owner of the object. + * @return string|null + */ + function getOwner(); + + /** + * If this object is part of a collection return a reference + * to the parent object, otherwise return null. + * @return IPIMObject|null + */ + function getParent(); + + + /** CRUDS permissions (Create, Read, Update, Delete, Share) using a bitmask of + * + * \OCP\PERMISSION_CREATE + * \OCP\PERMISSION_READ + * \OCP\PERMISSION_UPDATE + * \OCP\PERMISSION_DELETE + * \OCP\PERMISSION_SHARE + * or + * \OCP\PERMISSION_ALL + * + * @return integer + */ + function getPermissions(); + + /** + * @return AbstractBackend + */ + function getBackend(); + + /** + * @param integer $permission + * @return boolean + */ + function hasPermission($permission); + + /** + * Save the contact data to backend + * FIXME: This isn't consistent. We need a proper interface + * for creating new instances and saving to storage. + * + * @param array $data + * @return bool + */ + public function update(array $data); + + /** + * @brief Get the last modification time for the object. + * + * Must return a UNIX time stamp or null if the backend + * doesn't support it. + * + * @returns int | null + */ + public function lastModified(); + + /** + * Delete the data from backend + * + * @return bool + */ + public function delete(); + +} diff --git a/lib/request.php b/lib/request.php new file mode 100644 index 00000000..408a31b1 --- /dev/null +++ b/lib/request.php @@ -0,0 +1,208 @@ +. + * + */ + +namespace OCA\Contacts; + +/** + * Class for accessing variables in the request. + * This class provides an immutable object with request variables. + */ + +class Request implements \ArrayAccess, \Countable { + + protected $items = array(); + + protected $varnames = array('get', 'post', 'files', 'server', 'env', 'session', 'cookies', 'urlParams', 'params', 'parameters', 'method'); + + /** + * @param array $vars And associative array with the following optional values: + * @param array 'params' the parsed json array + * @param array 'urlParams' the parameters which were matched from the URL + * @param array 'get' the $_GET array + * @param array 'post' the $_POST array + * @param array 'files' the $_FILES array + * @param array 'server' the $_SERVER array + * @param array 'env' the $_ENV array + * @param array 'session' the $_SESSION array + * @param array 'cookies' the $_COOKIE array + * @param string 'method' the request method (GET, POST etc) + * @see http://www.php.net/manual/en/reserved.variables.php + */ + public function __construct(array $vars = array()) { + + foreach($this->varnames as $name) { + $this->items[$name] = isset($vars[$name]) ? $vars[$name] : array(); + } + + $this->items['parameters'] = array_merge( + $this->items['params'], + $this->items['get'], + $this->items['post'], + $this->items['urlParams'] + ); + + } + + /** + * Returns an instance of Request using default request variables. + */ + public static function getRequest($urlParams) { + // Ensure that params is an array, not null + $params = json_decode(file_get_contents('php://input'), true); + \OCP\Util::writeLog('contacts', __METHOD__.' params: '.print_r($params, true), \OCP\Util::DEBUG); + $params = is_null($params) ? array() : $params; + return new self( + array( + 'get' => $_GET, + 'post' => $_POST, + 'files' => $_FILES, + 'server' => $_SERVER, + 'env' => $_ENV, + 'session' => $_SESSION, + 'cookies' => $_COOKIE, + 'method' => (isset($_SERVER) && isset($_SERVER['REQUEST_METHOD'])) + ? $_SERVER['REQUEST_METHOD'] + : '', + 'params' => $params, + 'urlParams' => $urlParams + ) + ); + } + + // Countable method. + public function count() { + return count(array_keys($this->items['parameters'])); + } + + /** + * ArrayAccess methods + * + * Gives access to the combined GET, POST and urlParams arrays + * + * Examples: + * + * $var = $request['myvar']; + * + * or + * + * if(!isset($request['myvar']) { + * // Do something + * } + * + * $request['myvar'] = 'something'; // This throws an exception. + * + * @param string offset The key to lookup + * @return string|null + */ + public function offsetExists($offset) { + return isset($this->items['parameters'][$offset]); + } + + /** + * @see offsetExists + */ + public function offsetGet($offset) { + return isset($this->items['parameters'][$offset]) + ? $this->items['parameters'][$offset] + : null; + } + + /** + * @see offsetExists + */ + public function offsetSet($offset, $value) { + throw new \RuntimeException('You cannot change the contents of the request object'); + } + + /** + * @see offsetExists + */ + public function offsetUnset($offset) { + throw new \RuntimeException('You cannot change the contents of the request object'); + } + + /** + * Get a value from a request variable or the default value e.g: + * $request->get('post', 'some_key', 'default value'); + * + * @param string $vars Which variables to look in e.g. 'get', 'post, 'session' + * @param string $name + * @param string $default + */ + public function getVar($vars, $name, $default = null) { + return isset($this->{$vars}[$name]) ? $this->{$vars}[$name] : $default; + } + + // Magic property accessors + public function __set($name, $value) { + throw new \RuntimeException('You cannot change the contents of the request object'); + } + + /** + * Access request variables by method and name. + * Examples: + * + * $request->post['myvar']; // Only look for POST variables + * $request->myvar; or $request->{'myvar'}; or $request->{$myvar} + * Looks in the combined GET, POST and urlParams array. + * + * if($request->method !== 'POST') { + * throw new Exception('This function can only be invoked using POST'); + * } + * + * @param string $name The key to look for. + * @return mixed|null + */ + public function __get($name) { + switch($name) { + case 'get': + case 'post': + case 'files': + case 'server': + case 'env': + case 'session': + case 'cookies': + case 'params': + case 'parameters': + case 'urlParams': + return isset($this->items[$name]) + ? $this->items[$name] + : null; + break; + case 'method': + return $this->items['method']; + break; + default; + return isset($this[$name]) ? $this[$name] : null; + break; + } + } + + public function __isset($name) { + return isset($this->items['parameters'][$name]); + } + + public function __unset($id) { + throw new \RunTimeException('You cannot change the contents of the request object'); + } + +} diff --git a/lib/sabre/backend.php b/lib/sabre/backend.php deleted file mode 100644 index 0c1e66e6..00000000 --- a/lib/sabre/backend.php +++ /dev/null @@ -1,212 +0,0 @@ -. - * - */ - -/** - * This CardDAV backend uses PDO to store addressbooks - */ -class OC_Connector_Sabre_CardDAV extends Sabre_CardDAV_Backend_Abstract { - /** - * Returns the list of addressbooks for a specific user. - * - * @param string $principaluri - * @return array - */ - public function getAddressBooksForUser($principaluri) { - $data = OCA\Contacts\Addressbook::allWherePrincipalURIIs($principaluri); - $addressbooks = array(); - - foreach($data as $i) { - if($i['userid'] != OCP\USER::getUser()) { - $i['uri'] = $i['uri'] . '_shared_by_' . $i['userid']; - } - $addressbooks[] = array( - 'id' => $i['id'], - 'uri' => $i['uri'], - 'principaluri' => 'principals/'.$i['userid'], - '{DAV:}displayname' => $i['displayname'], - '{' . Sabre_CardDAV_Plugin::NS_CARDDAV . '}addressbook-description' - => $i['description'], - '{http://calendarserver.org/ns/}getctag' => $i['ctag'], - ); - } - - return $addressbooks; - } - - - /** - * Updates an addressbook's properties - * - * See Sabre_DAV_IProperties for a description of the mutations array, as - * well as the return value. - * - * @param mixed $addressbookid - * @param array $mutations - * @see Sabre_DAV_IProperties::updateProperties - * @return bool|array - */ - public function updateAddressBook($addressbookid, array $mutations) { - $name = null; - $description = null; - - foreach($mutations as $property=>$newvalue) { - switch($property) { - case '{DAV:}displayname' : - $name = $newvalue; - break; - case '{' . Sabre_CardDAV_Plugin::NS_CARDDAV - . '}addressbook-description' : - $description = $newvalue; - break; - default : - // If any unsupported values were being updated, we must - // let the entire request fail. - return false; - } - } - - OCA\Contacts\Addressbook::edit($addressbookid, $name, $description); - - return true; - - } - - /** - * Creates a new address book - * - * @param string $principaluri - * @param string $url Just the 'basename' of the url. - * @param array $properties - * @return void - */ - public function createAddressBook($principaluri, $url, array $properties) { - - $displayname = null; - $description = null; - - foreach($properties as $property=>$newvalue) { - - switch($property) { - case '{DAV:}displayname' : - $name = $newvalue; - break; - case '{' . Sabre_CardDAV_Plugin::NS_CARDDAV - . '}addressbook-description' : - $description = $newvalue; - break; - default : - throw new Sabre_DAV_Exception_BadRequest('Unknown property: ' - . $property); - } - - } - - OCA\Contacts\Addressbook::addFromDAVData( - $principaluri, - $url, - $name, - $description - ); - } - - /** - * Deletes an entire addressbook and all its contents - * - * @param int $addressbookid - * @return void - */ - public function deleteAddressBook($addressbookid) { - OCA\Contacts\Addressbook::delete($addressbookid); - } - - /** - * Returns all cards for a specific addressbook id. - * - * @param mixed $addressbookid - * @return array - */ - public function getCards($addressbookid) { - $data = OCA\Contacts\VCard::all($addressbookid); - $cards = array(); - foreach($data as $i) { - //OCP\Util::writeLog('contacts', __METHOD__.', uri: ' . $i['uri'], OCP\Util::DEBUG); - $cards[] = array( - 'id' => $i['id'], - //'carddata' => $i['carddata'], - 'size' => strlen($i['carddata']), - 'etag' => '"' . md5($i['carddata']) . '"', - 'uri' => $i['uri'], - 'lastmodified' => $i['lastmodified'] ); - } - - return $cards; - } - - /** - * Returns a specfic card - * - * @param mixed $addressbookid - * @param string $carduri - * @return array - */ - public function getCard($addressbookid, $carduri) { - return OCA\Contacts\VCard::findWhereDAVDataIs($addressbookid, $carduri); - - } - - /** - * Creates a new card - * - * @param mixed $addressbookid - * @param string $carduri - * @param string $carddata - * @return bool - */ - public function createCard($addressbookid, $carduri, $carddata) { - OCA\Contacts\VCard::addFromDAVData($addressbookid, $carduri, $carddata); - } - - /** - * Updates a card - * - * @param mixed $addressbookid - * @param string $carduri - * @param string $carddata - * @return bool - */ - public function updateCard($addressbookid, $carduri, $carddata) { - return OCA\Contacts\VCard::editFromDAVData( - $addressbookid, $carduri, $carddata - ); - } - - /** - * Deletes a card - * - * @param mixed $addressbookid - * @param string $carduri - * @return bool - */ - public function deleteCard($addressbookid, $carduri) { - return OCA\Contacts\VCard::deleteFromDAVData($addressbookid, $carduri); - } -} diff --git a/lib/share/addressbook.php b/lib/share/addressbook.php index f90c1e5c..48b32f60 100644 --- a/lib/share/addressbook.php +++ b/lib/share/addressbook.php @@ -10,6 +10,14 @@ namespace OCA\Contacts; class Share_Backend_Addressbook implements \OCP\Share_Backend_Collection { const FORMAT_ADDRESSBOOKS = 1; + const FORMAT_COLLECTION = 2; + + public $backend; + + public function __construct() { + // Currently only share + $this->backend = new Backend\Database(); + } /** * @brief Get the source of the item to be stored in the database @@ -23,8 +31,8 @@ class Share_Backend_Addressbook implements \OCP\Share_Backend_Collection { * The formatItems() function will translate the source returned back into the item */ public function isValidSource($itemSource, $uidOwner) { - $addressbook = Addressbook::find( $itemSource ); - if( $addressbook === false || $addressbook['userid'] != $uidOwner) { + $addressbook = $this->backend->getAddressBook($itemSource); + if(!$addressbook || $addressbook['userid'] !== $uidOwner) { return false; } return true; @@ -41,17 +49,20 @@ class Share_Backend_Addressbook implements \OCP\Share_Backend_Collection { * If it does generate a new name e.g. name_# */ public function generateTarget($itemSource, $shareWith, $exclude = null) { - $addressbook = Addressbook::find( $itemSource ); + $addressbook = $this->backend->getAddressBook($itemSource); + $user_addressbooks = array(); - foreach(Addressbook::all($shareWith) as $user_addressbook) { + + foreach($this->backend->getAddressBooksForUser($shareWith) as $user_addressbook) { $user_addressbooks[] = $user_addressbook['displayname']; } - $name = $addressbook['displayname']; + $name = $addressbook['displayname'] . '(' . $addressbook['userid'] . ')'; $suffix = ''; while (in_array($name.$suffix, $user_addressbooks)) { $suffix++; } + $suffix = $suffix ? ' ' . $suffix : ''; return $name.$suffix; } @@ -68,26 +79,33 @@ class Share_Backend_Addressbook implements \OCP\Share_Backend_Collection { * This function allows the backend to control the output of shared items with custom formats. * It is only called through calls to the public getItem(s)Shared(With) functions. */ - public function formatItems($items, $format, $parameters = null) { + public function formatItems($items, $format, $parameters = null, $include = false) { + //\OCP\Util::writeLog('contacts', __METHOD__ + // . ' ' . $include . ' ' . print_r($items, true), \OCP\Util::DEBUG); $addressbooks = array(); - if ($format == self::FORMAT_ADDRESSBOOKS) { + if ($format === self::FORMAT_ADDRESSBOOKS) { foreach ($items as $item) { - $addressbook = Addressbook::find($item['item_source']); + //\OCP\Util::writeLog('contacts', __METHOD__.' item_source: ' . $item['item_source'] . ' include: ' + // . (int)$include, \OCP\Util::DEBUG); + $addressbook = $this->backend->getAddressBook($item['item_source']); if ($addressbook) { - $addressbook['displayname'] = $item['item_target']; + $addressbook['displayname'] = $addressbook['displayname'] . ' (' . $addressbook['owner'] . ')'; $addressbook['permissions'] = $item['permissions']; $addressbooks[] = $addressbook; } } + } elseif ($format === self::FORMAT_COLLECTION) { + foreach ($items as $item) { + } } return $addressbooks; } public function getChildren($itemSource) { - $query = \OCP\DB::prepare('SELECT `id`, `fullname` FROM `*PREFIX*contacts_cards` WHERE `addressbookid` = ?'); - $result = $query->execute(array($itemSource)); + \OCP\Util::writeLog('contacts', __METHOD__.' item_source: ' . $itemSource, \OCP\Util::DEBUG); + $contacts = $this->backend->getContacts($itemSource, null, null, true); $children = array(); - while ($contact = $result->fetchRow()) { + foreach($contacts as $contact) { $children[] = array('source' => $contact['id'], 'target' => $contact['fullname']); } return $children; diff --git a/lib/share/contact.php b/lib/share/contact.php index f415bf28..4d5f633f 100644 --- a/lib/share/contact.php +++ b/lib/share/contact.php @@ -27,6 +27,11 @@ class Share_Backend_Contact implements \OCP\Share_Backend { private static $contact; + public function __construct() { + // Currently only share + $this->backend = new Backend\Database(); + } + public function isValidSource($itemSource, $uidOwner) { self::$contact = VCard::find($itemSource); if (self::$contact) { diff --git a/lib/utils/jsonserializer.php b/lib/utils/jsonserializer.php new file mode 100644 index 00000000..52dd7fcf --- /dev/null +++ b/lib/utils/jsonserializer.php @@ -0,0 +1,207 @@ +. + * + */ + +namespace OCA\Contacts\Utils; + +use OCA\Contacts\VObject; +use OCA\Contacts\Contact; + +/** + * This class serializes properties, components an + * arrays of components into a format suitable for + * passing to a JSON response. + * TODO: Return jCard (almost) compliant data, but still omitting unneeded data. + * http://tools.ietf.org/html/draft-kewisch-vcard-in-json-01 + */ + +class JSONSerializer { + + /** + * General method serialize method. Use this for arrays + * of contacts. + * + * @param Contact[] $input + * @return array + */ + public static function serialize($input) { + $response = array(); + if(is_array($input)) { + foreach($input as $object) { + if($object instanceof Contact) { + \OCP\Util::writeLog('contacts', __METHOD__.' serializing: ' . print_r($object, true), \OCP\Util::DEBUG); + $tmp = self::serializeContact($object); + if($tmp !== null) { + $response[] = $tmp; + } + } else { + throw new \Exception( + 'Only arrays of OCA\\Contacts\\VObject\\VCard ' + . 'and Sabre\VObject\Property are accepted.' + ); + } + } + } else { + if($input instanceof VObject\VCard) { + return self::serializeContact($input); + } elseif($input instanceof Sabre\VObject\Property) { + return self::serializeProperty($input); + } else { + throw new \Exception( + 'Only instances of OCA\\Contacts\\VObject\\VCard ' + . 'and Sabre\VObject\Property are accepted.' + ); + } + } + return $response; + } + + /** + * @brief Data structure of vCard + * @param VObject\VCard $contact + * @return associative array|null + */ + public static function serializeContact(Contact $contact) { + + if(!$contact->retrieve()) { + \OCP\Util::writeLog('contacts', __METHOD__.' error reading: ' . print_r($contact, true), \OCP\Util::DEBUG); + return null; + } + + $details = array(); + + if(isset($contact->PHOTO) || isset($contact->LOGO)) { + $details['thumbnail'] = $contact->cacheThumbnail(); + } + + foreach($contact->children as $property) { + $pname = $property->name; + $temp = self::serializeProperty($property); + if(!is_null($temp)) { + // Get Apple X-ABLabels + if(isset($contact->{$property->group . '.X-ABLABEL'})) { + $temp['label'] = $contact->{$property->group . '.X-ABLABEL'}->value; + if($temp['label'] == '_$!!$_') { + $temp['label'] = Properties::$l10n->t('Other'); + } + if($temp['label'] == '_$!!$_') { + $temp['label'] = Properties::$l10n->t('HomePage'); + } + } + if(array_key_exists($pname, $details)) { + $details[$pname][] = $temp; + } + else{ + $details[$pname] = array($temp); + } + } + } + return array('data' =>$details, 'metadata' => $contact->getMetaData()); + } + + /** + * @brief Get data structure of property. + * @param \Sabre\VObject\Property $property + * @return associative array + * + * returns an associative array with + * ['name'] name of property + * ['value'] htmlspecialchars escaped value of property + * ['parameters'] associative array name=>value + * ['checksum'] checksum of whole property + * NOTE: $value is not escaped anymore. It shouldn't make any difference + * but we should look out for any problems. + */ + public static function serializeProperty(\Sabre\VObject\Property $property) { + if(!in_array($property->name, Properties::$index_properties)) { + return; + } + $value = $property->value; + if($property->name == 'ADR' || $property->name == 'N' || $property->name == 'ORG' || $property->name == 'CATEGORIES') { + $value = $property->getParts(); + $value = array_map('trim', $value); + } + elseif($property->name == 'BDAY') { + if(strpos($value, '-') === false) { + if(strlen($value) >= 8) { + $value = substr($value, 0, 4).'-'.substr($value, 4, 2).'-'.substr($value, 6, 2); + } else { + return null; // Badly malformed :-( + } + } + } elseif($property->name == 'PHOTO') { + $value = true; + } + elseif($property->name == 'IMPP') { + if(strpos($value, ':') !== false) { + $value = explode(':', $value); + $protocol = array_shift($value); + if(!isset($property['X-SERVICE-TYPE'])) { + $property['X-SERVICE-TYPE'] = strtoupper($protocol); + } + $value = implode('', $value); + } + } + if(is_string($value)) { + $value = strtr($value, array('\,' => ',', '\;' => ';')); + } + $temp = array( + //'name' => $property->name, + 'value' => $value, + 'parameters' => array() + ); + + // This cuts around a 3rd off of the json response size. + if(in_array($property->name, Properties::$multi_properties)) { + $temp['checksum'] = substr(md5($property->serialize()), 0, 8); + } + foreach($property->parameters as $parameter) { + // Faulty entries by kaddressbook + // Actually TYPE=PREF is correct according to RFC 2426 + // but this way is more handy in the UI. Tanghus. + if($parameter->name == 'TYPE' && strtoupper($parameter->value) == 'PREF') { + $parameter->name = 'PREF'; + $parameter->value = '1'; + } + // NOTE: Apparently Sabre_VObject_Reader can't always deal with value list parameters + // like TYPE=HOME,CELL,VOICE. Tanghus. + // TODO: Check if parameter is has commas and split + merge if so. + if ($parameter->name == 'TYPE') { + $pvalue = $parameter->value; + if(is_string($pvalue) && strpos($pvalue, ',') !== false) { + $pvalue = array_map('trim', explode(',', $pvalue)); + } + $pvalue = is_array($pvalue) ? $pvalue : array($pvalue); + if (isset($temp['parameters'][$parameter->name])) { + $temp['parameters'][$parameter->name][] = \OCP\Util::sanitizeHTML($pvalue); + } + else { + $temp['parameters'][$parameter->name] = \OCP\Util::sanitizeHTML($pvalue); + } + } + else{ + $temp['parameters'][$parameter->name] = \OCP\Util::sanitizeHTML($parameter->value); + } + } + return $temp; + } +} diff --git a/lib/utils/properties.php b/lib/utils/properties.php new file mode 100644 index 00000000..cd286045 --- /dev/null +++ b/lib/utils/properties.php @@ -0,0 +1,263 @@ +. + * + */ + +namespace OCA\Contacts\Utils; + +Properties::$l10n = \OC_L10N::get('contacts'); + +Class Properties { + + private static $deleteindexstmt; + private static $updateindexstmt; + protected static $cardsTableName = '*PREFIX*contacts_cards'; + protected static $indexTableName = '*PREFIX*contacts_cards_properties'; + + /** + * @brief language object for calendar app + * + * @var OC_L10N + */ + public static $l10n; + + /** + * Properties there can be more than one of. + * + * @var array + */ + public static $multi_properties = array('EMAIL', 'TEL', 'IMPP', 'ADR', 'URL'); + + /** + * Properties to index. + * + * @var array + */ + public static $index_properties = array( + 'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME', + 'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'PHOTO'); + + /** + * Get options for IMPP properties + * @param string $im + * @return array of vcard prop => label + */ + public static function getIMOptions($im = null) { + $l10n = self::$l10n; + $ims = array( + 'jabber' => array( + 'displayname' => (string)$l10n->t('Jabber'), + 'xname' => 'X-JABBER', + 'protocol' => 'xmpp', + ), + 'aim' => array( + 'displayname' => (string)$l10n->t('AIM'), + 'xname' => 'X-AIM', + 'protocol' => 'aim', + ), + 'msn' => array( + 'displayname' => (string)$l10n->t('MSN'), + 'xname' => 'X-MSN', + 'protocol' => 'msn', + ), + 'twitter' => array( + 'displayname' => (string)$l10n->t('Twitter'), + 'xname' => 'X-TWITTER', + 'protocol' => 'twitter', + ), + 'googletalk' => array( + 'displayname' => (string)$l10n->t('GoogleTalk'), + 'xname' => null, + 'protocol' => 'xmpp', + ), + 'facebook' => array( + 'displayname' => (string)$l10n->t('Facebook'), + 'xname' => null, + 'protocol' => 'xmpp', + ), + 'xmpp' => array( + 'displayname' => (string)$l10n->t('XMPP'), + 'xname' => null, + 'protocol' => 'xmpp', + ), + 'icq' => array( + 'displayname' => (string)$l10n->t('ICQ'), + 'xname' => 'X-ICQ', + 'protocol' => 'icq', + ), + 'yahoo' => array( + 'displayname' => (string)$l10n->t('Yahoo'), + 'xname' => 'X-YAHOO', + 'protocol' => 'ymsgr', + ), + 'skype' => array( + 'displayname' => (string)$l10n->t('Skype'), + 'xname' => 'X-SKYPE', + 'protocol' => 'skype', + ), + 'qq' => array( + 'displayname' => (string)$l10n->t('QQ'), + 'xname' => 'X-SKYPE', + 'protocol' => 'x-apple', + ), + 'gadugadu' => array( + 'displayname' => (string)$l10n->t('GaduGadu'), + 'xname' => 'X-SKYPE', + 'protocol' => 'x-apple', + ), + ); + if(is_null($im)) { + return $ims; + } else { + $ims['ymsgr'] = $ims['yahoo']; + $ims['gtalk'] = $ims['googletalk']; + return isset($ims[$im]) ? $ims[$im] : null; + } + } + + /** + * Get standard set of TYPE values for different properties. + * + * @param string $prop + * @return array Type values for property $prop + */ + public static function getTypesForProperty($prop) { + $l = self::$l10n; + switch($prop) { + case 'LABEL': + case 'ADR': + case 'IMPP': + return array( + 'WORK' => (string)$l->t('Work'), + 'HOME' => (string)$l->t('Home'), + 'OTHER' => (string)$l->t('Other'), + ); + case 'TEL': + return array( + 'HOME' => (string)$l->t('Home'), + 'CELL' => (string)$l->t('Mobile'), + 'WORK' => (string)$l->t('Work'), + 'TEXT' => (string)$l->t('Text'), + 'VOICE' => (string)$l->t('Voice'), + 'MSG' => (string)$l->t('Message'), + 'FAX' => (string)$l->t('Fax'), + 'VIDEO' => (string)$l->t('Video'), + 'PAGER' => (string)$l->t('Pager'), + 'OTHER' => (string)$l->t('Other'), + ); + case 'EMAIL': + return array( + 'WORK' => (string)$l->t('Work'), + 'HOME' => (string)$l->t('Home'), + 'INTERNET' => (string)$l->t('Internet'), + 'OTHER' => (string)$l->t('Other'), + ); + } + } + + /** + * @brief returns the default categories of ownCloud + * @return (array) $categories + */ + public static function getDefaultCategories() { + $l10n = self::$l10n; + return array( + (string)$l10n->t('Friends'), + (string)$l10n->t('Family'), + (string)$l10n->t('Work'), + (string)$l10n->t('Other'), + ); + } + + public static function generateUID($app = 'contacts') { + return date('Ymd\\THis') . '.' . time(). '@' . OCP\Util::getServerHostName(); + } + + /** + * Update the contact property index. + * + * If vcard is null the properties for that contact will be purged. + * If it is a valid object the old properties will first be purged + * and the current properties indexed. + * + * @param string $contactid + * @param \OCA\VObject\VCard|null $vcard + */ + public static function updateIndex($contactid, $vcard = null) { + if(!isset(self::$deleteindexstmt)) { + self::$deleteindexstmt + = \OCP\DB::prepare('DELETE FROM `' . self::$indexTableName . '`' + . ' WHERE `contactid` = ?'); + } + try { + self::$deleteindexstmt->execute(array($contactid)); + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__. + ', exception: ' . $e->getMessage(), \OCP\Util::ERROR); + \OCP\Util::writeLog('contacts', __METHOD__.', id: ' + . $id, \OCP\Util::DEBUG); + throw new \Exception( + App::$l10n->t( + 'There was an error deleting properties for this contact.' + ) + ); + } + + if(is_null($vcard)) { + return; + } + + if(!isset(self::$updateindexstmt)) { + self::$updateindexstmt = \OCP\DB::prepare( 'INSERT INTO `' . self::$indexTableName . '` ' + . '(`userid`, `contactid`,`name`,`value`,`preferred`) VALUES(?,?,?,?,?)' ); + } + foreach($vcard->children as $property) { + if(!in_array($property->name, self::$index_properties)) { + continue; + } + $preferred = 0; + foreach($property->parameters as $parameter) { + if($parameter->name == 'TYPE' && strtoupper($parameter->value) == 'PREF') { + $preferred = 1; + break; + } + } + try { + $result = self::$updateindexstmt->execute( + array( + \OCP\User::getUser(), + $contactid, + $property->name, + $property->value, + $preferred, + ) + ); + if (\OC_DB::isError($result)) { + \OCP\Util::writeLog('contacts', __METHOD__. 'DB error: ' + . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); + return false; + } + } catch(\Exception $e) { + \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); + return false; + } + } + } +} diff --git a/lib/vcard.php b/lib/vcard.php index fb1a94e9..cb1781d8 100644 --- a/lib/vcard.php +++ b/lib/vcard.php @@ -4,7 +4,7 @@ * * @author Jakob Sack * @copyright 2011 Jakob Sack mail@jakobsack.de - * @copyright 2012 Thomas Tanghus + * @copyright 2012-2013 Thomas Tanghus * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE @@ -43,377 +43,6 @@ use Sabre\VObject; * This class manages our vCards */ class VCard { - /** - * @brief Returns all cards of an address book - * @param integer $id - * @param integer $offset - * @param integer $limit - * @param array $fields An array of the fields to return. Defaults to all. - * @return array|false - * - * The cards are associative arrays. You'll find the original vCard in - * ['carddata'] - */ - public static function all($id, $offset=null, $limit=null, $fields = array()) { - $result = null; - \OCP\Util::writeLog('contacts', __METHOD__.'count fields:' . count($fields), \OCP\Util::DEBUG); - $qfields = count($fields) > 0 - ? '`' . implode('`,`', $fields) . '`' - : '*'; - if(is_array($id) && count($id)) { - $id_sql = join(',', array_fill(0, count($id), '?')); - $sql = 'SELECT ' . $qfields . ' FROM `*PREFIX*contacts_cards` WHERE `addressbookid` IN ('.$id_sql.') ORDER BY `fullname`'; - try { - $stmt = \OCP\DB::prepare($sql, $limit, $offset); - $result = $stmt->execute($id); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: ' . $e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', ids: ' . join(',', $id), \OCP\Util::DEBUG); - \OCP\Util::writeLog('contacts', __METHOD__.'SQL:' . $sql, \OCP\Util::DEBUG); - return false; - } - } elseif(is_int($id) || is_string($id)) { - try { - $sql = 'SELECT ' . $qfields . ' FROM `*PREFIX*contacts_cards` WHERE `addressbookid` = ? ORDER BY `fullname`'; - $stmt = \OCP\DB::prepare($sql, $limit, $offset); - $result = $stmt->execute(array($id)); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', ids: '. $id, \OCP\Util::DEBUG); - return false; - } - } else { - \OCP\Util::writeLog('contacts', __METHOD__ - . '. Addressbook id(s) argument is empty: ' - . print_r($id, true), \OCP\Util::DEBUG); - return false; - } - $cards = array(); - if(!is_null($result)) { - while( $row = $result->fetchRow()) { - $cards[] = $row; - } - } - - return $cards; - } - - /** - * @brief Returns a card - * @param integer $id - * @param array $fields An array of the fields to return. Defaults to all. - * @return associative array or false. - */ - public static function find($id, $fields = array() ) { - if(count($fields) > 0 && !in_array('addressbookid', $fields)) { - $fields[] = 'addressbookid'; - } - try { - $qfields = count($fields) > 0 - ? '`' . implode('`,`', $fields) . '`' - : '*'; - $stmt = \OCP\DB::prepare( 'SELECT ' . $qfields . ' FROM `*PREFIX*contacts_cards` WHERE `id` = ?' ); - $result = $stmt->execute(array($id)); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', id: '. $id, \OCP\Util::DEBUG); - return false; - } - - $row = $result->fetchRow(); - if($row) { - try { - $addressbook = Addressbook::find($row['addressbookid']); - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', id: '. $id, \OCP\Util::DEBUG); - throw $e; - } - } - return $row; - } - - /** - * @brief finds a card by its DAV Data - * @param integer $aid Addressbook id - * @param string $uri the uri ('filename') - * @return associative array or false. - */ - public static function findWhereDAVDataIs($aid, $uri) { - try { - $stmt = \OCP\DB::prepare( 'SELECT * FROM `*PREFIX*contacts_cards` WHERE `addressbookid` = ? AND `uri` = ?' ); - $result = $stmt->execute(array($aid,$uri)); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', aid: '.$aid.' uri'.$uri, \OCP\Util::DEBUG); - return false; - } - - return $result->fetchRow(); - } - - /** - * @brief Format property TYPE parameters for upgrading from v. 2.1 - * @param $property Reference to a Sabre_VObject_Property. - * In version 2.1 e.g. a phone can be formatted like: TEL;HOME;CELL:123456789 - * This has to be changed to either TEL;TYPE=HOME,CELL:123456789 or TEL;TYPE=HOME;TYPE=CELL:123456789 - both are valid. - */ - public static function formatPropertyTypes(&$property) { - foreach($property->parameters as $key=>&$parameter) { - $types = App::getTypesOfProperty($property->name); - if(is_array($types) && in_array(strtoupper($parameter->name), array_keys($types)) || strtoupper($parameter->name) == 'PREF') { - $property->parameters[] = new \Sabre\VObject\Parameter('TYPE', $parameter->name); - } - unset($property->parameters[$key]); - } - } - - /** - * @brief Decode properties for upgrading from v. 2.1 - * @param $property Reference to a Sabre_VObject_Property. - * The only encoding allowed in version 3.0 is 'b' for binary. All encoded strings - * must therefor be decoded and the parameters removed. - */ - public static function decodeProperty(&$property) { - // Check out for encoded string and decode them :-[ - foreach($property->parameters as $key=>&$parameter) { - if(strtoupper($parameter->name) == 'ENCODING') { - if(strtoupper($parameter->value) == 'QUOTED-PRINTABLE') { // what kind of other encodings could be used? - // Decode quoted-printable and strip any control chars - // except \n and \r - $property->value = preg_replace( - '/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/', - '', - quoted_printable_decode($property->value) - ); - unset($property->parameters[$key]); - } - } elseif(strtoupper($parameter->name) == 'CHARSET') { - unset($property->parameters[$key]); - } - } - } - - /** - * @brief Checks if a contact with the same UID already exist in the address book. - * @param $aid Address book ID. - * @param $uid UID (passed by reference). - * @returns true if the UID has been changed. - */ - protected static function trueUID($aid, &$uid) { - $stmt = \OCP\DB::prepare( 'SELECT * FROM `*PREFIX*contacts_cards` WHERE `addressbookid` = ? AND `uri` = ?' ); - $uri = $uid.'.vcf'; - try { - $result = $stmt->execute(array($aid,$uri)); - if (\OC_DB::isError($result)) { - \OCP\Util::writeLog('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', aid: '.$aid.' uid'.$uid, \OCP\Util::DEBUG); - return false; - } - if($result->numRows() > 0) { - while(true) { - $tmpuid = substr(md5(rand().time()), 0, 10); - $uri = $tmpuid.'.vcf'; - $result = $stmt->execute(array($aid, $uri)); - if($result->numRows() > 0) { - continue; - } else { - $uid = $tmpuid; - return true; - } - } - } else { - return false; - } - } - - /** - * @brief Tries to update imported VCards to adhere to rfc2426 (VERSION: 3.0) and add mandatory fields if missing. - * @param aid Address book id. - * @param vcard A Sabre\VObject\Component of type VCARD (passed by reference). - */ - protected static function updateValuesFromAdd($aid, &$vcard) { // any suggestions for a better method name? ;-) - $stringprops = array('N', 'FN', 'ORG', 'NICK', 'ADR', 'NOTE'); - $typeprops = array('ADR', 'TEL', 'EMAIL'); - $upgrade = false; - $fn = $n = $uid = $email = $org = null; - $version = isset($vcard->VERSION) ? $vcard->VERSION : null; - // Add version if needed - if($version && $version < '3.0') { - $upgrade = true; - //OCP\Util::writeLog('contacts', 'OCA\Contacts\VCard::updateValuesFromAdd. Updating from version: '.$version, OCP\Util::DEBUG); - } - foreach($vcard->children as &$property) { - // Decode string properties and remove obsolete properties. - if($upgrade && in_array($property->name, $stringprops)) { - self::decodeProperty($property); - } - if(function_exists('iconv')) { - $property->value = str_replace("\r\n", "\n", iconv(mb_detect_encoding($property->value, 'UTF-8, ISO-8859-1'), 'utf-8', $property->value)); - } else { - $property->value = str_replace("\r\n", "\n", mb_convert_encoding($property->value, 'UTF-8', mb_detect_encoding($property->value, 'UTF-8, ISO-8859-1'), $property->value)); - } - if(in_array($property->name, $stringprops)) { - $property->value = strip_tags($property->value); - } - // Fix format of type parameters. - if($upgrade && in_array($property->name, $typeprops)) { - //OCP\Util::writeLog('contacts', 'OCA\Contacts\VCard::updateValuesFromAdd. before: '.$property->serialize(), OCP\Util::DEBUG); - self::formatPropertyTypes($property); - //OCP\Util::writeLog('contacts', 'OCA\Contacts\VCard::updateValuesFromAdd. after: '.$property->serialize(), OCP\Util::DEBUG); - } - if($property->name == 'FN') { - $fn = $property->value; - } - else if($property->name == 'N') { - $n = $property->value; - } - else if($property->name == 'UID') { - $uid = $property->value; - } - else if($property->name == 'ORG') { - $org = $property->value; - } - else if($property->name == 'EMAIL' && is_null($email)) { // only use the first email as substitute for missing N or FN. - $email = $property->value; - } - } - // Check for missing 'N', 'FN' and 'UID' properties - if(!$fn) { - if($n && $n != ';;;;') { - $fn = join(' ', array_reverse(array_slice(explode(';', $n), 0, 2))); - } elseif($email) { - $fn = $email; - } elseif($org) { - $fn = $org; - } else { - $fn = 'Unknown Name'; - } - $vcard->FN = $fn; - //OCP\Util::writeLog('contacts', 'OCA\Contacts\VCard::updateValuesFromAdd. Added missing \'FN\' field: '.$fn, OCP\Util::DEBUG); - } - if(!$n || $n == ';;;;') { // Fix missing 'N' field. Ugly hack ahead ;-) - $slice = array_reverse(array_slice(explode(' ', $fn), 0, 2)); // Take 2 first name parts of 'FN' and reverse. - if(count($slice) < 2) { // If not enought, add one more... - $slice[] = ""; - } - $n = implode(';', $slice).';;;'; - $vcard->N = $n; - //OCP\Util::writeLog('contacts', 'OCA\Contacts\VCard::updateValuesFromAdd. Added missing \'N\' field: '.$n, OCP\Util::DEBUG); - } - if(!$uid) { - $uid = substr(md5(rand().time()), 0, 10); - $vcard->add('UID', $uid); - //OCP\Util::writeLog('contacts', 'OCA\Contacts\VCard::updateValuesFromAdd. Added missing \'UID\' field: '.$uid, OCP\Util::DEBUG); - } - if(self::trueUID($aid, $uid)) { - $vcard->{'UID'} = $uid; - } - $now = new \DateTime; - $vcard->{'REV'} = $now->format(\DateTime::W3C); - } - - /** - * @brief Adds a card - * @param $aid integer Addressbook id - * @param $card Sabre\VObject\Component vCard file - * @param $uri string the uri of the card, default based on the UID - * @param $isChecked boolean If the vCard should be checked for validity and version. - * @return insertid on success or false. - */ - public static function add($aid, VObject\Component $card, $uri=null, $isChecked=false) { - if(is_null($card)) { - \OCP\Util::writeLog('contacts', __METHOD__ . ', No vCard supplied', \OCP\Util::ERROR); - return null; - }; - $addressbook = Addressbook::find($aid); - if ($addressbook['userid'] != \OCP\User::getUser()) { - $sharedAddressbook = \OCP\Share::getItemSharedWithBySource('addressbook', $aid); - if (!$sharedAddressbook || !($sharedAddressbook['permissions'] & \OCP\PERMISSION_CREATE)) { - throw new \Exception( - App::$l10n->t( - 'You do not have the permissions to add contacts to this addressbook.' - ) - ); - } - } - if(!$isChecked) { - self::updateValuesFromAdd($aid, $card); - } - $card->{'VERSION'} = '3.0'; - // Add product ID is missing. - //$prodid = trim($card->getAsString('PRODID')); - //if(!$prodid) { - if(!isset($card->PRODID)) { - $appinfo = \OCP\App::getAppInfo('contacts'); - $appversion = \OCP\App::getAppVersion('contacts'); - $prodid = '-//ownCloud//NONSGML '.$appinfo['name'].' '.$appversion.'//EN'; - $card->add('PRODID', $prodid); - } - - $fn = isset($card->FN) ? $card->FN : ''; - - $uri = isset($uri) ? $uri : $card->UID . '.vcf'; - - $data = $card->serialize(); - $stmt = \OCP\DB::prepare( 'INSERT INTO `*PREFIX*contacts_cards` (`addressbookid`,`fullname`,`carddata`,`uri`,`lastmodified`) VALUES(?,?,?,?,?)' ); - try { - $result = $stmt->execute(array($aid, $fn, $data, $uri, time())); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', aid: '.$aid.' uri'.$uri, \OCP\Util::DEBUG); - return false; - } - $newid = \OCP\DB::insertid('*PREFIX*contacts_cards'); - App::loadCategoriesFromVCard($newid, $card); - App::updateDBProperties($newid, $card); - App::cacheThumbnail($newid); - - Addressbook::touch($aid); - \OC_Hook::emit('\OCA\Contacts\VCard', 'post_createVCard', $newid); - return $newid; - } - - /** - * @brief Adds a card with the data provided by sabredav - * @param integer $id Addressbook id - * @param string $uri the uri the card will have - * @param string $data vCard file - * @returns integer|false insertid or false on error - */ - public static function addFromDAVData($id, $uri, $data) { - try { - $vcard = \Sabre\VObject\Reader::read($data); - return self::add($id, $vcard, $uri); - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); - return false; - } - } /** * @brief Mass updates an array of cards @@ -435,13 +64,6 @@ class VCard { return false; } - $addressbook = Addressbook::find($oldcard['addressbookid']); - if ($addressbook['userid'] != \OCP\User::getUser()) { - $sharedContact = \OCP\Share::getItemSharedWithBySource('contact', $object[0], \OCP\Share::FORMAT_NONE, null, true); - if (!$sharedContact || !($sharedContact['permissions'] & \OCP\PERMISSION_UPDATE)) { - return false; - } - } $vcard->{'REV'} = $now->format(\DateTime::W3C); $data = $vcard->serialize(); try { @@ -459,442 +81,4 @@ class VCard { } } - /** - * @brief edits a card - * @param integer $id id of card - * @param Sabre\VObject\Component $card vCard file - * @return boolean true on success, otherwise an exception will be thrown - */ - public static function edit($id, VObject\Component $card) { - $oldcard = self::find($id); - if (!$oldcard) { - \OCP\Util::writeLog('contacts', __METHOD__.', id: ' - . $id . ' not found.', \OCP\Util::DEBUG); - throw new \Exception( - App::$l10n->t( - 'Could not find the vCard with ID.' . $id - ) - ); - } - if(is_null($card)) { - return false; - } - // NOTE: Owner checks are being made in the ajax files, which should be done - // inside the lib files to prevent any redundancies with sharing checks - $addressbook = Addressbook::find($oldcard['addressbookid']); - if ($addressbook['userid'] != \OCP\User::getUser()) { - $sharedAddressbook = \OCP\Share::getItemSharedWithBySource( - 'addressbook', - $oldcard['addressbookid'], - \OCP\Share::FORMAT_NONE, null, true); - $sharedContact = \OCP\Share::getItemSharedWithBySource('contact', $id, \OCP\Share::FORMAT_NONE, null, true); - $addressbook_permissions = 0; - $contact_permissions = 0; - if ($sharedAddressbook) { - $addressbook_permissions = $sharedAddressbook['permissions']; - } - if ($sharedContact) { - $contact_permissions = $sharedEvent['permissions']; - } - $permissions = max($addressbook_permissions, $contact_permissions); - if (!($permissions & \OCP\PERMISSION_UPDATE)) { - throw new \Exception( - App::$l10n->t( - 'You do not have the permissions to edit this contact.' - ) - ); - } - } - App::loadCategoriesFromVCard($id, $card); - - $fn = isset($card->FN) ? $card->FN : ''; - - $now = new \DateTime; - $card->REV = $now->format(\DateTime::W3C); - - $data = $card->serialize(); - $stmt = \OCP\DB::prepare( 'UPDATE `*PREFIX*contacts_cards` SET `fullname` = ?,`carddata` = ?, `lastmodified` = ? WHERE `id` = ?' ); - try { - $result = $stmt->execute(array($fn, $data, time(), $id)); - if (\OC_DB::isError($result)) { - \OCP\Util::writeLog('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OCP\Util::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: ' - . $e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', id'.$id, \OCP\Util::DEBUG); - return false; - } - - App::cacheThumbnail($oldcard['id']); - App::updateDBProperties($id, $card); - Addressbook::touch($oldcard['addressbookid']); - \OC_Hook::emit('\OCA\Contacts\VCard', 'post_updateVCard', $id); - return true; - } - - /** - * @brief edits a card with the data provided by sabredav - * @param integer $id Addressbook id - * @param string $uri the uri of the card - * @param string $data vCard file - * @return boolean - */ - public static function editFromDAVData($aid, $uri, $data) { - $oldcard = self::findWhereDAVDataIs($aid, $uri); - try { - $vcard = \Sabre\VObject\Reader::read($data); - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__. - ', Unable to parse VCARD, : ' . $e->getMessage(), \OCP\Util::ERROR); - return false; - } - try { - self::edit($oldcard['id'], $vcard); - return true; - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: ' - . $e->getMessage() . ', ' - . \OCP\USER::getUser(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', uri' - . $uri, \OCP\Util::DEBUG); - return false; - } - } - - /** - * @brief deletes a card - * @param integer $id id of card - * @return boolean true on success, otherwise an exception will be thrown - */ - public static function delete($id) { - $contact = self::find($id); - if (!$contact) { - \OCP\Util::writeLog('contacts', __METHOD__.', id: ' - . $id . ' not found.', \OCP\Util::DEBUG); - throw new \Exception( - App::$l10n->t( - 'Could not find the vCard with ID: ' . $id, 404 - ) - ); - } - $addressbook = Addressbook::find($contact['addressbookid']); - if(!$addressbook) { - throw new \Exception( - App::$l10n->t( - 'Could not find the Addressbook with ID: ' - . $contact['addressbookid'], 404 - ) - ); - } - - if ($addressbook['userid'] != \OCP\User::getUser() && !\OC_Group::inGroup(\OCP\User::getUser(), 'admin')) { - \OCP\Util::writeLog('contacts', __METHOD__.', ' - . $addressbook['userid'] . ' != ' . \OCP\User::getUser(), \OCP\Util::DEBUG); - $sharedAddressbook = \OCP\Share::getItemSharedWithBySource( - 'addressbook', - $contact['addressbookid'], - \OCP\Share::FORMAT_NONE, null, true); - $sharedContact = \OCP\Share::getItemSharedWithBySource( - 'contact', - $id, - \OCP\Share::FORMAT_NONE, null, true); - $addressbook_permissions = 0; - $contact_permissions = 0; - if ($sharedAddressbook) { - $addressbook_permissions = $sharedAddressbook['permissions']; - } - if ($sharedContact) { - $contact_permissions = $sharedEvent['permissions']; - } - $permissions = max($addressbook_permissions, $contact_permissions); - - if (!($permissions & \OCP\PERMISSION_DELETE)) { - throw new \Exception( - App::$l10n->t( - 'You do not have the permissions to delete this contact.', 403 - ) - ); - } - } - $aid = $contact['addressbookid']; - \OC_Hook::emit('\OCA\Contacts\VCard', 'pre_deleteVCard', - array('aid' => null, 'id' => $id, 'uri' => null) - ); - $stmt = \OCP\DB::prepare('DELETE FROM `*PREFIX*contacts_cards` WHERE `id` = ?'); - try { - $stmt->execute(array($id)); - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__. - ', exception: ' . $e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', id: ' - . $id, \OCP\Util::DEBUG); - throw new \Exception( - App::$l10n->t( - 'There was an error deleting this contact.' - ) - ); - } - - App::updateDBProperties($id); - App::getVCategories()->purgeObjects(array($id)); - Addressbook::touch($addressbook['id']); - - // Contact sharing not enabled, so comment out this - //\OCP\Share::unshareAll('contact', $id); - return true; - } - - /** - * @brief deletes a card with the data provided by sabredav - * @param integer $aid Addressbook id - * @param string $uri the uri of the card - * @return boolean - */ - public static function deleteFromDAVData($aid, $uri) { - $contact = self::findWhereDAVDataIs($aid, $uri); - if(!$contact) { - \OCP\Util::writeLog('contacts', __METHOD__.', contact not found: ' - . $uri, \OCP\Util::DEBUG); - throw new \Sabre_DAV_Exception_NotFound( - App::$l10n->t( - 'Contact not found.' - ) - ); - } - $id = $contact['id']; - try { - return self::delete($id); - } catch (\Exception $e) { - switch($e->getCode()) { - case 403: - \OCP\Util::writeLog('contacts', __METHOD__.', forbidden: ' - . $uri, \OCP\Util::DEBUG); - throw new \Sabre_DAV_Exception_Forbidden( - App::$l10n->t( - $e->getMessage() - ) - ); - break; - case 404: - \OCP\Util::writeLog('contacts', __METHOD__.', contact not found: ' - . $uri, \OCP\Util::DEBUG); - throw new \Sabre_DAV_Exception_NotFound( - App::$l10n->t( - $e->getMessage() - ) - ); - break; - default: - throw $e; - break; - } - } - return true; - } - - /** - * @brief Data structure of vCard - * @param Sabre\VObject\Component $property - * @return associative array - * - * look at code ... - */ - public static function structureContact($vcard) { - $details = array(); - - foreach($vcard->children as $property) { - $pname = $property->name; - $temp = self::structureProperty($property); - if(!is_null($temp)) { - // Get Apple X-ABLabels - if(isset($vcard->{$property->group . '.X-ABLABEL'})) { - $temp['label'] = $vcard->{$property->group . '.X-ABLABEL'}->value; - if($temp['label'] == '_$!!$_') { - $temp['label'] = App::$l10n->t('Other'); - } - if($temp['label'] == '_$!!$_') { - $temp['label'] = App::$l10n->t('HomePage'); - } - } - if(array_key_exists($pname, $details)) { - $details[$pname][] = $temp; - } - else{ - $details[$pname] = array($temp); - } - } - } - return $details; - } - - /** - * @brief Data structure of properties - * @param object $property - * @return associative array - * - * returns an associative array with - * ['name'] name of property - * ['value'] htmlspecialchars escaped value of property - * ['parameters'] associative array name=>value - * ['checksum'] checksum of whole property - * NOTE: $value is not escaped anymore. It shouldn't make any difference - * but we should look out for any problems. - */ - public static function structureProperty($property) { - if(!in_array($property->name, App::$index_properties)) { - return; - } - $value = $property->value; - if($property->name == 'ADR' || $property->name == 'N' || $property->name == 'ORG' || $property->name == 'CATEGORIES') { - $value = $property->getParts(); - $value = array_map('trim', $value); - } - elseif($property->name == 'BDAY') { - if(strpos($value, '-') === false) { - if(strlen($value) >= 8) { - $value = substr($value, 0, 4).'-'.substr($value, 4, 2).'-'.substr($value, 6, 2); - } else { - return null; // Badly malformed :-( - } - } - } elseif($property->name == 'PHOTO') { - $value = true; - } - elseif($property->name == 'IMPP') { - if(strpos($value, ':') !== false) { - $value = explode(':', $value); - $protocol = array_shift($value); - if(!isset($property['X-SERVICE-TYPE'])) { - $property['X-SERVICE-TYPE'] = strtoupper(\OCP\Util::sanitizeHTML($protocol)); - } - $value = implode('', $value); - } - } - if(is_string($value)) { - $value = strtr($value, array('\,' => ',', '\;' => ';')); - } - $temp = array( - //'name' => $property->name, - 'value' => $value, - 'parameters' => array() - ); - - // This cuts around a 3rd off of the json response size. - if(in_array($property->name, App::$multi_properties)) { - $temp['checksum'] = substr(md5($property->serialize()), 0, 8); - } - foreach($property->parameters as $parameter) { - // Faulty entries by kaddressbook - // Actually TYPE=PREF is correct according to RFC 2426 - // but this way is more handy in the UI. Tanghus. - if($parameter->name == 'TYPE' && strtoupper($parameter->value) == 'PREF') { - $parameter->name = 'PREF'; - $parameter->value = '1'; - } - // NOTE: Apparently Sabre_VObject_Reader can't always deal with value list parameters - // like TYPE=HOME,CELL,VOICE. Tanghus. - // TODO: Check if parameter is has commas and split + merge if so. - if ($parameter->name == 'TYPE') { - $pvalue = $parameter->value; - if(is_string($pvalue) && strpos($pvalue, ',') !== false) { - $pvalue = array_map('trim', explode(',', $pvalue)); - } - $pvalue = is_array($pvalue) ? $pvalue : array($pvalue); - if (isset($temp['parameters'][$parameter->name])) { - $temp['parameters'][$parameter->name][] = \OCP\Util::sanitizeHTML($pvalue); - } - else { - $temp['parameters'][$parameter->name] = \OCP\Util::sanitizeHTML($pvalue); - } - } - else{ - $temp['parameters'][$parameter->name] = \OCP\Util::sanitizeHTML($parameter->value); - } - } - return $temp; - } - - /** - * @brief Move card(s) to an address book - * @param integer $aid Address book id - * @param $id Array or integer of cards to be moved. - * @return boolean - * - */ - public static function moveToAddressBook($aid, $id, $isAddressbook = false) { - Addressbook::find($aid); - $addressbook = Addressbook::find($aid); - if ($addressbook['userid'] != \OCP\User::getUser()) { - $sharedAddressbook = \OCP\Share::getItemSharedWithBySource('addressbook', $aid); - if (!$sharedAddressbook || !($sharedAddressbook['permissions'] & \OCP\PERMISSION_CREATE)) { - return false; - } - } - if(is_array($id)) { - foreach ($id as $index => $cardId) { - $card = self::find($cardId); - if (!$card) { - unset($id[$index]); - } - $oldAddressbook = Addressbook::find($card['addressbookid']); - if ($oldAddressbook['userid'] != \OCP\User::getUser()) { - $sharedContact = \OCP\Share::getItemSharedWithBySource('contact', $cardId, \OCP\Share::FORMAT_NONE, null, true); - if (!$sharedContact || !($sharedContact['permissions'] & \OCP\PERMISSION_DELETE)) { - unset($id[$index]); - } - } - } - $id_sql = join(',', array_fill(0, count($id), '?')); - $prep = 'UPDATE `*PREFIX*contacts_cards` SET `addressbookid` = ? WHERE `id` IN ('.$id_sql.')'; - try { - $stmt = \OCP\DB::prepare( $prep ); - //$aid = array($aid); - $vals = array_merge((array)$aid, $id); - $result = $stmt->execute($vals); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::ERROR); - \OCP\Util::writeLog('contacts', __METHOD__.', ids: '.join(',', $vals), \OCP\Util::DEBUG); - \OCP\Util::writeLog('contacts', __METHOD__.', SQL:'.$prep, \OCP\Util::DEBUG); - return false; - } - } else { - $stmt = null; - if($isAddressbook) { - $stmt = \OCP\DB::prepare( 'UPDATE `*PREFIX*contacts_cards` SET `addressbookid` = ? WHERE `addressbookid` = ?' ); - } else { - $card = self::find($id); - if (!$card) { - return false; - } - $oldAddressbook = Addressbook::find($card['addressbookid']); - if ($oldAddressbook['userid'] != \OCP\User::getUser()) { - $sharedContact = \OCP\Share::getItemSharedWithBySource('contact', $id, \OCP\Share::FORMAT_NONE, null, true); - if (!$sharedContact || !($sharedContact['permissions'] & \OCP\PERMISSION_DELETE)) { - return false; - } - } - $stmt = \OCP\DB::prepare( 'UPDATE `*PREFIX*contacts_cards` SET `addressbookid` = ? WHERE `id` = ?' ); - } - try { - $result = $stmt->execute(array($aid, $id)); - if (\OC_DB::isError($result)) { - \OC_Log::write('contacts', __METHOD__. 'DB error: ' . \OC_DB::getErrorMessage($result), \OC_Log::ERROR); - return false; - } - } catch(\Exception $e) { - \OCP\Util::writeLog('contacts', __METHOD__.', exception: '.$e->getMessage(), \OCP\Util::DEBUG); - \OCP\Util::writeLog('contacts', __METHOD__.' id: '.$id, \OCP\Util::DEBUG); - return false; - } - } - \OC_Hook::emit('\OCA\Contacts\VCard', 'post_moveToAddressbook', array('aid' => $aid, 'id' => $id)); - Addressbook::touch($aid); - return true; - } } diff --git a/lib/vobject/stringproperty.php b/lib/vobject/stringproperty.php new file mode 100644 index 00000000..5b686470 --- /dev/null +++ b/lib/vobject/stringproperty.php @@ -0,0 +1,82 @@ +. + * + */ + +namespace OCA\Contacts\VObject; + +use Sabre\VObject; + +/** + * This class overrides \Sabre\VObject\Property::serialize() properly + * escape commas and semi-colons in string properties. +*/ +class StringProperty extends VObject\Property { + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() { + + $str = $this->name; + if ($this->group) { + $str = $this->group . '.' . $this->name; + } + + foreach($this->parameters as $param) { + $str.=';' . $param->serialize(); + } + + $src = array( + '\\', + "\n", + ';', + ',', + ); + $out = array( + '\\\\', + '\n', + '\;', + '\,', + ); + $value = strtr($this->value, array('\,' => ',', '\;' => ';', '\\\\' => '\\')); + $str.=':' . str_replace($src, $out, $value); + + $out = ''; + while(strlen($str) > 0) { + if (strlen($str) > 75) { + $out .= mb_strcut($str, 0, 75, 'utf-8') . "\r\n"; + $str = ' ' . mb_strcut($str, 75, strlen($str), 'utf-8'); + } else { + $out .= $str . "\r\n"; + $str = ''; + break; + } + } + + return $out; + + } + +} \ No newline at end of file diff --git a/lib/vobject/vcard.php b/lib/vobject/vcard.php new file mode 100644 index 00000000..36085ec1 --- /dev/null +++ b/lib/vobject/vcard.php @@ -0,0 +1,229 @@ +. + * + */ + +namespace OCA\Contacts\VObject; + +use OCA\Contacts\Utils; +use Sabre\VObject; + +/** + * This class overrides \Sabre\VObject\Component\VCard::validate() to be add + * to import partially invalid vCards by ignoring invalid lines and to + * validate and upgrade using .... +*/ +class VCard extends VObject\Component\VCard { + + /** + * The following constants are used by the validate() method. + */ + const REPAIR = 1; + const UPGRADE = 2; + + /** + * VCards with version 2.1, 3.0 and 4.0 are found. + * + * If the VCARD doesn't know its version, 3.0 is assumed and if + * option UPGRADE is given it will be upgraded to version 3.0. + */ + const DEFAULT_VERSION = '3.0'; + + /** + * @brief Format property TYPE parameters for upgrading from v. 2.1 + * @param $property Reference to a \Sabre\VObject\Property. + * In version 2.1 e.g. a phone can be formatted like: TEL;HOME;CELL:123456789 + * This has to be changed to either TEL;TYPE=HOME,CELL:123456789 or TEL;TYPE=HOME;TYPE=CELL:123456789 - both are valid. + */ + protected function formatPropertyTypes(&$property) { + foreach($property->parameters as $key=>&$parameter) { + $types = Utils\Properties::getTypesForProperty($property->name); + if(is_array($types) && in_array(strtoupper($parameter->name), array_keys($types)) + || strtoupper($parameter->name) == 'PREF') { + unset($property->parameters[$key]); + $property->add('TYPE', $parameter->name); + } + } + } + + /** + * @brief Decode properties for upgrading from v. 2.1 + * @param $property Reference to a \Sabre\VObject\Property. + * The only encoding allowed in version 3.0 is 'b' for binary. All encoded strings + * must therefore be decoded and the parameters removed. + */ + protected function decodeProperty(&$property) { + // Check out for encoded string and decode them :-[ + foreach($property->parameters as $key=>&$parameter) { + if(strtoupper($parameter->name) == 'ENCODING') { + if(strtoupper($parameter->value) == 'QUOTED-PRINTABLE') { + // Decode quoted-printable and strip any control chars + // except \n and \r + $property->value = str_replace( + "\r\n", "\n", + VObject\StringUtil::convertToUTF8( + quoted_printable_decode($property->value) + ) + ); + unset($property->parameters[$key]); + } else if(strtoupper($parameter->value) == 'BASE64') { + $parameter->value = 'b'; + } + } elseif(strtoupper($parameter->name) == 'CHARSET') { + unset($property->parameters[$key]); + } + } + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - VCard::REPAIR - If something is broken, and automatic repair may + * be attempted. + * - VCard::UPGRADE - If needed the vCard will be upgraded to version 3.0. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * @return array + */ + public function validate($options = 0) { + + $warnings = array(); + + $version = $this->select('VERSION'); + if (count($version) !== 1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The VERSION property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ); + if ($options & self::REPAIR) { + $this->VERSION = self::DEFAULT_VERSION; + if (!$options & self::UPGRADE) { + $options |= self::UPGRADE; + } + } + } else { + $version = (string)$this->VERSION; + if ($version!=='2.1' && $version!=='3.0' && $version!=='4.0') { + $warnings[] = array( + 'level' => 1, + 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.', + 'node' => $this, + ); + if ($options & self::REPAIR) { + $this->VERSION = self::DEFAULT_VERSION; + if (!$options & self::UPGRADE) { + $options |= self::UPGRADE; + } + } + } + + } + $fn = $this->select('FN'); + if (count($fn) !== 1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The FN property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ); + if (($options & self::REPAIR) && count($fn) === 0) { + // We're going to try to see if we can use the contents of the + // N property. + if (isset($this->N)) { + $value = explode(';', (string)$this->N); + if (isset($value[1]) && $value[1]) { + $this->FN = $value[1] . ' ' . $value[0]; + } else { + $this->FN = $value[0]; + } + // Otherwise, the ORG property may work + } elseif (isset($this->ORG)) { + $this->FN = (string)$this->ORG; + } elseif (isset($this->EMAIL)) { + $this->FN = (string)$this->EMAIL; + } + + } + } + + $n = $this->select('N'); + if (count($n) !== 1) { + $warnings[] = array( + 'level' => 1, + 'message' => 'The N property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ); + // TODO: Make a better effort parsing FN. + if (($options & self::REPAIR) && count($n) === 0) { + // Take 2 first name parts of 'FN' and reverse. + $slice = array_reverse(array_slice(explode(' ', (string)$this->FN), 0, 2)); + if(count($slice) < 2) { // If not enought, add one more... + $slice[] = ""; + } + $this->N = implode(';', $slice).';;;'; + } + } + + if (!isset($this->UID)) { + $warnings[] = array( + 'level' => 1, + 'message' => 'Every vCard must have a UID', + 'node' => $this, + ); + if ($options & self::REPAIR) { + $this->UID = Utils\Properties::generateUID(); + } + } + + if ($options & self::UPGRADE) { + $this->VERSION = self::DEFAULT_VERSION; + foreach($this->children as &$property) { + $this->decodeProperty($property); + $this->formatPropertyTypes($property); + //\OCP\Util::writeLog('contacts', __METHOD__.' upgrade: '.$property->name, \OCP\Util::DEBUG); + switch((string)$property->name) { + case 'LOGO': + case 'SOUND': + case 'PHOTO': + if(isset($property['TYPE']) && strpos((string)$property['TYPE'], '/') === false) { + $property['TYPE'] = 'image/' . strtolower($property['TYPE']); + } + } + } + } + + return array_merge( + parent::validate($options), + $warnings + ); + + } +} \ No newline at end of file diff --git a/photo.php b/photo.php index 9044af14..b7347c78 100644 --- a/photo.php +++ b/photo.php @@ -21,6 +21,8 @@ function getStandardImage() { } $id = isset($_GET['id']) ? $_GET['id'] : null; +$parent = isset($_GET['parent']) ? $_GET['parent'] : null; +$backend = isset($_GET['backend']) ? $_GET['backend'] : null; $etag = null; $caching = null; $max_size = 170; @@ -35,7 +37,8 @@ if(!extension_loaded('gd') || !function_exists('gd_info')) { getStandardImage(); } -$contact = OCA\Contacts\App::getContactVCard($id); +$app = new OCA\Contacts\App(); +$contact = $app->getContact($backend, $parent, $id); $image = new OC_Image(); if (!$image || !$contact) { getStandardImage(); @@ -58,10 +61,10 @@ if (is_null($contact)) { $etag = md5($contact->LOGO); } if ($image->valid()) { - $modified = OCA\Contacts\App::lastModified($contact); + $modified = $contact->lastModified(); // Force refresh if modified within the last minute. if(!is_null($modified)) { - $caching = (time() - $modified->format('U') > 60) ? null : 0; + $caching = (time() - $modified > 60) ? null : 0; } OCP\Response::enableCaching($caching); if(!is_null($modified)) { diff --git a/templates/contacts.php b/templates/contacts.php index 9ee6c38a..35df3e64 100644 --- a/templates/contacts.php +++ b/templates/contacts.php @@ -1,5 +1,7 @@
- + + + @@ -25,17 +27,30 @@ + +

t('Import')); ?>

@@ -113,7 +129,9 @@ enctype="multipart/form-data" target="crop_target" action=""> - + + +
@@ -134,10 +152,24 @@ + + @@ -173,10 +204,10 @@
  • @@ -207,6 +238,9 @@
    +
    + +
    t('Nickname')); ?> @@ -289,7 +323,7 @@
  • ' - + '{description}