* Addressbook: store S/Mime & PGP pubkey and photo (SQL backend only) in filesystem

This commit is contained in:
Ralf Becker 2017-09-19 11:38:02 +02:00
parent 23e654ab89
commit 48554590f4
12 changed files with 360 additions and 48 deletions

View File

@ -35,7 +35,7 @@ class addressbook_bo extends Api\Contacts
*/ */
public function get_pgp_keys($recipients) 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) 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 // add all keys to public keyserver too
$message .= "\n".lang('%1 key(s) added to public keyserver "%2".', $message .= "\n".lang('%1 key(s) added to public keyserver "%2".',
self::set_pgp_keyserver($keys), PARSE_URL(self::KEYSERVER_ADD, PHP_URL_HOST)); 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; 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 * 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 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 * @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 * @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'])) 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'); 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(); $criteria = array();
foreach($keys as $recipient => $key) foreach($keys as $recipient => $key)
{ {
@ -210,7 +231,21 @@ class addressbook_bo extends Api\Contacts
{ {
$key = $keys[$contact['email']]; $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; $contact['pubkey'] .= $key;
} }
@ -220,7 +255,17 @@ class addressbook_bo extends Api\Contacts
} }
if ($this->check_perms(Acl::EDIT, $contact) && $this->save($contact)) 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']))) 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 * EMail addresses are lowercased to make search case-insensitive
* *
* @param string|int|array $recipients (array of) email addresses or numeric account-ids * @param string|int|array $recipients (array of) email addresses or numeric account-ids
* @param string $key_regexp * @param boolean $pgp true: PGP, false: S/Mime public keys
* @param string $criteria_filter
*
* @return array email|account_id => key pairs * @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 (!$recipients) return array();
if (!is_array($recipients)) $recipients = array($recipients); 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(); $criteria = $result = array();
foreach($recipients as &$recipient) foreach($recipients as &$recipient)
{ {
@ -264,11 +319,14 @@ class addressbook_bo extends Api\Contacts
$criteria['contact_email'][] = $recipient = strtolower($recipient); $criteria['contact_email'][] = $recipient = strtolower($recipient);
} }
} }
foreach($this->search($criteria, array('account_id', 'contact_email', 'contact_pubkey'), '', '', '', false, 'OR', false, foreach($this->search($criteria, array('account_id', 'contact_email', 'contact_pubkey'),
"contact_pubkey LIKE '". $criteria_filter ."'" ) as $contact) '', '', '', false, 'OR', false, null) as $contact)
{ {
$matches = null; $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']); $contact['email'] = strtolower($contact['email']);
if (empty($criteria['account_id']) || in_array($contact['email'], $recipients)) 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) 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) 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);
} }
} }

View File

@ -861,7 +861,7 @@ class addressbook_ui extends addressbook_bo
unset($query['col_filter']['org_name']); unset($query['col_filter']['org_name']);
unset($query['col_filter']['org_unit']); unset($query['col_filter']['org_unit']);
unset($query['col_filter']['adr_one_locality']); 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]); unset($query['col_filter'][$field]);
} }
@ -1429,7 +1429,7 @@ window.egw_LAB.wait(function() {
* Used for action on organisation and duplicate views * 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 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 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 &$success number of succeded actions
* @param int &$failed number of failed actions (not enought permissions) * @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' * @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) protected function find_grouped_ids($action,&$checked,$use_all,&$success,&$failed,&$action_msg,$session_name,&$msg)
{ {
unset($use_all);
$grouped_contacts = array(); $grouped_contacts = array();
foreach((array)$checked as $n => $id) 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 the duplicate_filed after submit because we don't need to warn user for second time about contact duplication
unset($content['presets_fields']); 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']) if ($content['delete_photo'])
{ {
$content['jpegphoto'] = null; $content['jpegphoto'] = null;
unset($content['delete_photo']); unset($content['delete_photo']);
$content['photo_unchanged'] = false;
} }
if (is_array($content['upload_photo']) && !empty($content['upload_photo']['tmp_name']) && if (is_array($content['upload_photo']) && !empty($content['upload_photo']['tmp_name']) &&
$content['upload_photo']['tmp_name'] != 'none' && $content['upload_photo']['tmp_name'] != 'none' &&
@ -2031,7 +2035,8 @@ window.egw_LAB.wait(function() {
$content['jpegphoto'] = $this->resize_photo($f); $content['jpegphoto'] = $this->resize_photo($f);
fclose($f); fclose($f);
unset($content['upload_photo']); unset($content['upload_photo']);
} $content['photo_unchanged'] = false;
}*/
$links = false; $links = false;
if (!$content['id'] && is_array($content['link_to']['to_id'])) 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) // Registry has view_id as contact_id, so set it (custom fields uses it)
$content['contact_id'] = $content['id']; $content['contact_id'] = $content['id'];
// Avoid ID conflict with tree & selectboxes // Avoid ID conflict with tree & selectboxes
$content['cat_id_tree'] = $content['cat_id']; $content['cat_id_tree'] = $content['cat_id'];
@ -2363,7 +2368,7 @@ window.egw_LAB.wait(function() {
$content['private_cfs']['#'.$name] = $content['#'.$name]; $content['private_cfs']['#'.$name] = $content['#'.$name];
} }
} }
// how to display addresses // how to display addresses
$content['addr_format'] = $this->addr_format_by_country($content['adr_one_countryname']); $content['addr_format'] = $this->addr_format_by_country($content['adr_one_countryname']);
$content['addr_format2'] = $this->addr_format_by_country($content['adr_two_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 * Ajax method to update edited avatar photo via avatar widget
* avatar widget.
* *
* @param int $contact_id * @param int $contact_id
* @param file string $file = null null means to delete * @param file string $file = null null means to delete
@ -3093,15 +3097,17 @@ window.egw_LAB.wait(function() {
$contact = $this->read($contact_id); $contact = $this->read($contact_id);
if ($file) if ($file)
{ {
$filteredFile=substr($file, strpos($file, ",")+1); $filteredFile = substr($file, strpos($file, ",")+1);
$decoded = base64_decode($filteredFile); // 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); $success = $this->save($contact);
if (!$success) if (!$success)
{ {
$response->alert($message); $response->alert($this->error);
} }
else else
{ {
@ -3122,12 +3128,15 @@ window.egw_LAB.wait(function() {
{ {
$contact_id = $GLOBALS['egw']->accounts->id2name(substr($contact_id,8),'person_id'); $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')); Egw::redirect(Api\Image::find('addressbook','photo'));
} }
// use an etag over the image mapp // use an etag over the image mapp
$etag = '"'.$contact['id'].':'.$contact['etag'].'"'; $etag = '"'.$contact_id.':'.$contact['etag'].'"';
if (!ob_get_contents()) if (!ob_get_contents())
{ {
header('Content-type: image/jpeg'); header('Content-type: image/jpeg');
@ -3143,11 +3152,16 @@ window.egw_LAB.wait(function() {
{ {
header("HTTP/1.1 304 Not Modified"); header("HTTP/1.1 304 Not Modified");
} }
else elseif(!empty($contact['jpegphoto']))
{ {
header('Content-length: '.bytes($contact['jpegphoto'])); header('Content-length: '.bytes($contact['jpegphoto']));
echo $contact['jpegphoto']; echo $contact['jpegphoto'];
} }
else
{
header('Content-length: '.$size);
readfile($url);
}
exit(); exit();
} }
} }

View File

@ -72,6 +72,7 @@ class addressbook_vcal extends Api\Contacts
'X-ASSISTANT-TEL' => array('tel_assistent'), 'X-ASSISTANT-TEL' => array('tel_assistent'),
'UID' => array('uid'), 'UID' => array('uid'),
'REV' => array('modified'), '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! //set for Apple: 'X-ABSHOWAS' => array('fileas_type'), // Horde vCard class uses uppercase prop-names!
); );

View File

@ -176,6 +176,14 @@
<description value="Next date"/> <description value="Next date"/>
<link id="next_link"/> <link id="next_link"/>
</row> </row>
<row valign="top">
<description value="SMIME key"/>
<vfs-upload id="addressbook:$cont[id]:.files/smime-pubkey.crt" accept=".crt,.pem,application/x-x509-ca-cert,application/x-x509-user-cert" mime="/application\/x-x509-(ca|user)-cert/"/>
</row>
<row valign="top">
<description value="PGP key"/>
<vfs-upload id="addressbook:$cont[id]:.files/pgp-pubkey.asc" accept=".asc,application/pgp-keys" mime="application/pgp-keys"/>
</row>
<row valign="top"> <row valign="top">
<description for="pubkey" value="Public key"/> <description for="pubkey" value="Public key"/>
<textbox multiline="true" id="pubkey" rows="4" resize_ratio="0" class="et2_fullWidth"/> <textbox multiline="true" id="pubkey" rows="4" resize_ratio="0" class="et2_fullWidth"/>

View File

@ -12,7 +12,7 @@
/* Basic information about this app */ /* Basic information about this app */
$setup_info['api']['name'] = 'api'; $setup_info['api']['name'] = 'api';
$setup_info['api']['title'] = 'EGroupware 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'; $setup_info['api']['versions']['current_header'] = '1.29';
// maintenance release in sync with changelog in doc/rpm-build/debian.changes // maintenance release in sync with changelog in doc/rpm-build/debian.changes
$setup_info['api']['versions']['maintenance_release'] = $setup_info['api']['version']; $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']['license'] = 'GPL';
$setup_info['groupdav']['hooks']['preferences'] = 'EGroupware\\Api\\CalDAV\\Hooks::menus'; $setup_info['groupdav']['hooks']['preferences'] = 'EGroupware\\Api\\CalDAV\\Hooks::menus';
$setup_info['groupdav']['hooks']['settings'] = 'EGroupware\\Api\\CalDAV\\Hooks::settings'; $setup_info['groupdav']['hooks']['settings'] = 'EGroupware\\Api\\CalDAV\\Hooks::settings';

View File

@ -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_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_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_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'), '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_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'), '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_one_countrycode' => array('type' => 'ascii','precision' => '2','comment' => 'countrycode (business)'),
'adr_two_countrycode' => array('type' => 'ascii','precision' => '2','comment' => 'countrycode (private)'), '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'), 'pk' => array('contact_id'),
'fk' => array(), 'fk' => array(),

View File

@ -235,3 +235,164 @@ function api_upgrade16_9_001()
return $GLOBALS['setup_info']['api']['currentver'] = '16.9.002'; 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';
}

View File

@ -734,7 +734,7 @@ class Contacts extends Contacts\Storage
$data[$name] = DateTime::server2user($data[$name], $date_format); $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 // set freebusy_uri for accounts
if (!$data['freebusy_uri'] && !$data['owner'] && $data['account_id'] && !is_object($GLOBALS['egw_setup'])) 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 string|FILE $photo string with image or open filedescribtor
* @param int $dst_w =240 max width to resize to * @param int $dst_w =240 max width to resize to
* @return string with resized jpeg photo, null on error * @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)) if (is_resource($photo))
{ {

View File

@ -250,7 +250,7 @@ class Sql extends Api\Storage
* We join egw_addressbook to itself, and count how many fields match. If * 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 * enough of the fields we care about match, we count those two records as
* duplicates. * duplicates.
* *
* @var array $param * @var array $param
* @var string $param[grouped_view] 'duplicate', 'duplicate,adr_one_location', 'duplicate,org_name' how to group * @var string $param[grouped_view] 'duplicate', 'duplicate,adr_one_location', 'duplicate,org_name' how to group
* @var int $param[owner] addressbook to search * @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 '); $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)), 'DISTINCT ' . implode(', ',array_merge($columns, $extra)),
$query, $query,
False, False, 0, $append, False, -1, False, False, 0, $append, False, -1,
@ -380,7 +380,7 @@ class Sql extends Api\Storage
{ {
$mysql_calc_rows = 'SQL_CALC_FOUND_ROWS '; $mysql_calc_rows = 'SQL_CALC_FOUND_ROWS ';
} }
$rows = $this->db->query( $rows = $this->db->query(
"SELECT $mysql_calc_rows " . $columns. ', COUNT(contact_id) AS group_count' . "SELECT $mysql_calc_rows " . $columns. ', COUNT(contact_id) AS group_count' .
' FROM (' . $sub_query . ') AS matches GROUP BY ' . implode(',',$group) . ' 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']; $row['email_home'] = $row['contact_email_home'];
$dupes[] = $this->db2data($row); $dupes[] = $this->db2data($row);
} }
if ($mysql_calc_rows) if ($mysql_calc_rows)
{ {
$this->total = $this->db->query('SELECT FOUND_ROWS()')->fetchColumn(); $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['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) if (!$err && $update)
{ {
parent::update($update); parent::update($update);

View File

@ -159,6 +159,21 @@ class Storage
*/ */
var $content_types = array(); 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 * These fields are options for checking for duplicate contacts
* *
@ -816,7 +831,7 @@ class Storage
/** /**
* Find contacts that appear to be duplicates * Find contacts that appear to be duplicates
* *
* @param Array $param * @param Array $param
* @param string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group * @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 * @param int $param[owner] addressbook to search
@ -825,7 +840,7 @@ class Storage
* @param int $param[start] * @param int $param[start]
* @param int $param[num_rows] * @param int $param[num_rows]
* @param string $param[sort] ASC or DESC * @param string $param[sort] ASC or DESC
* *
* @return array of arrays * @return array of arrays
*/ */
public function duplicates($param) public function duplicates($param)
@ -886,7 +901,7 @@ class Storage
foreach($rows as $n => $row) foreach($rows as $n => $row)
{ {
$rows[$n]['id'] = 'duplicate:'; $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 (strpos($row[$by],'&')!==false) $row[$by] = str_replace('&','*AND*',$row[$by]);
if($row[$by]) if($row[$by])

View File

@ -84,10 +84,10 @@ class Vfs extends File
// Single file, missing extension in path // Single file, missing extension in path
else if (substr($path, -1) != '/' && !Api\Vfs::file_exists($path) && $relpath && substr($relpath,-4,1) !== '.') 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', 'type' => 'f',
'maxdepth' => 1, 'maxdepth' => 1,
'name' => $relpath . '*' 'name' => Api\Vfs::basename($path).'.*',
)); ));
foreach($find as $file) foreach($find as $file)
{ {
@ -264,11 +264,14 @@ class Vfs extends File
{ {
// add extension to path // add extension to path
$parts = explode('.',$filename); $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; $path .= '.'.$extension;
$file['name'] = Api\Vfs::basename($path);
} }
$file['name'] = Api\Vfs::basename($path);
} }
else if ($path) // multiple upload with dir given (trailing slash) else if ($path) // multiple upload with dir given (trailing slash)
{ {

View File

@ -2534,6 +2534,34 @@ class Vfs
{ {
return Vfs\StreamWrapper::load_wrapper($scheme); 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(); Vfs::init_static();