From 48554590f40d6bbc35bae5547f4458e7f5f3f761 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 19 Sep 2017 11:38:02 +0200 Subject: [PATCH] * Addressbook: store S/Mime & PGP pubkey and photo (SQL backend only) in filesystem --- addressbook/inc/class.addressbook_bo.inc.php | 88 ++++++++-- addressbook/inc/class.addressbook_ui.inc.php | 42 +++-- .../inc/class.addressbook_vcal.inc.php | 1 + addressbook/templates/default/edit.xet | 8 + api/setup/setup.inc.php | 4 +- api/setup/tables_current.inc.php | 4 +- api/setup/tables_update.inc.php | 161 ++++++++++++++++++ api/src/Contacts.php | 8 +- api/src/Contacts/Sql.php | 32 +++- api/src/Contacts/Storage.php | 21 ++- api/src/Etemplate/Widget/Vfs.php | 11 +- api/src/Vfs.php | 28 +++ 12 files changed, 360 insertions(+), 48 deletions(-) diff --git a/addressbook/inc/class.addressbook_bo.inc.php b/addressbook/inc/class.addressbook_bo.inc.php index f4cfb94bb7..277c995b52 100755 --- a/addressbook/inc/class.addressbook_bo.inc.php +++ b/addressbook/inc/class.addressbook_bo.inc.php @@ -35,7 +35,7 @@ class addressbook_bo extends Api\Contacts */ public function get_pgp_keys($recipients) { - return $this->get_keys($recipients, self::$pgp_key_regexp, '%-----BEGIN PGP PUBLIC KEY BLOCK-----%'); + return $this->get_keys($recipients, true); } /** @@ -106,7 +106,7 @@ class addressbook_bo extends Api\Contacts */ public function ajax_set_pgp_keys($keys, $allow_user_updates=null) { - $message = $this->set_keys($keys, self::$pgp_key_regexp, $allow_user_updates); + $message = $this->set_keys($keys, true, $allow_user_updates); // add all keys to public keyserver too $message .= "\n".lang('%1 key(s) added to public keyserver "%2".', self::set_pgp_keyserver($keys), PARSE_URL(self::KEYSERVER_ADD, PHP_URL_HOST)); @@ -154,16 +154,28 @@ class addressbook_bo extends Api\Contacts return $added; } + /** + * Where to store public key delpending on type and storage backend + * + * @param boolean $pgp true: PGP, false: S/Mime + * @param array $contact =null contact array to pass to get_backend() + * @return boolean true: store as file, false: store with contact + */ + public function pubkey_use_file($pgp, array $contact=null) + { + return $pgp || empty($contact) || get_class($this->get_backend($contact)) == 'EGroupware\\Api\\Contacts\\Sql'; + } + /** * Set keys for given email or account_id and key type based on regexp (SMIME or PGP), if user has necessary rights * * @param array $keys email|account_id => public key pairs to store - * @param string $key_regexp regular expresion for key type indication (SMIME|PGP) + * @param boolean $pgp true: PGP, false: S/Mime * @param boolean $allow_user_updates = null for admins, set config to allow regular users to store their key * * @return string message of the update operation result */ - public function set_keys ($keys, $key_regexp, $allow_user_updates = null) + public function set_keys ($keys, $pgp, $allow_user_updates = null) { if (isset($allow_user_updates) && isset($GLOBALS['egw_info']['user']['apps']['admin'])) { @@ -183,6 +195,15 @@ class addressbook_bo extends Api\Contacts Config::save_value('own_account_acl', $this->own_account_acl, 'phpgwapi'); } } + + $key_regexp = $pgp ? self::$pgp_key_regexp : Api\Mail\Smime::$certificate_regexp; + $file = $pgp ? Api\Contacts::FILES_PGP_PUBKEY : Api\Contacts::FILES_SMIME_PUBKEY; + + if (!preg_match($key_regexp, $key)) + { + return lang('File is not a %1 public key!', $pgp ? lang('PGP') : lang('S/MIME')); + } + $criteria = array(); foreach($keys as $recipient => $key) { @@ -210,7 +231,21 @@ class addressbook_bo extends Api\Contacts { $key = $keys[$contact['email']]; } - if (empty($contact['pubkey']) || !preg_match($key_regexp, $contact['pubkey'])) + + // key is stored in file for sql backend or allways for pgp key + $path = null; + if ($contact['id'] && $this->pubkey_use_file($pgp, $contact)) + { + $path = Api\Link::vfs_path('addressbook', $contact['id'], $file); + $contact['contact_files'] |= $pgp ? self::FILES_BIT_PGP_PUBKEY : self::FILES_BIT_SMIME_PUBKEY; + // remove evtl. existing old pubkey + if (preg_match($key_regexp, $contact['pubkey'])) + { + $contact['pubkey'] = preg_replace($key_regexp, '', $contact['pubkey']); + } + $updated++; + } + elseif (empty($contact['pubkey']) || !preg_match($key_regexp, $contact['pubkey'])) { $contact['pubkey'] .= $key; } @@ -220,7 +255,17 @@ class addressbook_bo extends Api\Contacts } if ($this->check_perms(Acl::EDIT, $contact) && $this->save($contact)) { - ++$updated; + if ($path) + { + // check_perms && save check ACL, in case of access only via own-account we have to use root to allow the update + $backup = Api\Vfs::$is_root; Api\Vfs::$is_root = true; + if (file_put_contents($path, $key)) ++$updated; + Api\Vfs::$is_root = $backup; + } + else + { + ++$updated; + } } } if ($criteria == array('egw.addressbook.account_id' => array((int)$GLOBALS['egw_info']['user']['account_id']))) @@ -241,17 +286,27 @@ class addressbook_bo extends Api\Contacts * EMail addresses are lowercased to make search case-insensitive * * @param string|int|array $recipients (array of) email addresses or numeric account-ids - * @param string $key_regexp - * @param string $criteria_filter - * + * @param boolean $pgp true: PGP, false: S/Mime public keys * @return array email|account_id => key pairs */ - public function get_keys ($recipients, $key_regexp, $criteria_filter) + protected function get_keys ($recipients, $pgp) { if (!$recipients) return array(); if (!is_array($recipients)) $recipients = array($recipients); + if ($pgp) + { + $key_regexp = self::$pgp_key_regexp; + $criteria_filter = '%-----BEGIN PGP PUBLIC KEY BLOCK-----%'; + $file = Api\Contacts::FILES_PGP_PUBKEY; + } + else + { + $key_regexp = Api\Mail\Smime::$certificate_regexp; + $criteria_filter = '%-----BEGIN CERTIFICATE-----%'; + $file = Api\Contacts::FILES_SMIME_PUBKEY; + } $criteria = $result = array(); foreach($recipients as &$recipient) { @@ -264,11 +319,14 @@ class addressbook_bo extends Api\Contacts $criteria['contact_email'][] = $recipient = strtolower($recipient); } } - foreach($this->search($criteria, array('account_id', 'contact_email', 'contact_pubkey'), '', '', '', false, 'OR', false, - "contact_pubkey LIKE '". $criteria_filter ."'" ) as $contact) + foreach($this->search($criteria, array('account_id', 'contact_email', 'contact_pubkey'), + '', '', '', false, 'OR', false, null) as $contact) { $matches = null; - if (preg_match($key_regexp, $contact['pubkey'], $matches)) + // first check for file and second for pubkey field (LDAP, AD or old SQL) + if (($content = @file_get_contents(vfs_path('addressbook', $contact['id'], $file))) && + preg_match($key_regexp, $content, $matches) || + preg_match($key_regexp, $contact['pubkey'], $matches)) { $contact['email'] = strtolower($contact['email']); if (empty($criteria['account_id']) || in_array($contact['email'], $recipients)) @@ -294,7 +352,7 @@ class addressbook_bo extends Api\Contacts */ public function get_smime_keys($recipients) { - return $this->get_keys($recipients, Api\Mail\Smime::$certificate_regexp, '%-----BEGIN CERTIFICATE-----%'); + return $this->get_keys($recipients, false); } /** @@ -307,6 +365,6 @@ class addressbook_bo extends Api\Contacts */ public function set_smime_keys($keys, $allow_user_updates=null) { - return $this->set_keys($keys, Api\Mail\Smime::$certificate_regexp, $allow_user_updates); + return $this->set_keys($keys, false, $allow_user_updates); } } diff --git a/addressbook/inc/class.addressbook_ui.inc.php b/addressbook/inc/class.addressbook_ui.inc.php index b9de816aba..d4a4c65a10 100644 --- a/addressbook/inc/class.addressbook_ui.inc.php +++ b/addressbook/inc/class.addressbook_ui.inc.php @@ -861,7 +861,7 @@ class addressbook_ui extends addressbook_bo unset($query['col_filter']['org_name']); unset($query['col_filter']['org_unit']); unset($query['col_filter']['adr_one_locality']); - foreach(static::$duplicate_fields as $field => $label) + foreach(array_keys(static::$duplicate_fields) as $field) { unset($query['col_filter'][$field]); } @@ -1429,7 +1429,7 @@ window.egw_LAB.wait(function() { * Used for action on organisation and duplicate views * @param string/int $action 'delete', 'vcard', 'csv' or nummerical account_id to move contacts to that addessbook * @param array $checked contact id's to use if !$use_all - * @param boolean $use_all if true use all contacts of the current selection (in the session) + * @param boolean $use_all if true use all contacts of the current selection in the session (NOT used!) * @param int &$success number of succeded actions * @param int &$failed number of failed actions (not enought permissions) * @param string &$action_msg translated verb for the actions, to be used in a message like %1 contacts 'deleted' @@ -1439,6 +1439,7 @@ window.egw_LAB.wait(function() { */ protected function find_grouped_ids($action,&$checked,$use_all,&$success,&$failed,&$action_msg,$session_name,&$msg) { + unset($use_all); $grouped_contacts = array(); foreach((array)$checked as $n => $id) { @@ -2019,10 +2020,13 @@ window.egw_LAB.wait(function() { // unset the duplicate_filed after submit because we don't need to warn user for second time about contact duplication unset($content['presets_fields']); } + $content['photo_unchanged'] = true; // hint no need to store photo + /* seems not to be used any more in favor or ajax_update_photo if ($content['delete_photo']) { $content['jpegphoto'] = null; unset($content['delete_photo']); + $content['photo_unchanged'] = false; } if (is_array($content['upload_photo']) && !empty($content['upload_photo']['tmp_name']) && $content['upload_photo']['tmp_name'] != 'none' && @@ -2031,7 +2035,8 @@ window.egw_LAB.wait(function() { $content['jpegphoto'] = $this->resize_photo($f); fclose($f); unset($content['upload_photo']); - } + $content['photo_unchanged'] = false; + }*/ $links = false; if (!$content['id'] && is_array($content['link_to']['to_id'])) { @@ -2350,7 +2355,7 @@ window.egw_LAB.wait(function() { // Registry has view_id as contact_id, so set it (custom fields uses it) $content['contact_id'] = $content['id']; - + // Avoid ID conflict with tree & selectboxes $content['cat_id_tree'] = $content['cat_id']; @@ -2363,7 +2368,7 @@ window.egw_LAB.wait(function() { $content['private_cfs']['#'.$name] = $content['#'.$name]; } } - + // how to display addresses $content['addr_format'] = $this->addr_format_by_country($content['adr_one_countryname']); $content['addr_format2'] = $this->addr_format_by_country($content['adr_two_countryname']); @@ -3081,8 +3086,7 @@ window.egw_LAB.wait(function() { } /** - * Ajax method to update edited avatar photo via - * avatar widget. + * Ajax method to update edited avatar photo via avatar widget * * @param int $contact_id * @param file string $file = null null means to delete @@ -3093,15 +3097,17 @@ window.egw_LAB.wait(function() { $contact = $this->read($contact_id); if ($file) { - $filteredFile=substr($file, strpos($file, ",")+1); - $decoded = base64_decode($filteredFile); + $filteredFile = substr($file, strpos($file, ",")+1); + // resize photo if wider then default width of 240pixel (keeping aspect ratio) + $decoded = $this->resize_photo(base64_decode($filteredFile)); } - $contact ['jpegphoto'] = is_null($file)? $file: $decoded; + $contact['jpegphoto'] = is_null($file) ? $file : $decoded; + $contact['photo_unchanged'] = false; // hint photo is changed $success = $this->save($contact); if (!$success) { - $response->alert($message); + $response->alert($this->error); } else { @@ -3122,12 +3128,15 @@ window.egw_LAB.wait(function() { { $contact_id = $GLOBALS['egw']->accounts->id2name(substr($contact_id,8),'person_id'); } - if (!($contact = $this->read($contact_id)) || !$contact['jpegphoto']) + if (!($contact = $this->read($contact_id)) || + empty($contact['jpegphoto']) && // LDAP/AD (not updated SQL) + !(($contact['files'] & Api\Contacts::FILES_BIT_PHOTO) && // new SQL in VFS + ($size = filesize($url=Api\Link::vfs_path('addressbook', $contact_id, Api\Contacts::FILES_PHOTO))))) { Egw::redirect(Api\Image::find('addressbook','photo')); } // use an etag over the image mapp - $etag = '"'.$contact['id'].':'.$contact['etag'].'"'; + $etag = '"'.$contact_id.':'.$contact['etag'].'"'; if (!ob_get_contents()) { header('Content-type: image/jpeg'); @@ -3143,11 +3152,16 @@ window.egw_LAB.wait(function() { { header("HTTP/1.1 304 Not Modified"); } - else + elseif(!empty($contact['jpegphoto'])) { header('Content-length: '.bytes($contact['jpegphoto'])); echo $contact['jpegphoto']; } + else + { + header('Content-length: '.$size); + readfile($url); + } exit(); } } diff --git a/addressbook/inc/class.addressbook_vcal.inc.php b/addressbook/inc/class.addressbook_vcal.inc.php index 3cca3a7ec1..f35928bfd1 100644 --- a/addressbook/inc/class.addressbook_vcal.inc.php +++ b/addressbook/inc/class.addressbook_vcal.inc.php @@ -72,6 +72,7 @@ class addressbook_vcal extends Api\Contacts 'X-ASSISTANT-TEL' => array('tel_assistent'), 'UID' => array('uid'), 'REV' => array('modified'), + //'KEY' multivalued with mime-type to export PGP and S/Mime public keys //set for Apple: 'X-ABSHOWAS' => array('fileas_type'), // Horde vCard class uses uppercase prop-names! ); diff --git a/addressbook/templates/default/edit.xet b/addressbook/templates/default/edit.xet index 2d65ca6d50..e52ed4dc0e 100644 --- a/addressbook/templates/default/edit.xet +++ b/addressbook/templates/default/edit.xet @@ -176,6 +176,14 @@ + + + + + + + + diff --git a/api/setup/setup.inc.php b/api/setup/setup.inc.php index 7253acad68..f55a103a15 100644 --- a/api/setup/setup.inc.php +++ b/api/setup/setup.inc.php @@ -12,7 +12,7 @@ /* Basic information about this app */ $setup_info['api']['name'] = 'api'; $setup_info['api']['title'] = 'EGroupware API'; -$setup_info['api']['version'] = '16.9.002'; +$setup_info['api']['version'] = '16.9.004'; $setup_info['api']['versions']['current_header'] = '1.29'; // maintenance release in sync with changelog in doc/rpm-build/debian.changes $setup_info['api']['versions']['maintenance_release'] = $setup_info['api']['version']; @@ -130,5 +130,3 @@ $setup_info['groupdav']['author'] = $setup_info['groupdav']['maintainer'] = arra $setup_info['groupdav']['license'] = 'GPL'; $setup_info['groupdav']['hooks']['preferences'] = 'EGroupware\\Api\\CalDAV\\Hooks::menus'; $setup_info['groupdav']['hooks']['settings'] = 'EGroupware\\Api\\CalDAV\\Hooks::settings'; - - diff --git a/api/setup/tables_current.inc.php b/api/setup/tables_current.inc.php index fd8676afff..4353f837c8 100644 --- a/api/setup/tables_current.inc.php +++ b/api/setup/tables_current.inc.php @@ -253,13 +253,13 @@ $phpgw_baseline = array( 'contact_creator' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False,'comment' => 'account id of the creator'), 'contact_modified' => array('type' => 'int','meta' => 'timestamp','precision' => '8','nullable' => False,'comment' => 'timestamp of the last modified'), 'contact_modifier' => array('type' => 'int','meta' => 'user','precision' => '4','comment' => 'account id of the last modified'), - 'contact_jpegphoto' => array('type' => 'blob','comment' => 'photo of the contact (attachment)'), 'account_id' => array('type' => 'int','meta' => 'user','precision' => '4','comment' => 'account id'), 'contact_etag' => array('type' => 'int','precision' => '4','default' => '0','comment' => 'etag of the changes'), 'contact_uid' => array('type' => 'ascii','precision' => '128','comment' => 'unique id of the contact'), 'adr_one_countrycode' => array('type' => 'ascii','precision' => '2','comment' => 'countrycode (business)'), 'adr_two_countrycode' => array('type' => 'ascii','precision' => '2','comment' => 'countrycode (private)'), - 'carddav_name' => array('type' => 'ascii','precision' => '128','comment' => 'name part of CardDAV URL, if specified by client') + 'carddav_name' => array('type' => 'ascii','precision' => '128','comment' => 'name part of CardDAV URL, if specified by client'), + 'contact_files' => array('type' => 'int','precision' => '1','default' => '0','comment' => '&1: photo, &2: pgp, &4: smime') ), 'pk' => array('contact_id'), 'fk' => array(), diff --git a/api/setup/tables_update.inc.php b/api/setup/tables_update.inc.php index 7525d85cea..8501d6bd97 100644 --- a/api/setup/tables_update.inc.php +++ b/api/setup/tables_update.inc.php @@ -235,3 +235,164 @@ function api_upgrade16_9_001() return $GLOBALS['setup_info']['api']['currentver'] = '16.9.002'; } + +/** + * Add contact_files bit-field and strip jpeg photo, PGP & S/Mime pubkeys from table + * + * @return string + */ +function api_upgrade16_9_002() +{ + $GLOBALS['egw_setup']->oProc->AddColumn('egw_addressbook','contact_files',array( + 'type' => 'int', + 'precision' => '1', + 'default' => '0', + 'comment' => '&1: photo, &2: pgp, &4: smime' + )); + + $junk_size = 100; + $total = 0; + Api\Vfs::$is_root = true; + do { + $n = 0; + foreach($GLOBALS['egw_setup']->db->query("SELECT contact_id,contact_jpegphoto,contact_pubkey +FROM egw_addressbook +WHERE contact_jpegphoto IS NOT NULL OR contact_pubkey IS NOT NULL AND contact_pubkey LIKE '%-----%'", + __LINE__, __FILE__, 0, $junk_size, false, Api\Db::FETCH_ASSOC) as $row) + { + $row['contact_files'] = 0; + $contact_id = $row['contact_id']; + unset($row['contact_id']); + if ($row['contact_jpegphoto'] && ($fp = Api\Vfs::string_stream($row['contact_jpegphoto']))) + { + if (Api\Link::attach_file('addressbook', $contact_id, array( + 'name' => Api\Contacts::FILES_PHOTO, + 'type' => 'image/jpeg', + 'tmp_name' => $fp, + ))) + { + $row['contact_files'] |= Api\Contacts::FILES_BIT_PHOTO; + $row['contact_jpegphoto'] = null; + } + fclose($fp); + } + foreach(array( + array(addressbook_bo::$pgp_key_regexp, Api\Contacts::FILES_PGP_PUBKEY, Api\Contacts::FILES_BIT_PGP_PUBKEY, 'application/pgp-keys'), + array(Api\Mail\Smime::$pubkey_regexp, Api\Contacts::FILES_SMIME_PUBKEY, Api\Contacts::FILES_BIT_SMIME_PUBKEY, 'application/x-pem-file'), + ) as $data) + { + list($regexp, $file, $bit, $mime) = $data; + $matches = null; + if ($row['contact_pubkey'] && preg_match($regexp, $row['contact_pubkey'], $matches) && + ($fp = Api\Vfs::string_stream($matches[0]))) + { + if (Api\Link::attach_file('addressbook', $contact_id, array( + 'name' => $file, + 'type' => $mime, + 'tmp_name' => $fp, + ))) + { + $row['contact_files'] |= $bit; + $row['contact_pubkey'] = str_replace($matches[0], '', $row['contact_pubkey']); + } + fclose($fp); + } + } + if (!trim($row['contact_pubkey'])) $row['contact_pubkey'] = null; + + if ($row['contact_files']) + { + $GLOBALS['egw_setup']->db->update('egw_addressbook', $row, array('contact_id' => $contact_id), __LINE__, __FILE__); + $total++; + } + $n++; + } + } + while($n == $junk_size); + Api\Vfs::$is_root = false; + + return $GLOBALS['setup_info']['api']['currentver'] = '16.9.003'; +} + +/** + * Drop contact_jpegphoto column + * + * @return string + */ +function api_upgrade16_9_003() +{ + $GLOBALS['egw_setup']->oProc->DropColumn('egw_addressbook',array( + 'fd' => array( + 'contact_id' => array('type' => 'auto','nullable' => False), + 'contact_tid' => array('type' => 'char','precision' => '1','default' => 'n'), + 'contact_owner' => array('type' => 'int','meta' => 'account','precision' => '8','nullable' => False,'comment' => 'account or group id of the adressbook'), + 'contact_private' => array('type' => 'int','precision' => '1','default' => '0','comment' => 'privat or personal'), + 'cat_id' => array('type' => 'ascii','meta' => 'category','precision' => '255','comment' => 'Category(s)'), + 'n_family' => array('type' => 'varchar','precision' => '64','comment' => 'Family name'), + 'n_given' => array('type' => 'varchar','precision' => '64','comment' => 'Given Name'), + 'n_middle' => array('type' => 'varchar','precision' => '64'), + 'n_prefix' => array('type' => 'varchar','precision' => '64','comment' => 'Prefix'), + 'n_suffix' => array('type' => 'varchar','precision' => '64','comment' => 'Suffix'), + 'n_fn' => array('type' => 'varchar','precision' => '128','comment' => 'Full name'), + 'n_fileas' => array('type' => 'varchar','precision' => '255','comment' => 'sort as'), + 'contact_bday' => array('type' => 'varchar','precision' => '12','comment' => 'Birtday'), + 'org_name' => array('type' => 'varchar','precision' => '128','comment' => 'Organisation'), + 'org_unit' => array('type' => 'varchar','precision' => '64','comment' => 'Department'), + 'contact_title' => array('type' => 'varchar','precision' => '64','comment' => 'jobtittle'), + 'contact_role' => array('type' => 'varchar','precision' => '64','comment' => 'role'), + 'contact_assistent' => array('type' => 'varchar','precision' => '64','comment' => 'Name of the Assistent (for phone number)'), + 'contact_room' => array('type' => 'varchar','precision' => '64','comment' => 'room'), + 'adr_one_street' => array('type' => 'varchar','precision' => '64','comment' => 'street (business)'), + 'adr_one_street2' => array('type' => 'varchar','precision' => '64','comment' => 'street (business) - 2. line'), + 'adr_one_locality' => array('type' => 'varchar','precision' => '64','comment' => 'city (business)'), + 'adr_one_region' => array('type' => 'varchar','precision' => '64','comment' => 'region (business)'), + 'adr_one_postalcode' => array('type' => 'varchar','precision' => '64','comment' => 'postalcode (business)'), + 'adr_one_countryname' => array('type' => 'varchar','precision' => '64','comment' => 'countryname (business)'), + 'contact_label' => array('type' => 'text','comment' => 'currently not used'), + 'adr_two_street' => array('type' => 'varchar','precision' => '64','comment' => 'street (private)'), + 'adr_two_street2' => array('type' => 'varchar','precision' => '64','comment' => 'street (private) - 2. line'), + 'adr_two_locality' => array('type' => 'varchar','precision' => '64','comment' => 'city (private)'), + 'adr_two_region' => array('type' => 'varchar','precision' => '64','comment' => 'region (private)'), + 'adr_two_postalcode' => array('type' => 'varchar','precision' => '64','comment' => 'postalcode (private)'), + 'adr_two_countryname' => array('type' => 'varchar','precision' => '64','comment' => 'countryname (private)'), + 'tel_work' => array('type' => 'varchar','precision' => '40','comment' => 'phone-number (business)'), + 'tel_cell' => array('type' => 'varchar','precision' => '40','comment' => 'mobil phone (business)'), + 'tel_fax' => array('type' => 'varchar','precision' => '40','comment' => 'fax-number (business)'), + 'tel_assistent' => array('type' => 'varchar','precision' => '40','comment' => 'phone-number assistent'), + 'tel_car' => array('type' => 'varchar','precision' => '40'), + 'tel_pager' => array('type' => 'varchar','precision' => '40','comment' => 'pager'), + 'tel_home' => array('type' => 'varchar','precision' => '40','comment' => 'phone-number (private)'), + 'tel_fax_home' => array('type' => 'varchar','precision' => '40','comment' => 'fax-number (private)'), + 'tel_cell_private' => array('type' => 'varchar','precision' => '40','comment' => 'mobil phone (private)'), + 'tel_other' => array('type' => 'varchar','precision' => '40','comment' => 'other phone'), + 'tel_prefer' => array('type' => 'varchar','precision' => '32','comment' => 'prefered phone-number'), + 'contact_email' => array('type' => 'varchar','precision' => '128','comment' => 'email address (business)'), + 'contact_email_home' => array('type' => 'varchar','precision' => '128','comment' => 'email address (private)'), + 'contact_url' => array('type' => 'varchar','precision' => '128','comment' => 'website (business)'), + 'contact_url_home' => array('type' => 'varchar','precision' => '128','comment' => 'website (private)'), + 'contact_freebusy_uri' => array('type' => 'ascii','precision' => '128','comment' => 'freebusy-url for calendar of the contact'), + 'contact_calendar_uri' => array('type' => 'ascii','precision' => '128','comment' => 'url for users calendar - currently not used'), + 'contact_note' => array('type' => 'varchar','precision' => '8192','comment' => 'notes field'), + 'contact_tz' => array('type' => 'varchar','precision' => '8','comment' => 'timezone difference'), + 'contact_geo' => array('type' => 'ascii','precision' => '32','comment' => 'currently not used'), + 'contact_pubkey' => array('type' => 'ascii','precision' => '16384','comment' => 'public key'), + 'contact_created' => array('type' => 'int','meta' => 'timestamp','precision' => '8','comment' => 'timestamp of the creation'), + 'contact_creator' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False,'comment' => 'account id of the creator'), + 'contact_modified' => array('type' => 'int','meta' => 'timestamp','precision' => '8','nullable' => False,'comment' => 'timestamp of the last modified'), + 'contact_modifier' => array('type' => 'int','meta' => 'user','precision' => '4','comment' => 'account id of the last modified'), + 'account_id' => array('type' => 'int','meta' => 'user','precision' => '4','comment' => 'account id'), + 'contact_etag' => array('type' => 'int','precision' => '4','default' => '0','comment' => 'etag of the changes'), + 'contact_uid' => array('type' => 'ascii','precision' => '128','comment' => 'unique id of the contact'), + 'adr_one_countrycode' => array('type' => 'ascii','precision' => '2','comment' => 'countrycode (business)'), + 'adr_two_countrycode' => array('type' => 'ascii','precision' => '2','comment' => 'countrycode (private)'), + 'carddav_name' => array('type' => 'ascii','precision' => '128','comment' => 'name part of CardDAV URL, if specified by client'), + 'contact_files' => array('type' => 'int','precision' => '1','default' => '0','comment' => '&1: photo, &2: pgp, &4: smime') + ), + 'pk' => array('contact_id'), + 'fk' => array(), + 'ix' => array('contact_owner','cat_id','n_fileas','contact_modified','contact_uid','carddav_name',array('n_family','n_given'),array('n_given','n_family'),array('org_name','n_family','n_given')), + 'uc' => array('account_id') + ),'contact_jpegphoto'); + + return $GLOBALS['setup_info']['api']['currentver'] = '16.9.004'; +} diff --git a/api/src/Contacts.php b/api/src/Contacts.php index d0d624517e..58587d212e 100755 --- a/api/src/Contacts.php +++ b/api/src/Contacts.php @@ -734,7 +734,7 @@ class Contacts extends Contacts\Storage $data[$name] = DateTime::server2user($data[$name], $date_format); } } - $data['photo'] = $this->photo_src($data['id'],$data['jpegphoto'] || $data['files'] & self::FILES_PHOTO,'',$data['etag']); + $data['photo'] = $this->photo_src($data['id'],$data['jpegphoto'] || ($data['files'] & self::FILES_BIT_PHOTO), '', $data['etag']); // set freebusy_uri for accounts if (!$data['freebusy_uri'] && !$data['owner'] && $data['account_id'] && !is_object($GLOBALS['egw_setup'])) @@ -1054,13 +1054,15 @@ class Contacts extends Contacts\Storage } /** - * Resizes photo to 60*80 pixel and returns it + * Resize photo down to 240pixel width and returns it + * + * Also makes sures photo is a JPEG. * * @param string|FILE $photo string with image or open filedescribtor * @param int $dst_w =240 max width to resize to * @return string with resized jpeg photo, null on error */ - public static function resize_photo($photo,$dst_w=240) + public static function resize_photo($photo, $dst_w=240) { if (is_resource($photo)) { diff --git a/api/src/Contacts/Sql.php b/api/src/Contacts/Sql.php index 857cd71660..0ac5eb0d13 100644 --- a/api/src/Contacts/Sql.php +++ b/api/src/Contacts/Sql.php @@ -250,7 +250,7 @@ class Sql extends Api\Storage * We join egw_addressbook to itself, and count how many fields match. If * enough of the fields we care about match, we count those two records as * duplicates. - * + * * @var array $param * @var string $param[grouped_view] 'duplicate', 'duplicate,adr_one_location', 'duplicate,org_name' how to group * @var int $param[owner] addressbook to search @@ -368,7 +368,7 @@ class Sql extends Api\Storage } $query = $this->parse_search(array_merge($criteria, $filter), $wildcard, false, ' AND '); - $sub_query = $this->db->select($this->table_name, + $sub_query = $this->db->select($this->table_name, 'DISTINCT ' . implode(', ',array_merge($columns, $extra)), $query, False, False, 0, $append, False, -1, @@ -380,7 +380,7 @@ class Sql extends Api\Storage { $mysql_calc_rows = 'SQL_CALC_FOUND_ROWS '; } - + $rows = $this->db->query( "SELECT $mysql_calc_rows " . $columns. ', COUNT(contact_id) AS group_count' . ' FROM (' . $sub_query . ') AS matches GROUP BY ' . implode(',',$group) . @@ -396,7 +396,7 @@ class Sql extends Api\Storage $row['email_home'] = $row['contact_email_home']; $dupes[] = $this->db2data($row); } - + if ($mysql_calc_rows) { $this->total = $this->db->query('SELECT FOUND_ROWS()')->fetchColumn(); @@ -1022,6 +1022,30 @@ class Sql extends Api\Storage { $update['carddav_name'] = $this->data['id'].'.vcf'; } + // update photo in entry-directory, unless hinted it is unchanged + if (!$err && $this->data['photo_unchanged'] !== true) + { + // in case files bit-field is not available read it from DB + if (!isset($this->data['files'])) + { + $this->data['files'] = (int)$this->db->select($this->table_name, 'contact_files', array( + 'contact_id' => $this->data['id'], + ), __LINE__, __FILE__)->fetchColumn(); + } + $path = Api\Link::vfs_path('addressbook', $this->data['id'], Api\Contacts::FILES_PHOTO); + $backup = Api\Vfs::$is_root; Api\Vfs::$is_root = true; + if (empty($this->data['jpegphoto'])) + { + unlink($path); + $update['files'] = $this->data['files'] & ~Api\Contacts::FILES_BIT_PHOTO; + } + else + { + file_put_contents($path, $this->data['jpegphoto']); + $update['files'] = $this->data['files'] | Api\Contacts::FILES_BIT_PHOTO; + } + Api\Vfs::$is_root = $backup; + } if (!$err && $update) { parent::update($update); diff --git a/api/src/Contacts/Storage.php b/api/src/Contacts/Storage.php index 74a0dccc07..38053a63f8 100755 --- a/api/src/Contacts/Storage.php +++ b/api/src/Contacts/Storage.php @@ -159,6 +159,21 @@ class Storage */ var $content_types = array(); + /** + * Directory to store striped photo or public keys in VFS directory of entry + */ + const FILES_DIRECTORY = '.files'; + const FILES_PHOTO = '.files/photo.jpeg'; + const FILES_PGP_PUBKEY = '.files/pgp-pubkey.asc'; + const FILES_SMIME_PUBKEY = '.files/smime-pubkey.crt'; + + /** + * Constant for bit-field "contact_files" storing what files are available + */ + const FILES_BIT_PHOTO = 1; + const FILES_BIT_PGP_PUBKEY = 2; + const FILES_BIT_SMIME_PUBKEY = 4; + /** * These fields are options for checking for duplicate contacts * @@ -816,7 +831,7 @@ class Storage /** * Find contacts that appear to be duplicates - * + * * @param Array $param * @param string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group * @param int $param[owner] addressbook to search @@ -825,7 +840,7 @@ class Storage * @param int $param[start] * @param int $param[num_rows] * @param string $param[sort] ASC or DESC - * + * * @return array of arrays */ public function duplicates($param) @@ -886,7 +901,7 @@ class Storage foreach($rows as $n => $row) { $rows[$n]['id'] = 'duplicate:'; - foreach(static::$duplicate_fields as $by => $by_label) + foreach(array_keys(static::$duplicate_fields) as $by) { if (strpos($row[$by],'&')!==false) $row[$by] = str_replace('&','*AND*',$row[$by]); if($row[$by]) diff --git a/api/src/Etemplate/Widget/Vfs.php b/api/src/Etemplate/Widget/Vfs.php index 12943f8504..f131785a0e 100644 --- a/api/src/Etemplate/Widget/Vfs.php +++ b/api/src/Etemplate/Widget/Vfs.php @@ -84,10 +84,10 @@ class Vfs extends File // Single file, missing extension in path else if (substr($path, -1) != '/' && !Api\Vfs::file_exists($path) && $relpath && substr($relpath,-4,1) !== '.') { - $find = Api\Vfs::find(substr($path,0, - strlen($relpath)), array( + $find = Api\Vfs::find(Api\Vfs::dirname($path), array( 'type' => 'f', 'maxdepth' => 1, - 'name' => $relpath . '*' + 'name' => Api\Vfs::basename($path).'.*', )); foreach($find as $file) { @@ -264,11 +264,14 @@ class Vfs extends File { // add extension to path $parts = explode('.',$filename); - if (($extension = array_pop($parts)) && Api\MimeMagic::ext2mime($extension)) // really an extension --> add it to path + // check if path already contains a valid extension --> dont add an other one + $path_parts = explode('.', $path); + if (count($path_parts) > 2 && (!($extension = array_pop($path_parts)) || !Api\MimeMagic::ext2mime($extension)) && + ($extension = array_pop($parts)) && Api\MimeMagic::ext2mime($extension)) // really an extension --> add it to path { $path .= '.'.$extension; - $file['name'] = Api\Vfs::basename($path); } + $file['name'] = Api\Vfs::basename($path); } else if ($path) // multiple upload with dir given (trailing slash) { diff --git a/api/src/Vfs.php b/api/src/Vfs.php index 18d4c1e745..41e321b7da 100644 --- a/api/src/Vfs.php +++ b/api/src/Vfs.php @@ -2534,6 +2534,34 @@ class Vfs { return Vfs\StreamWrapper::load_wrapper($scheme); } + + /** + * Return stream with given string as content + * + * @param string $string + * @return boolean|resource stream or false on error + */ + static function string_stream($string) + { + if (!($fp = fopen('php://temp', 'rw'))) + { + return false; + } + $pos = 0; + $len = strlen($string); + do { + if (!($written = fwrite($fp, substr($string, $pos)))) + { + return false; + } + $pos += $written; + } + while ($len < $pos); + + rewind($fp); + + return $fp; + } } Vfs::init_static();