forked from extern/egroupware
55fc673dc5
users and groups: * No: Every user can invite other users and groups (default and old behavior) * Groups: other users can allways be invited, only groups require an invite_grant * Users + groups: inviting both allways requires an invite grant One need to keep in mind, that setting an invitation ACL via a group, gives each groupmember the right to invite the group / create a group event. So the last option propable only works, if users manage invitations grants on their own, or admin only sets it in small working groups, where every member is allowed to invite the whole group. --> calendar backend code removes participants a user is not allowed to invite - new "only groupevents" filter, showing only real groupenvents not events of groupmembers (added tooltips to explain filters)
1599 lines
55 KiB
PHP
1599 lines
55 KiB
PHP
<?php
|
|
/**
|
|
* eGroupWare - Calendar's buisness-object - access only
|
|
*
|
|
* @link http://www.egroupware.org
|
|
* @package calendar
|
|
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
|
|
* @author Joerg Lehrke <jlehrke@noc.de>
|
|
* @copyright (c) 2004-9 by RalfBecker-At-outdoor-training.de
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @version $Id$
|
|
*/
|
|
|
|
if (!defined('ACL_TYPE_IDENTIFER')) // used to mark ACL-values for the debug_message methode
|
|
{
|
|
define('ACL_TYPE_IDENTIFER','***ACL***');
|
|
}
|
|
|
|
/**
|
|
* Gives read access to the calendar, but all events the user is not participating are private!
|
|
* Used by addressbook.
|
|
*/
|
|
define('EGW_ACL_READ_FOR_PARTICIPANTS',EGW_ACL_CUSTOM_1);
|
|
define('EGW_ACL_FREEBUSY',EGW_ACL_CUSTOM_2);
|
|
/**
|
|
* Allows to invite an other user (if configured to be used!)
|
|
*/
|
|
define('EGW_ACL_INVITE',EGW_ACL_CUSTOM_3);
|
|
|
|
/**
|
|
* Required (!) include, as we use the MCAL_* constants, BEFORE instanciating (and therefore autoloading) the class
|
|
*/
|
|
require_once(EGW_INCLUDE_ROOT.'/calendar/inc/class.calendar_so.inc.php');
|
|
|
|
/**
|
|
* Class to access all calendar data
|
|
*
|
|
* For updating calendar data look at the bocalupdate class, which extends this class.
|
|
*
|
|
* The new UI, BO and SO classes have a strikt definition, in which time-zone they operate:
|
|
* UI only operates in user-time, so there have to be no conversation at all !!!
|
|
* BO's functions take and return user-time only (!), they convert internaly everything to servertime, because
|
|
* SO operates only in server-time
|
|
*
|
|
* As this BO class deals with dates/times of several types and timezone, each variable should have a postfix
|
|
* appended, telling with type it is: _s = seconds, _su = secs in user-time, _ss = secs in server-time, _h = hours
|
|
*
|
|
* All new BO code (should be true for eGW in general) NEVER use any $_REQUEST ($_POST or $_GET) vars itself.
|
|
* Nor does it store the state of any UI-elements (eg. cat-id selectbox). All this is the task of the UI class(es) !!!
|
|
*
|
|
* All permanent debug messages of the calendar-code should done via the debug-message method of this class !!!
|
|
*/
|
|
class calendar_bo
|
|
{
|
|
/**
|
|
* @var int $debug name of method to debug or level of debug-messages:
|
|
* False=Off as higher as more messages you get ;-)
|
|
* 1 = function-calls incl. parameters to general functions like search, read, write, delete
|
|
* 2 = function-calls to exported helper-functions like check_perms
|
|
* 4 = function-calls to exported conversation-functions like date2ts, date2array, ...
|
|
* 5 = function-calls to private functions
|
|
*/
|
|
var $debug=false;
|
|
|
|
/**
|
|
* @var int $now_su timestamp of actual user-time
|
|
*/
|
|
var $now_su;
|
|
|
|
/**
|
|
* @var array $cal_prefs calendar-specific prefs
|
|
*/
|
|
var $cal_prefs;
|
|
|
|
/**
|
|
* @var array $common_prefs common preferences
|
|
*/
|
|
var $common_prefs;
|
|
|
|
/**
|
|
* @var int $user nummerical id of the current user-id
|
|
*/
|
|
var $user=0;
|
|
|
|
/**
|
|
* @var array $grants grants of the current user, array with user-id / ored-ACL-rights pairs
|
|
*/
|
|
var $grants=array();
|
|
|
|
/**
|
|
* @var array $verbose_status translated 1-char status values to a verbose name, run through lang() by the constructor
|
|
*/
|
|
var $verbose_status = array(
|
|
'A' => 'Accepted',
|
|
'R' => 'Rejected',
|
|
'T' => 'Tentative',
|
|
'U' => 'No Response',
|
|
'G' => 'Group invitation',
|
|
);
|
|
/**
|
|
* @var array recur_types translates MCAL recur-types to verbose labels
|
|
*/
|
|
var $recur_types = Array(
|
|
MCAL_RECUR_NONE => 'None',
|
|
MCAL_RECUR_DAILY => 'Daily',
|
|
MCAL_RECUR_WEEKLY => 'Weekly',
|
|
MCAL_RECUR_MONTHLY_WDAY => 'Monthly (by day)',
|
|
MCAL_RECUR_MONTHLY_MDAY => 'Monthly (by date)',
|
|
MCAL_RECUR_YEARLY => 'Yearly'
|
|
);
|
|
/**
|
|
* @var array recur_days translates MCAL recur-days to verbose labels
|
|
*/
|
|
var $recur_days = array(
|
|
MCAL_M_MONDAY => 'Monday',
|
|
MCAL_M_TUESDAY => 'Tuesday',
|
|
MCAL_M_WEDNESDAY => 'Wednesday',
|
|
MCAL_M_THURSDAY => 'Thursday',
|
|
MCAL_M_FRIDAY => 'Friday',
|
|
MCAL_M_SATURDAY => 'Saturday',
|
|
MCAL_M_SUNDAY => 'Sunday',
|
|
);
|
|
/**
|
|
* Standard iCal attendee roles
|
|
*
|
|
* @var array
|
|
*/
|
|
var $roles = array(
|
|
'REQ-PARTICIPANT' => 'Requested',
|
|
'CHAIR' => 'Chair',
|
|
'OPT-PARTICIPANT' => 'Optional',
|
|
'NON-PARTICIPANT' => 'None',
|
|
);
|
|
/**
|
|
* @var array $resources registered scheduling resources of the calendar (gets chached in the session for performance reasons)
|
|
*/
|
|
var $resources;
|
|
/**
|
|
* @var array $cached_event here we do some caching to read single events only once
|
|
*/
|
|
protected static $cached_event = array();
|
|
protected static $cached_event_date_format = false;
|
|
protected static $cached_event_date = 0;
|
|
/**
|
|
* @var array $cached_holidays holidays plus birthdays (gets cached in the session for performance reasons)
|
|
*/
|
|
var $cached_holidays;
|
|
/**
|
|
* Instance of the socal class
|
|
*
|
|
* @var calendar_so
|
|
*/
|
|
var $so;
|
|
/**
|
|
* Instance of the datetime class
|
|
*
|
|
* @var egw_datetime
|
|
*/
|
|
var $datetime;
|
|
|
|
/**
|
|
* Does a user require an extra invite grant, to be able to invite an other user, default no
|
|
*
|
|
* @var boolean
|
|
*/
|
|
public $require_acl_invite = false;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
function __construct()
|
|
{
|
|
if ($this->debug > 0) $this->debug_message('bocal::bocal() started',True,$param);
|
|
|
|
$this->so = new calendar_so();
|
|
$this->datetime = $GLOBALS['egw']->datetime;
|
|
|
|
$this->common_prefs =& $GLOBALS['egw_info']['user']['preferences']['common'];
|
|
$this->cal_prefs =& $GLOBALS['egw_info']['user']['preferences']['calendar'];
|
|
|
|
$this->now_su = egw_time::to('now','ts');
|
|
|
|
$this->user = $GLOBALS['egw_info']['user']['account_id'];
|
|
|
|
$this->grants = $GLOBALS['egw']->acl->get_grants('calendar');
|
|
|
|
foreach($this->verbose_status as $status => $text)
|
|
{
|
|
$this->verbose_status[$status] = lang($text);
|
|
}
|
|
if (!is_array($this->resources = $GLOBALS['egw']->session->appsession('resources','calendar')))
|
|
{
|
|
$this->resources = array();
|
|
foreach($GLOBALS['egw']->hooks->process('calendar_resources') as $app => $data)
|
|
{
|
|
if ($data && $data['type'])
|
|
{
|
|
$this->resources[$data['type']] = $data + array('app' => $app);
|
|
}
|
|
}
|
|
$this->resources['e'] = array(
|
|
'type' => 'e',
|
|
'info' => __CLASS__.'::email_info',
|
|
'app' => 'email',
|
|
);
|
|
$GLOBALS['egw']->session->appsession('resources','calendar',$this->resources);
|
|
}
|
|
//echo "registered resources="; _debug_array($this->resources);
|
|
|
|
$this->config = config::read('calendar'); // only used for horizont, regular calendar config is under phpgwapi
|
|
|
|
$this->require_acl_invite = $GLOBALS['egw_info']['server']['require_acl_invite'];
|
|
}
|
|
|
|
/**
|
|
* 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'
|
|
*
|
|
* @param array $event event-array
|
|
* @return int number of added participants
|
|
*/
|
|
function enum_groups(&$event)
|
|
{
|
|
$added = 0;
|
|
foreach($event['participants'] as $uid => $status)
|
|
{
|
|
if (is_numeric($uid) && $GLOBALS['egw']->accounts->get_type($uid) == 'g' &&
|
|
($members = $GLOBALS['egw']->accounts->member($uid)))
|
|
{
|
|
foreach($members as $member)
|
|
{
|
|
$member = $member['account_id'];
|
|
if (!isset($event['participants'][$member]))
|
|
{
|
|
$event['participants'][$member] = 'G';
|
|
++$added;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $added;
|
|
}
|
|
|
|
/**
|
|
* Searches / lists calendar entries, including repeating ones
|
|
*
|
|
* @param array $params array with the following keys
|
|
* start date startdate of the search/list, defaults to today
|
|
* end date enddate of the search/list, defaults to start + one day
|
|
* users mixed integer user-id or array of user-id's to use, defaults to the current user
|
|
* cat_id mixed category-id or array of cat-id's, defaults to all if unset, 0 or False
|
|
* Please note: only a single cat-id, will include all sub-cats (if the common-pref 'cats_no_subs' is False)
|
|
* filter string all (not rejected), accepted, unknown, tentative, rejected or hideprivate
|
|
* query string pattern so search for, if unset or empty all matching entries are returned (no search)
|
|
* Please Note: a search never returns repeating events more then once AND does not honor start+end date !!!
|
|
* dayswise boolean on True it returns an array with YYYYMMDD strings as keys and an array with events
|
|
* (events spanning multiple days are returned each day again (!)) otherwise it returns one array with
|
|
* the events (default), not honored in a search ==> always returns an array of events !
|
|
* date_format string date-formats: 'ts'=timestamp (default), 'array'=array, or string with format for date
|
|
* offset boolean/int false (default) to return all entries or integer offset to return only a limited result
|
|
* enum_recuring boolean if true or not set (default) or daywise is set, each recurence of a recuring events is returned,
|
|
* otherwise the original recuring event (with the first start- + enddate) is returned
|
|
* num_rows int number of entries to return, default or if 0, max_entries from the prefs
|
|
* order column-names plus optional DESC|ASC separted by comma
|
|
* ignore_acl if set and true no check_perms for a general EGW_ACL_READ grants is performed
|
|
* enum_groups boolean if set and true, group-members will be added as participants with status 'G'
|
|
* cols string|array columns to select, if set an iterator will be returned
|
|
* append string to append to the query, eg. GROUP BY
|
|
* cfs array if set, query given custom fields or all for empty array, none are returned, if not set (default)
|
|
* @return iterator|array|boolean array of events or array with YYYYMMDD strings / array of events pairs (depending on $daywise param)
|
|
* or false if there are no read-grants from _any_ of the requested users or iterator/recordset if cols are given
|
|
*/
|
|
function &search($params)
|
|
{
|
|
$params_in = $params;
|
|
|
|
unset($params['sql_filter']); // dont allow to set it via UI or xmlrpc
|
|
|
|
// check if any resource wants to hook into
|
|
foreach($this->resources as $app => $data)
|
|
{
|
|
if (isset($data['search_filter']))
|
|
{
|
|
$params = ExecMethod($data['search_filter'],$params);
|
|
}
|
|
}
|
|
|
|
if (!isset($params['users']) || !$params['users'] ||
|
|
count($params['users']) == 1 && isset($params['users'][0]) && !$params['users'][0]) // null or '' casted to an array
|
|
{
|
|
// for a search use all account you have read grants from
|
|
$params['users'] = $params['query'] ? array_keys($this->grants) : $this->user;
|
|
}
|
|
if (!is_array($params['users']))
|
|
{
|
|
$params['users'] = $params['users'] ? array($params['users']) : array();
|
|
}
|
|
// only query calendars of users, we have READ-grants from
|
|
$users = array();
|
|
foreach($params['users'] as $user)
|
|
{
|
|
if ($params['ignore_acl'] || $this->check_perms(EGW_ACL_READ|EGW_ACL_READ_FOR_PARTICIPANTS|EGW_ACL_FREEBUSY,0,$user))
|
|
{
|
|
if ($user && !in_array($user,$users)) // already added?
|
|
{
|
|
$users[] = $user;
|
|
}
|
|
}
|
|
elseif ($GLOBALS['egw']->accounts->get_type($user) != 'g')
|
|
{
|
|
continue; // for non-groups (eg. users), we stop here if we have no read-rights
|
|
}
|
|
// the further code is only for real users
|
|
if (!is_numeric($user)) continue;
|
|
|
|
// for groups we have to include the members
|
|
if ($GLOBALS['egw']->accounts->get_type($user) == 'g')
|
|
{
|
|
if ($params['filter'] == 'no-enum-groups') continue;
|
|
|
|
$members = $GLOBALS['egw']->accounts->member($user);
|
|
if (is_array($members))
|
|
{
|
|
foreach($members as $member)
|
|
{
|
|
// use only members which gave the user a read-grant
|
|
if (!in_array($member['account_id'],$users) &&
|
|
($params['ignore_acl'] || $this->check_perms(EGW_ACL_READ|EGW_ACL_FREEBUSY,0,$member['account_id'])))
|
|
{
|
|
$users[] = $member['account_id'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else // for users we have to include all the memberships, to get the group-events
|
|
{
|
|
$memberships = $GLOBALS['egw']->accounts->membership($user);
|
|
if (is_array($memberships))
|
|
{
|
|
foreach($memberships as $group)
|
|
{
|
|
if (!in_array($group['account_id'],$users))
|
|
{
|
|
$users[] = $group['account_id'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// replace (by so not understood filter 'no-enum-groups' with 'default' filter
|
|
if ($params['filter'] == 'no-enum-groups')
|
|
{
|
|
$params['filter'] = 'default';
|
|
}
|
|
// if we have no grants from the given user(s), we directly return no events / an empty array,
|
|
// as calling the so-layer without users would give the events of all users (!)
|
|
if (!count($users))
|
|
{
|
|
return false;
|
|
}
|
|
if (isset($params['start'])) $start = $this->date2ts($params['start']);
|
|
|
|
if (isset($params['end']))
|
|
{
|
|
$end = $this->date2ts($params['end']);
|
|
$this->check_move_horizont($end);
|
|
}
|
|
$daywise = !isset($params['daywise']) ? False : !!$params['daywise'];
|
|
$enum_recuring = $daywise || !isset($params['enum_recuring']) || !!$params['enum_recuring'];
|
|
$cat_id = isset($params['cat_id']) ? $params['cat_id'] : 0;
|
|
$filter = isset($params['filter']) ? $params['filter'] : 'all';
|
|
$offset = isset($params['offset']) && $params['offset'] !== false ? (int) $params['offset'] : false;
|
|
if ($this->debug && ($this->debug > 1 || $this->debug == 'search'))
|
|
{
|
|
$this->debug_message('bocal::search(%1) start=%2, end=%3, daywise=%4, cat_id=%5, filter=%6, query=%7, offset=%8, num_rows=%9, order=%10, sql_filter=%11)',
|
|
True,$params,$start,$end,$daywise,$cat_id,$filter,$params['query'],$offset,(int)$params['num_rows'],$params['order'],$params['sql_filter']);
|
|
}
|
|
// date2ts(,true) converts to server time, db2data converts again to user-time
|
|
$events =& $this->so->search(isset($start) ? $this->date2ts($start,true) : null,isset($end) ? $this->date2ts($end,true) : null,
|
|
$users,$cat_id,$filter,$params['query'],$offset,(int)$params['num_rows'],$params['order'],$params['sql_filter'],$params['cols'],$params['append'],$params['cfs']);
|
|
|
|
if (isset($params['cols']))
|
|
{
|
|
return $events;
|
|
}
|
|
$this->total = $this->so->total;
|
|
$this->db2data($events,isset($params['date_format']) ? $params['date_format'] : 'ts');
|
|
|
|
// socal::search() returns rejected group-invitations, as only the user not also the group is rejected
|
|
// as we cant remove them efficiantly in SQL, we kick them out here, but only if just one user is displayed
|
|
$remove_rejected_by_user = !in_array($filter,array('all','rejected')) && count($params['users']) == 1 ? $params['users'][0] : false;
|
|
//echo "<p align=right>remove_rejected_by_user=$remove_rejected_by_user, filter=$filter, params[users]=".print_r($param['users'])."</p>\n";
|
|
foreach($events as $id => $event)
|
|
{
|
|
if ($remove_rejected_by_user && $event['participants'][$remove_rejected_by_user] == 'R')
|
|
{
|
|
unset($events[$id]); // remove the rejected event
|
|
$this->total--;
|
|
continue;
|
|
}
|
|
if ($params['enum_groups'] && $this->enum_groups($event))
|
|
{
|
|
$events[$id] = $event;
|
|
}
|
|
if (!$this->check_perms(EGW_ACL_READ,$event) || (!$event['public'] && $filter == 'hideprivate'))
|
|
{
|
|
if($params['query'])
|
|
{
|
|
unset($events[$id]);
|
|
$this->total--;
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
$this->clear_private_infos($events[$id],$users);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($daywise)
|
|
{
|
|
if ($this->debug && ($this->debug > 2 || $this->debug == 'search'))
|
|
{
|
|
$this->debug_message('socalendar::search daywise sorting from %1 to %2 of %3',False,$start,$end,$events);
|
|
}
|
|
// create empty entries for each day in the reported time
|
|
for($ts = $start; $ts <= $end; $ts += DAY_s)
|
|
{
|
|
$daysEvents[$this->date2string($ts)] = array();
|
|
}
|
|
foreach($events as $k => $event)
|
|
{
|
|
$e_start = max($this->date2ts($event['start']),$start);
|
|
// $event['end']['raw']-1 to allow events to end on a full hour/day without the need to enter it as minute=59
|
|
$e_end = min($this->date2ts($event['end'])-1,$end);
|
|
|
|
// add event to each day in the reported time
|
|
for($ts = $e_start; $ts <= $e_end; $ts += DAY_s)
|
|
{
|
|
$daysEvents[$ymd = $this->date2string($ts)][] =& $events[$k];
|
|
}
|
|
if ($ymd != ($last = $this->date2string($e_end)))
|
|
{
|
|
$daysEvents[$last][] =& $events[$k];
|
|
}
|
|
}
|
|
$events =& $daysEvents;
|
|
if ($this->debug && ($this->debug > 2 || $this->debug == 'search'))
|
|
{
|
|
$this->debug_message('socalendar::search daywise events=%1',False,$events);
|
|
}
|
|
}
|
|
elseif(!$enum_recuring)
|
|
{
|
|
$recur_ids = array();
|
|
foreach($events as $k => $event)
|
|
{
|
|
if ($event['recur_type'] != MCAL_RECUR_NONE)
|
|
{
|
|
if (!in_array($event['id'],$recur_ids))
|
|
{
|
|
$recur_ids[] = $event['id'];
|
|
}
|
|
unset($events[$k]);
|
|
}
|
|
}
|
|
if (count($recur_ids))
|
|
{
|
|
$events = array_merge($this->read($recur_ids,null,false,$params['date_format']),$events);
|
|
}
|
|
}
|
|
if ($this->debug && ($this->debug > 0 || $this->debug == 'search'))
|
|
{
|
|
$this->debug_message('bocal::search(%1)=%2',True,$params,$events);
|
|
}
|
|
return $events;
|
|
}
|
|
|
|
/**
|
|
* Clears all non-private info from a privat event
|
|
*
|
|
* That function only returns the infos allowed to be viewed by people without EGW_ACL_PRIVATE grants
|
|
*
|
|
* @param array &$event
|
|
* @param array $allowed_participants ids of the allowed participants, eg. the ones the search is over or eg. the owner of the calendar
|
|
*/
|
|
function clear_private_infos(&$event,$allowed_participants = array())
|
|
{
|
|
$event = array(
|
|
'id' => $event['id'],
|
|
'start' => $event['start'],
|
|
'end' => $event['end'],
|
|
'title' => lang('private'),
|
|
'participants' => array_intersect_key($event['participants'],array_flip($allowed_participants)),
|
|
'public'=> 0,
|
|
'category' => $event['category'], // category is visible anyway, eg. by using planner by cat
|
|
'non_blocking' => $event['non_blocking'],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* check and evtl. move the horizont (maximum date for unlimited recuring events) to a new date
|
|
*
|
|
* @internal automaticaly called by search
|
|
* @param mixed $new_horizont time to set the horizont to (user-time)
|
|
*/
|
|
function check_move_horizont($new_horizont)
|
|
{
|
|
if ((int) $this->debug >= 2 || $this->debug == 'check_move_horizont')
|
|
{
|
|
$this->debug_message('bocal::check_move_horizont(%1) horizont=%2',true,$new_horizont,$this->config['horizont']);
|
|
}
|
|
$new_horizont = $this->date2ts($new_horizont,true); // now we are in server-time, where this function operates
|
|
|
|
if ($new_horizont > time()+1000*DAY_s) // some user tries to "look" more then 1000 days in the future
|
|
{
|
|
if ($this->debug == 'check_move_horizont') $this->debug_message('bocal::check_move_horizont(%1) horizont=%2 new horizont more then 1000 days from now --> ignoring it',true,$new_horizont,$this->config['horizont']);
|
|
return;
|
|
}
|
|
if ($new_horizont <= $this->config['horizont']) // no move necessary
|
|
{
|
|
if ($this->debug == 'check_move_horizont') $this->debug_message('bocal::check_move_horizont(%1) horizont=%2 is bigger ==> nothing to do',true,$new_horizont,$this->config['horizont']);
|
|
return;
|
|
}
|
|
if ($new_horizont < time()+31*DAY_s)
|
|
{
|
|
$new_horizont = time()+31*DAY_s;
|
|
}
|
|
$old_horizont = $this->config['horizont'];
|
|
$this->config['horizont'] = $new_horizont;
|
|
|
|
// create further recurrences for all recuring and not yet (at the old horizont) ended events
|
|
if (($recuring = $this->so->unfinished_recuring($old_horizont)))
|
|
{
|
|
foreach($this->read(array_keys($recuring)) as $cal_id => $event)
|
|
{
|
|
if ($this->debug == 'check_move_horizont')
|
|
{
|
|
$this->debug_message('bocal::check_move_horizont(%1): calling set_recurrences(%2,%3)',true,$new_horizont,$event,$old_horizont);
|
|
}
|
|
// insert everything behind max(cal_start), which can be less then $old_horizont because of bugs in the past
|
|
$this->set_recurrences($event,egw_time::server2user($recuring[$cal_id]+1)); // set_recurences operates in user-time!
|
|
}
|
|
}
|
|
// update the horizont
|
|
$config = CreateObject('phpgwapi.config','calendar');
|
|
$config->save_value('horizont',$this->config['horizont'],'calendar');
|
|
|
|
if ($this->debug == 'check_move_horizont') $this->debug_message('bocal::check_move_horizont(%1) new horizont=%2, exiting',true,$new_horizont,$this->config['horizont']);
|
|
}
|
|
|
|
/**
|
|
* set all recurrences for an event until defined horizont $this->config['horizont']
|
|
*
|
|
* This methods operates in usertime, while $this->config['horizont'] is in servertime!
|
|
*
|
|
* @param array $event
|
|
* @param mixed $start=0 minimum start-time for new recurrences or !$start = since the start of the event
|
|
*/
|
|
function set_recurrences($event,$start=0)
|
|
{
|
|
if ($this->debug && ((int) $this->debug >= 2 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont'))
|
|
{
|
|
$this->debug_message('bocal::set_recurrences(%1,%2)',true,$event,$start);
|
|
}
|
|
// check if the caller gave the participants and if not read them from the DB
|
|
if (!isset($event['participants']))
|
|
{
|
|
list(,$event_read) = each($this->so->read($event['id']));
|
|
$event['participants'] = $event_read['participants'];
|
|
}
|
|
if (!$start) $start = $event['start'];
|
|
|
|
$events = array();
|
|
$this->insert_all_recurrences($event,$start,$this->date2usertime($this->config['horizont']),$events);
|
|
|
|
$days = $this->so->get_recurrence_exceptions($event); // content of array is in server-time!
|
|
//error_log('set_recurrences: days=' . array2string($days) );
|
|
foreach($events as $event)
|
|
{
|
|
$start_servertime = $this->date2ts($event['start'],true);
|
|
if (in_array($start_servertime, (array)$days))
|
|
{
|
|
// we don't change the stati of recurrence exceptions
|
|
$event['participants'] = array();
|
|
}
|
|
$this->so->recurrence($event['id'],$start_servertime,$this->date2ts($event['end'],true),$event['participants']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* convert data read from the db, eg. convert server to user-time
|
|
*
|
|
* @param array &$events array of event-arrays (reference)
|
|
* @param $date_format='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format
|
|
*/
|
|
function db2data(&$events,$date_format='ts')
|
|
{
|
|
if (!is_array($events)) echo "<p>bocal::db2data(\$events,$date_format) \$events is no array<br />\n".function_backtrace()."</p>\n";
|
|
foreach($events as $id => &$event)
|
|
{
|
|
// convert timezone id of event to tzid (iCal id like 'Europe/Berlin')
|
|
unset($event_timezone);
|
|
if (!$event['tz_id'] || !($event['tzid'] = calendar_timezones::id2tz($event['tz_id'])))
|
|
{
|
|
$event['tzid'] = egw_time::$server_timezone->getName();
|
|
}
|
|
// we convert here from the server-time timestamps to user-time and (optional) to a different date-format!
|
|
foreach(array('start','end','modified','created','recur_enddate','recurrence') as $ts)
|
|
{
|
|
if (empty($event[$ts])) continue;
|
|
|
|
$event[$ts] = $this->date2usertime($event[$ts],$date_format);
|
|
}
|
|
// same with the recur exceptions
|
|
if (isset($event['recur_exception']) && is_array($event['recur_exception']))
|
|
{
|
|
foreach($event['recur_exception'] as $n => &$date)
|
|
{
|
|
$date = $this->date2usertime($date,$date_format);
|
|
}
|
|
}
|
|
// same with the alarms
|
|
if (isset($event['alarm']) && is_array($event['alarm']))
|
|
{
|
|
foreach($event['alarm'] as $n => &$alarm)
|
|
{
|
|
$alarm['time'] = $this->date2usertime($alarm['time'],$date_format);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* convert a date from server to user-time
|
|
*
|
|
* @param int $ts timestamp in server-time
|
|
* @param string $date_format='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format
|
|
* @return mixed depending of $date_format
|
|
*/
|
|
function date2usertime($ts,$date_format='ts')
|
|
{
|
|
if (empty($ts) || $date_format == 'server') return $ts;
|
|
|
|
return egw_time::server2user($ts,$date_format);
|
|
}
|
|
|
|
/**
|
|
* Reads a calendar-entry
|
|
*
|
|
* @param int|array|string $ids id or array of id's of the entries to read, or string with a single uid
|
|
* @param mixed $date=null date to specify a single event of a series
|
|
* @param boolean $ignore_acl should we ignore the acl, default False for a single id, true for multiple id's
|
|
* @param string $date_format='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in servertime, 'array'=array, or string with date-format
|
|
* @return boolean/array event or array of id => event pairs, false if the acl-check went wrong, null if $ids not found
|
|
*/
|
|
function read($ids,$date=null,$ignore_acl=False,$date_format='ts')
|
|
{
|
|
if ($date) $date = $this->date2ts($date);
|
|
|
|
if ($ignore_acl || is_array($ids) || ($return = $this->check_perms(EGW_ACL_READ,$ids,0,$date_format,$date)))
|
|
{
|
|
if (is_array($ids) || !isset(self::$cached_event['id']) || self::$cached_event['id'] != $ids ||
|
|
self::$cached_event_date_format != $date_format ||
|
|
self::$cached_event['recur_type'] != MCAL_RECUR_NONE && !is_null($date) && self::$cached_event_date != $date || (!$date || self::$cached_event['start'] < $date))
|
|
{
|
|
$events = $this->so->read($ids,$date ? $this->date2ts($date,true) : 0);
|
|
|
|
if ($events)
|
|
{
|
|
$this->db2data($events,$date_format);
|
|
|
|
if (is_array($ids))
|
|
{
|
|
$return =& $events;
|
|
}
|
|
else
|
|
{
|
|
self::$cached_event = array_shift($events);
|
|
self::$cached_event_date_format = $date_format;
|
|
self::$cached_event_date = $date;
|
|
$return =& self::$cached_event;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$return =& self::$cached_event;
|
|
}
|
|
}
|
|
if ($this->debug && ($this->debug > 1 || $this->debug == 'read'))
|
|
{
|
|
$this->debug_message('bocal::read(%1,%2,%3,%4)=%5',True,$ids,$date,$ignore_acl,$date_format,$return);
|
|
}
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Inserts all repetions of $event in the timespan between $start and $end into $events
|
|
*
|
|
* The new entries are just appended to $events, so $events is no longer sorted by startdate !!!
|
|
*
|
|
* Recurrences get calculated by rrule iterator implemented in calendar_rrule class.
|
|
*
|
|
* @param array $event repeating event whos repetions should be inserted
|
|
* @param mixed $start start-date
|
|
* @param mixed $end end-date
|
|
* @param array $events where the repetions get inserted
|
|
* @param array $recur_exceptions with date (in Ymd) as key (and True as values), seems not to be used anymore
|
|
*/
|
|
function insert_all_recurrences($event,$start,$end,&$events)
|
|
{
|
|
if ((int) $this->debug >= 3 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_recurrences')
|
|
{
|
|
$this->debug_message(__METHOD__.'(%1,%2,%3,&$event)',true,$event,$start,$end);
|
|
}
|
|
$start_in = $start; $end_in = $end;
|
|
|
|
$start = $this->date2ts($start);
|
|
$event_start_ts = $this->date2ts($event['start']);
|
|
$event_length = $this->date2ts($event['end']) - $event_start_ts; // we use a constant event-length, NOT a constant end-time!
|
|
|
|
// if $end is before recur_enddate, use it instead
|
|
if (!$event['recur_enddate'] || $this->date2ts($event['recur_enddate']) > $this->date2ts($end))
|
|
{
|
|
//echo "<p>recur_enddate={$event['recur_enddate']}=".egw_time::to($event['recur_enddate'])." > end=$end=".egw_time::to($end)." --> using end instead of recur_enddate</p>\n";
|
|
$event['recur_enddate'] = $end;
|
|
}
|
|
// loop over all recurrences and insert them, if they are after $start
|
|
$rrule = calendar_rrule::event2rrule($event,true); // true = we operate in usertime, like the rest of calendar_bo
|
|
foreach($rrule as $time)
|
|
{
|
|
$time->setUser(); // $time is in timezone of event, convert it to usertime used here
|
|
if (($ts = $this->date2ts($time)) < $start-$event_length)
|
|
{
|
|
//echo "<p>".$time." --> ignored as $ts < $start-$event_length</p>\n";
|
|
continue; // to early or original event (returned by interator too)
|
|
}
|
|
//echo "<p>".$time." --> adding recurrence</p>\n";
|
|
$event['start'] = $ts;
|
|
$event['end'] = $ts + $event_length;
|
|
$events[] = $event;
|
|
}
|
|
if ($this->debug && ((int) $this->debug > 2 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_recurrences'))
|
|
{
|
|
$event['start'] = $event_start_ts;
|
|
$event['end'] = $event_start_ts + $event_length;
|
|
$this->debug_message(__METHOD__.'(%1,start=%2,end=%3,events) events=%5',True,$event,$start_in,$end_in,$events);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds one repetion of $event for $date_ymd to the $events array, after adjusting its start- and end-time
|
|
*
|
|
* @param array $events array in which the event gets inserted
|
|
* @param array $event event to insert, it has start- and end-date of the first recurrence, not of $date_ymd
|
|
* @param int|string $date_ymd of the date of the event
|
|
*/
|
|
function add_adjusted_event(&$events,$event,$date_ymd)
|
|
{
|
|
$event_in = $event;
|
|
// calculate the new start- and end-time
|
|
$length_s = $this->date2ts($event['end']) - $this->date2ts($event['start']);
|
|
$event_start_arr = $this->date2array($event['start']);
|
|
|
|
$date_arr = $this->date2array((string) $date_ymd);
|
|
$date_arr['hour'] = $event_start_arr['hour'];
|
|
$date_arr['minute'] = $event_start_arr['minute'];
|
|
$date_arr['second'] = $event_start_arr['second'];
|
|
unset($date_arr['raw']); // else date2ts would use it
|
|
$event['start'] = $this->date2ts($date_arr);
|
|
$event['end'] = $event['start'] + $length_s;
|
|
|
|
$events[] = $event;
|
|
|
|
if ($this->debug && ($this->debug > 2 || $this->debug == 'add_adjust_event'))
|
|
{
|
|
$this->debug_message('bocal::add_adjust_event(,%1,%2) as %3',True,$event_in,$date_ymd,$event);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch information about a resource
|
|
*
|
|
* We do some caching here, as the resource itself might not do it.
|
|
*
|
|
* @param string $uid string with one-letter resource-type and numerical resource-id, eg. "r19"
|
|
* @return array|boolean array with keys res_id,cat_id,name,useable (name definied by max_quantity in $this->resources),rights,responsible or false if $uid is not found
|
|
*/
|
|
function resource_info($uid)
|
|
{
|
|
static $res_info_cache = array();
|
|
|
|
if (!isset($res_info_cache[$uid]))
|
|
{
|
|
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'))
|
|
{
|
|
$this->debug_message('bocal::resource_info(%1) = %2',True,$uid,$res_info_cache[$uid]);
|
|
}
|
|
return $res_info_cache[$uid];
|
|
}
|
|
|
|
/**
|
|
* Checks if the current user has the necessary ACL rights
|
|
*
|
|
* The check is performed on an event or generally on the cal of an other user
|
|
*
|
|
* Note: Participating in an event is considered as haveing read-access on that event,
|
|
* even if you have no general read-grant from that user.
|
|
*
|
|
* @param int $needed necessary ACL right: EGW_ACL_{READ|EDIT|DELETE}
|
|
* @param mixed $event event as array or the event-id or 0 for a general check
|
|
* @param int $other uid to check (if event==0) or 0 to check against $this->user
|
|
* @param string $date_format='ts' date-format used for reading: 'ts'=timestamp, 'array'=array, 'string'=iso8601 string for xmlrpc
|
|
* @param mixed $date_to_read=null date used for reading, internal param for the caching
|
|
* @return boolean true permission granted, false for permission denied or null if event not found
|
|
*/
|
|
function check_perms($needed,$event=0,$other=0,$date_format='ts',$date_to_read=null)
|
|
{
|
|
$event_in = $event;
|
|
if ($other && !is_numeric($other))
|
|
{
|
|
$resource = $this->resource_info($other);
|
|
return $needed & $resource['rights'];
|
|
}
|
|
if (is_int($event) && $event == 0)
|
|
{
|
|
$owner = $other ? $other : $this->user;
|
|
}
|
|
else
|
|
{
|
|
if (!is_array($event))
|
|
{
|
|
$event = $this->read($event,$date_to_read,True,$date_format); // = no ACL check !!!
|
|
}
|
|
if (!is_array($event))
|
|
{
|
|
if ($this->xmlrpc)
|
|
{
|
|
$GLOBALS['server']->xmlrpc_error($GLOBALS['xmlrpcerr']['not_exist'],$GLOBALS['xmlrpcstr']['not_exist']);
|
|
}
|
|
return null; // event not found
|
|
}
|
|
$owner = $event['owner'];
|
|
$private = !$event['public'];
|
|
}
|
|
$user = $GLOBALS['egw_info']['user']['account_id'];
|
|
$grants = $this->grants[$owner];
|
|
if (is_array($event) && $needed == EGW_ACL_READ)
|
|
{
|
|
// Check if the $user is one of the participants or has a read-grant from one of them
|
|
// in that case he has an implicite READ grant for that event
|
|
//
|
|
if ($event['participants'] && is_array($event['participants']))
|
|
{
|
|
foreach($event['participants'] as $uid => $accept)
|
|
{
|
|
if ($uid == $user || $uid < 0 && in_array($user,$GLOBALS['egw']->accounts->members($uid,true)))
|
|
{
|
|
// if we are a participant, we have an implicite READ and PRIVAT grant
|
|
$grants |= EGW_ACL_READ | EGW_ACL_PRIVATE;
|
|
break;
|
|
}
|
|
elseif ($this->grants[$uid] & EGW_ACL_READ)
|
|
{
|
|
// if we have a READ grant from a participant, we dont give an implicit privat grant too
|
|
$grants |= EGW_ACL_READ;
|
|
// we cant break here, as we might be a participant too, and would miss the privat grant
|
|
}
|
|
elseif (!is_numeric($uid))
|
|
{
|
|
// if we have a resource as participant
|
|
$resource = $this->resource_info($uid);
|
|
$grants |= $resource['rights'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ($GLOBALS['egw']->accounts->get_type($owner) == 'g' && $needed == EGW_ACL_ADD)
|
|
{
|
|
$access = False; // a group can't be the owner of an event
|
|
}
|
|
else
|
|
{
|
|
$access = $user == $owner || $grants & $needed && (!$private || $grants & EGW_ACL_PRIVATE);
|
|
}
|
|
if ($this->debug && ($this->debug > 2 || $this->debug == 'check_perms'))
|
|
{
|
|
$this->debug_message('bocal::check_perms(%1,%2,%3)=%4',True,ACL_TYPE_IDENTIFER.$needed,$event,$other,$access);
|
|
}
|
|
return $access;
|
|
}
|
|
|
|
/**
|
|
* Converts several date-types to a timestamp and optionaly converts user- to server-time
|
|
*
|
|
* @param mixed $date date to convert, should be one of the following types
|
|
* string (!) in form YYYYMMDD or iso8601 YYYY-MM-DDThh:mm:ss or YYYYMMDDThhmmss
|
|
* int already a timestamp
|
|
* array with keys 'second', 'minute', 'hour', 'day' or 'mday' (depricated !), 'month' and 'year'
|
|
* @param boolean $user2server_time conversation between user- and server-time default False == Off
|
|
*/
|
|
static function date2ts($date,$user2server=False)
|
|
{
|
|
return $user2server ? egw_time::user2server($date,'ts') : egw_time::to($date,'ts');
|
|
}
|
|
|
|
/**
|
|
* Converts a date to an array and optionaly converts server- to user-time
|
|
*
|
|
* @param mixed $date date to convert
|
|
* @param boolean $server2user_time conversation between user- and server-time default False == Off
|
|
* @return array with keys 'second', 'minute', 'hour', 'day', 'month', 'year', 'raw' (timestamp) and 'full' (Ymd-string)
|
|
*/
|
|
static function date2array($date,$server2user=False)
|
|
{
|
|
return $server2user ? egw_time::server2user($date,'array') : egw_time::to($date,'array');
|
|
}
|
|
|
|
/**
|
|
* Converts a date as timestamp or array to a date-string and optionaly converts server- to user-time
|
|
*
|
|
* @param mixed $date integer timestamp or array with ('year','month',..,'second') to convert
|
|
* @param boolean $server2user_time conversation between user- and server-time default False == Off, not used if $format ends with \Z
|
|
* @param string $format='Ymd' format of the date to return, eg. 'Y-m-d\TH:i:sO' (2005-11-01T15:30:00+0100)
|
|
* @return string date formatted according to $format
|
|
*/
|
|
static function date2string($date,$server2user=False,$format='Ymd')
|
|
{
|
|
return $server2user ? egw_time::server2user($date,$format) : egw_time::to($date,$format);
|
|
}
|
|
|
|
/**
|
|
* Formats a date given as timestamp or array
|
|
*
|
|
* @param mixed $date integer timestamp or array with ('year','month',..,'second') to convert
|
|
* @param string|boolean $format='' default common_prefs[dateformat], common_prefs[timeformat], false=time only, true=date only
|
|
* @return string the formated date (incl. time)
|
|
*/
|
|
static function format_date($date,$format='')
|
|
{
|
|
return egw_time::to($date,$format);
|
|
}
|
|
|
|
/**
|
|
* Gives out a debug-message with certain parameters
|
|
*
|
|
* All permanent debug-messages in the calendar should be done by this function !!!
|
|
* (In future they may be logged or sent as xmlrpc-faults back.)
|
|
*
|
|
* Permanent debug-message need to make sure NOT to give secret information like passwords !!!
|
|
*
|
|
* This function do NOT honor the setting of the debug variable, you may use it like
|
|
* if ($this->debug > N) $this->debug_message('Error ;-)');
|
|
*
|
|
* The parameters get formated depending on their type. ACL-values need a ACL_TYPE_IDENTIFER prefix.
|
|
*
|
|
* @param string $msg message with parameters/variables like lang(), eg. '%1'
|
|
* @param boolean $backtrace=True include a function-backtrace, default True=On
|
|
* should only be set to False=Off, if your code ensures a call with backtrace=On was made before !!!
|
|
* @param mixed $param a variable number of parameters, to be inserted in $msg
|
|
* arrays get serialized with print_r() !
|
|
*/
|
|
function debug_message($msg,$backtrace=True)
|
|
{
|
|
static $acl2string = array(
|
|
0 => 'ACL-UNKNOWN',
|
|
EGW_ACL_READ => 'ACL_READ',
|
|
EGW_ACL_ADD => 'ACL_ADD',
|
|
EGW_ACL_EDIT => 'ACL_EDIT',
|
|
EGW_ACL_DELETE => 'ACL_DELETE',
|
|
EGW_ACL_PRIVATE => 'ACL_PRIVATE',
|
|
EGW_ACL_FREEBUSY => 'ACL_FREEBUSY',
|
|
);
|
|
for($i = 2; $i < func_num_args(); ++$i)
|
|
{
|
|
$param = func_get_arg($i);
|
|
|
|
if (is_null($param))
|
|
{
|
|
$param='NULL';
|
|
}
|
|
else
|
|
{
|
|
switch(gettype($param))
|
|
{
|
|
case 'string':
|
|
if (substr($param,0,strlen(ACL_TYPE_IDENTIFER))== ACL_TYPE_IDENTIFER)
|
|
{
|
|
$param = (int) substr($param,strlen(ACL_TYPE_IDENTIFER));
|
|
$param = (isset($acl2string[$param]) ? $acl2string[$param] : $acl2string[0])." ($param)";
|
|
}
|
|
else
|
|
{
|
|
$param = "'$param'";
|
|
}
|
|
break;
|
|
case 'egw_time':
|
|
case 'datetime':
|
|
$p = $param;
|
|
unset($param);
|
|
$param = $p->format('l, Y-m-d H:i:s').' ('.$p->getTimeZone()->getName().')';
|
|
break;
|
|
case 'array':
|
|
case 'object':
|
|
list(,$content) = @each($param);
|
|
$do_pre = is_array($param) ? count($param) > 6 || is_array($content)&&count($content) : True;
|
|
$param = ($do_pre ? '<pre>' : '').print_r($param,True).($do_pre ? '</pre>' : '');
|
|
break;
|
|
case 'boolean':
|
|
$param = $param ? 'True' : 'False';
|
|
break;
|
|
case 'integer':
|
|
if ($param >= mktime(0,0,0,1,1,2000)) $param = adodb_date('Y-m-d H:i:s',$param)." ($param)";
|
|
break;
|
|
}
|
|
}
|
|
$msg = str_replace('%'.($i-1),$param,$msg);
|
|
}
|
|
echo '<p>'.$msg."<br>\n".($backtrace ? 'Backtrace: '.function_backtrace(1)."</p>\n" : '').str_repeat(' ',4096);
|
|
}
|
|
|
|
/**
|
|
* Formats one or two dates (range) as long date (full monthname), optionaly with a time
|
|
*
|
|
* @param mixed $first first date
|
|
* @param mixed $last=0 last date if != 0 (default)
|
|
* @param boolean $display_time=false should a time be displayed too
|
|
* @param boolean $display_day=false should a day-name prefix the date, eg. monday June 20, 2006
|
|
* @return string with formated date
|
|
*/
|
|
function long_date($first,$last=0,$display_time=false,$display_day=false)
|
|
{
|
|
$first = $this->date2array($first);
|
|
if ($last)
|
|
{
|
|
$last = $this->date2array($last);
|
|
}
|
|
$datefmt = $this->common_prefs['dateformat'];
|
|
$timefmt = $this->common_prefs['timeformat'] == 12 ? 'h:i a' : 'H:i';
|
|
|
|
$month_before_day = strtolower($datefmt[0]) == 'm' ||
|
|
strtolower($datefmt[2]) == 'm' && $datefmt[4] == 'd';
|
|
|
|
if ($display_day)
|
|
{
|
|
$range = lang(adodb_date('l',$first['raw'])).($this->common_prefs['dateformat'][0] != 'd' ? ' ' : ', ');
|
|
}
|
|
for ($i = 0; $i < 5; $i += 2)
|
|
{
|
|
switch($datefmt[$i])
|
|
{
|
|
case 'd':
|
|
$range .= $first['day'] . ($datefmt[1] == '.' ? '.' : '');
|
|
if ($first['month'] != $last['month'] || $first['year'] != $last['year'])
|
|
{
|
|
if (!$month_before_day)
|
|
{
|
|
$range .= ' '.lang(strftime('%B',$first['raw']));
|
|
}
|
|
if ($first['year'] != $last['year'] && $datefmt[0] != 'Y')
|
|
{
|
|
$range .= ($datefmt[0] != 'd' ? ', ' : ' ') . $first['year'];
|
|
}
|
|
if ($display_time)
|
|
{
|
|
$range .= ' '.adodb_date($timefmt,$first['raw']);
|
|
}
|
|
if (!$last)
|
|
{
|
|
return $range;
|
|
}
|
|
$range .= ' - ';
|
|
|
|
if ($first['year'] != $last['year'] && $datefmt[0] == 'Y')
|
|
{
|
|
$range .= $last['year'] . ', ';
|
|
}
|
|
|
|
if ($month_before_day)
|
|
{
|
|
$range .= lang(strftime('%B',$last['raw']));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ($display_time)
|
|
{
|
|
$range .= ' '.adodb_date($timefmt,$first['raw']);
|
|
}
|
|
$range .= ' - ';
|
|
}
|
|
$range .= ' ' . $last['day'] . ($datefmt[1] == '.' ? '.' : '');
|
|
break;
|
|
case 'm':
|
|
case 'M':
|
|
$range .= ' '.lang(strftime('%B',$month_before_day ? $first['raw'] : $last['raw'])) . ' ';
|
|
break;
|
|
case 'Y':
|
|
if ($datefmt[0] != 'm')
|
|
{
|
|
$range .= ' ' . ($datefmt[0] == 'Y' ? $first['year'].($datefmt[2] == 'd' ? ', ' : ' ') : $last['year'].' ');
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if ($display_time && $last)
|
|
{
|
|
$range .= ' '.adodb_date($timefmt,$last['raw']);
|
|
}
|
|
if ($datefmt[4] == 'Y' && $datefmt[0] == 'm')
|
|
{
|
|
$range .= ', ' . $last['year'];
|
|
}
|
|
return $range;
|
|
}
|
|
|
|
/**
|
|
* Displays a timespan, eg. $both ? "10:00 - 13:00: 3h" (10:00 am - 1 pm: 3h) : "10:00 3h" (10:00 am 3h)
|
|
*
|
|
* @param int $start_m start time in minutes since 0h
|
|
* @param int $end_m end time in minutes since 0h
|
|
* @param boolean $both=false display the end-time too, duration is always displayed
|
|
*/
|
|
function timespan($start_m,$end_m,$both=false)
|
|
{
|
|
$duration = $end_m - $start_m;
|
|
if ($end_m == 24*60-1) ++$duration;
|
|
$duration = floor($duration/60).lang('h').($duration%60 ? $duration%60 : '');
|
|
|
|
$timespan = $t = $GLOBALS['egw']->common->formattime(sprintf('%02d',$start_m/60),sprintf('%02d',$start_m%60));
|
|
|
|
if ($both) // end-time too
|
|
{
|
|
$timespan .= ' - '.$GLOBALS['egw']->common->formattime(sprintf('%02d',$end_m/60),sprintf('%02d',$end_m%60));
|
|
// dont double am/pm if they are the same in both times
|
|
if ($this->common_prefs['timeformat'] == 12 && substr($timespan,-2) == substr($t,-2))
|
|
{
|
|
$timespan = str_replace($t,substr($t,0,-3),$timespan);
|
|
}
|
|
$timespan .= ':';
|
|
}
|
|
return $timespan . ' ' . $duration;
|
|
}
|
|
|
|
/**
|
|
* Converts a participant into a (readable) user- or resource-name
|
|
*
|
|
* @param string|int $id id of user or resource
|
|
* @return string with name
|
|
*/
|
|
function participant_name($id,$use_type=false)
|
|
{
|
|
static $id2lid = array();
|
|
|
|
if ($use_type && $use_type != 'u') $id = $use_type.$id;
|
|
|
|
if (!isset($id2lid[$id]))
|
|
{
|
|
if (!is_numeric($id))
|
|
{
|
|
$id2lid[$id] = '#'.$id;
|
|
if (($info = $this->resource_info($id)))
|
|
{
|
|
$id2lid[$id] = $info['name'] ? $info['name'] : $info['email'];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$id2lid[$id] = $GLOBALS['egw']->common->grab_owner_name($id);
|
|
}
|
|
}
|
|
return $id2lid[$id];
|
|
}
|
|
|
|
/**
|
|
* Converts participants array of an event into array of (readable) participant-names with status
|
|
*
|
|
* @param array $event event-data
|
|
* @param boolean $long_status=false should the long/verbose status or an icon be use
|
|
* @param boolean $show_group_invitation=false show group-invitations (status == 'G') or not (default)
|
|
* @return array with id / names with status pairs
|
|
*/
|
|
function participants($event,$long_status=false,$show_group_invitation=false)
|
|
{
|
|
//_debug_array($event);
|
|
$names = array();
|
|
foreach($event['participants'] as $id => $status)
|
|
{
|
|
calendar_so::split_status($status,$quantity,$role);
|
|
|
|
if ($status == 'G' && !$show_group_invitation) continue; // dont show group-invitation
|
|
|
|
if (!$long_status)
|
|
{
|
|
switch($status[0])
|
|
{
|
|
case 'A': // accepted
|
|
$status = html::image('calendar','agt_action_success',$this->verbose_status[$status]);
|
|
break;
|
|
case 'R': // rejected
|
|
$status = html::image('calendar','agt_action_fail',$this->verbose_status[$status]);
|
|
break;
|
|
case 'T': // tentative
|
|
$status = html::image('calendar','tentative',$this->verbose_status[$status]);
|
|
break;
|
|
case 'U': // no response = unknown
|
|
$status = html::image('calendar','cnr-pending',$this->verbose_status[$status]);
|
|
break;
|
|
case 'G': // group invitation
|
|
// Todo: Image, seems not to be used
|
|
$status = '('.$this->verbose_status[$status].')';
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$status = '('.$this->verbose_status[$status].')';
|
|
}
|
|
$names[$id] = $this->participant_name($id).($quantity > 1 ? ' ('.$quantity.')' : '').' '.$status;
|
|
|
|
// add role, if not a regular participant
|
|
if ($role != 'REQ-PARTICIPANT')
|
|
{
|
|
if (isset($this->roles[$role]))
|
|
{
|
|
$role = lang($this->roles[$role]);
|
|
}
|
|
// allow to use cats as roles (beside regular iCal ones)
|
|
elseif (substr($role,0,6) == 'X-CAT-' && ($cat_id = (int)substr($role,6)) > 0)
|
|
{
|
|
$role = $GLOBALS['egw']->categories->id2name($cat_id);
|
|
}
|
|
else
|
|
{
|
|
$role = lang(str_replace('X-','',$role));
|
|
}
|
|
$names[$id] .= ' '.$role;
|
|
}
|
|
}
|
|
return $names;
|
|
}
|
|
|
|
/**
|
|
* Converts category string of an event into array of (readable) category-names
|
|
*
|
|
* @param string $category cat-id (multiple id's commaseparated)
|
|
* @param int $color color of the category, if multiple cats, the color of the last one with color is returned
|
|
* @return array with id / names
|
|
*/
|
|
function categories($category,&$color)
|
|
{
|
|
static $id2cat = array();
|
|
$cats = array();
|
|
$color = 0;
|
|
if (!is_object($this->cats))
|
|
{
|
|
$this->cats = CreateObject('phpgwapi.categories','','calendar');
|
|
}
|
|
foreach(explode(',',$category) as $cat_id)
|
|
{
|
|
if (!$cat_id) continue;
|
|
|
|
if (!isset($id2cat[$cat_id]))
|
|
{
|
|
list($id2cat[$cat_id]) = $this->cats->return_single($cat_id);
|
|
$id2cat[$cat_id]['data'] = unserialize($id2cat[$cat_id]['data']);
|
|
}
|
|
$cat = $id2cat[$cat_id];
|
|
|
|
if ($cat['data']['color'] || preg_match('/(#[0-9A-Fa-f]{6})/',$cat['description'],$parts))
|
|
{
|
|
$color = $cat['data']['color'] ? $cat['data']['color'] : $parts[1];
|
|
}
|
|
$cats[$cat_id] = stripslashes($cat['name']);
|
|
}
|
|
return $cats;
|
|
}
|
|
|
|
/**
|
|
* This is called only by list_cals(). It was moved here to remove fatal error in php5 beta4
|
|
*/
|
|
function _list_cals_add($id,&$users,&$groups)
|
|
{
|
|
$name = $GLOBALS['egw']->common->grab_owner_name($id);
|
|
if (($type = $GLOBALS['egw']->accounts->get_type($id)) == 'g')
|
|
{
|
|
$arr = &$groups;
|
|
}
|
|
else
|
|
{
|
|
$arr = &$users;
|
|
}
|
|
$arr[$name] = Array(
|
|
'grantor' => $id,
|
|
'value' => ($type == 'g' ? 'g_' : '') . $id,
|
|
'name' => $name
|
|
);
|
|
}
|
|
|
|
/**
|
|
* generate list of user- / group-calendars for the selectbox in the header
|
|
*
|
|
* @return array alphabeticaly sorted array with groups first and then users: $name => array('grantor'=>$id,'value'=>['g_'.]$id,'name'=>$name)
|
|
*/
|
|
function list_cals()
|
|
{
|
|
$users = $groups = array();
|
|
foreach($this->grants as $id => $rights)
|
|
{
|
|
$this->_list_cals_add($id,$users,$groups);
|
|
}
|
|
if ($memberships = $GLOBALS['egw']->accounts->membership($GLOBALS['egw_info']['user']['account_id']))
|
|
{
|
|
foreach($memberships as $group_info)
|
|
{
|
|
$this->_list_cals_add($group_info['account_id'],$users,$groups);
|
|
|
|
if ($account_perms = $GLOBALS['egw']->acl->get_ids_for_location($group_info['account_id'],EGW_ACL_READ,'calendar'))
|
|
{
|
|
foreach($account_perms as $id)
|
|
{
|
|
$this->_list_cals_add($id,$users,$groups);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
uksort($users,'strnatcasecmp');
|
|
uksort($groups,'strnatcasecmp');
|
|
|
|
return $users + $groups; // users first and then groups, both alphabeticaly
|
|
}
|
|
|
|
/**
|
|
* Convert the recurrence-information of an event, into a human readable string
|
|
*
|
|
* @param array $event
|
|
* @return string
|
|
*/
|
|
function recure2string($event)
|
|
{
|
|
return (string)calendar_rrule::event2rrule($event);
|
|
}
|
|
|
|
/**
|
|
* Read the holidays for a given $year
|
|
*
|
|
* The holidays get cached in the session (performance), so changes in holidays or birthdays do NOT affect a current session!!!
|
|
*
|
|
* @param int $year=0 year, defaults to 0 = current year
|
|
* @return array indexed with Ymd of array of holidays. A holiday is an array with the following fields:
|
|
* index: numerical unique id
|
|
* locale: string, 2-char short for the nation
|
|
* name: string
|
|
* day: numerical day in month
|
|
* month: numerical month
|
|
* occurence: numerical year or 0 for every year
|
|
* dow: day of week, 0=sunday, .., 6= saturday
|
|
* observande_rule: boolean
|
|
*/
|
|
function read_holidays($year=0)
|
|
{
|
|
if (!$year) $year = (int) date('Y',$this->now_su);
|
|
|
|
if (!$this->cached_holidays) // try reading the holidays from the session
|
|
{
|
|
$this->cached_holidays = $GLOBALS['egw']->session->appsession('holidays','calendar');
|
|
}
|
|
if (!isset($this->cached_holidays[$year]))
|
|
{
|
|
if (!is_object($this->holidays))
|
|
{
|
|
$this->holidays = CreateObject('calendar.boholiday');
|
|
}
|
|
$this->holidays->prepare_read_holidays($year);
|
|
$this->cached_holidays[$year] = $this->holidays->read_holiday();
|
|
|
|
// search for birthdays
|
|
if ($GLOBALS['egw_info']['server']['hide_birthdays'] != 'yes')
|
|
{
|
|
$contacts = CreateObject('phpgwapi.contacts');
|
|
$bdays =& $contacts->read(0,0,array('id','n_family','n_given','n_prefix','n_middle','bday'),'',"bday=!'',n_family=!''",'ASC','bday');
|
|
if ($bdays)
|
|
{
|
|
// sort by month and day only
|
|
usort($bdays,create_function('$a,$b','return (int) $a[\'bday\'] == (int) $b[\'bday\'] ? strcmp($a[\'bday\'],$b[\'bday\']) : (int) $a[\'bday\'] - (int) $b[\'bday\'];'));
|
|
foreach($bdays as $pers)
|
|
{
|
|
list($m,$d,$y) = explode('/',$pers['bday']);
|
|
if ($y > $year) continue; // not yet born
|
|
$this->cached_holidays[$year][sprintf('%04d%02d%02d',$year,$m,$d)][] = array(
|
|
'day' => $d,
|
|
'month' => $m,
|
|
'occurence' => 0,
|
|
'name' => lang('Birthday').' '.($pers['n_given'] ? $pers['n_given'] : $pers['n_prefix']).' '.$pers['n_middle'].' '.
|
|
$pers['n_family'].($y && !$GLOBALS['egw_info']['server']['hide_birthdays'] ? ' ('.$y.')' : ''),
|
|
'birthyear' => $y, // this can be used to identify birthdays from holidays
|
|
);
|
|
}
|
|
}
|
|
}
|
|
// store holidays and birthdays in the session
|
|
$this->cached_holidays = $GLOBALS['egw']->session->appsession('holidays','calendar',$this->cached_holidays);
|
|
}
|
|
if ((int) $this->debug >= 2 || $this->debug == 'read_holidays')
|
|
{
|
|
$this->debug_message('bocal::read_holidays(%1)=%2',true,$year,$this->cached_holidays[$year]);
|
|
}
|
|
return $this->cached_holidays[$year];
|
|
}
|
|
|
|
/**
|
|
* get title for an event identified by $event
|
|
*
|
|
* Is called as hook to participate in the linking
|
|
*
|
|
* @param int|array $entry int cal_id or array with event
|
|
* @param string|boolean string with title, null if not found or false if not read perms
|
|
*/
|
|
function link_title($event)
|
|
{
|
|
if (!is_array($event) && (int) $event > 0)
|
|
{
|
|
$event = $this->read($event);
|
|
}
|
|
if (!is_array($event))
|
|
{
|
|
return $event;
|
|
}
|
|
return $this->format_date($event['start']) . ': ' . $event['title'];
|
|
}
|
|
|
|
/**
|
|
* query calendar for events matching $pattern
|
|
*
|
|
* Is called as hook to participate in the linking
|
|
*
|
|
* @param string $pattern pattern to search
|
|
* @return array with cal_id - title pairs of the matching entries
|
|
*/
|
|
function link_query($pattern)
|
|
{
|
|
$result = array();
|
|
foreach((array) $this->search(array('query' => $pattern)) as $event)
|
|
{
|
|
$result[$event['id']] = $this->link_title($event);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Check access to the projects file store
|
|
*
|
|
* @param int $id id of entry
|
|
* @param int $check EGW_ACL_READ for read and EGW_ACL_EDIT for write or delete access
|
|
* @return boolean true if access is granted or false otherwise
|
|
*/
|
|
function file_access($id,$check,$rel_path)
|
|
{
|
|
return $this->check_perms($check,$id);
|
|
}
|
|
|
|
/**
|
|
* sets the default prefs, if they are not already set (on a per pref. basis)
|
|
*
|
|
* It sets a flag in the app-session-data to be called only once per session
|
|
*/
|
|
function check_set_default_prefs()
|
|
{
|
|
if ($this->cal_prefs['interval'] && ($set = $GLOBALS['egw']->session->appsession('default_prefs_set','calendar')))
|
|
{
|
|
return;
|
|
}
|
|
$GLOBALS['egw']->session->appsession('default_prefs_set','calendar','set');
|
|
|
|
$default_prefs =& $GLOBALS['egw']->preferences->default['calendar'];
|
|
$forced_prefs =& $GLOBALS['egw']->preferences->forced['calendar'];
|
|
|
|
if (!($planner_start_with_group = $GLOBALS['egw']->accounts->name2id('Default')))
|
|
{
|
|
$planner_start_with_group = '0';
|
|
}
|
|
$subject = lang('Calendar Event') . ' - $$action$$: $$startdate$$ $$title$$'."\n";
|
|
$values = array(
|
|
'notifyAdded' => $subject . lang ('You have a meeting scheduled for %1','$$startdate$$'),
|
|
'notifyCanceled' => $subject . lang ('Your meeting scheduled for %1 has been canceled','$$startdate$$'),
|
|
'notifyModified' => $subject . lang ('Your meeting that had been scheduled for %1 has been rescheduled to %2','$$olddate$$','$$startdate$$'),
|
|
'notifyDisinvited'=> $subject . lang ('You have been disinvited from the meeting at %1','$$startdate$$'),
|
|
'notifyResponse' => $subject . lang ('On %1 %2 %3 your meeting request for %4','$$date$$','$$fullname$$','$$action$$','$$startdate$$'),
|
|
'notifyAlarm' => lang('Alarm for %1 at %2 in %3','$$title$$','$$startdate$$','$$location$$')."\n".lang ('Here is your requested alarm.'),
|
|
'planner_start_with_group' => $planner_start_with_group,
|
|
'interval' => 30,
|
|
);
|
|
foreach($values as $var => $default)
|
|
{
|
|
$type = substr($var,0,6) == 'notify' ? 'forced' : 'default';
|
|
|
|
// only set, if neither default nor forced pref exists
|
|
if ((!isset($default_prefs[$var]) || (string)$default_prefs[$var] === '') && (!isset($forced_prefs[$var]) || (string)$forced_prefs[$var] === ''))
|
|
{
|
|
$GLOBALS['egw']->preferences->add('calendar',$var,$default,'default'); // always store default, even if we have a forced too
|
|
if ($type == 'forced') $GLOBALS['egw']->preferences->add('calendar',$var,$default,'forced');
|
|
$this->cal_prefs[$var] = $default;
|
|
$need_save = True;
|
|
}
|
|
}
|
|
if ($need_save)
|
|
{
|
|
$GLOBALS['egw']->preferences->save_repository(False,'default');
|
|
$GLOBALS['egw']->preferences->save_repository(False,'forced');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the freebusy URL of a user
|
|
*
|
|
* @param int|string $user account_id or account_lid
|
|
* @param string $pw=null password
|
|
*/
|
|
static function freebusy_url($user,$pw=null)
|
|
{
|
|
if (is_numeric($user)) $user = $GLOBALS['egw']->accounts->id2name($user);
|
|
|
|
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) : '');
|
|
}
|
|
|
|
/**
|
|
* Check if the event is the whole day
|
|
*
|
|
* @param array $event event
|
|
* @return boolean true if whole day event, false othwerwise
|
|
*/
|
|
function isWholeDay($event)
|
|
{
|
|
// check if the event is the whole day
|
|
$start = $this->date2array($event['start']);
|
|
$end = $this->date2array($event['end']);
|
|
|
|
return !$start['hour'] && !$start['minute'] && $end['hour'] == 23 && $end['minute'] == 59;
|
|
}
|
|
}
|