* Calendar: fix SQL error on storing events containing rfc822 email addresses with non-ascii characters eg. "Hugo Müller <hm@test.org>"

egw_cal_user.cal_user_id is now an md5 hash of lowercased raw email address (eg. "hm@test.org" in above example). Full attendee information is now stored in egw_cal_user.cal_user_attendee.
Will allow in a further step also to store attendee information for accounts to eg. answer with correct email to external organizers
This commit is contained in:
Ralf Becker 2015-08-17 14:06:18 +00:00
parent df2c751037
commit 0f834be527
5 changed files with 175 additions and 40 deletions

View File

@ -71,7 +71,7 @@ define('WEEK_s',7*DAY_s);
* DB-model uses egw_cal_user.cal_status='E' for participants only participating in exceptions of recurring
* events, so whole recurring event get found for these participants too!
*
* All update methods not take care to update modification time of (evtl. existing) series master too,
* All update methods now take care to update modification time of (evtl. existing) series master too,
* to force an etag, ctag and sync-token change! Methods not doing that are private to this class.
*
* range_start/_end in main-table contains start and end of whole event series (range_end is NULL for unlimited recuring events),
@ -80,6 +80,14 @@ define('WEEK_s',7*DAY_s);
* (few milisecs instead of more then 2 minutes on huge installations)!
* It's set in calendar_so::save from start and end or recur_enddate, so nothing changes for higher level classes.
*
* egw_cal_user.cal_user_id contains since 14.3.001 only an md5-hash of a lowercased raw email address (not rfc822 address!).
* Real email address and other possible attendee information for iCal or CalDAV are stored in cal_user_attendee.
* This allows a short 32byte ascii cal_user_id and also storing attendee information for accounts and contacts.
* Outside of this class uid for email address is still "e$cn <$email>" or "e$email".
* We use calendar_so::split_user($uid, &$user_type, &$user_id, $md5_email=false) with last param true to generate
* egw_cal_user.cal_user_id for DB and calendar_so::combine_user($user_type, $user_id, $user_attendee) to generate
* uid used outside of this class. Both methods are unchanged when using with their default parameters.
*
* @ToDo drop egw_cal_repeats table in favor of a rrule colum in main table (saves always used left join and allows to store all sorts of rrules)
*/
class calendar_so
@ -255,7 +263,9 @@ class calendar_so
if (!is_array($users)) $users = $users ? (array)$users : array();
foreach($users as &$uid)
{
if (is_numeric($uid)) $uid = 'u'.$uid;
$user_type = $user_id = null;
self::split_user($uid, $user_type, $user_id, true);
$uid = $user_type.$user_id;
}
$sql .= " AND\n CONCAT(cal_user_type,cal_user_id) IN (".implode(',', array_map(array($this->db, 'quote'), $users)).")";
}
@ -453,11 +463,13 @@ class calendar_so
),__LINE__,__FILE__,false,'ORDER BY cal_user_type DESC,cal_recur_date ASC,'.self::STATUS_SORT,'calendar') as $row) // DESC puts users before resources and contacts
{
// combine all participant data in uid and status values
$uid = self::combine_user($row['cal_user_type'],$row['cal_user_id']);
$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
$status = self::combine_status($row['cal_status'],$row['cal_quantity'],$row['cal_role']);
$events[$row['cal_id']]['participants'][$uid] = $status;
$events[$row['cal_id']]['participant_types'][$row['cal_user_type']][$row['cal_user_id']] = $status;
$events[$row['cal_id']]['participant_types'][$row['cal_user_type']][is_numeric($uid) ? $uid : substr($uid, 1)] = $status;
// make extra attendee information available eg. for iCal export (attendee used eg. in response to organizer for an account)
$events[$row['cal_id']]['attendee'][$uid] = $row['cal_user_attendee'];
}
// custom fields
@ -514,7 +526,7 @@ class calendar_so
foreach((array)$users as $uid)
{
$type = $id = null;
self::split_user($uid, $type, $id);
self::split_user($uid, $type, $id, true);
$types[$type][] = $id;
}
foreach($types as $type => $ids)
@ -749,7 +761,9 @@ class calendar_so
}
else
{
$users_by_type[$user[0]][] = substr($user,1);
$user_type = $user_id = null;
self::split_user($user, $user_type, $user_id, true);
$users_by_type[$user_type][] = $user_id;
}
}
$to_or = $user_or = array();
@ -1028,7 +1042,7 @@ class calendar_so
if ($row['cal_recur_date']) $id .= '-'.$row['cal_recur_date'];
// combine all participant data in uid and status values
$uid = self::combine_user($row['cal_user_type'],$row['cal_user_id']);
$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
$status = self::combine_status($row['cal_status'],$row['cal_quantity'],$row['cal_role']);
// set accept/reject/tentative of series for all recurrences
@ -1442,20 +1456,21 @@ ORDER BY cal_user_type, cal_usre_id
if ($event['cal_reference'])
{
$master_participants = array();
foreach($this->db->select($this->user_table, 'cal_user_type,cal_user_id', array(
foreach($this->db->select($this->user_table, 'cal_user_type,cal_user_id,cal_user_attendee', array(
'cal_id' => $event['cal_reference'],
'cal_recur_date' => 0,
"cal_status != 'X'", // deleted need to be replaced with exception marker too
), __LINE__, __FILE__, 'calendar') as $row)
{
$master_participants[] = self::combine_user($row['cal_user_type'], $row['cal_user_id']);
$master_participants[] = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
}
foreach(array_diff(array_keys((array)$event['cal_participants']), $master_participants) as $uid)
{
$user_type = $user_id = null;
self::split_user($uid, $user_type, $user_id);
self::split_user($uid, $user_type, $user_id, true);
$this->db->insert($this->user_table, array(
'cal_status' => 'E',
'cal_user_attendee' => $user_type == 'e' ? substr($uid, 1) : null,
), array(
'cal_id' => $event['cal_reference'],
'cal_recur_date' => 0,
@ -1490,10 +1505,10 @@ ORDER BY cal_user_type, cal_usre_id
'cal_recur_date' => 0,
);
$old_participants = array();
foreach ($this->db->select($this->user_table,'cal_user_type,cal_user_id,cal_status,cal_quantity,cal_role', $where,
foreach ($this->db->select($this->user_table,'cal_user_type,cal_user_id,cal_user_attendee,cal_status,cal_quantity,cal_role', $where,
__LINE__,__FILE__,false,'','calendar') as $row)
{
$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id']);
$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
$status = self::combine_status($row['cal_status'], $row['cal_quantity'], $row['cal_role']);
$old_participants[$uid] = $status;
}
@ -1746,36 +1761,74 @@ ORDER BY cal_user_type, cal_usre_id
return $this->db->affected_rows();
}
/**
* Format attendee as email
*
* @param string|array $attendee attendee information: email, json or array with attr cn and url
* @return type
*/
static function attendee2email($attendee)
{
if (is_string($attendee) && $attendee[0] == '{' && substr($attendee, -1) == '}')
{
$user_attendee = json_decode($user_attendee, true);
}
if (is_array($attendee))
{
$email = !empty($attendee['email']) ? $user_attendee['email'] :
(strtolower(substr($attendee['url'], 0, 7)) == 'mailto:' ? substr($user_attendee['url'], 7) : $attendee['url']);
$attendee = !empty($attendee['cn']) ? $attendee['cn'].' <'.$email.'>' : $email;
}
return $attendee;
}
/**
* combines user_type and user_id into a single string or integer (for users)
*
* @param string $user_type 1-char type: 'u' = user, ...
* @param string|int $user_id id
* @param string|array $attendee attendee information: email, json or array with attr cn and url
* @return string|int combined id
*/
static function combine_user($user_type,$user_id)
static function combine_user($user_type, $user_id, $attendee=null)
{
if (!$user_type || $user_type == 'u')
{
return (int) $user_id;
}
if ($user_type == 'e' && $attendee)
{
$user_id = self::attendee2email($attendee);
}
return $user_type.$user_id;
}
/**
* splits the combined user_type and user_id into a single values
*
* This is the only method building (normalized) md5 hashes for user_type="e",
* if called with $md5_email=true parameter!
*
* @param string|int $uid
* @param string &$user_type 1-char type: 'u' = user, ...
* @param string|int &$user_id id
* @param boolean $md5_email =false md5 hash user_id for email / user_type=="e"
*/
static function split_user($uid,&$user_type,&$user_id)
static function split_user($uid, &$user_type, &$user_id, $md5_email=false)
{
if (is_numeric($uid))
{
$user_type = 'u';
$user_id = (int) $uid;
}
// create md5 hash from lowercased and trimed raw email ("rb@stylite.de", not "Ralf Becker <rb@stylite.de>")
elseif ($md5_email && $uid[0] == 'e')
{
$user_type = $uid[0];
$email = substr($uid, 1);
$matches = null;
if (preg_match('/<([^<>]+)>$/', $email, $matches)) $email = $matches[1];
$user_id = md5(trim(strtolower($email)));
}
else
{
$user_type = $uid[0];
@ -1877,7 +1930,7 @@ ORDER BY cal_user_type, cal_usre_id
$old_participants = array();
foreach($existing_entries as $row)
{
$uid = self::combine_user($row['cal_user_type'],$row['cal_user_id']);
$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
if ($row['cal_recur_date'] || !isset($old_participants[$uid]))
{
$old_participants[$uid] = self::combine_status($row['cal_status'],$row['cal_quantity'],$row['cal_role']);
@ -1890,7 +1943,7 @@ ORDER BY cal_user_type, cal_usre_id
$deleted = array();
foreach($existing_entries as $row)
{
$uid = self::combine_user($row['cal_user_type'],$row['cal_user_id']);
$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
// delete not longer set participants
if (!isset($participants[$uid]))
{
@ -1937,12 +1990,13 @@ ORDER BY cal_user_type, cal_usre_id
foreach($participants as $uid => $status)
{
$type = $id = $quantity = $role = null;
self::split_user($uid,$type,$id);
self::split_user($uid, $type, $id, true);
self::split_status($status,$quantity,$role);
$set = array(
'cal_status' => $status,
'cal_quantity' => $quantity,
'cal_role' => $role,
'cal_user_attendee' => $type == 'e' ? substr($uid, 1) : null,
);
foreach($recurrences as $recur_date)
{
@ -1979,13 +2033,14 @@ ORDER BY cal_user_type, cal_usre_id
*
* @param int $cal_id
* @param char $user_type 'u' regular user, 'r' resource, 'c' contact
* @param int $user_id
* @param int|string $user_id
* @param int|char $status numeric status (defines) or 1-char code: 'R', 'U', 'T' or 'A'
* @param int $recur_date =0 date to change, or 0 = all since now
* @param string $role =null role to set if !is_null($role)
* @param string $attendee =null extra attendee information to set for all types (incl. accounts!)
* @return int number of changed recurrences
*/
function set_status($cal_id,$user_type,$user_id,$status,$recur_date=0,$role=null)
function set_status($cal_id,$user_type,$user_id,$status,$recur_date=0,$role=null,$attendee=null)
{
static $status_code_short = array(
REJECTED => 'R',
@ -2001,12 +2056,14 @@ ORDER BY cal_user_type, cal_usre_id
if (is_numeric($status)) $status = $status_code_short[$status];
if (!$user_type) $user_type == 'u';
$uid = self::combine_user($user_type, $user_id);
$user_id_md5 = null;
self::split_user($uid, $user_type, $user_id_md5, true);
$where = array(
'cal_id' => $cal_id,
'cal_user_type' => $user_type,
'cal_user_id' => $user_id,
'cal_user_id' => $user_id_md5,
);
if ((int) $recur_date)
{
@ -2025,6 +2082,7 @@ ORDER BY cal_user_type, cal_usre_id
else
{
$set = array('cal_status' => $status);
if ($user_type == 'e' || $attendee) $set['cal_user_attendee'] = $attendee ? $attendee : $user_id;
if (!is_null($role) && $role != 'REQ-PARTICIPANT') $set['cal_role'] = $role;
$this->db->insert($this->user_table,$set,$where,__LINE__,__FILE__,'calendar');
// for new or changed group-invitations, remove previously deleted members, so they show up again
@ -2077,13 +2135,14 @@ ORDER BY cal_user_type, cal_usre_id
$type = '';
$id = null;
self::split_user($uid,$type,$id);
self::split_user($uid, $type, $id, true);
$quantity = $role = null;
self::split_status($status,$quantity,$role);
$this->db->insert($this->user_table,array(
'cal_status' => $status,
'cal_quantity' => $quantity,
'cal_role' => $role
'cal_role' => $role,
'cal_attendee' => $type == 'e' ? substr($uid, 1) : null,
),array(
'cal_id' => $cal_id,
'cal_recur_date' => $start,
@ -2458,7 +2517,7 @@ ORDER BY cal_user_type, cal_usre_id
}
if (is_null($uid)) return $participant_status;
$user_type = $user_id = null;
self::split_user($uid, $user_type, $user_id);
self::split_user($uid, $user_type, $user_id, true);
$where2 = array(
'cal_id' => $cal_id,
@ -2489,6 +2548,7 @@ ORDER BY cal_user_type, cal_usre_id
*
* @return array participants
*/
/* seems NOT to be used anywhere, NOT ported to new md5-email schema!
function get_participants($cal_id, $recur_date=0)
{
$participants = array();
@ -2508,7 +2568,7 @@ ORDER BY cal_user_type, cal_usre_id
$participants[$id]['uid'] = $uid;
}
return $participants;
}
}*/
/**
* get all releated events
@ -2721,9 +2781,11 @@ ORDER BY cal_user_type, cal_usre_id
// get default stati
$recurrence_zero = array();
$user = $GLOBALS['egw_info']['user']['account_id'];
$where = array('cal_id' => $cal_id,
'cal_recur_date' => 0);
foreach ($this->db->select($this->user_table,'cal_user_id,cal_user_type,cal_status',$where,
$where = array(
'cal_id' => $cal_id,
'cal_recur_date' => 0,
);
foreach ($this->db->select($this->user_table,'cal_user_type,cal_user_id,cal_user_attendee,cal_status',$where,
__LINE__,__FILE__,false,'','calendar') as $row)
{
switch ($row['cal_user_type'])
@ -2731,7 +2793,7 @@ ORDER BY cal_user_type, cal_usre_id
case 'u': // account
case 'c': // contact
case 'e': // email address
$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id']);
$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
$recurrence_zero[$uid] = $row['cal_status'];
}
}
@ -2743,9 +2805,11 @@ ORDER BY cal_user_type, cal_usre_id
// array2string($recurrence_zero));
$participants = array();
$where = array('cal_id' => $cal_id,
'cal_recur_date' => $recur_date);
foreach ($this->db->select($this->user_table,'cal_user_id,cal_user_type,cal_status',$where,
$where = array(
'cal_id' => $cal_id,
'cal_recur_date' => $recur_date,
);
foreach ($this->db->select($this->user_table,'cal_user_type,cal_user_id,cal_user_attendee,cal_status',$where,
__LINE__,__FILE__,false,'','calendar') as $row)
{
switch ($row['cal_user_type'])
@ -2753,7 +2817,7 @@ ORDER BY cal_user_type, cal_usre_id
case 'u': // account
case 'c': // contact
case 'e': // email address
$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id']);
$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
$participants[$uid] = $row['cal_status'];
}
}

View File

@ -10,7 +10,7 @@
*/
$setup_info['calendar']['name'] = 'calendar';
$setup_info['calendar']['version'] = '14.3';
$setup_info['calendar']['version'] = '14.3.001';
$setup_info['calendar']['app_order'] = 3;
$setup_info['calendar']['enable'] = 1;
$setup_info['calendar']['index'] = 'calendar.calendar_uiviews.index';
@ -71,6 +71,3 @@ $setup_info['calendar']['check_install'] = array(
'from' => 'Calendar',
),
);

View File

@ -74,12 +74,13 @@ $phpgw_baseline = array(
'cal_id' => array('type' => 'int','precision' => '4','nullable' => False),
'cal_recur_date' => array('type' => 'int','meta' => 'timestamp','precision' => '8','nullable' => False,'default' => '0'),
'cal_user_type' => array('type' => 'ascii','precision' => '1','nullable' => False,'default' => 'u','comment' => 'u=user, g=group, c=contact, r=resource, e=email'),
'cal_user_id' => array('type' => 'ascii','meta' => array("cal_user_type='u'" => 'account'),'precision' => '128','nullable' => False,'comment' => 'id or email-address for type=e'),
'cal_user_id' => array('type' => 'ascii','meta' => array("cal_user_type='u'" => 'account'),'precision' => '32','nullable' => False,'comment' => 'id or md5(email-address) for type=e'),
'cal_status' => array('type' => 'ascii','precision' => '1','default' => 'A','comment' => 'U=unknown, A=accepted, R=rejected, T=tentative'),
'cal_quantity' => array('type' => 'int','precision' => '4','default' => '1','comment' => 'only for certain types (eg. resources)'),
'cal_role' => array('type' => 'ascii','precision' => '64','default' => 'REQ-PARTICIPANT','comment' => 'CHAIR, REQ-PARTICIPANT, OPT-PARTICIPANT, NON-PARTICIPANT, X-CAT-$cat_id'),
'cal_user_modified' => array('type' => 'timestamp','default' => 'current_timestamp','comment' => 'automatic timestamp of last update'),
'cal_user_auto' => array('type' => 'auto','nullable' => False)
'cal_user_auto' => array('type' => 'auto','nullable' => False),
'cal_user_attendee' => array('type' => 'varchar','precision' => '255','comment' => 'email or json object with attr. cn, url, ...')
),
'pk' => array('cal_user_auto'),
'fk' => array(),

View File

@ -2520,7 +2520,7 @@ function calendar_upgrade14_2_004()
'cal_id' => array('type' => 'int','precision' => '4','nullable' => False),
'cal_recur_date' => array('type' => 'int','meta' => 'timestamp','precision' => '8','nullable' => False,'default' => '0'),
'cal_user_type' => array('type' => 'ascii','precision' => '1','nullable' => False,'default' => 'u','comment' => 'u=user, g=group, c=contact, r=resource, e=email'),
'cal_user_id' => array('type' => 'ascii','meta' => array("cal_user_type='u'" => 'account'),'precision' => '128','nullable' => False,'comment' => 'id or email-address for type=e'),
'cal_user_id' => array('type' => 'varchar','meta' => array("cal_user_type='u'" => 'account'),'precision' => '128','nullable' => False,'comment' => 'id or email-address for type=e'),
'cal_status' => array('type' => 'ascii','precision' => '1','default' => 'A','comment' => 'U=unknown, A=accepted, R=rejected, T=tentative'),
'cal_quantity' => array('type' => 'int','precision' => '4','default' => '1','comment' => 'only for certain types (eg. resources)'),
'cal_role' => array('type' => 'ascii','precision' => '64','default' => 'REQ-PARTICIPANT','comment' => 'CHAIR, REQ-PARTICIPANT, OPT-PARTICIPANT, NON-PARTICIPANT, X-CAT-$cat_id'),
@ -2553,3 +2553,55 @@ function calendar_upgrade14_2_005()
return $GLOBALS['setup_info']['calendar']['currentver'] = '14.3';
}
/**
* Store md5 of lowercased raw email-address as cal_user_id to only have a short ascii column for indexes and full rfc822 email in cal_user_attendee
*
* @return string
*/
function calendar_upgrade14_3()
{
$GLOBALS['egw_setup']->oProc->AddColumn('egw_cal_user','cal_user_attendee',array(
'type' => 'varchar',
'precision' => '255',
'comment' => 'email or json object with attr. cn, url, ...'
));
$email = "TRIM(LOWER(CASE SUBSTR(cal_user_id, -1, 1) WHEN '>' THEN SUBSTR(cal_user_id, 1+".
$GLOBALS['egw_setup']->db->strpos("cal_user_id", "'<'").", CHAR_LENGTH(cal_user_id)-".
$GLOBALS['egw_setup']->db->strpos("cal_user_id", "'<'")."-1) ELSE cal_user_id END))";
// delete all but one row, which would give a doublicate key, after above normalising of email addresses
// by ordering by status we prever accepted over tentative over unknow over deleted
foreach($GLOBALS['egw_setup']->db->select('egw_cal_user', "cal_id,cal_recur_date,$email AS email", array(
'cal_user_type' => 'e',
), __LINE__, __FILE__, false, "GROUP BY cal_id,cal_recur_date,$email HAVING COUNT(*)>1") as $row)
{
$n = 0;
foreach($GLOBALS['egw_setup']->db->select('egw_cal_user', "*,$email AS email", array(
'cal_id' => $row['cal_id'],
'cal_recur_date' => $row['cal_recur_date'],
'cal_user_type' => 'e',
$email.'='.$GLOBALS['egw_setup']->db->quote($row['email']),
), __LINE__, __FILE__, 'ORDER BY cal_status') as $user) // order A, T, U, X
{
if (strpos($user['email'], '@') !== false && $n++) continue;
$GLOBALS['egw_setup']->db->delete('egw_cal_user', array_intersect_key($user, array_flip(array('cal_id','cal_recur_date','cal_user_type','cal_user_id','cal_status'))));
}
}
// store only md5 of normalized email to always fit in 32 ascii chars (and allow non-ascii email)
$GLOBALS['egw_setup']->db->query(
"UPDATE egw_cal_user SET cal_user_attendee=cal_user_id,cal_user_id=MD5($email) WHERE cal_user_type='e'",
__LINE__, __FILE__);
$GLOBALS['egw_setup']->oProc->AlterColumn('egw_cal_user','cal_user_id',array(
'type' => 'ascii',
'meta' => array(
"cal_user_type='u'" => 'account'
),
'precision' => '32',
'nullable' => False,
'comment' => 'id or md5(email-address) for type=e'
));
return $GLOBALS['setup_info']['calendar']['currentver'] = '14.3.001';
}

View File

@ -1323,6 +1323,27 @@ class egw_db
return $sql;
}
/**
* SQL returning character (not byte!) positions for $substr in $str
*
* @param string $str
* @param string $substr
* @return string SQL returning character (not byte!) positions for $substr in $str
*/
function strpos($str, $substr)
{
switch($this->Type)
{
case 'mysql':
return "LOCATE($substr,$str)";
case 'pgsql':
return "STRPOS($str,$substr)";
case 'mssql':
return "CHARINDEX($substr,$str)";
}
die(__METHOD__." not implemented for DB type '$this->Type'!");
}
/**
* Convert a DB specific timestamp in a unix timestamp stored as integer, like MySQL: UNIX_TIMESTAMP(ts)
*