Calendar can now store participants which are no accounts or contacts.

- as required by iCal/CalDAV/GroupDAV/SyncML
- this also fixes problems with LDAP contacts, which have non-numeric
  ids
- iCal code now converts to and from all participant types supported by
  eGroupWare: some types (eg. ressources) require that the clients keeps
  the new X-EGROUPWARE-UID attribute
- calendar UI allows to enter email addresses via the addressbook search
  box (dont type search, but direct add)
This commit is contained in:
Ralf Becker 2008-05-08 15:02:35 +00:00
parent 5d50d41004
commit cb9212e691
9 changed files with 271 additions and 151 deletions

View File

@ -185,6 +185,11 @@ class bocal
$this->resources[$data['type']] = $data + array('app' => $app);
}
}
$this->resources['e'] = array(
'type' => 'e',
'info' => 'bocal::email_info',
'app' => 'email',
);
$GLOBALS['egw']->session->appsession('resources','calendar',$this->resources);
}
//echo "registered resources="; _debug_array($this->resources);
@ -192,6 +197,37 @@ class bocal
$this->config = config::read('calendar');
}
/**
* returns info about email addresses as participants
*
* @param int/array $ids single contact-id or array of id's
* @return array
*/
static function email_info($ids)
{
if (!$ids) return null;
$data = array();
foreach(!is_array($ids) ? array($ids) : $ids as $id)
{
$email = $id;
$name = '';
if (preg_match('/^(.*) *<([a-z0-9_.@-]{8,})>$/i',$email,$matches))
{
$name = $matches[1];
$email = $matches[2];
}
$data[] = array(
'res_id' => $id,
'email' => $email,
'rights' => EGW_ACL_READ_FOR_PARTICIPANTS,
'name' => $name,
);
}
//echo "<p>email_info(".print_r($ids,true).")="; _debug_array($data);
return $data;
}
/**
* Add group-members as participants with status 'G'
*
@ -882,7 +918,29 @@ class bocal
if (!isset($res_info_cache[$uid]))
{
list($res_info_cache[$uid]) = $this->resources[$uid{0}]['info'] ? ExecMethod($this->resources[$uid{0}]['info'],substr($uid,1)) : false;
if (is_numeric($uid))
{
$info = array(
'res_id' => $uid,
'email' => $GLOBALS['egw']->accounts->id2name($uid,'account_email'),
'name' => trim($GLOBALS['egw']->accounts->id2name($uid,'account_firstname'). ' ' .
$GLOBALS['egw']->accounts->id2name($uid,'account_lastname')),
'type' => $GLOBALS['egw']->accounts->get_type($uid),
);
}
else
{
list($info) = $this->resources[$uid[0]]['info'] ? ExecMethod($this->resources[$uid[0]]['info'],substr($uid,1)) : false;
if ($info)
{
$info['type'] = $uid[0];
if (!$info['email'] && $info['responsible'])
{
$info['email'] = $GLOBALS['egw']->accounts->id2name($info['responsible'],'account_email');
}
}
}
$res_info_cache[$uid] = $info;
}
if ($this->debug && ($this->debug > 2 || $this->debug == 'resource_info'))
{
@ -1249,7 +1307,7 @@ class bocal
if ($display_day)
{
$range = lang(adodb_date('l',$first['raw'])).($this->common_prefs['dateformat']{0} != 'd' ? ' ' : ', ');
$range = lang(adodb_date('l',$first['raw'])).($this->common_prefs['dateformat'][0] != 'd' ? ' ' : ', ');
}
for ($i = 0; $i < 5; $i += 2)
{
@ -1364,7 +1422,11 @@ class bocal
{
if (!is_numeric($id))
{
$id2lid[$id] = egw_link::title($this->resources[$id{0}]['app'],substr($id,1));
$id2lid[$id] = '#'.$id;
if (($info = $this->resource_info($id)))
{
$id2lid[$id] = $info['name'] ? $info['name'] : $info['email'];
}
}
else
{
@ -1755,7 +1817,7 @@ class bocal
{
if (is_numeric($user)) $user = $GLOBALS['egw']->accounts->id2name($user);
return (!$GLOBALS['egw_info']['server']['webserver_url'] || $GLOBALS['egw_info']['server']['webserver_url']{0} == '/' ?
return (!$GLOBALS['egw_info']['server']['webserver_url'] || $GLOBALS['egw_info']['server']['webserver_url'][0] == '/' ?
($_SERVER['HTTPS'] ? 'https://' : 'http://').$_SERVER['HTTP_HOST'] : '').
$GLOBALS['egw_info']['server']['webserver_url'].'/calendar/freebusy.php?user='.urlencode($user).
($pw ? '&password='.urlencode($pw) : '');

View File

@ -155,7 +155,7 @@ class bocalupdate extends bocal
$users = array_unique($users);
}
$users[] = $uid;
if (in_array($uid{0},$types_with_quantity))
if (in_array($uid[0],$types_with_quantity))
{
$quantity[$uid] = max(1,(int) substr($status,2));
}
@ -188,19 +188,19 @@ class bocalupdate extends bocal
$common_parts = array_intersect($users,array_keys($overlap['participants']));
foreach($common_parts as $n => $uid)
{
if ($overlap['participants'][$uid]{0} == 'R')
if ($overlap['participants'][$uid][0] == 'R')
{
unset($common_parts[$uid]);
continue;
}
if (is_numeric($uid) || !in_array($uid{0},$types_with_quantity))
if (is_numeric($uid) || !in_array($uid[0],$types_with_quantity))
{
continue; // no quantity check: quantity allways 1 ==> conflict
}
if (!isset($max_quantity[$uid]))
{
$res_info = $this->resource_info($uid);
$max_quantity[$uid] = $res_info[$this->resources[$uid{0}]['max_quantity']];
$max_quantity[$uid] = $res_info[$this->resources[$uid[0]]['max_quantity']];
}
$quantity[$uid] += max(1,(int) substr($overlap['participants'][$uid],2));
if ($quantity[$uid] <= $max_quantity[$uid])
@ -741,7 +741,7 @@ class bocalupdate extends bocal
*/
function check_status_perms($uid,$event)
{
if ($uid{0} == 'c') // for contact we use the owner of the event
if ($uid[0] == 'c' || $uid['0'] == 'e') // for contact we use the owner of the event
{
if (!is_array($event) && !($event = $this->read($event))) return false;
@ -774,7 +774,7 @@ class bocalupdate extends bocal
{
return false;
}
if (($Ok = $this->so->set_status($cal_id,is_numeric($uid)?'u':$uid{0},is_numeric($uid)?$uid:substr($uid,1),$status,$recur_date ? $this->date2ts($recur_date,true) : 0)))
if (($Ok = $this->so->set_status($cal_id,is_numeric($uid)?'u':$uid[0],is_numeric($uid)?$uid:substr($uid,1),$status,$recur_date ? $this->date2ts($recur_date,true) : 0)))
{
$GLOBALS['egw']->contenthistory->updateTimeStamp('calendar',$cal_id,'modify',time());
@ -850,7 +850,7 @@ class bocalupdate extends bocal
$eventStart_arr = $this->date2array($event['start']); // give this as 'date' to the link to pick the right recurrence for the participants state
$link = $GLOBALS['egw_info']['server']['webserver_url'].'/index.php?menuaction=calendar.uiforms.edit&cal_id='.$event['id'].'&date='.$eventStart_arr['full'].'&no_popup=1';
// if url is only a path, try guessing the rest ;-)
if ($link{0} == '/')
if ($link[0] == '/')
{
$link = ($GLOBALS['egw_info']['server']['enforce_ssl'] || $_SERVER['HTTPS'] ? 'https://' : 'http://').
($GLOBALS['egw_info']['server']['hostname'] ? $GLOBALS['egw_info']['server']['hostname'] : $_SERVER['HTTP_HOST']).

View File

@ -191,14 +191,9 @@
case 'ATTENDEE':
foreach((array)$event['participants'] as $uid => $status)
{
// ToDo, this needs to deal with resources too!!!
if (!is_numeric($uid)) continue;
$mailto = $GLOBALS['egw']->accounts->id2name($uid,'account_email');
$cn = trim($GLOBALS['egw']->accounts->id2name($uid,'account_firstname'). ' ' .
$GLOBALS['egw']->accounts->id2name($uid,'account_lastname'));
if (!($info = $this->resource_info($uid))) continue;
// RB: MAILTO href contains only the email-address, NO cn!
$attributes['ATTENDEE'][] = $mailto ? 'MAILTO:'.$mailto : '';
$attributes['ATTENDEE'][] = $info['email'] ? 'MAILTO:'.$info['email'] : '';
// ROLE={CHAIR|REQ-PARTICIPANT|OPT-PARTICIPANT|NON-PARTICIPANT} NOT used by eGW atm.
$role = $uid == $event['owner'] ? 'CHAIR' : 'REQ-PARTICIPANT';
// RSVP={TRUE|FALSE} // resonse expected, not set in eGW => status=U
@ -206,7 +201,7 @@
// PARTSTAT={NEEDS-ACTION|ACCEPTED|DECLINED|TENTATIVE|DELEGATED|COMPLETED|IN-PROGRESS} everything from delegated is NOT used by eGW atm.
$status = $this->status_egw2ical[$status];
// CUTYPE={INDIVIDUAL|GROUP|RESOURCE|ROOM|UNKNOWN}
switch (is_numeric($uid) ? $GLOBALS['egw']->accounts->get_type($uid) : $uid{0})
switch ($info['type'])
{
case 'g':
$cutype = 'GROUP';
@ -214,21 +209,22 @@
case 'r':
$cutype = 'RESOURCE';
break;
case 'u':
case 'u': // account
case 'c': // contact
case 'e': // email address
$cutype = 'INDIVIDUAL';
break;
default:
$cutype = 'UNKNOWN';
$cutype = 'INDIVIDUAL';
break;
};
$parameters['ATTENDEE'][] = array(
'CN' => $cn,
'CN' => $info['name'],
'ROLE' => $role,
'PARTSTAT' => $status,
'CUTYPE' => $cutype,
'RSVP' => $rsvp,
);
)+($info['type'] != 'e' ? array('X-EGROUPWARE-UID' => $uid) : array());
}
break;
@ -563,7 +559,7 @@
break;
case 'RRULE':
$recurence = $attributes['value'];
$type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence{0};
$type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence[0];
// vCard 2.0 values for all types
if (preg_match('/UNTIL=([0-9T]+)/',$recurence,$matches))
{
@ -768,28 +764,46 @@
}
break;
case 'ATTENDEE':
if (preg_match('/MAILTO:([@.a-z0-9_-]+)/i',$attributes['value'],$matches) &&
($uid = $GLOBALS['egw']->accounts->name2id($matches[1],'account_email')))
if (preg_match('/MAILTO:([@.a-z0-9_-]+)/i',$attributes['value'],$matches) ||
preg_match('/<([@.a-z0-9_-]+)>/i',$attributes['value'],$matches))
{
$email = $matches[1];
}
elseif(strpos($attributes['value'],'@') !== false)
{
$email = $attributes['value'];
}
if (($uid = $attributes['params']['X-EGROUPWARE-UID']) &&
($info = $this->resource_info($uid)) && $info['email'] == $email)
{
// we use the (checked) X-EGROUPWARE-UID
}
/*elseif($attributes['params']['CUTYPE'] == 'RESOURCE')
{
}*/
elseif($attributes['value'] == 'Unknown')
{
$uid = $GLOBALS['egw_info']['user']['account_id'];
}
elseif (($uid = $GLOBALS['egw']->accounts->name2id($email,'account_email')))
{
// we use the account we found
}
elseif ((list($data) = ExecMethod2('addressbook.bocontacts.search',array(
'email' => $email,
'email_home' => $email,
),true,'','','',false,'OR')))
{
$uid = 'c'.$data['id'];
}
else
{
$uid = 'e'.($attributes['params']['CN'] ? $attributes['params']['CN'].' <'.$email.'>' : $email);
}
$event['participants'][$uid] = isset($attributes['params']['PARTSTAT']) ?
$this->status_ical2egw[strtoupper($attributes['params']['PARTSTAT'])] :
($uid == $event['owner'] ? 'A' : 'U');
}
if (preg_match('/<([@.a-z0-9_-]+)>/i',$attributes['value'],$matches)) {
$uid = '';
$uid = $GLOBALS['egw']->accounts->name2id($matches[1],'account_email');
if(!empty($uid)) {
$event['participants'][$uid] = isset($attributes['params']['PARTSTAT']) ?
$this->status_ical2egw[strtoupper($attributes['params']['PARTSTAT'])] :
($uid == $event['owner'] ? 'A' : 'U');
}
}
if($attributes['value'] == 'Unknown') {
$event['participants'][$GLOBALS['egw_info']['user']['account_id']] = 'A';
}
break;
case 'ORGANIZER': // will be written direct to the event
if (preg_match('/MAILTO:([@.a-z0-9_-]+)/i',$attributes['value'],$matches) &&
@ -1195,7 +1209,7 @@
break;
case 'RRULE':
$recurence = $attributes['value'];
$type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence{0};
$type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence[0];
// vCard 2.0 values for all types
if (preg_match('/UNTIL=([0-9T]+)/',$recurence,$matches))
{

View File

@ -690,8 +690,8 @@ ORDER BY cal_user_type, cal_usre_id
* combines user_type and user_id into a single string or integer (for users)
*
* @param string $user_type 1-char type: 'u' = user, ...
* @param int $user_id id
* @return string/int combined id
* @param string|int $user_id id
* @return string|int combined id
*/
function combine_user($user_type,$user_id)
{
@ -705,9 +705,9 @@ ORDER BY cal_user_type, cal_usre_id
/**
* splits the combined user_type and user_id into a single values
*
* @param string $user_type 1-char type: 'u' = user, ...
* @param int $user_id id
* @return string/int
* @param string|int $uid
* @param string &$user_type 1-char type: 'u' = user, ...
* @param string|int &$user_id id
*/
function split_user($uid,&$user_type,&$user_id)
{
@ -719,7 +719,7 @@ ORDER BY cal_user_type, cal_usre_id
else
{
$user_type = $uid[0];
$user_id = (int) substr($uid,1);
$user_id = substr($uid,1);
}
}

View File

@ -282,11 +282,11 @@ class uical
}
else // change only the owners of the given type
{
$res_type = is_numeric($set_owners[0]) ? false : $set_owners[0]{0};
$res_type = is_numeric($set_owners[0]) ? false : $set_owners[0][0];
$owners = explode(',',$states['owner'] ? $states['owner'] : $default);
foreach($owners as $key => $owner)
{
if (!$res_type && is_numeric($owner) || $res_type && $owner{0} == $res_type)
if (!$res_type && is_numeric($owner) || $res_type && $owner[0] == $res_type)
{
unset($owners[$key]);
}
@ -390,7 +390,7 @@ class uical
// icons for single user, multiple users or group(s) and resources
foreach($event['participants'] as $uid => $status)
{
if(is_numeric($uid) || $uid{0} == 'c')
if(is_numeric($uid) || !isset($this->bo->resources[$uid[0]]['icon']))
{
if (isset($icons['single']) || $GLOBALS['egw']->accounts->get_type($uid) == 'g')
{
@ -402,11 +402,11 @@ class uical
$icons['single'] = html::image('calendar','single');
}
}
elseif(!isset($icons[$uid{0}]) && isset($this->bo->resources[$uid{0}]) && isset($this->bo->resources[$uid{0}]['icon']))
elseif(!isset($icons[$uid[0]]) && isset($this->bo->resources[$uid[0]]) && isset($this->bo->resources[$uid[0]]['icon']))
{
$icons[$uid{0}] = html::image($this->bo->resources[$uid{0}]['app'],
($this->bo->resources[$uid{0}]['icon'] ? $this->bo->resources[$uid{0}]['icon'] : 'navbar'),
lang($this->bo->resources[$uid{0}]['app']),
$icons[$uid[0]] = html::image($this->bo->resources[$uid[0]]['app'],
($this->bo->resources[$uid[0]]['icon'] ? $this->bo->resources[$uid[0]]['icon'] : 'navbar'),
lang($this->bo->resources[$uid[0]]['app']),
'width="16px" height="16px"');
}
}

View File

@ -123,16 +123,16 @@ class uiforms extends uical
{
$participants[$uid] = $participant_types['u'][$uid] = $uid == $this->user ? 'A' : 'U';
}
elseif (is_array($this->bo->resources[$uid{0}]))
elseif (is_array($this->bo->resources[$uid[0]]))
{
$res_data = $this->bo->resources[$uid{0}];
$res_data = $this->bo->resources[$uid[0]];
list($id,$quantity) = explode(':',substr($uid,1));
$participants[$uid] = $participant_types[$uid{0}][$id] = ($res_data['new_status'] ? ExecMethod($res_data['new_status'],$id) : 'U').
$participants[$uid] = $participant_types[$uid[0]][$id] = ($res_data['new_status'] ? ExecMethod($res_data['new_status'],$id) : 'U').
((int) $quantity > 1 ? (int)$quantity : '');
// if new_status == 'x', resource is not bookable
if(strpos($participant_types[$uid{0}][$id],'x') !== false)
if(strpos($participant_types[$uid[0]][$id],'x') !== false)
{
unset($participant_types[$uid{0}][$id]);
unset($participant_types[$uid[0]][$id]);
unset($participants[$uid]);
}
}
@ -235,18 +235,40 @@ class uiforms extends uical
{
switch($key)
{
case 'add':
if (!$content['participants']['account'] && !$content['participants']['resource'])
{
$msg = lang('You need to select an account, contact or resource first!');
}
break;
case 'delete': // handled in default
case 'quantity': // handled in new_resource
case 'cal_resources':
break;
case 'add':
// email or rfc822 addresse (eg. "Ralf Becker <ralf@domain.com>") in the search field
// ToDo: get eTemplate to return that field
if (($email = $_POST['exec']['participants']['resource']['query']) &&
(preg_match('/^(.*<)?([a-z0-9_.@-]{8,})>?$/i',$email,$matches)))
{
// check if email belongs to account or contact --> prefer them over just emails
if (($data = $GLOBALS['egw']->accounts->name2id($matches[2],'account_email')))
{
$event['participants'][$data] = $event['participant_types']['u'][$data] = 'U';
}
elseif ((list($data) = ExecMethod2('addressbook.bocontacts.search',array(
'email' => $matches[2],
'email_home' => $matches[2],
),true,'','','',false,'OR')))
{
$event['participants']['c'.$data['id']] = $event['participant_types']['c'][$data['id']] = 'U';
}
else
{
$event['participants']['e'.$email] = $event['participant_types']['e'][$email] = 'U';
}
}
elseif (!$content['participants']['account'] && !$content['participants']['resource'])
{
$msg = lang('You need to select an account, contact or resource first!');
}
break;
case 'resource':
list($app,$id) = explode(':',$data);
foreach($this->bo->resources as $type => $data) if ($data['app'] == $app) break;
@ -288,7 +310,7 @@ class uiforms extends uical
else
{
$id = substr($uid,1);
$type = $uid{0};
$type = $uid[0];
}
if ($data['old_status'] != $status)
{
@ -563,6 +585,7 @@ class uiforms extends uical
function custom_mail($event,$added)
{
$to = array();
foreach($event['participants'] as $uid => $status)
{
if ($status == 'R' || $uid == $this->user) continue;
@ -586,17 +609,9 @@ class uiforms extends uical
$to[] = $firstname.' '.$lastname.' <'.$email.'>';
}
}
elseif ($uid{0} == 'c' )
elseif(($info = $this->bo->resource_info($uid)))
{
if (!is_object($GLOBALS['egw']->contacts))
{
require_once(EGW_API_INC.'/class.contacts.inc.php');
$GLOBALS['egw']->contacts = new contacts();
}
if (($contact = $GLOBALS['egw']->contacts->read(substr($uid,1))) && ($contact['email'] || $contact['email_home']))
{
$to[] = $contact['n_fn'].' <'.($contact['email']?$contact['email']:$contact['email_home']).'>';
}
$to[] = $info['email'];
}
}
list($subject,$body) = $this->bo->get_update_message($event,$added ? MSG_ADDED : MSG_MODIFIED); // update-message is in TZ of the user
@ -767,16 +782,26 @@ class uiforms extends uical
$preserv['participants'][$row] = $content['participants'][$row] = array(
'app' => $name == 'accounts' ? ($GLOBALS['egw']->accounts->get_type($id) == 'g' ? 'Group' : 'User') : $name,
'uid' => $uid,
'status' => $status{0},
'old_status' => $status{0},
'status' => $status[0],
'old_status' => $status[0],
'quantity' => substr($status,1),
);
$readonlys[$row.'[quantity]'] = $type == 'u' || !isset($this->bo->resources[$type]['max_quantity']);
$readonlys[$row.'[status]'] = $readonlys[$row.'[status_recurrence]'] = !$this->bo->check_status_perms($uid,$event);
$readonlys["delete[$uid]"] = !$this->bo->check_perms(EGW_ACL_EDIT,$event);
$content['participants'][$row++]['title'] = $name == 'accounts' ?
$GLOBALS['egw']->common->grab_owner_name($id) : egw_link::title($name,$id);
// todo: make the participants available as links with email as title
if ($name == 'accounts')
{
$content['participants'][$row++]['title'] = $GLOBALS['egw']->common->grab_owner_name($id);
}
elseif (($info = $this->bo->resource_info($uid)))
{
$content['participants'][$row++]['title'] = $info['name'] ? $info['name'] : $info['email'];
}
else
{
$content['participants'][$row++]['title'] = '#'.$uid;
}
// enumerate group-invitations, so people can accept/reject them
if ($name == 'accounts' && $GLOBALS['egw']->accounts->get_type($id) == 'g' &&
($members = $GLOBALS['egw']->accounts->members($id,true)))

View File

@ -12,7 +12,7 @@
/* $Id$ */
$setup_info['calendar']['name'] = 'calendar';
$setup_info['calendar']['version'] = '1.5.001';
$setup_info['calendar']['version'] = '1.5.002';
$setup_info['calendar']['app_order'] = 3;
$setup_info['calendar']['enable'] = 1;
@ -80,3 +80,4 @@

View File

@ -67,10 +67,10 @@
),
'egw_cal_user' => array(
'fd' => array(
'cal_id' => array('type' => 'int','precision' => '4','nullable' => False,'default' => '0'),
'cal_id' => array('type' => 'int','precision' => '4','nullable' => False),
'cal_recur_date' => array('type' => 'int','precision' => '8','default' => '0'),
'cal_user_type' => array('type' => 'varchar','precision' => '1','nullable' => False,'default' => 'u'),
'cal_user_id' => array('type' => 'int','precision' => '4','nullable' => False,'default' => '0'),
'cal_user_id' => array('type' => 'varchar','precision' => '128','nullable' => False),
'cal_status' => array('type' => 'char','precision' => '1','default' => 'A'),
'cal_quantity' => array('type' => 'int','precision' => '4','default' => '1')
),

View File

@ -1599,4 +1599,22 @@
return $GLOBALS['setup_info']['calendar']['currentver'] = '1.5.001';
}
$test[] = '1.5.001';
function calendar_upgrade1_5_001()
{
$GLOBALS['egw_setup']->oProc->AlterColumn('egw_cal_user','cal_id',array(
'type' => 'int',
'precision' => '4',
'nullable' => False
));
$GLOBALS['egw_setup']->oProc->AlterColumn('egw_cal_user','cal_user_id',array(
'type' => 'varchar',
'precision' => '128',
'nullable' => False
));
return $GLOBALS['setup_info']['calendar']['currentver'] = '1.5.002';
}
?>