diff --git a/calendar/inc/class.calendar_so.inc.php b/calendar/inc/class.calendar_so.inc.php index 4c6585eb9e..75badcdd21 100644 --- a/calendar/inc/class.calendar_so.inc.php +++ b/calendar/inc/class.calendar_so.inc.php @@ -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 ") + 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']; } } diff --git a/calendar/setup/setup.inc.php b/calendar/setup/setup.inc.php index 783b0a028b..fde44fd6d6 100755 --- a/calendar/setup/setup.inc.php +++ b/calendar/setup/setup.inc.php @@ -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', ), ); - - - diff --git a/calendar/setup/tables_current.inc.php b/calendar/setup/tables_current.inc.php index 3b2e68984e..433a347897 100644 --- a/calendar/setup/tables_current.inc.php +++ b/calendar/setup/tables_current.inc.php @@ -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(), diff --git a/calendar/setup/tables_update.inc.php b/calendar/setup/tables_update.inc.php index 1a759e1a40..db11543097 100644 --- a/calendar/setup/tables_update.inc.php +++ b/calendar/setup/tables_update.inc.php @@ -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'; +} diff --git a/phpgwapi/inc/class.egw_db.inc.php b/phpgwapi/inc/class.egw_db.inc.php index 63e239ce7a..2004481d1c 100644 --- a/phpgwapi/inc/class.egw_db.inc.php +++ b/phpgwapi/inc/class.egw_db.inc.php @@ -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) *