3052 lines
115 KiB
PHP
3052 lines
115 KiB
PHP
<?php
|
|
/**
|
|
* Kronolith base library.
|
|
*
|
|
* Copyright 1999-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (GPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/gpl.
|
|
*
|
|
* @category Horde
|
|
* @license http://www.horde.org/licenses/gpl GPL
|
|
* @package Kronolith
|
|
*/
|
|
|
|
/**
|
|
* The Kronolith:: class provides functionality common to all of Kronolith.
|
|
*
|
|
* @author Chuck Hagenbuch <chuck@horde.org>
|
|
* @category Horde
|
|
* @license http://www.horde.org/licenses/gpl GPL
|
|
* @package Kronolith
|
|
*/
|
|
class Kronolith
|
|
{
|
|
/** Event status */
|
|
const STATUS_NONE = 0;
|
|
const STATUS_TENTATIVE = 1;
|
|
const STATUS_CONFIRMED = 2;
|
|
const STATUS_CANCELLED = 3;
|
|
const STATUS_FREE = 4;
|
|
|
|
/** Invitation responses */
|
|
const RESPONSE_NONE = 1;
|
|
const RESPONSE_ACCEPTED = 2;
|
|
const RESPONSE_DECLINED = 3;
|
|
const RESPONSE_TENTATIVE = 4;
|
|
|
|
/** Attendee status */
|
|
const PART_REQUIRED = 1;
|
|
const PART_OPTIONAL = 2;
|
|
const PART_NONE = 3;
|
|
const PART_IGNORE = 4;
|
|
|
|
/** iTip requests */
|
|
const ITIP_REQUEST = 1;
|
|
const ITIP_CANCEL = 2;
|
|
|
|
const RANGE_THISANDFUTURE = 'THISANDFUTURE';
|
|
|
|
/** The event can be delegated. */
|
|
const PERMS_DELEGATE = 1024;
|
|
|
|
/** Calendar Manager Constants */
|
|
const DISPLAY_CALENDARS = 'displayCalendars';
|
|
const DISPLAY_REMOTE_CALENDARS = 'displayRemote';
|
|
const DISPLAY_EXTERNAL_CALENDARS= 'displayExternal';
|
|
const DISPLAY_RESOURCE_CALENDARS= 'displayResource';
|
|
const DISPLAY_HOLIDAYS = 'displayHolidays';
|
|
const ALL_CALENDARS = 'allCalendars';
|
|
const ALL_REMOTE_CALENDARS = 'allRemote';
|
|
const ALL_EXTERNAL_CALENDARS = 'allExternal';
|
|
const ALL_HOLIDAYS = 'allHolidays';
|
|
const ALL_RESOURCE_CALENDARS = 'allResource';
|
|
|
|
/**
|
|
* @var Kronolith_Tagger
|
|
*/
|
|
static private $_tagger;
|
|
|
|
/**
|
|
* Converts a permission object to a json object.
|
|
*
|
|
* This methods filters out any permissions for the owner and converts the
|
|
* user name if necessary.
|
|
*
|
|
* @param Horde_Perms_Permission $perm A permission object.
|
|
*
|
|
* @return array A hash suitable for json.
|
|
*/
|
|
static public function permissionToJson(Horde_Perms_Permission $perm)
|
|
{
|
|
$json = $perm->data;
|
|
if (isset($json['users'])) {
|
|
$users = array();
|
|
foreach ($json['users'] as $user => $value) {
|
|
if ($user == $GLOBALS['registry']->getAuth()) {
|
|
continue;
|
|
}
|
|
$user = $GLOBALS['registry']->convertUsername($user, false);
|
|
$users[$user] = $value;
|
|
}
|
|
if ($users) {
|
|
$json['users'] = $users;
|
|
} else {
|
|
unset($json['users']);
|
|
}
|
|
}
|
|
return $json;
|
|
}
|
|
|
|
/**
|
|
* Returns all the alarms active on a specific date.
|
|
*
|
|
* @param Horde_Date $date The date to check for alarms.
|
|
* @param array $calendars The calendars to check for events.
|
|
* @param boolean $fullevent Whether to return complete alarm objects or
|
|
* only alarm IDs.
|
|
*
|
|
* @return array The alarms active on the date. A hash with calendar names
|
|
* as keys and arrays of events or event ids as values.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function listAlarms($date, $calendars, $fullevent = false)
|
|
{
|
|
$kronolith_driver = self::getDriver();
|
|
|
|
$alarms = array();
|
|
foreach ($calendars as $cal) {
|
|
$kronolith_driver->open($cal);
|
|
$alarms[$cal] = $kronolith_driver->listAlarms($date, $fullevent);
|
|
}
|
|
|
|
return $alarms;
|
|
}
|
|
|
|
/**
|
|
* Searches for events with the given properties.
|
|
*
|
|
* @param object $query The search query.
|
|
* @param string $calendar The calendar to search in the form
|
|
* "Driver|calendar_id".
|
|
*
|
|
* @return array The events.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function search($query, $calendar = null)
|
|
{
|
|
if ($calendar) {
|
|
$driver = explode('|', $calendar, 2);
|
|
$calendars = array($driver[0] => array($driver[1]));
|
|
} elseif (!empty($query->calendars)) {
|
|
$calendars = $query->calendars;
|
|
} else {
|
|
$calendars = array(
|
|
Horde_String::ucfirst($GLOBALS['conf']['calendar']['driver']) => $GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_CALENDARS),
|
|
'Horde' => $GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_EXTERNAL_CALENDARS),
|
|
'Ical' => $GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_REMOTE_CALENDARS),
|
|
'Holidays' => $GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_HOLIDAYS));
|
|
}
|
|
|
|
$events = array();
|
|
foreach ($calendars as $type => $list) {
|
|
if (!empty($list)) {
|
|
$kronolith_driver = self::getDriver($type);
|
|
}
|
|
foreach ($list as $cal) {
|
|
$kronolith_driver->open($cal);
|
|
$retevents = $kronolith_driver->search($query);
|
|
self::mergeEvents($events, $retevents);
|
|
}
|
|
}
|
|
|
|
return $events;
|
|
}
|
|
|
|
/**
|
|
* Returns all the events that happen each day within a time period
|
|
*
|
|
* @deprecated
|
|
*
|
|
* @param Horde_Date $startDate The start of the time range.
|
|
* @param Horde_Date $endDate The end of the time range.
|
|
* @param array $calendars The calendars to check for events.
|
|
* @param array $options Additional options:
|
|
* - show_recurrence: (boolean) Return every instance of a recurring
|
|
* event?
|
|
* DEFAULT: false (Only return recurring events once
|
|
* inside $startDate - $endDate range)
|
|
* - has_alarm: (boolean) Only return events with alarms.
|
|
* DEFAULT: false (Return all events)
|
|
* - json: (boolean) Store the results of the event's toJson()
|
|
* method?
|
|
* DEFAULT: false
|
|
* - cover_dates: (boolean) Add the events to all days that they
|
|
* cover?
|
|
* DEFAULT: true
|
|
* - hide_exceptions: (boolean) Hide events that represent exceptions to
|
|
* a recurring event.
|
|
* DEFAULT: false (Do not hide exception events)
|
|
* - fetch_tags: (boolean) Fetch tags for all events.
|
|
* DEFAULT: false (Do not fetch event tags)
|
|
*
|
|
* @return array The events happening in this time period.
|
|
*/
|
|
static public function listEvents(
|
|
$startDate, $endDate, $calendars = null, array $options = array())
|
|
{
|
|
$options = array_merge(array(
|
|
'show_recurrence' => true,
|
|
'has_alarm' => false,
|
|
'show_remote' => true,
|
|
'hide_exceptions' => false,
|
|
'cover_dates' => true,
|
|
'fetch_tags' => false), $options);
|
|
|
|
$results = array();
|
|
|
|
/* Internal calendars. */
|
|
if (!isset($calendars)) {
|
|
$calendars = $GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_CALENDARS);
|
|
}
|
|
$driver = self::getDriver();
|
|
foreach ($calendars as $calendar) {
|
|
try {
|
|
$driver->open($calendar);
|
|
$events = $driver->listEvents($startDate, $endDate, $options);
|
|
self::mergeEvents($results, $events);
|
|
} catch (Kronolith_Exception $e) {
|
|
$GLOBALS['notification']->push($e);
|
|
}
|
|
}
|
|
|
|
// Resource calendars
|
|
if (count($GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_RESOURCE_CALENDARS)) &&
|
|
!empty($GLOBALS['conf']['resource']['driver'])) {
|
|
|
|
$driver = self::getDriver('Resource');
|
|
foreach ($GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_RESOURCE_CALENDARS) as $calendar) {
|
|
try {
|
|
$driver->open($calendar);
|
|
$events = $driver->listEvents(
|
|
$startDate, $endDate, array('show_recurrence' => $options['show_recurrence']));
|
|
self::mergeEvents($results, $events);
|
|
} catch (Kronolith_Exception $e) {
|
|
$GLOBALS['notification']->push($e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($options['show_remote']) {
|
|
/* Horde applications providing listTimeObjects. */
|
|
if (count($GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_EXTERNAL_CALENDARS))) {
|
|
$driver = self::getDriver('Horde');
|
|
foreach ($GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_EXTERNAL_CALENDARS) as $external_cal) {
|
|
try {
|
|
$driver->open($external_cal);
|
|
$events = $driver->listEvents(
|
|
$startDate, $endDate, array('show_recurrence' => $options['show_recurrence']));
|
|
self::mergeEvents($results, $events);
|
|
} catch (Kronolith_Exception $e) {
|
|
$GLOBALS['notification']->push($e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Remote Calendars. */
|
|
foreach ($GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_REMOTE_CALENDARS) as $url) {
|
|
try {
|
|
$driver = self::getDriver('Ical', $url);
|
|
$events = $driver->listEvents(
|
|
$startDate, $endDate, array('show_recurrence' => $options['show_recurrence']));
|
|
self::mergeEvents($results, $events);
|
|
} catch (Kronolith_Exception $e) {
|
|
$GLOBALS['notification']->push($e);
|
|
}
|
|
}
|
|
|
|
/* Holidays. */
|
|
$display_holidays = $GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_HOLIDAYS);
|
|
if (count($display_holidays) && !empty($GLOBALS['conf']['holidays']['enable'])) {
|
|
$driver = self::getDriver('Holidays');
|
|
foreach ($display_holidays as $holiday) {
|
|
try {
|
|
$driver->open($holiday);
|
|
$events = $driver->listEvents(
|
|
$startDate, $endDate, array('show_recurrence' => $options['show_recurrence']));
|
|
self::mergeEvents($results, $events);
|
|
} catch (Kronolith_Exception $e) {
|
|
$GLOBALS['notification']->push($e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Sort events. */
|
|
$results = self::sortEvents($results);
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Merges results from two listEvents() result sets.
|
|
*
|
|
* @param array $results First list of events.
|
|
* @param array $events List of events to be merged into the first one.
|
|
*/
|
|
static public function mergeEvents(&$results, $events)
|
|
{
|
|
foreach ($events as $day => $day_events) {
|
|
if (isset($results[$day])) {
|
|
$results[$day] = array_merge($results[$day], $day_events);
|
|
} else {
|
|
$results[$day] = $day_events;
|
|
}
|
|
}
|
|
ksort($results);
|
|
}
|
|
|
|
/**
|
|
* Calculates recurrences of an event during a certain period.
|
|
*/
|
|
static public function addEvents(&$results, &$event, $startDate, $endDate,
|
|
$showRecurrence, $json, $coverDates = true)
|
|
{
|
|
/* If the event has a custom timezone, we need to convert the
|
|
* recurrence object to the event's timezone while calculating next
|
|
* recurrences, to take DST changes in both the event's and the local
|
|
* timezone into account. */
|
|
$convert = $event->timezone &&
|
|
$event->getDriver()->supportsTimezones();
|
|
if ($convert) {
|
|
$timezone = date_default_timezone_get();
|
|
}
|
|
|
|
// If we are adding coverDates, but have no $endDate, default to
|
|
// +5 years from $startDate. This protects against hitting memory
|
|
// limit and other issues due to extremely long loops if a single event
|
|
// was added with a duration of thousands of years while still
|
|
// providing for reasonable alarm trigger times.
|
|
if ($coverDates && empty($endDate)) {
|
|
$endDate = clone $startDate;
|
|
$endDate->year += 5;
|
|
}
|
|
|
|
if ($event->recurs() && $showRecurrence) {
|
|
/* Recurring Event. */
|
|
|
|
/* If the event ends at 12am and does not end at the same time
|
|
* that it starts (0 duration), set the end date to the previous
|
|
* day's end date. */
|
|
if ($event->end->hour == 0 &&
|
|
$event->end->min == 0 &&
|
|
$event->end->sec == 0 &&
|
|
$event->start->compareDateTime($event->end) != 0) {
|
|
$event->end = new Horde_Date(
|
|
array('hour' => 23,
|
|
'min' => 59,
|
|
'sec' => 59,
|
|
'month' => $event->end->month,
|
|
'mday' => $event->end->mday - 1,
|
|
'year' => $event->end->year));
|
|
}
|
|
|
|
/* We can't use the event duration here because we might cover a
|
|
* daylight saving time switch. */
|
|
$diff = array($event->end->year - $event->start->year,
|
|
$event->end->month - $event->start->month,
|
|
$event->end->mday - $event->start->mday,
|
|
$event->end->hour - $event->start->hour,
|
|
$event->end->min - $event->start->min);
|
|
|
|
if ($event->start->compareDateTime($startDate) < 0) {
|
|
/* The first time the event happens was before the period
|
|
* started. Start searching for recurrences from the start of
|
|
* the period. */
|
|
$next = new Horde_Date(array('year' => $startDate->year,
|
|
'month' => $startDate->month,
|
|
'mday' => $startDate->mday),
|
|
$event->timezone);
|
|
} else {
|
|
/* The first time the event happens is in the range; unless
|
|
* there is an exception for this ocurrence, add it. */
|
|
if (!$event->recurrence->hasException($event->start->year,
|
|
$event->start->month,
|
|
$event->start->mday)) {
|
|
if ($coverDates) {
|
|
self::addCoverDates($results, $event, $event->start, $event->end, $json, $endDate);
|
|
} else {
|
|
$results[$event->start->dateString()][$event->id] = $json ? $event->toJson() : $event;
|
|
}
|
|
}
|
|
|
|
/* Start searching for recurrences from the day after it
|
|
* starts. */
|
|
$next = clone $event->start;
|
|
++$next->mday;
|
|
}
|
|
|
|
if ($convert) {
|
|
$event->recurrence->start->setTimezone($event->timezone);
|
|
if ($event->recurrence->hasRecurEnd()) {
|
|
$event->recurrence->recurEnd->setTimezone($event->timezone);
|
|
}
|
|
}
|
|
|
|
/* Add all recurrences of the event. */
|
|
$next = $event->recurrence->nextRecurrence($next);
|
|
if ($next && $convert) {
|
|
/* Resetting after the nextRecurrence() call, because
|
|
* we need to test if the next recurrence in the
|
|
* event's timezone actually matches the interval we
|
|
* check in the local timezone. This is done on each
|
|
* nextRecurrence() further below. */
|
|
$next->setTimezone($timezone);
|
|
}
|
|
while ($next !== false && $next->compareDate($endDate) <= 0) {
|
|
if (!$event->recurrence->hasException($next->year, $next->month, $next->mday)) {
|
|
/* Add the event to all the days it covers. */
|
|
$nextEnd = clone $next;
|
|
$nextEnd->year += $diff[0];
|
|
$nextEnd->month += $diff[1];
|
|
$nextEnd->mday += $diff[2];
|
|
$nextEnd->hour += $diff[3];
|
|
$nextEnd->min += $diff[4];
|
|
$addEvent = clone $event;
|
|
$addEvent->start = $addEvent->originalStart = $next;
|
|
$addEvent->end = $addEvent->originalEnd = $nextEnd;
|
|
if ($coverDates) {
|
|
self::addCoverDates($results, $addEvent, $next, $nextEnd, $json, $endDate);
|
|
} else {
|
|
$addEvent->start = $next;
|
|
$addEvent->end = $nextEnd;
|
|
$results[$addEvent->start->dateString()][$addEvent->id] = $json ? $addEvent->toJson() : $addEvent;
|
|
|
|
}
|
|
}
|
|
if ($convert) {
|
|
$next->setTimezone($event->timezone);
|
|
}
|
|
$next = $event->recurrence->nextRecurrence(
|
|
array('year' => $next->year,
|
|
'month' => $next->month,
|
|
'mday' => $next->mday + 1,
|
|
'hour' => $next->hour,
|
|
'min' => $next->min,
|
|
'sec' => $next->sec));
|
|
if ($next && $convert) {
|
|
$next->setTimezone($timezone);
|
|
}
|
|
}
|
|
} else {
|
|
/* Event only occurs once. */
|
|
if (!$coverDates) {
|
|
$results[$event->start->dateString()][$event->id] = $json ? $event->toJson() : $event;
|
|
} else {
|
|
$allDay = $event->isAllDay();
|
|
|
|
/* Work out what day it starts on. */
|
|
if ($startDate &&
|
|
$event->start->compareDateTime($startDate) < 0) {
|
|
/* It started before the beginning of the period. */
|
|
if ($event->recurs()) {
|
|
$eventStart = $event->recurrence->nextRecurrence($startDate);
|
|
$originalStart = clone $eventStart;
|
|
} else {
|
|
$eventStart = clone $startDate;
|
|
$originalStart = clone $event->start;
|
|
}
|
|
} else {
|
|
$eventStart = clone $event->start;
|
|
$originalStart = clone $event->start;
|
|
}
|
|
|
|
/* Work out what day it ends on. */
|
|
if ($endDate &&
|
|
$event->end->compareDateTime($endDate) > 0) {
|
|
/* Ends after the end of the period. */
|
|
if (is_object($endDate)) {
|
|
$eventEnd = clone $endDate;
|
|
$originalEnd = clone $event->end;
|
|
} else {
|
|
$eventEnd = $endDate;
|
|
$originalEnd = new Horde_Date($endDate);
|
|
}
|
|
} else {
|
|
/* Need to perform some magic if this is a single instance
|
|
* of a recurring event since $event->end would be the
|
|
* original end date, not the recurrence's end date. */
|
|
if ($event->recurs()) {
|
|
|
|
$diff = array($event->end->year - $event->start->year,
|
|
$event->end->month - $event->start->month,
|
|
$event->end->mday - $event->start->mday,
|
|
$event->end->hour - $event->start->hour,
|
|
$event->end->min - $event->start->min);
|
|
|
|
$theEnd = $event->recurrence->nextRecurrence($eventStart);
|
|
$theEnd->year += $diff[0];
|
|
$theEnd->month += $diff[1];
|
|
$theEnd->mday += $diff[2];
|
|
$theEnd->hour += $diff[3];
|
|
$theEnd->min += $diff[4];
|
|
if ($convert) {
|
|
$eventStart->setTimezone($timezone);
|
|
$theEnd->setTimezone($timezone);
|
|
}
|
|
} else {
|
|
$theEnd = clone $event->end;
|
|
}
|
|
$originalEnd = clone $theEnd;
|
|
|
|
/* If the event doesn't end at 12am set the end date to
|
|
* the current end date. If it ends at 12am and does not
|
|
* end at the same time that it starts (0 duration), set
|
|
* the end date to the previous day's end date. */
|
|
if ($theEnd->hour != 0 ||
|
|
$theEnd->min != 0 ||
|
|
$theEnd->sec != 0 ||
|
|
$event->start->compareDateTime($theEnd) == 0 ||
|
|
$allDay) {
|
|
$eventEnd = clone $theEnd;
|
|
} else {
|
|
$eventEnd = new Horde_Date(
|
|
array('hour' => 23,
|
|
'min' => 59,
|
|
'sec' => 59,
|
|
'month' => $theEnd->month,
|
|
'mday' => $theEnd->mday - 1,
|
|
'year' => $theEnd->year));
|
|
}
|
|
}
|
|
|
|
/* Add the event to all the days it covers. This is similar to
|
|
* Kronolith::addCoverDates(), but for days in between the
|
|
* start and end day, the range is midnight to midnight, and
|
|
* for the edge days it's start to midnight, and midnight to
|
|
* end. */
|
|
$i = $eventStart->mday;
|
|
$loopDate = new Horde_Date(array('month' => $eventStart->month,
|
|
'mday' => $i,
|
|
'year' => $eventStart->year));
|
|
while ($loopDate->compareDateTime($eventEnd) <= 0 &&
|
|
$loopDate->compareDateTime($endDate) <=0) {
|
|
if (!$allDay ||
|
|
$loopDate->compareDateTime($eventEnd) != 0) {
|
|
$addEvent = clone $event;
|
|
$addEvent->originalStart = $originalStart;
|
|
$addEvent->originalEnd = $originalEnd;
|
|
|
|
/* If this is the start day, set the start time to
|
|
* the real start time, otherwise set it to
|
|
* 00:00 */
|
|
if ($loopDate->compareDate($eventStart) == 0) {
|
|
$addEvent->start = $eventStart;
|
|
} else {
|
|
$addEvent->start = clone $loopDate;
|
|
$addEvent->start->hour = $addEvent->start->min = $addEvent->start->sec = 0;
|
|
$addEvent->first = false;
|
|
}
|
|
|
|
/* If this is the end day, set the end time to the
|
|
* real event end, otherwise set it to 23:59. */
|
|
if ($loopDate->compareDate($eventEnd) == 0) {
|
|
$addEvent->end = $eventEnd;
|
|
} else {
|
|
$addEvent->end = clone $loopDate;
|
|
$addEvent->end->hour = 23;
|
|
$addEvent->end->min = $addEvent->end->sec = 59;
|
|
$addEvent->last = false;
|
|
}
|
|
|
|
$results[$loopDate->dateString()][$addEvent->id] = $json ? $addEvent->toJson($allDay) : $addEvent;
|
|
}
|
|
|
|
$loopDate = new Horde_Date(
|
|
array('month' => $eventStart->month,
|
|
'mday' => ++$i,
|
|
'year' => $eventStart->year));
|
|
}
|
|
}
|
|
}
|
|
ksort($results);
|
|
}
|
|
|
|
/**
|
|
* Adds an event to all the days it covers.
|
|
*
|
|
* @param array $result The current result list.
|
|
* @param Kronolith_Event $event An event object.
|
|
* @param Horde_Date $eventStart The event's start at the actual
|
|
* recurrence.
|
|
* @param Horde_Date $eventEnd The event's end at the actual recurrence.
|
|
* @param boolean $json Store the results of the events' toJson()
|
|
* method?
|
|
* @param Horde_Date $endDate The ending date of the current view.
|
|
*/
|
|
static public function addCoverDates(&$results, $event, $eventStart,
|
|
$eventEnd, $json, Horde_Date $endDate = null)
|
|
{
|
|
$loopDate = new Horde_Date($eventStart->year, $eventStart->month, $eventStart->mday);
|
|
$allDay = $event->isAllDay();
|
|
$endDate = empty($endDate) ? $eventEnd : $endDate;
|
|
while ($loopDate->compareDateTime($eventEnd) <= 0 &&
|
|
$loopDate->compareDateTime($endDate) <= 0) {
|
|
if (!$allDay ||
|
|
$loopDate->compareDateTime($eventEnd) != 0) {
|
|
$addEvent = clone $event;
|
|
$addEvent->start = $eventStart;
|
|
$addEvent->end = $eventEnd;
|
|
if ($loopDate->compareDate($eventStart) != 0) {
|
|
$addEvent->first = false;
|
|
}
|
|
if ($loopDate->compareDate($eventEnd) != 0) {
|
|
$addEvent->last = false;
|
|
}
|
|
if ($addEvent->recurs() &&
|
|
$addEvent->recurrence->hasCompletion($loopDate->year, $loopDate->month, $loopDate->mday)) {
|
|
$addEvent->status = Kronolith::STATUS_CANCELLED;
|
|
}
|
|
$results[$loopDate->dateString()][$addEvent->id] = $json ? $addEvent->toJson($allDay) : $addEvent;
|
|
}
|
|
$loopDate->mday++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds an event to set of search results.
|
|
*
|
|
* @param array $events The list of events to update with the new
|
|
* event.
|
|
* @param Kronolith_Event $event An event from a search result.
|
|
* @param stdClass $query A search query.
|
|
* @param boolean $json Store the results of the events' toJson()
|
|
* method?
|
|
*/
|
|
static public function addSearchEvents(&$events, $event, $query, $json)
|
|
{
|
|
static $now;
|
|
if (!isset($now)) {
|
|
$now = new Horde_Date($_SERVER['REQUEST_TIME']);
|
|
}
|
|
$showRecurrence = true;
|
|
if ($event->recurs()) {
|
|
if (empty($query->start) && empty($query->end)) {
|
|
$eventStart = $event->start;
|
|
$eventEnd = $event->end;
|
|
} else {
|
|
if (empty($query->end)) {
|
|
$convert = $event->timezone &&
|
|
$event->getDriver()->supportsTimezones();
|
|
if ($convert) {
|
|
$timezone = date_default_timezone_get();
|
|
$event->recurrence->start->setTimezone($event->timezone);
|
|
if ($event->recurrence->hasRecurEnd()) {
|
|
$event->recurrence->recurEnd->setTimezone($event->timezone);
|
|
}
|
|
}
|
|
$eventEnd = $event->recurrence->nextRecurrence($now);
|
|
if (!$eventEnd) {
|
|
return;
|
|
}
|
|
if ($convert) {
|
|
$eventEnd->setTimezone($timezone);
|
|
}
|
|
} else {
|
|
$eventEnd = $query->end;
|
|
}
|
|
if (empty($query->start)) {
|
|
$eventStart = $event->start;
|
|
$showRecurrence = false;
|
|
} else {
|
|
$eventStart = $query->start;
|
|
}
|
|
}
|
|
} else {
|
|
// Don't include any results that are outside the query range.
|
|
if ((!empty($query->end) && $event->start->after($query->end)) ||
|
|
(!empty($query->start) && $event->end->before($query->start))) {
|
|
return;
|
|
}
|
|
$eventStart = $event->start;
|
|
$eventEnd = $event->end;
|
|
}
|
|
self::addEvents($events, $event, $eventStart, $eventEnd, $showRecurrence, $json, false);
|
|
}
|
|
|
|
/**
|
|
* Returns the number of events in calendars that the current user owns.
|
|
*
|
|
* @return integer The number of events.
|
|
*/
|
|
static public function countEvents()
|
|
{
|
|
static $count;
|
|
if (isset($count)) {
|
|
return $count;
|
|
}
|
|
|
|
$kronolith_driver = self::getDriver();
|
|
$calendars = self::listInternalCalendars(true, Horde_Perms::ALL);
|
|
$current_calendar = $kronolith_driver->calendar;
|
|
|
|
$count = 0;
|
|
foreach (array_keys($calendars) as $calendar) {
|
|
$kronolith_driver->open($calendar);
|
|
try {
|
|
$count += $kronolith_driver->countEvents();
|
|
} catch (Exception $e) {
|
|
}
|
|
}
|
|
|
|
/* Reopen last calendar. */
|
|
$kronolith_driver->open($current_calendar);
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Imports an event parsed from a string.
|
|
*
|
|
* @param string $text The text to parse into an event
|
|
* @param string $calendar The calendar into which the event will be
|
|
* imported. If 'null', the user's default
|
|
* calendar will be used.
|
|
*
|
|
* @return array The UID of all events that were added.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
public function quickAdd($text, $calendar = null)
|
|
{
|
|
$text = trim($text);
|
|
if (strpos($text, "\n") !== false) {
|
|
list($title, $description) = explode($text, "\n", 2);
|
|
} else {
|
|
$title = $text;
|
|
$description = '';
|
|
}
|
|
$title = trim($title);
|
|
$description = trim($description);
|
|
|
|
$dateParser = Horde_Date_Parser::factory(array('locale' => $GLOBALS['language']));
|
|
$r = $dateParser->parse($title, array('return' => 'result'));
|
|
if (!($d = $r->guess())) {
|
|
throw new Horde_Exception(sprintf(_("Cannot parse event description \"%s\""), Horde_String::truncate($text)));
|
|
}
|
|
|
|
$title = $r->untaggedText();
|
|
|
|
$kronolith_driver = self::getDriver(null, $calendar);
|
|
$event = $kronolith_driver->getEvent();
|
|
$event->initialized = true;
|
|
$event->title = $title;
|
|
$event->description = $description;
|
|
$event->start = $d;
|
|
$event->end = $d->add(array('hour' => 1));
|
|
$event->save();
|
|
|
|
return $event;
|
|
}
|
|
|
|
/**
|
|
* Initial app setup code.
|
|
*
|
|
* Globals defined:
|
|
*/
|
|
static public function initialize()
|
|
{
|
|
$GLOBALS['calendar_manager'] = $GLOBALS['injector']->createInstance('Kronolith_CalendarsManager');
|
|
}
|
|
|
|
/**
|
|
* Returns the real name, if available, of a user.
|
|
*/
|
|
static public function getUserName($uid)
|
|
{
|
|
static $names = array();
|
|
|
|
if (!isset($names[$uid])) {
|
|
$ident = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Identity')->create($uid);
|
|
$ident->setDefault($ident->getDefault());
|
|
$names[$uid] = $ident->getValue('fullname');
|
|
if (empty($names[$uid])) {
|
|
$names[$uid] = $uid;
|
|
}
|
|
}
|
|
|
|
return $names[$uid];
|
|
}
|
|
|
|
/**
|
|
* Returns the email address, if available, of a user.
|
|
*/
|
|
static public function getUserEmail($uid)
|
|
{
|
|
static $emails = array();
|
|
|
|
if (!isset($emails[$uid])) {
|
|
$ident = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Identity')->create($uid);
|
|
$emails[$uid] = $ident->getValue('from_addr');
|
|
if (empty($emails[$uid])) {
|
|
$emails[$uid] = $uid;
|
|
}
|
|
}
|
|
|
|
return $emails[$uid];
|
|
}
|
|
|
|
/**
|
|
* Checks if an email address belongs to a user.
|
|
*/
|
|
static public function isUserEmail($uid, $email)
|
|
{
|
|
static $emails = array();
|
|
|
|
if (!isset($emails[$uid])) {
|
|
$ident = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Identity')->create($uid);
|
|
|
|
$addrs = $ident->getAll('from_addr');
|
|
$addrs[] = $uid;
|
|
|
|
$emails[$uid] = $addrs;
|
|
}
|
|
|
|
return in_array($email, $emails[$uid]);
|
|
}
|
|
|
|
/**
|
|
* Maps a Kronolith recurrence value to a translated string suitable for
|
|
* display.
|
|
*
|
|
* @param integer $type The recurrence value; one of the
|
|
* Horde_Date_Recurrence::RECUR_XXX constants.
|
|
*
|
|
* @return string The translated displayable recurrence value string.
|
|
*/
|
|
static public function recurToString($type)
|
|
{
|
|
switch ($type) {
|
|
case Horde_Date_Recurrence::RECUR_NONE:
|
|
return _("Does not recur");
|
|
|
|
case Horde_Date_Recurrence::RECUR_DAILY:
|
|
return _("Recurs daily");
|
|
|
|
case Horde_Date_Recurrence::RECUR_WEEKLY:
|
|
return _("Recurs weekly");
|
|
|
|
case Horde_Date_Recurrence::RECUR_MONTHLY_DATE:
|
|
case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY:
|
|
return _("Recurs monthly");
|
|
|
|
case Horde_Date_Recurrence::RECUR_YEARLY_DATE:
|
|
case Horde_Date_Recurrence::RECUR_YEARLY_DAY:
|
|
case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY:
|
|
return _("Recurs yearly");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps a Kronolith meeting status string to a translated string suitable
|
|
* for display.
|
|
*
|
|
* @param integer $status The meeting status; one of the
|
|
* Kronolith::STATUS_XXX constants.
|
|
*
|
|
* @return string The translated displayable meeting status string.
|
|
*/
|
|
static public function statusToString($status)
|
|
{
|
|
switch ($status) {
|
|
case self::STATUS_CONFIRMED:
|
|
return _("Confirmed");
|
|
|
|
case self::STATUS_CANCELLED:
|
|
return _("Cancelled");
|
|
|
|
case self::STATUS_FREE:
|
|
return _("Free");
|
|
|
|
case self::STATUS_TENTATIVE:
|
|
default:
|
|
return _("Tentative");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps a Kronolith attendee response string to a translated string
|
|
* suitable for display.
|
|
*
|
|
* @param integer $response The attendee response; one of the
|
|
* Kronolith::RESPONSE_XXX constants.
|
|
*
|
|
* @return string The translated displayable attendee response string.
|
|
*/
|
|
static public function responseToString($response)
|
|
{
|
|
switch ($response) {
|
|
case self::RESPONSE_ACCEPTED:
|
|
return _("Accepted");
|
|
|
|
case self::RESPONSE_DECLINED:
|
|
return _("Declined");
|
|
|
|
case self::RESPONSE_TENTATIVE:
|
|
return _("Tentative");
|
|
|
|
case self::RESPONSE_NONE:
|
|
default:
|
|
return _("None");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps a Kronolith attendee participation string to a translated string
|
|
* suitable for display.
|
|
*
|
|
* @param integer $part The attendee participation; one of the
|
|
* Kronolith::PART_XXX constants.
|
|
*
|
|
* @return string The translated displayable attendee participation
|
|
* string.
|
|
*/
|
|
static public function partToString($part)
|
|
{
|
|
switch ($part) {
|
|
case self::PART_OPTIONAL:
|
|
return _("Optional");
|
|
|
|
case self::PART_NONE:
|
|
return _("None");
|
|
|
|
case self::PART_REQUIRED:
|
|
default:
|
|
return _("Required");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps an iCalendar attendee response string to the corresponding
|
|
* Kronolith value.
|
|
*
|
|
* @param string $response The attendee response.
|
|
*
|
|
* @return string The Kronolith response value.
|
|
*/
|
|
static public function responseFromICal($response)
|
|
{
|
|
switch (Horde_String::upper($response)) {
|
|
case 'ACCEPTED':
|
|
return self::RESPONSE_ACCEPTED;
|
|
|
|
case 'DECLINED':
|
|
return self::RESPONSE_DECLINED;
|
|
|
|
case 'TENTATIVE':
|
|
return self::RESPONSE_TENTATIVE;
|
|
|
|
case 'NEEDS-ACTION':
|
|
default:
|
|
return self::RESPONSE_NONE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the HTML for an event status widget.
|
|
*
|
|
* @param string $name The name of the widget.
|
|
* @param string $current The selected status value.
|
|
* @param string $any Whether an 'any' item should be added
|
|
*
|
|
* @return string The HTML <select> widget.
|
|
*/
|
|
static public function buildStatusWidget($name,
|
|
$current = self::STATUS_CONFIRMED,
|
|
$any = false)
|
|
{
|
|
$html = "<select id=\"$name\" name=\"$name\">";
|
|
|
|
$statii = array(
|
|
self::STATUS_FREE,
|
|
self::STATUS_TENTATIVE,
|
|
self::STATUS_CONFIRMED,
|
|
self::STATUS_CANCELLED
|
|
);
|
|
|
|
if (!isset($current)) {
|
|
$current = self::STATUS_NONE;
|
|
}
|
|
|
|
if ($any) {
|
|
$html .= "<option value=\"" . self::STATUS_NONE . "\"";
|
|
$html .= ($current == self::STATUS_NONE) ? ' selected="selected">' : '>';
|
|
$html .= _("Any") . "</option>";
|
|
}
|
|
|
|
foreach ($statii as $status) {
|
|
$html .= "<option value=\"$status\"";
|
|
$html .= ($status == $current) ? ' selected="selected">' : '>';
|
|
$html .= self::statusToString($status) . "</option>";
|
|
}
|
|
$html .= '</select>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Returns all internal calendars a user has access to, according
|
|
* to several parameters/permission levels.
|
|
*
|
|
* This method takes the $conf['share']['hidden'] setting into account. If
|
|
* this setting is enabled, even if requesting permissions different than
|
|
* SHOW, it will only return calendars that the user owns or has SHOW
|
|
* permissions for. For checking individual calendar's permissions, use
|
|
* hasPermission() instead.
|
|
*
|
|
* @param boolean $owneronly Only return calenders that this user owns?
|
|
* Defaults to false.
|
|
* @param integer $permission The permission to filter calendars by.
|
|
* @param string $user The user to list calendars for, if not
|
|
* the current.
|
|
*
|
|
* @return array The calendar list.
|
|
*/
|
|
public static function listInternalCalendars($owneronly = false,
|
|
$permission = Horde_Perms::SHOW,
|
|
$user = null)
|
|
{
|
|
if ($owneronly && !$GLOBALS['registry']->getAuth()) {
|
|
return array();
|
|
}
|
|
|
|
if (empty($user)) {
|
|
$user = $GLOBALS['registry']->getAuth();
|
|
}
|
|
|
|
$kronolith_shares = $GLOBALS['injector']->getInstance('Kronolith_Shares');
|
|
|
|
if ($owneronly || empty($GLOBALS['conf']['share']['hidden'])) {
|
|
try {
|
|
$calendars = $kronolith_shares->listShares(
|
|
$user,
|
|
array('perm' => $permission,
|
|
'attributes' => $owneronly ? $user : null,
|
|
'sort_by' => 'name'));
|
|
} catch (Horde_Share_Exception $e) {
|
|
Horde::log($e);
|
|
return array();
|
|
}
|
|
} else {
|
|
try {
|
|
$calendars = $kronolith_shares->listShares(
|
|
$GLOBALS['registry']->getAuth(),
|
|
array('perm' => $permission,
|
|
'attributes' => $user,
|
|
'sort_by' => 'name'));
|
|
} catch (Horde_Share_Exception $e) {
|
|
Horde::log($e);
|
|
return array();
|
|
}
|
|
$display_calendars = @unserialize($GLOBALS['prefs']->getValue('display_cals'));
|
|
if (is_array($display_calendars)) {
|
|
foreach ($display_calendars as $id) {
|
|
try {
|
|
$calendar = $kronolith_shares->getShare($id);
|
|
if ($calendar->hasPermission($user, $permission)) {
|
|
$calendars[$id] = $calendar;
|
|
}
|
|
} catch (Horde_Exception_NotFound $e) {
|
|
} catch (Horde_Share_Exception $e) {
|
|
Horde::log($e);
|
|
return array();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$default_share = $GLOBALS['prefs']->getValue('default_share');
|
|
if (isset($calendars[$default_share])) {
|
|
$calendar = $calendars[$default_share];
|
|
unset($calendars[$default_share]);
|
|
if (!$owneronly || ($owneronly && $calendar->get('owner') == $GLOBALS['registry']->getAuth())) {
|
|
$calendars = array($default_share => $calendar) + $calendars;
|
|
}
|
|
}
|
|
|
|
return $calendars;
|
|
}
|
|
|
|
/**
|
|
* Returns all calendars a user has access to, according to several
|
|
* parameters/permission levels.
|
|
*
|
|
* @param boolean $owneronly Only return calenders that this user owns?
|
|
* Defaults to false.
|
|
* @param boolean $display Only return calendars that are supposed to
|
|
* be displayed per configuration and user
|
|
* preference.
|
|
* @param integer $permission The permission to filter calendars by.
|
|
*
|
|
* @return array The calendar list.
|
|
*/
|
|
static public function listCalendars($permission = Horde_Perms::SHOW,
|
|
$display = false,
|
|
$flat = true)
|
|
{
|
|
$calendars = array();
|
|
foreach ($GLOBALS['calendar_manager']->get(Kronolith::ALL_CALENDARS) as $id => $calendar) {
|
|
if ($calendar->hasPermission($permission) &&
|
|
(!$display || $calendar->display())) {
|
|
if ($flat) {
|
|
$calendars['internal_' . $id] = $calendar;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($GLOBALS['calendar_manager']->get(Kronolith::ALL_REMOTE_CALENDARS) as $id => $calendar) {
|
|
try {
|
|
if ($calendar->hasPermission($permission) &&
|
|
(!$display || $calendar->display())) {
|
|
if ($flat) {
|
|
$calendars['remote_' . $id] = $calendar;
|
|
}
|
|
}
|
|
} catch (Kronolith_Exception $e) {
|
|
$GLOBALS['notification']->push(sprintf(_("The calendar %s returned the error: %s"), $calendar->name(), $e->getMessage()), 'horde.error');
|
|
}
|
|
}
|
|
|
|
foreach ($GLOBALS['calendar_manager']->get(Kronolith::ALL_EXTERNAL_CALENDARS) as $id => $calendar) {
|
|
if ($calendar->hasPermission($permission) &&
|
|
(!$display || $calendar->display())) {
|
|
if ($flat) {
|
|
$calendars['external_' . $id] = $calendar;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($GLOBALS['calendar_manager']->get(Kronolith::ALL_HOLIDAYS) as $id => $calendar) {
|
|
if ($calendar->hasPermission($permission) &&
|
|
(!$display || $calendar->display())) {
|
|
if ($flat) {
|
|
$calendars['holiday_' . $id] = $calendar;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $calendars;
|
|
}
|
|
|
|
/**
|
|
* Returns the default calendar for the current user at the specified
|
|
* permissions level.
|
|
*
|
|
* @param integer $permission Horde_Perms constant for permission level
|
|
* required.
|
|
* @param boolean $owner_only Only consider owner-owned calendars.
|
|
*
|
|
* @return string The calendar id, or null if none.
|
|
*/
|
|
static public function getDefaultCalendar($permission = Horde_Perms::SHOW,
|
|
$owner_only = false)
|
|
{
|
|
$calendars = self::listInternalCalendars($owner_only, $permission);
|
|
|
|
$default_share = $GLOBALS['prefs']->getValue('default_share');
|
|
if (isset($calendars[$default_share])) {
|
|
return $default_share;
|
|
}
|
|
|
|
$default_share = $GLOBALS['injector']
|
|
->getInstance('Kronolith_Factory_Calendars')
|
|
->create()
|
|
->getDefaultShare();
|
|
|
|
// If no default share identified via share backend, use the
|
|
// first found share, and set it in the prefs to make it stick.
|
|
if (!isset($calendars[$default_share])) {
|
|
reset($calendars);
|
|
$default_share = key($calendars);
|
|
}
|
|
|
|
$GLOBALS['prefs']->setValue('default_share', $default_share);
|
|
return $default_share;
|
|
}
|
|
|
|
/**
|
|
* Returns the calendars that should be used for syncing.
|
|
*
|
|
* @param boolean $prune Remove calendar ids from the sync list that no
|
|
* longer exist. The values are pruned *after* the
|
|
* results are passed back to the client to give
|
|
* sync clients a chance to remove their entries.
|
|
*
|
|
* @return array An array of calendar ids
|
|
*/
|
|
static public function getSyncCalendars($prune = false)
|
|
{
|
|
$haveRemoved = false;
|
|
$cs = unserialize($GLOBALS['prefs']->getValue('sync_calendars'));
|
|
if (!empty($cs)) {
|
|
if ($prune) {
|
|
$calendars = self::listInternalCalendars(false, Horde_Perms::DELETE);
|
|
$cscopy = array_flip($cs);
|
|
foreach ($cs as $c) {
|
|
if (empty($calendars[$c])) {
|
|
unset($cscopy[$c]);
|
|
$haveRemoved = true;
|
|
}
|
|
}
|
|
if ($haveRemoved) {
|
|
$GLOBALS['prefs']->setValue('sync_calendars', serialize(array_flip($cscopy)));
|
|
}
|
|
}
|
|
return $cs;
|
|
}
|
|
|
|
if ($cs = self::getDefaultCalendar(Horde_Perms::EDIT, true)) {
|
|
return array($cs);
|
|
}
|
|
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Adds <link> tags for calendar feeds to the HTML header.
|
|
*/
|
|
static public function addCalendarLinks()
|
|
{
|
|
foreach ($GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_CALENDARS) as $calendar) {
|
|
$GLOBALS['page_output']->addLinkTag(array(
|
|
'href' => Kronolith::feedUrl($calendar),
|
|
'type' => 'application/atom+xml'
|
|
));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the label to be used for a calendar.
|
|
*
|
|
* Attaches the owner name of shared calendars if necessary.
|
|
*
|
|
* @param Horde_Share_Object A calendar.
|
|
*
|
|
* @return string The calendar's label.
|
|
*/
|
|
public static function getLabel($calendar)
|
|
{
|
|
$label = $calendar->get('name');
|
|
if ($calendar->get('owner') &&
|
|
$calendar->get('owner') != $GLOBALS['registry']->getAuth()) {
|
|
$label .= ' [' . $GLOBALS['registry']->convertUsername($calendar->get('owner'), false) . ']';
|
|
}
|
|
return $label;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the current user has certain permissions on a calendar.
|
|
*
|
|
* @param string $calendar A calendar id.
|
|
* @param integer $perm A Horde_Perms permission mask.
|
|
*
|
|
* @return boolean True if the current user has the requested permissions.
|
|
*/
|
|
static public function hasPermission($calendar, $perm)
|
|
{
|
|
try {
|
|
$share = $GLOBALS['injector']->getInstance('Kronolith_Shares')->getShare($calendar);
|
|
if (!$share->hasPermission($GLOBALS['registry']->getAuth(), $perm)) {
|
|
throw new Horde_Exception_NotFound();
|
|
}
|
|
} catch (Horde_Exception_NotFound $e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Creates a new share.
|
|
*
|
|
* @param array $info Hash with calendar information.
|
|
*
|
|
* @return Horde_Share The new share.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function addShare($info)
|
|
{
|
|
global $calendar_manager, $injector, $prefs, $registry;
|
|
|
|
$kronolith_shares = $injector->getInstance('Kronolith_Shares');
|
|
|
|
try {
|
|
$calendar = $kronolith_shares->newShare(
|
|
$registry->getAuth(),
|
|
strval(new Horde_Support_Randomid()),
|
|
$info['name']
|
|
);
|
|
} catch (Horde_Share_Exception $e) {
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
|
|
$calendar->set('color', $info['color']);
|
|
$calendar->set('desc', $info['description']);
|
|
if (!empty($info['system'])) {
|
|
$calendar->set('owner', null);
|
|
}
|
|
$tagger = self::getTagger();
|
|
$tagger->tag(
|
|
$calendar->getName(),
|
|
$info['tags'],
|
|
$calendar->get('owner'),
|
|
Kronolith_Tagger::TYPE_CALENDAR
|
|
);
|
|
|
|
try {
|
|
$kronolith_shares->addShare($calendar);
|
|
} catch (Horde_Share_Exception $e) {
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
|
|
$all_cals = $calendar_manager->get(Kronolith::ALL_CALENDARS);
|
|
$all_cals[$calendar->getName()] = new Kronolith_Calendar_Internal(array('share' => $calendar));
|
|
$calendar_manager->set(Kronolith::ALL_CALENDARS, $all_cals);
|
|
$display_cals = $calendar_manager->get(Kronolith::DISPLAY_CALENDARS);
|
|
$display_cals[] = $calendar->getName();
|
|
$calendar_manager->set(Kronolith::DISPLAY_CALENDARS, $display_cals);
|
|
$prefs->setValue('display_cals', serialize($display_cals));
|
|
|
|
return $calendar;
|
|
}
|
|
|
|
/**
|
|
* Updates an existing share.
|
|
*
|
|
* @param Horde_Share $share The share to update.
|
|
* @param array $info Hash with calendar information.
|
|
*
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function updateShare(&$calendar, $info)
|
|
{
|
|
if (!$GLOBALS['registry']->getAuth() ||
|
|
($calendar->get('owner') != $GLOBALS['registry']->getAuth() &&
|
|
(!is_null($calendar->get('owner')) || !$GLOBALS['registry']->isAdmin()))) {
|
|
throw new Kronolith_Exception(_("You are not allowed to change this calendar."));
|
|
}
|
|
|
|
$calendar->set('name', $info['name']);
|
|
$calendar->set('color', $info['color']);
|
|
$calendar->set('desc', $info['description']);
|
|
$calendar->set('owner', empty($info['system']) ? $GLOBALS['registry']->getAuth() : null);
|
|
|
|
try {
|
|
$calendar->save();
|
|
} catch (Horde_Share_Exception $e) {
|
|
throw new Kronolith_Exception(sprintf(_("Unable to save calendar \"%s\": %s"), $info['name'], $e->getMessage()));
|
|
}
|
|
|
|
$tagger = self::getTagger();
|
|
$tagger->replaceTags($calendar->getName(), $info['tags'], $calendar->get('owner'), Kronolith_Tagger::TYPE_CALENDAR);
|
|
}
|
|
|
|
/**
|
|
* Deletes a share and removes all events associated with it.
|
|
*
|
|
* @param Horde_Share $calendar The share to delete.
|
|
*
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function deleteShare($calendar)
|
|
{
|
|
if (!$GLOBALS['registry']->getAuth() ||
|
|
($calendar->get('owner') != $GLOBALS['registry']->getAuth() &&
|
|
(!is_null($calendar->get('owner')) ||
|
|
!$GLOBALS['registry']->isAdmin()))) {
|
|
throw new Kronolith_Exception(_("You are not allowed to delete this calendar."));
|
|
}
|
|
|
|
// Delete the calendar.
|
|
try {
|
|
self::getDriver()->delete($calendar->getName());
|
|
} catch (Exception $e) {
|
|
throw new Kronolith_Exception(sprintf(_("Unable to delete \"%s\": %s"), $calendar->get('name'), $e->getMessage()));
|
|
}
|
|
|
|
// Remove share and all groups/permissions.
|
|
try {
|
|
$GLOBALS['injector']
|
|
->getInstance('Kronolith_Shares')
|
|
->removeShare($calendar);
|
|
} catch (Horde_Share_Exception $e) {
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a list of user with read access to a share.
|
|
*
|
|
* @param Horde_Share_Object $share A share object.
|
|
*
|
|
* @return array A hash of share users.
|
|
*/
|
|
static public function listShareUsers($share)
|
|
{
|
|
global $injector;
|
|
|
|
$users = $share->listUsers(Horde_Perms::READ);
|
|
$groups = $share->listGroups(Horde_Perms::READ);
|
|
if (count($groups)) {
|
|
$horde_group = $injector->getInstance('Horde_Group');
|
|
foreach ($groups as $group) {
|
|
$users = array_merge(
|
|
$users,
|
|
$horde_group->listUsers($group)
|
|
);
|
|
}
|
|
}
|
|
|
|
$users = array_flip($users);
|
|
$factory = $injector->getInstance('Horde_Core_Factory_Identity');
|
|
foreach (array_keys($users) as $user) {
|
|
$fullname = $factory->create($user)->getValue('fullname');
|
|
$users[$user] = strlen($fullname) ? $fullname : $user;
|
|
}
|
|
|
|
return $users;
|
|
}
|
|
|
|
/**
|
|
* Reads a submitted permissions form and updates the share permissions.
|
|
*
|
|
* @param Horde_Share_Object $share The share to update.
|
|
*
|
|
* @return array A list of error messages.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function readPermsForm($share)
|
|
{
|
|
$auth = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create();
|
|
$perm = $share->getPermission();
|
|
$errors = array();
|
|
|
|
if ($GLOBALS['conf']['share']['notify']) {
|
|
$identity = $GLOBALS['injector']
|
|
->getInstance('Horde_Core_Factory_Identity')
|
|
->create();
|
|
$mail = new Horde_Mime_Mail(array(
|
|
'From' => $identity->getDefaultFromAddress(true),
|
|
'User-Agent' => 'Kronolith ' . $GLOBALS['registry']->getVersion()));
|
|
$image = self::getImagePart('big_share.png');
|
|
$view = new Horde_View(array('templatePath' => KRONOLITH_TEMPLATES . '/share'));
|
|
new Horde_View_Helper_Text($view);
|
|
$view->identity = $identity;
|
|
$view->calendar = $share->get('name');
|
|
$view->imageId = $image->getContentId();
|
|
}
|
|
|
|
// Process owner and owner permissions.
|
|
$old_owner = $share->get('owner');
|
|
$new_owner_backend = Horde_Util::getFormData('owner_select', Horde_Util::getFormData('owner_input', $old_owner));
|
|
$new_owner = $GLOBALS['registry']->convertUsername($new_owner_backend, true);
|
|
if ($old_owner !== $new_owner && !empty($new_owner)) {
|
|
if ($old_owner != $GLOBALS['registry']->getAuth() && !$GLOBALS['registry']->isAdmin()) {
|
|
$errors[] = _("Only the owner or system administrator may change ownership or owner permissions for a share");
|
|
} elseif ($auth->hasCapability('list') && !$auth->exists($new_owner_backend)) {
|
|
$errors[] = sprintf(_("The user \"%s\" does not exist."), $new_owner_backend);
|
|
} else {
|
|
$share->set('owner', $new_owner);
|
|
$share->save();
|
|
if ($GLOBALS['conf']['share']['notify']) {
|
|
$view->ownerChange = true;
|
|
$multipart = self::buildMimeMessage($view, 'notification', $image);
|
|
$to = $GLOBALS['injector']
|
|
->getInstance('Horde_Core_Factory_Identity')
|
|
->create($new_owner)
|
|
->getDefaultFromAddress(true);
|
|
$mail->addHeader('Subject', _("Ownership assignment"));
|
|
$mail->addHeader('To', $to);
|
|
$mail->setBasePart($multipart);
|
|
$mail->send($GLOBALS['injector']->getInstance('Horde_Mail'));
|
|
$view->ownerChange = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($GLOBALS['conf']['share']['notify']) {
|
|
if ($GLOBALS['conf']['share']['hidden']) {
|
|
$view->subscribe = Horde::url('calendars/subscribe.php', true)->add('calendar', $share->getName());
|
|
}
|
|
$multipart = self::buildMimeMessage($view, 'notification', $image);
|
|
}
|
|
|
|
if ($GLOBALS['registry']->isAdmin() ||
|
|
!empty($GLOBALS['conf']['share']['world'])) {
|
|
// Process default permissions.
|
|
if (Horde_Util::getFormData('default_show')) {
|
|
$perm->addDefaultPermission(Horde_Perms::SHOW, false);
|
|
} else {
|
|
$perm->removeDefaultPermission(Horde_Perms::SHOW, false);
|
|
}
|
|
if (Horde_Util::getFormData('default_read')) {
|
|
$perm->addDefaultPermission(Horde_Perms::READ, false);
|
|
} else {
|
|
$perm->removeDefaultPermission(Horde_Perms::READ, false);
|
|
}
|
|
if (Horde_Util::getFormData('default_edit')) {
|
|
$perm->addDefaultPermission(Horde_Perms::EDIT, false);
|
|
} else {
|
|
$perm->removeDefaultPermission(Horde_Perms::EDIT, false);
|
|
}
|
|
if (Horde_Util::getFormData('default_delete')) {
|
|
$perm->addDefaultPermission(Horde_Perms::DELETE, false);
|
|
} else {
|
|
$perm->removeDefaultPermission(Horde_Perms::DELETE, false);
|
|
}
|
|
if (Horde_Util::getFormData('default_delegate')) {
|
|
$perm->addDefaultPermission(self::PERMS_DELEGATE, false);
|
|
} else {
|
|
$perm->removeDefaultPermission(self::PERMS_DELEGATE, false);
|
|
}
|
|
|
|
// Process guest permissions.
|
|
if (Horde_Util::getFormData('guest_show')) {
|
|
$perm->addGuestPermission(Horde_Perms::SHOW, false);
|
|
} else {
|
|
$perm->removeGuestPermission(Horde_Perms::SHOW, false);
|
|
}
|
|
if (Horde_Util::getFormData('guest_read')) {
|
|
$perm->addGuestPermission(Horde_Perms::READ, false);
|
|
} else {
|
|
$perm->removeGuestPermission(Horde_Perms::READ, false);
|
|
}
|
|
if (Horde_Util::getFormData('guest_edit')) {
|
|
$perm->addGuestPermission(Horde_Perms::EDIT, false);
|
|
} else {
|
|
$perm->removeGuestPermission(Horde_Perms::EDIT, false);
|
|
}
|
|
if (Horde_Util::getFormData('guest_delete')) {
|
|
$perm->addGuestPermission(Horde_Perms::DELETE, false);
|
|
} else {
|
|
$perm->removeGuestPermission(Horde_Perms::DELETE, false);
|
|
}
|
|
if (Horde_Util::getFormData('guest_delegate')) {
|
|
$perm->addGuestPermission(self::PERMS_DELEGATE, false);
|
|
} else {
|
|
$perm->removeGuestPermission(self::PERMS_DELEGATE, false);
|
|
}
|
|
|
|
// Process creator permissions.
|
|
if (Horde_Util::getFormData('creator_show')) {
|
|
$perm->addCreatorPermission(Horde_Perms::SHOW, false);
|
|
} else {
|
|
$perm->removeCreatorPermission(Horde_Perms::SHOW, false);
|
|
}
|
|
if (Horde_Util::getFormData('creator_read')) {
|
|
$perm->addCreatorPermission(Horde_Perms::READ, false);
|
|
} else {
|
|
$perm->removeCreatorPermission(Horde_Perms::READ, false);
|
|
}
|
|
if (Horde_Util::getFormData('creator_edit')) {
|
|
$perm->addCreatorPermission(Horde_Perms::EDIT, false);
|
|
} else {
|
|
$perm->removeCreatorPermission(Horde_Perms::EDIT, false);
|
|
}
|
|
if (Horde_Util::getFormData('creator_delete')) {
|
|
$perm->addCreatorPermission(Horde_Perms::DELETE, false);
|
|
} else {
|
|
$perm->removeCreatorPermission(Horde_Perms::DELETE, false);
|
|
}
|
|
if (Horde_Util::getFormData('creator_delegate')) {
|
|
$perm->addCreatorPermission(self::PERMS_DELEGATE, false);
|
|
} else {
|
|
$perm->removeCreatorPermission(self::PERMS_DELEGATE, false);
|
|
}
|
|
}
|
|
|
|
// Process user permissions.
|
|
$u_names = Horde_Util::getFormData('u_names');
|
|
$u_show = Horde_Util::getFormData('u_show');
|
|
$u_read = Horde_Util::getFormData('u_read');
|
|
$u_edit = Horde_Util::getFormData('u_edit');
|
|
$u_delete = Horde_Util::getFormData('u_delete');
|
|
$u_delegate = Horde_Util::getFormData('u_delegate');
|
|
|
|
$current = $perm->getUserPermissions();
|
|
if ($GLOBALS['conf']['share']['notify']) {
|
|
$mail->addHeader('Subject', _("Access permissions"));
|
|
}
|
|
|
|
$perm->removeUserPermission(null, null, false);
|
|
foreach ($u_names as $key => $user_backend) {
|
|
// Apply backend hooks
|
|
$user = $GLOBALS['registry']->convertUsername($user_backend, true);
|
|
// If the user is empty, or we've already set permissions
|
|
// via the owner_ options, don't do anything here.
|
|
if (empty($user) || $user == $new_owner) {
|
|
continue;
|
|
}
|
|
if ($auth->hasCapability('list') && !$auth->exists($user_backend)) {
|
|
$errors[] = sprintf(_("The user \"%s\" does not exist."), $user_backend);
|
|
continue;
|
|
}
|
|
|
|
$has_perms = false;
|
|
if (!empty($u_show[$key])) {
|
|
$perm->addUserPermission($user, Horde_Perms::SHOW, false);
|
|
$has_perms = true;
|
|
}
|
|
if (!empty($u_read[$key])) {
|
|
$perm->addUserPermission($user, Horde_Perms::READ, false);
|
|
$has_perms = true;
|
|
}
|
|
if (!empty($u_edit[$key])) {
|
|
$perm->addUserPermission($user, Horde_Perms::EDIT, false);
|
|
$has_perms = true;
|
|
}
|
|
if (!empty($u_delete[$key])) {
|
|
$perm->addUserPermission($user, Horde_Perms::DELETE, false);
|
|
$has_perms = true;
|
|
}
|
|
if (!empty($u_delegate[$key])) {
|
|
$perm->addUserPermission($user, self::PERMS_DELEGATE, false);
|
|
$has_perms = true;
|
|
}
|
|
|
|
// Notify users that have been added.
|
|
if ($GLOBALS['conf']['share']['notify'] &&
|
|
!isset($current[$user]) && $has_perms) {
|
|
$to = $GLOBALS['injector']
|
|
->getInstance('Horde_Core_Factory_Identity')
|
|
->create($user)
|
|
->getDefaultFromAddress(true);
|
|
$mail->addHeader('To', $to);
|
|
$mail->setBasePart($multipart);
|
|
$mail->send($GLOBALS['injector']->getInstance('Horde_Mail'));
|
|
}
|
|
}
|
|
|
|
// Process group permissions.
|
|
$g_names = Horde_Util::getFormData('g_names');
|
|
$g_show = Horde_Util::getFormData('g_show');
|
|
$g_read = Horde_Util::getFormData('g_read');
|
|
$g_edit = Horde_Util::getFormData('g_edit');
|
|
$g_delete = Horde_Util::getFormData('g_delete');
|
|
$g_delegate = Horde_Util::getFormData('g_delegate');
|
|
|
|
$current = $perm->getGroupPermissions();
|
|
$perm->removeGroupPermission(null, null, false);
|
|
foreach ($g_names as $key => $group) {
|
|
if (empty($group)) {
|
|
continue;
|
|
}
|
|
|
|
$has_perms = false;
|
|
if (!empty($g_show[$key])) {
|
|
$perm->addGroupPermission($group, Horde_Perms::SHOW, false);
|
|
$has_perms = true;
|
|
}
|
|
if (!empty($g_read[$key])) {
|
|
$perm->addGroupPermission($group, Horde_Perms::READ, false);
|
|
$has_perms = true;
|
|
}
|
|
if (!empty($g_edit[$key])) {
|
|
$perm->addGroupPermission($group, Horde_Perms::EDIT, false);
|
|
$has_perms = true;
|
|
}
|
|
if (!empty($g_delete[$key])) {
|
|
$perm->addGroupPermission($group, Horde_Perms::DELETE, false);
|
|
$has_perms = true;
|
|
}
|
|
if (!empty($g_delegate[$key])) {
|
|
$perm->addGroupPermission($group, self::PERMS_DELEGATE, false);
|
|
$has_perms = true;
|
|
}
|
|
|
|
// Notify users that have been added.
|
|
if ($GLOBALS['conf']['share']['notify'] &&
|
|
!isset($current[$group]) && $has_perms) {
|
|
$groupOb = $GLOBALS['injector']
|
|
->getInstance('Horde_Group')
|
|
->getData($group);
|
|
if (!empty($groupOb['email'])) {
|
|
$mail->addHeader('To', $groupOb['name'] . ' <' . $groupOb['email'] . '>');
|
|
$mail->setBasePart($multipart);
|
|
$mail->send($GLOBALS['injector']->getInstance('Horde_Mail'));
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
$share->setPermission($perm);
|
|
} catch (Horde_Share_Exception $e) {
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
/**
|
|
* Subscribes to or updates a remote calendar.
|
|
*
|
|
* @param array $info Hash with calendar information.
|
|
* @param string $update If present, the original URL of the calendar to
|
|
* update.
|
|
*
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function subscribeRemoteCalendar(&$info, $update = false)
|
|
{
|
|
if (!(strlen($info['name']) && strlen($info['url']))) {
|
|
throw new Kronolith_Exception(_("You must specify a name and a URL."));
|
|
}
|
|
|
|
if (!empty($info['user']) || !empty($info['password'])) {
|
|
$key = $GLOBALS['registry']->getAuthCredential('password');
|
|
if ($key) {
|
|
$secret = $GLOBALS['injector']->getInstance('Horde_Secret');
|
|
$info['user'] = base64_encode($secret->write($key, $info['user']));
|
|
$info['password'] = base64_encode($secret->write($key, $info['password']));
|
|
}
|
|
}
|
|
|
|
$remote_calendars = unserialize($GLOBALS['prefs']->getValue('remote_cals'));
|
|
if ($update) {
|
|
foreach ($remote_calendars as $key => $calendar) {
|
|
if ($calendar['url'] == $update) {
|
|
$remote_calendars[$key] = $info;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
$remote_calendars[] = $info;
|
|
$display_remote = $GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_REMOTE_CALENDARS);
|
|
$display_remote[] = $info['url'];
|
|
$GLOBALS['calendar_manager']->set(Kronolith::DISPLAY_REMOTE_CALENDARS, $display_remote);
|
|
$GLOBALS['prefs']->setValue('display_remote_cals', serialize($display_remote));
|
|
}
|
|
|
|
$GLOBALS['prefs']->setValue('remote_cals', serialize($remote_calendars));
|
|
}
|
|
|
|
/**
|
|
* Unsubscribes from a remote calendar.
|
|
*
|
|
* @param string $url The calendar URL.
|
|
*
|
|
* @return array Hash with the deleted calendar's information.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function unsubscribeRemoteCalendar($url)
|
|
{
|
|
$url = trim($url);
|
|
if (!strlen($url)) {
|
|
return false;
|
|
}
|
|
|
|
$remote_calendars = unserialize($GLOBALS['prefs']->getValue('remote_cals'));
|
|
$remote_calendar = null;
|
|
foreach ($remote_calendars as $key => $calendar) {
|
|
if ($calendar['url'] == $url) {
|
|
$remote_calendar = $calendar;
|
|
unset($remote_calendars[$key]);
|
|
break;
|
|
}
|
|
}
|
|
if (!$remote_calendar) {
|
|
throw new Kronolith_Exception(_("The remote calendar was not found."));
|
|
}
|
|
|
|
$GLOBALS['prefs']->setValue('remote_cals', serialize($remote_calendars));
|
|
|
|
return $remote_calendar;
|
|
}
|
|
|
|
/**
|
|
* Returns the feed URL for a calendar.
|
|
*
|
|
* @param string $calendar A calendar name.
|
|
*
|
|
* @return string The calendar's feed URL.
|
|
*/
|
|
static public function feedUrl($calendar)
|
|
{
|
|
if (isset($GLOBALS['conf']['urls']['pretty']) &&
|
|
$GLOBALS['conf']['urls']['pretty'] == 'rewrite') {
|
|
return Horde::url('feed/' . $calendar, true, -1);
|
|
}
|
|
return Horde::url('feed/index.php', true, -1)
|
|
->add('c', $calendar);
|
|
}
|
|
|
|
/**
|
|
* Returs the HTML/javascript snippit needed to embed a calendar in an
|
|
* external website.
|
|
*
|
|
* @param string $calendar A calendar name.
|
|
*
|
|
* @return string The calendar's embed snippit.
|
|
*/
|
|
static public function embedCode($calendar)
|
|
{
|
|
/* Get the base url */
|
|
$url = $GLOBALS['registry']->getServiceLink('ajax', 'kronolith', true)->add(array(
|
|
'calendar' => 'internal_' . $calendar,
|
|
'container' => 'kronolithCal',
|
|
'view' => 'Month'
|
|
));
|
|
$url->url .= 'embed';
|
|
|
|
return '<div id="kronolithCal"></div><script src="' . $url .
|
|
'" type="text/javascript"></script>';
|
|
}
|
|
|
|
/**
|
|
* Parses a comma separated list of names and e-mail addresses into a list
|
|
* of attendee hashes.
|
|
*
|
|
* @param string $newAttendees A comma separated attendee list.
|
|
*
|
|
* @return array The attendee list with e-mail addresses as keys and
|
|
* attendee information as values.
|
|
*/
|
|
static public function parseAttendees($newAttendees)
|
|
{
|
|
global $injector, $notification;
|
|
|
|
if (empty($newAttendees)) {
|
|
return array();
|
|
}
|
|
|
|
$parser = $injector->getInstance('Horde_Mail_Rfc822');
|
|
$attendees = array();
|
|
|
|
/* Parse the address without validation to see what we can get out
|
|
* of it. We allow email addresses (john@example.com), email
|
|
* address with user information (John Doe <john@example.com>),
|
|
* and plain names (John Doe). */
|
|
$result = $parser->parseAddressList($newAttendees);
|
|
$result->setIteratorFilter(Horde_Mail_Rfc822_List::HIDE_GROUPS);
|
|
|
|
foreach ($result as $newAttendee) {
|
|
if (!$newAttendee->valid) {
|
|
// If we can't even get a mailbox out of the address, then it
|
|
// is likely unuseable. Reject it entirely.
|
|
$notification->push(
|
|
sprintf(_("Unable to recognize \"%s\" as an email address."), $newAttendee),
|
|
'horde.error'
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// If there is only a mailbox part, then it is just a local name.
|
|
if (!is_null($newAttendee->host)) {
|
|
// Build a full email address again and validate it.
|
|
try {
|
|
$parser->parseAddressList($newAttendee->writeAddress(true));
|
|
} catch (Horde_Mail_Exception $e) {
|
|
$notification->push($e, 'horde.error');
|
|
continue;
|
|
}
|
|
$name = $newAttendee->label != $newAttendee->bare_address ? $newAttendee->label : '';
|
|
} else {
|
|
$name = $newAttendee->bare_address;
|
|
}
|
|
|
|
// Avoid overwriting existing attendees with the default
|
|
// values.
|
|
$attendees[$newAttendee->bare_address] = array(
|
|
'attendance' => self::PART_REQUIRED,
|
|
'response' => self::RESPONSE_NONE,
|
|
'name' => $name
|
|
);
|
|
}
|
|
|
|
return $attendees;
|
|
}
|
|
|
|
/**
|
|
* Returns a comma separated list of attendees and resources
|
|
*
|
|
* @return string Attendee/Resource list.
|
|
*/
|
|
static public function attendeeList()
|
|
{
|
|
/* Attendees */
|
|
$attendees = self::getAttendeeEmailList($GLOBALS['session']->get('kronolith', 'attendees', Horde_Session::TYPE_ARRAY))->addresses;
|
|
|
|
/* Resources */
|
|
foreach ($GLOBALS['session']->get('kronolith', 'resources', Horde_Session::TYPE_ARRAY) as $resource) {
|
|
$attendees[] = $resource['name'];
|
|
}
|
|
|
|
return implode(', ', $attendees);
|
|
}
|
|
|
|
/**
|
|
* Sends out iTip event notifications to all attendees of a specific
|
|
* event.
|
|
*
|
|
* Can be used to send event invitations, event updates as well as event
|
|
* cancellations.
|
|
*
|
|
* @param Kronolith_Event $event
|
|
* The event in question.
|
|
* @param Horde_Notification_Handler $notification
|
|
* A notification object used to show result status.
|
|
* @param integer $action
|
|
* The type of notification to send. One of the Kronolith::ITIP_*
|
|
* values.
|
|
* @param Horde_Date $instance
|
|
* If cancelling a single instance of a recurring event, the date of
|
|
* this instance.
|
|
* @param string $range The range parameter if this is a recurring event.
|
|
* Possible values are self::RANGE_THISANDFUTURE
|
|
* @param array $cancellations If $action is 'CANCEL', but it is due to
|
|
* removing attendees and not canceling the
|
|
* entire event, these are the email addresses
|
|
* of the uninvited attendees and are the ONLY
|
|
* people that will receive the CANCEL iTIP.
|
|
* @since 4.2.10
|
|
*
|
|
*/
|
|
static public function sendITipNotifications(
|
|
Kronolith_Event $event, Horde_Notification_Handler $notification,
|
|
$action, Horde_Date $instance = null, $range = null, array $cancellations = array())
|
|
{
|
|
global $injector, $registry;
|
|
|
|
if (!$event->attendees) {
|
|
return;
|
|
}
|
|
|
|
$ident = $injector->getInstance('Horde_Core_Factory_Identity')->create($event->creator);
|
|
if (!$ident->getValue('from_addr')) {
|
|
$notification->push(sprintf(_("You do not have an email address configured in your Personal Information Preferences. You must set one %shere%s before event notifications can be sent."), $registry->getServiceLink('prefs', 'kronolith')->add(array('app' => 'horde', 'group' => 'identities'))->link(), '</a>'), 'horde.error', array('content.raw'));
|
|
return;
|
|
}
|
|
|
|
// Generate image mime part first and only once, because we
|
|
// need the Content-ID.
|
|
$image = self::getImagePart('big_invitation.png');
|
|
|
|
$share = $injector->getInstance('Kronolith_Shares')->getShare($event->calendar);
|
|
$view = new Horde_View(array('templatePath' => KRONOLITH_TEMPLATES . '/itip'));
|
|
new Horde_View_Helper_Text($view);
|
|
$view->identity = $ident;
|
|
$view->event = clone $event;
|
|
$view->imageId = $image->getContentId();
|
|
if ($action == self::ITIP_CANCEL && !empty($cancellations)) {
|
|
$mail_attendees = $cancellations;
|
|
} else {
|
|
$mail_attendees = $event->attendees;
|
|
}
|
|
foreach ($mail_attendees as $email => $status) {
|
|
/* Don't bother sending an invitation/update if the recipient does
|
|
* not need to participate, or has declined participating, or
|
|
* doesn't have an email address. */
|
|
if (strpos($email, '@') === false ||
|
|
$status['response'] == self::RESPONSE_DECLINED) {
|
|
continue;
|
|
}
|
|
|
|
/* Determine all notification-specific strings. */
|
|
switch ($action) {
|
|
case self::ITIP_CANCEL:
|
|
/* Cancellation. */
|
|
$method = 'CANCEL';
|
|
$filename = 'event-cancellation.ics';
|
|
$view->subject = sprintf(_("Cancelled: %s"), $event->getTitle());
|
|
if (empty($instance)) {
|
|
$view->header = sprintf(_("%s has cancelled \"%s\"."), $ident->getName(), $event->getTitle());
|
|
} else {
|
|
$view->header = sprintf(_("%s has cancelled an instance of the recurring \"%s\"."), $ident->getName(), $event->getTitle());
|
|
}
|
|
break;
|
|
|
|
case self::ITIP_REQUEST:
|
|
default:
|
|
$method = 'REQUEST';
|
|
if ($status['response'] == self::RESPONSE_NONE) {
|
|
/* Invitation. */
|
|
$filename = 'event-invitation.ics';
|
|
$view->subject = $event->getTitle();
|
|
$view->header = sprintf(_("%s wishes to make you aware of \"%s\"."), $ident->getName(), $event->getTitle());
|
|
} else {
|
|
/* Update. */
|
|
$filename = 'event-update.ics';
|
|
$view->subject = sprintf(_("Updated: %s."), $event->getTitle());
|
|
$view->header = sprintf(_("%s wants to notify you about changes of \"%s\"."), $ident->getName(), $event->getTitle());
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ($event->attendees) {
|
|
$view->attendees = strval(self::getAttendeeEmailList($event->attendees));
|
|
$view->organizer = $registry->convertUserName($event->creator, false);
|
|
}
|
|
|
|
if ($action == self::ITIP_REQUEST) {
|
|
$attend_link = Horde::url('attend.php', true, -1)
|
|
->add(array('c' => $event->calendar,
|
|
'e' => $event->id,
|
|
'u' => $email));
|
|
$view->linkAccept = (string)$attend_link->add('a', 'accept');
|
|
$view->linkTentative = (string)$attend_link->add('a', 'tentative');
|
|
$view->linkDecline = (string)$attend_link->add('a', 'decline');
|
|
}
|
|
|
|
/* Build the iCalendar data */
|
|
$iCal = new Horde_Icalendar();
|
|
$iCal->setAttribute('METHOD', $method);
|
|
$iCal->setAttribute('X-WR-CALNAME', $share->get('name'));
|
|
$vevent = $event->toiCalendar($iCal);
|
|
if ($action == self::ITIP_CANCEL && !empty($instance)) {
|
|
// Recurring event instance deletion, need to specify the
|
|
// RECURRENCE-ID but NOT the EXDATE.
|
|
foreach($vevent as &$ve) {
|
|
try {
|
|
$uid = $ve->getAttribute('UID');
|
|
} catch (Horde_Icalendar_Exception $e) {
|
|
continue;
|
|
}
|
|
if ($event->uid == $uid) {
|
|
$ve->setAttribute('RECURRENCE-ID', $instance);
|
|
if (!empty($range)) {
|
|
$ve->setParameter('RECURRENCE-ID', array('RANGE' => $range));
|
|
}
|
|
$ve->setAttribute('DTSTART', $instance, array(), false);
|
|
$diff = $event->end->timestamp() - $event->start->timestamp();
|
|
$end = clone $instance;
|
|
$end->sec += $diff;
|
|
$ve->setAttribute('DTEND', $end, array(), false);
|
|
$ve->removeAttribute('EXDATE');
|
|
$view->event->fromiCalendar($ve);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$iCal->addComponent($vevent);
|
|
|
|
/* text/calendar part */
|
|
$ics = new Horde_Mime_Part();
|
|
$ics->setType('text/calendar');
|
|
$ics->setContents($iCal->exportvCalendar());
|
|
$ics->setName($filename);
|
|
$ics->setContentTypeParameter('METHOD', $method);
|
|
$ics->setCharset('UTF-8');
|
|
$ics->setEOL("\r\n");
|
|
|
|
/* application/ics part */
|
|
$ics2 = clone $ics;
|
|
$ics2->setType('application/ics');
|
|
|
|
/* multipart/mixed part */
|
|
$multipart = new Horde_Mime_Part();
|
|
$multipart->setType('multipart/mixed');
|
|
$inner = self::buildMimeMessage($view, 'notification', $image);
|
|
$inner->addPart($ics);
|
|
$multipart->addPart($inner);
|
|
$multipart->addPart($ics2);
|
|
|
|
$recipient = new Horde_Mail_Rfc822_Address($email);
|
|
if (!empty($status['name'])) {
|
|
$recipient->personal = $status['name'];
|
|
}
|
|
|
|
$mail = new Horde_Mime_Mail(
|
|
array('Subject' => $view->subject,
|
|
'To' => $recipient,
|
|
'From' => $ident->getDefaultFromAddress(true),
|
|
'User-Agent' => 'Kronolith ' . $registry->getVersion()));
|
|
$mail->setBasePart($multipart);
|
|
|
|
try {
|
|
$mail->send($injector->getInstance('Horde_Mail'));
|
|
$notification->push(
|
|
sprintf(_("The event notification to %s was successfully sent."), $recipient),
|
|
'horde.success'
|
|
);
|
|
} catch (Horde_Mime_Exception $e) {
|
|
$notification->push(
|
|
sprintf(_("There was an error sending an event notification to %s: %s"), $recipient, $e->getMessage(), $e->getCode()),
|
|
'horde.error'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends email notifications that a event has been added, edited, or
|
|
* deleted to users that want such notifications.
|
|
*
|
|
* @param Kronolith_Event $event An event.
|
|
* @param string $action The event action. One of "add", "edit",
|
|
* or "delete".
|
|
*
|
|
* @throws Horde_Mime_Exception
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function sendNotification($event, $action)
|
|
{
|
|
global $injector, $registry;
|
|
|
|
if (!in_array($action, array('add', 'edit', 'delete'))) {
|
|
throw new Kronolith_Exception('Unknown event action: ' . $action);
|
|
}
|
|
|
|
// @TODO: Send notifications to the email addresses stored in the
|
|
// resource object?
|
|
if ($event->calendarType == 'resource') {
|
|
return;
|
|
}
|
|
|
|
$groups = $injector->getInstance('Horde_Group');
|
|
$calendar = $event->calendar;
|
|
$recipients = array();
|
|
try {
|
|
$share = $injector->getInstance('Kronolith_Shares')->getShare($calendar);
|
|
} catch (Horde_Share_Exception $e) {
|
|
throw new Kronolith_Exception($e);
|
|
}
|
|
|
|
$owner = $share->get('owner');
|
|
if ($owner) {
|
|
if ($notificationPrefs = self::_notificationPref($owner, 'owner')) {
|
|
$recipients[$owner] = $notificationPrefs;
|
|
$recipients[$owner]['private'] = $event->isPrivate($owner);
|
|
}
|
|
}
|
|
|
|
$senderIdentity = $injector->getInstance('Horde_Core_Factory_Identity')
|
|
->create($registry->getAuth() ?: $event->creator ?: $owner);
|
|
|
|
foreach ($share->listUsers(Horde_Perms::READ) as $user) {
|
|
if (empty($recipients[$user]) && ($notificationPrefs = self::_notificationPref($user, 'read', $calendar))) {
|
|
$recipients[$user] = $notificationPrefs;
|
|
$recipients[$user]['private'] = $event->isPrivate($user);
|
|
}
|
|
}
|
|
|
|
foreach ($share->listGroups(Horde_Perms::READ) as $group) {
|
|
try {
|
|
$group_users = $groups->listUsers($group);
|
|
} catch (Horde_Group_Exception $e) {
|
|
Horde::log($e, 'ERR');
|
|
continue;
|
|
}
|
|
|
|
foreach ($group_users as $user) {
|
|
if (empty($recipients[$user]) && ($notificationPrefs = self::_notificationPref($user, 'read', $calendar))) {
|
|
$recipients[$user] = $notificationPrefs;
|
|
$recipients[$user]['private'] = $event->isPrivate($user);
|
|
}
|
|
}
|
|
}
|
|
|
|
$addresses = array();
|
|
foreach ($recipients as $user => $vals) {
|
|
if (!$vals) {
|
|
continue;
|
|
}
|
|
$identity = $injector->getInstance('Horde_Core_Factory_Identity')->create($user);
|
|
$email = $identity->getValue('from_addr');
|
|
if (strpos($email, '@') === false) {
|
|
continue;
|
|
}
|
|
if (!isset($addresses[$vals['lang']][$vals['tf']][$vals['df']][$vals['private']])) {
|
|
$addresses[$vals['lang']][$vals['tf']][$vals['df']][$vals['private']] = array();
|
|
}
|
|
|
|
$tmp = new Horde_Mail_Rfc822_Address($email);
|
|
$tmp->personal = $identity->getValue('fullname');
|
|
$addresses[$vals['lang']][$vals['tf']][$vals['df']][$vals['private']][] = strval($tmp);
|
|
}
|
|
|
|
if (!$addresses) {
|
|
return;
|
|
}
|
|
|
|
foreach ($addresses as $lang => $twentyFour) {
|
|
$registry->setLanguageEnvironment($lang);
|
|
|
|
switch ($action) {
|
|
case 'add':
|
|
$subject = _("Event added:");
|
|
$notification_message = _("You requested to be notified when events are added to your calendars.") . "\n\n" . _("The event \"%s\" has been added to \"%s\" calendar, which is on %s at %s.");
|
|
break;
|
|
|
|
case 'edit':
|
|
$subject = _("Event edited:");
|
|
$notification_message = _("You requested to be notified when events are edited in your calendars.") . "\n\n" . _("The event \"%s\" has been edited on \"%s\" calendar, which is on %s at %s.");
|
|
break;
|
|
|
|
case 'delete':
|
|
$subject = _("Event deleted:");
|
|
$notification_message = _("You requested to be notified when events are deleted from your calendars.") . "\n\n" . _("The event \"%s\" has been deleted from \"%s\" calendar, which was on %s at %s.");
|
|
break;
|
|
}
|
|
|
|
foreach ($twentyFour as $tf => $dateFormat) {
|
|
foreach ($dateFormat as $df => $recipients) {
|
|
foreach ($recipients as $is_private => $df_recipients) {
|
|
$event_title = $is_private ? _("busy") : $event->title;
|
|
$message = "\n"
|
|
. sprintf($notification_message,
|
|
$event_title,
|
|
Kronolith::getLabel($share),
|
|
$event->start->strftime($df),
|
|
$event->start->strftime($tf ? '%R' : '%I:%M%p'))
|
|
. "\n\n" . ($is_private ? "" : $event->description);
|
|
|
|
$mime_mail = new Horde_Mime_Mail(array(
|
|
'Subject' => $subject . ' ' . $event_title,
|
|
'To' => implode(',', $df_recipients),
|
|
'From' => $senderIdentity->getDefaultFromAddress(true),
|
|
'User-Agent' => 'Kronolith ' . $registry->getVersion(),
|
|
'body' => $message));
|
|
|
|
Horde::log(sprintf('Sending event notifications for %s to %s', $event_title, implode(', ', $df_recipients)), 'DEBUG');
|
|
try {
|
|
$mime_mail->send($injector->getInstance('Horde_Mail'));
|
|
} catch (Horde_Mime_Exception $e) {
|
|
Horde::log(sprintf('Could not send notification to all recipients %s: %s', implode(', ', $df_recipients), $e->getMessage()), 'ERR');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for resource declines and push notice to stack if found.
|
|
*
|
|
* @param Kronolith_Event $event
|
|
*
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function notifyOfResourceRejection($event)
|
|
{
|
|
$accepted = $declined = array();
|
|
|
|
foreach ($event->getResources() as $id => $resource) {
|
|
if ($resource['response'] == self::RESPONSE_DECLINED) {
|
|
$r = self::getDriver('Resource')->getResource($id);
|
|
$declined[] = $r->get('name');
|
|
} elseif ($resource['response'] == self::RESPONSE_ACCEPTED) {
|
|
$r = self::getDriver('Resource')->getResource($id);
|
|
$accepted[] = $r->get('name');
|
|
}
|
|
|
|
|
|
}
|
|
if (count($declined)) {
|
|
$GLOBALS['notification']->push(
|
|
sprintf(
|
|
ngettext(
|
|
"The following resource has declined your request: %s",
|
|
"The following resources have declined your request: %s",
|
|
count($declined)
|
|
),
|
|
implode(", ", $declined)
|
|
),
|
|
'horde.error'
|
|
);
|
|
}
|
|
if (count($accepted)) {
|
|
$GLOBALS['notification']->push(
|
|
sprintf(
|
|
ngettext(
|
|
"The following resource has accepted your request: %s",
|
|
"The following resources have accepted your request: %s",
|
|
count($accepted)
|
|
),
|
|
implode(", ", $accepted)
|
|
),
|
|
'horde.success'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether a user wants email notifications for a calendar.
|
|
*
|
|
* @access private
|
|
*
|
|
* @todo This method is causing a memory leak somewhere, noticeable if
|
|
* importing a large amount of events.
|
|
*
|
|
* @param string $user A user name.
|
|
* @param string $mode The check "mode". If "owner", the method checks
|
|
* if the user wants notifications only for
|
|
* calendars he owns. If "read", the method checks
|
|
* if the user wants notifications for all
|
|
* calendars he has read access to, or only for
|
|
* shown calendars and the specified calendar is
|
|
* currently shown.
|
|
* @param string $calendar The name of the calendar if mode is "read".
|
|
*
|
|
* @return array|boolen The user's email, time, and language preferences if
|
|
* they want a notification for this calendar. False
|
|
* if no notification should be sent.
|
|
*/
|
|
static public function _notificationPref($user, $mode, $calendar = null)
|
|
{
|
|
$prefs = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Prefs')->create('kronolith', array(
|
|
'cache' => false,
|
|
'user' => $user
|
|
));
|
|
$vals = array('lang' => $prefs->getValue('language'),
|
|
'tf' => $prefs->getValue('twentyFour'),
|
|
'df' => $prefs->getValue('date_format'));
|
|
|
|
if ($prefs->getValue('event_notification_exclude_self') &&
|
|
$user == $GLOBALS['registry']->getAuth()) {
|
|
return false;
|
|
}
|
|
|
|
switch ($prefs->getValue('event_notification')) {
|
|
case 'owner':
|
|
return $mode == 'owner' ? $vals : false;
|
|
|
|
case 'read':
|
|
return $mode == 'read' ? $vals : false;
|
|
|
|
case 'show':
|
|
if ($mode == 'read') {
|
|
$display_calendars = unserialize($prefs->getValue('display_cals'));
|
|
return in_array($calendar, $display_calendars) ? $vals : false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Builds the body MIME part of a multipart message.
|
|
*
|
|
* @param Horde_View $view A view to render the HTML and plain text
|
|
* templates for the messate.
|
|
* @param string $template The template base name for the view.
|
|
* @param Horde_Mime_Part $image The MIME part of a related image.
|
|
*
|
|
* @return Horde_Mime_Part A multipart/alternative MIME part.
|
|
*/
|
|
static public function buildMimeMessage(Horde_View $view, $template,
|
|
Horde_Mime_Part $image)
|
|
{
|
|
$multipart = new Horde_Mime_Part();
|
|
$multipart->setType('multipart/alternative');
|
|
$bodyText = new Horde_Mime_Part();
|
|
$bodyText->setType('text/plain');
|
|
$bodyText->setCharset('UTF-8');
|
|
$bodyText->setContents($view->render($template . '.plain.php'));
|
|
$bodyText->setDisposition('inline');
|
|
$multipart->addPart($bodyText);
|
|
$bodyHtml = new Horde_Mime_Part();
|
|
$bodyHtml->setType('text/html');
|
|
$bodyHtml->setCharset('UTF-8');
|
|
$bodyHtml->setContents($view->render($template . '.html.php'));
|
|
$bodyHtml->setDisposition('inline');
|
|
$related = new Horde_Mime_Part();
|
|
$related->setType('multipart/related');
|
|
$related->setContentTypeParameter('start', $bodyHtml->setContentId());
|
|
$related->addPart($bodyHtml);
|
|
$related->addPart($image);
|
|
$multipart->addPart($related);
|
|
return $multipart;
|
|
}
|
|
|
|
/**
|
|
* Returns a MIME part for an image to be embedded into a HTML document.
|
|
*
|
|
* @param string $file An image file name.
|
|
*
|
|
* @return Horde_Mime_Part A MIME part representing the image.
|
|
*/
|
|
static public function getImagePart($file)
|
|
{
|
|
$background = Horde_Themes::img($file);
|
|
$image = new Horde_Mime_Part();
|
|
$image->setType('image/png');
|
|
$image->setContents(file_get_contents($background->fs));
|
|
$image->setContentId();
|
|
$image->setDisposition('attachment');
|
|
return $image;
|
|
}
|
|
|
|
/**
|
|
* @return Horde_Date
|
|
*/
|
|
static public function currentDate()
|
|
{
|
|
if ($date = Horde_Util::getFormData('date')) {
|
|
return new Horde_Date($date . '000000');
|
|
}
|
|
if ($date = Horde_Util::getFormData('datetime')) {
|
|
return new Horde_Date($date);
|
|
}
|
|
|
|
return new Horde_Date($_SERVER['REQUEST_TIME']);
|
|
}
|
|
|
|
/**
|
|
* Parses a complete date-time string into a Horde_Date object.
|
|
*
|
|
* @param string $date The date-time string to parse.
|
|
* @param boolean $withtime Whether time is included in the string.
|
|
* @þaram string $timezone The timezone of the string.
|
|
*
|
|
* @return Horde_Date The parsed date.
|
|
* @throws Horde_Date_Exception
|
|
*/
|
|
static public function parseDate($date, $withtime = true, $timezone = null)
|
|
{
|
|
// strptime() is not available on Windows.
|
|
if (!function_exists('strptime')) {
|
|
return new Horde_Date($date, $timezone);
|
|
}
|
|
|
|
// strptime() is locale dependent, i.e. %p is not always matching
|
|
// AM/PM. Set the locale to C to workaround this, but grab the
|
|
// locale's D_FMT before that.
|
|
$format = Horde_Nls::getLangInfo(D_FMT);
|
|
if ($withtime) {
|
|
$format .= ' '
|
|
. ($GLOBALS['prefs']->getValue('twentyFour') ? '%H:%M' : '%I:%M %p');
|
|
}
|
|
$old_locale = setlocale(LC_TIME, 0);
|
|
setlocale(LC_TIME, 'C');
|
|
|
|
// Try exact format match first.
|
|
$date_arr = strptime($date, $format);
|
|
setlocale(LC_TIME, $old_locale);
|
|
|
|
if (!$date_arr) {
|
|
// Try with locale dependent parsing next.
|
|
$date_arr = strptime($date, $format);
|
|
if (!$date_arr) {
|
|
// Try throwing at Horde_Date finally.
|
|
return new Horde_Date($date, $timezone);
|
|
}
|
|
}
|
|
|
|
return new Horde_Date(
|
|
array('year' => $date_arr['tm_year'] + 1900,
|
|
'month' => $date_arr['tm_mon'] + 1,
|
|
'mday' => $date_arr['tm_mday'],
|
|
'hour' => $date_arr['tm_hour'],
|
|
'min' => $date_arr['tm_min'],
|
|
'sec' => $date_arr['tm_sec']),
|
|
$timezone);
|
|
}
|
|
|
|
/**
|
|
* @param object $renderer A Kronolith view.
|
|
*/
|
|
static public function tabs($renderer)
|
|
{
|
|
global $injector, $prefs;
|
|
|
|
$view = $injector->createInstance('Horde_View');
|
|
|
|
$date = self::currentDate();
|
|
$date_stamp = array('date' => $date->dateString());
|
|
$tabname = basename($_SERVER['PHP_SELF']) == 'index.php'
|
|
? $GLOBALS['prefs']->getValue('defaultview')
|
|
: str_replace('.php', '', basename($_SERVER['PHP_SELF']));
|
|
|
|
$view->active = $tabname;
|
|
$view->previous = $renderer->link(-1);
|
|
$view->next = $renderer->link(1);
|
|
switch ($tabname) {
|
|
case 'day':
|
|
$view->current = $renderer->getTime($prefs->getValue('date_format'));
|
|
break;
|
|
case 'workweek':
|
|
case 'week':
|
|
$view->current =
|
|
$renderer->days[$renderer->startDay]
|
|
->getTime($prefs->getValue('date_format'))
|
|
. ' - '
|
|
. $renderer->days[$renderer->endDay]
|
|
->getTime($prefs->getValue('date_format'));
|
|
break;
|
|
case 'month':
|
|
$view->current = $renderer->date->strftime('%B %Y');
|
|
break;
|
|
case 'year':
|
|
$view->current = $renderer->year;
|
|
break;
|
|
}
|
|
$view->today = Horde::url($prefs->getValue('defaultview') . '.php')
|
|
->link(Horde::getAccessKeyAndTitle(_("_Today"), false, true))
|
|
. _("Today") . '</a>';
|
|
$view->day = Horde::widget(array(
|
|
'url' => Horde::url('day.php')->add($date_stamp),
|
|
'id' => 'kronolithNavDay',
|
|
'accesskey' => '1',
|
|
'title' => _("Day")
|
|
));
|
|
$view->workWeek = Horde::widget(array(
|
|
'url' => Horde::url('workweek.php')->add($date_stamp),
|
|
'id' => 'kronolithNavWorkweek',
|
|
'accesskey' => '2',
|
|
'title' => _("Work Week")
|
|
));
|
|
$view->week = Horde::widget(array(
|
|
'url' => Horde::url('week.php')->add($date_stamp),
|
|
'id' => 'kronolithNavWeek',
|
|
'accesskey' => '3',
|
|
'title' => _("Week")
|
|
));
|
|
$view->month = Horde::widget(array(
|
|
'url' => Horde::url('month.php')->add($date_stamp),
|
|
'id' => 'kronolithNavMonth',
|
|
'accesskey' => '4',
|
|
'title' => _("Month")
|
|
));
|
|
$view->year = Horde::widget(array(
|
|
'url' => Horde::url('year.php')->add($date_stamp),
|
|
'id' => 'kronolithNavYear',
|
|
'accesskey' => '5',
|
|
'title' => _("Year")
|
|
));
|
|
|
|
echo $view->render('buttonbar');
|
|
}
|
|
|
|
/**
|
|
* @param string $tabname
|
|
* @param Kronolith_Event $event
|
|
*/
|
|
static public function eventTabs($tabname, $event)
|
|
{
|
|
if (!$event->initialized) {
|
|
return;
|
|
}
|
|
|
|
$GLOBALS['page_output']->addScriptFile('views.js');
|
|
$tabs = new Horde_Core_Ui_Tabs('event', Horde_Variables::getDefaultVariables());
|
|
|
|
$date = self::currentDate();
|
|
$tabs->preserve('datetime', $date->dateString());
|
|
|
|
$tabs->addTab(
|
|
htmlspecialchars($event->getTitle()),
|
|
$event->getViewUrl(),
|
|
array('tabname' => 'Event',
|
|
'id' => 'tabEvent',
|
|
'onclick' => 'return ShowTab(\'Event\');'));
|
|
/* We check for read permissions, because we can always save a copy if
|
|
* we can read the event. */
|
|
if ((!$event->private ||
|
|
$event->creator == $GLOBALS['registry']->getAuth()) &&
|
|
$event->hasPermission(Horde_Perms::READ) &&
|
|
self::getDefaultCalendar(Horde_Perms::EDIT)) {
|
|
$tabs->addTab(
|
|
$event->hasPermission(Horde_Perms::EDIT) ? _("_Edit") : _("Save As New"),
|
|
$event->getEditUrl(),
|
|
array('tabname' => 'EditEvent',
|
|
'id' => 'tabEditEvent',
|
|
'onclick' => 'return ShowTab(\'EditEvent\');'));
|
|
}
|
|
if ($event->hasPermission(Horde_Perms::DELETE)) {
|
|
$tabs->addTab(
|
|
_("De_lete"),
|
|
$event->getDeleteUrl(array('confirm' => 1)),
|
|
array('tabname' => 'DeleteEvent',
|
|
'id' => 'tabDeleteEvent',
|
|
'onclick' => 'return ShowTab(\'DeleteEvent\');'));
|
|
}
|
|
$tabs->addTab(
|
|
_("Export"),
|
|
$event->getExportUrl(),
|
|
array('tabname' => 'ExportEvent',
|
|
'id' => 'tabExportEvent'));
|
|
|
|
echo $tabs->render($tabname);
|
|
}
|
|
|
|
/**
|
|
* Attempts to return a single, concrete Kronolith_Driver instance based
|
|
* on a driver name.
|
|
*
|
|
* This singleton method automatically retrieves all parameters required
|
|
* for the specified driver.
|
|
*
|
|
* @param string $driver The type of concrete Kronolith_Driver subclass
|
|
* to return.
|
|
* @param string $calendar The calendar name. The format depends on the
|
|
* driver being used.
|
|
*
|
|
* @return Kronolith_Driver The newly created concrete Kronolith_Driver
|
|
* instance.
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function getDriver($driver = null, $calendar = null)
|
|
{
|
|
$instance = $GLOBALS['injector']
|
|
->getInstance('Kronolith_Factory_Driver')
|
|
->create($driver);
|
|
|
|
if (!is_null($calendar)) {
|
|
$instance->open($calendar);
|
|
|
|
/* Remote calendar parameters are per calendar. */
|
|
if ($instance instanceof Kronolith_Driver_Ical) {
|
|
$instance->setParams(self::getRemoteParams($calendar));
|
|
}
|
|
}
|
|
|
|
return $instance;
|
|
}
|
|
|
|
/**
|
|
* Returns a Kronolith_Calendar object for a driver instance.
|
|
*
|
|
* @since Kronolith 4.0.1
|
|
*
|
|
* @param Kronolith_Driver A driver instance.
|
|
*
|
|
* @return Kronolith_Calendar The matching calendar instance.
|
|
*/
|
|
static public function getCalendar(Kronolith_Driver $driver)
|
|
{
|
|
global $calendar_manager;
|
|
|
|
switch (true) {
|
|
case $driver instanceof Kronolith_Driver_Sql:
|
|
case $driver instanceof Kronolith_Driver_Kolab:
|
|
return $calendar_manager->getEntry(Kronolith::ALL_CALENDARS, $driver->calendar);
|
|
|
|
case $driver instanceof Kronolith_Driver_Ical:
|
|
return $calendar_manager->getEntry(Kronolith::ALL_REMOTE_CALENDARS, $driver->calendar);
|
|
|
|
case $driver instanceof Kronolith_Driver_Horde:
|
|
$all = $calendar_manager->get(Kronolith::ALL_EXTERNAL_CALENDARS);
|
|
return $all[$driver->calendar];
|
|
|
|
case $driver instanceof Kronolith_Driver_Holidays:
|
|
return $calendar_manager->getEntry(Kronolith::ALL_HOLIDAYS, $driver->calendar);
|
|
|
|
case $driver instanceof Kronolith_Driver_Resource_Sql:
|
|
return $calendar_manager->getEntry(Kronolith::ALL_RESOURCE_CALENDARS, $driver->calendar);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for HTTP authentication credentials
|
|
*/
|
|
static public function getRemoteParams($calendar)
|
|
{
|
|
if (empty($calendar)) {
|
|
return array();
|
|
}
|
|
|
|
$cals = unserialize($GLOBALS['prefs']->getValue('remote_cals'));
|
|
foreach ($cals as $cal) {
|
|
if ($cal['url'] == $calendar) {
|
|
$user = isset($cal['user']) ? $cal['user'] : '';
|
|
$password = isset($cal['password']) ? $cal['password'] : '';
|
|
$key = $GLOBALS['registry']->getAuthCredential('password');
|
|
if ($key && $password) {
|
|
$secret = $GLOBALS['injector']->getInstance('Horde_Secret');
|
|
$user = $secret->read($key, base64_decode($user));
|
|
$password = $secret->read($key, base64_decode($password));
|
|
}
|
|
if (!empty($user)) {
|
|
return array('user' => $user, 'password' => $password);
|
|
}
|
|
return array();
|
|
}
|
|
}
|
|
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Returns a list of currently displayed calendars.
|
|
*
|
|
* @return array Currently displayed calendars.
|
|
*/
|
|
static public function displayedCalendars()
|
|
{
|
|
$calendars = array();
|
|
foreach ($GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_CALENDARS) as $calendarId) {
|
|
$calendars[] = $GLOBALS['calendar_manager']->getEntry(Kronolith::ALL_CALENDARS, $calendarId);
|
|
}
|
|
if (count($GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_RESOURCE_CALENDARS))) {
|
|
$r_driver = self::getDriver('Resource');
|
|
foreach ($GLOBALS['calendar_manager']->get(Kronolith::DISPLAY_RESOURCE_CALENDARS) as $c) {
|
|
try {
|
|
$resource = $r_driver->getResource($r_driver->getResourceIdByCalendar($c));
|
|
$calendars[] = new Kronolith_Calendar_Resource(array('resource' => $resource));
|
|
} catch (Horde_Exception_NotFound $e) {
|
|
}
|
|
}
|
|
}
|
|
return $calendars;
|
|
}
|
|
|
|
/**
|
|
* Get a named Kronolith_View_* object and load it with the
|
|
* appropriate date parameters.
|
|
*
|
|
* @param string $view The name of the view.
|
|
*/
|
|
static public function getView($view)
|
|
{
|
|
switch ($view) {
|
|
case 'Day':
|
|
case 'Month':
|
|
case 'Week':
|
|
case 'WorkWeek':
|
|
case 'Year':
|
|
$class = 'Kronolith_View_' . $view;
|
|
return new $class(self::currentDate());
|
|
|
|
case 'Event':
|
|
case 'EditEvent':
|
|
case 'DeleteEvent':
|
|
case 'ExportEvent':
|
|
try {
|
|
if ($uid = Horde_Util::getFormData('uid')) {
|
|
$event = self::getDriver()->getByUID($uid);
|
|
} else {
|
|
$event = self::getDriver(Horde_Util::getFormData('type'),
|
|
Horde_Util::getFormData('calendar'))
|
|
->getEvent(Horde_Util::getFormData('eventID'),
|
|
Horde_Util::getFormData('datetime'));
|
|
}
|
|
} catch (Horde_Exception $e) {
|
|
$event = $e->getMessage();
|
|
}
|
|
switch ($view) {
|
|
case 'Event':
|
|
if (!is_string($event) &&
|
|
!$event->hasPermission(Horde_Perms::READ)) {
|
|
$event = _("Permission Denied");
|
|
}
|
|
return new Kronolith_View_Event($event);
|
|
case 'EditEvent':
|
|
/* We check for read permissions, because we can always save a
|
|
* copy if we can read the event. */
|
|
if (!is_string($event) &&
|
|
!$event->hasPermission(Horde_Perms::READ)) {
|
|
$event = _("Permission Denied");
|
|
}
|
|
return new Kronolith_View_EditEvent($event);
|
|
case 'DeleteEvent':
|
|
if (!is_string($event) &&
|
|
!$event->hasPermission(Horde_Perms::DELETE)) {
|
|
$event = _("Permission Denied");
|
|
}
|
|
return new Kronolith_View_DeleteEvent($event);
|
|
case 'ExportEvent':
|
|
if (!is_string($event) &&
|
|
!$event->hasPermission(Horde_Perms::READ)) {
|
|
$event = _("Permission Denied");
|
|
}
|
|
return new Kronolith_View_ExportEvent($event);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Should we show event location, based on the show_location pref?
|
|
*/
|
|
static public function viewShowLocation()
|
|
{
|
|
$show = @unserialize($GLOBALS['prefs']->getValue('show_location'));
|
|
return @in_array('screen', $show);
|
|
}
|
|
|
|
/**
|
|
* Should we show event time, based on the show_time preference?
|
|
*/
|
|
static public function viewShowTime()
|
|
{
|
|
$show = @unserialize($GLOBALS['prefs']->getValue('show_time'));
|
|
return @in_array('screen', $show);
|
|
}
|
|
|
|
/**
|
|
* Returns the background color for a calendar.
|
|
*
|
|
* @param array|Horde_Share_Object $calendar A calendar share or a hash
|
|
* from a remote calender
|
|
* definition.
|
|
*
|
|
* @return string A HTML color code.
|
|
*/
|
|
static public function backgroundColor($calendar)
|
|
{
|
|
$color = '';
|
|
if (!is_array($calendar)) {
|
|
$color = $calendar->get('color');
|
|
} elseif (isset($calendar['color'])) {
|
|
$color = $calendar['color'];
|
|
}
|
|
return empty($color) ? '#dddddd' : $color;
|
|
}
|
|
|
|
/**
|
|
* Returns the foreground color for a calendar or a background color.
|
|
*
|
|
* @param array|Horde_Share_Object|string $calendar A color string, a
|
|
* calendar share or a
|
|
* hash from a remote
|
|
* calender definition.
|
|
*
|
|
* @return string A HTML color code.
|
|
*/
|
|
static public function foregroundColor($calendar)
|
|
{
|
|
return Horde_Image::brightness(is_string($calendar) ? $calendar : self::backgroundColor($calendar)) < 128 ? '#fff' : '#000';
|
|
}
|
|
|
|
/**
|
|
* Returns the CSS color definition for a calendar.
|
|
*
|
|
* @param array|Horde_Share_Object $calendar A calendar share or a hash
|
|
* from a remote calender
|
|
* definition.
|
|
* @param boolean $with_attribute Whether to wrap the colors
|
|
* inside a "style" attribute.
|
|
*
|
|
* @return string A CSS string with color definitions.
|
|
*/
|
|
static public function getCSSColors($calendar, $with_attribute = true)
|
|
{
|
|
$css = 'background-color:' . self::backgroundColor($calendar) . ';color:' . self::foregroundColor($calendar);
|
|
if ($with_attribute) {
|
|
$css = ' style="' . $css . '"';
|
|
}
|
|
return $css;
|
|
}
|
|
|
|
/**
|
|
* Returns a random CSS color.
|
|
*
|
|
* @return string A random CSS color string.
|
|
*/
|
|
static public function randomColor()
|
|
{
|
|
$color = '#';
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$color .= sprintf('%02x', mt_rand(0, 255));
|
|
}
|
|
return $color;
|
|
}
|
|
|
|
/**
|
|
* Returns whether to display the ajax view.
|
|
*
|
|
* return boolean True if the ajax view should be displayed.
|
|
*/
|
|
static public function showAjaxView()
|
|
{
|
|
return $GLOBALS['registry']->getView() == Horde_Registry::VIEW_DYNAMIC && $GLOBALS['prefs']->getValue('dynamic_view');
|
|
}
|
|
|
|
/**
|
|
* Sorts an event list.
|
|
*
|
|
* @param array $days A list of days with events.
|
|
*
|
|
* @return array The sorted day list.
|
|
*/
|
|
static public function sortEvents($days)
|
|
{
|
|
foreach ($days as $day => $devents) {
|
|
if (count($devents)) {
|
|
uasort($devents, array('Kronolith', '_sortEventStartTime'));
|
|
$days[$day] = $devents;
|
|
}
|
|
}
|
|
return $days;
|
|
}
|
|
|
|
/**
|
|
* Used with usort() to sort events based on their start times.
|
|
*/
|
|
static protected function _sortEventStartTime($a, $b)
|
|
{
|
|
$diff = $a->start->compareDateTime($b->start);
|
|
if ($diff == 0) {
|
|
return strcoll($a->title, $b->title);
|
|
} else {
|
|
return $diff;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtain a Kronolith_Tagger instance
|
|
*
|
|
* @return Kronolith_Tagger
|
|
*/
|
|
static public function getTagger()
|
|
{
|
|
if (empty(self::$_tagger)) {
|
|
self::$_tagger = new Kronolith_Tagger();
|
|
}
|
|
return self::$_tagger;
|
|
}
|
|
|
|
/**
|
|
* Obtain an internal calendar. Use this where we don't know if we will
|
|
* have a Horde_Share or a Kronolith_Resource based calendar.
|
|
*
|
|
* @param string $target The calendar id to retrieve.
|
|
*
|
|
* @return Kronolith_Resource|Horde_Share_Object
|
|
* @throws Kronolith_Exception
|
|
*/
|
|
static public function getInternalCalendar($target)
|
|
{
|
|
if ($GLOBALS['conf']['resource']['driver'] && self::getDriver('Resource')->isResourceCalendar($target)) {
|
|
$driver = self::getDriver('Resource');
|
|
$id = $driver->getResourceIdByCalendar($target);
|
|
return $driver->getResource($id);
|
|
} else {
|
|
return $GLOBALS['injector']->getInstance('Kronolith_Shares')->getShare($target);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines parameters needed to do an address search
|
|
*
|
|
* @return array An array with two keys: 'fields' and 'sources'.
|
|
*/
|
|
static public function getAddressbookSearchParams()
|
|
{
|
|
$src = json_decode($GLOBALS['prefs']->getValue('search_sources'));
|
|
if (empty($src)) {
|
|
$src = array();
|
|
}
|
|
|
|
$fields = json_decode($GLOBALS['prefs']->getValue('search_fields'), true);
|
|
if (empty($fields)) {
|
|
$fields = array();
|
|
}
|
|
|
|
return array(
|
|
'fields' => $fields,
|
|
'sources' => $src
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks whether an API (application) exists and the user has permission
|
|
* to access it.
|
|
*
|
|
* @param string $api The API (application) to check.
|
|
* @param integer $perm The permission to check.
|
|
*
|
|
* @return boolean True if the API can be accessed.
|
|
*/
|
|
static public function hasApiPermission($api, $perm = Horde_Perms::READ)
|
|
{
|
|
$app = $GLOBALS['registry']->hasInterface($api);
|
|
return ($app && $GLOBALS['registry']->hasPermission($app, $perm));
|
|
}
|
|
|
|
/**
|
|
* Remove all events owned by the specified user in all calendars.
|
|
*
|
|
* @param string $user The user name to delete events for.
|
|
*
|
|
* @throws Kronolith_Exception
|
|
* @throws Horde_Exception_NotFound
|
|
* @throws Horde_Exception_PermissionDenied
|
|
*/
|
|
static public function removeUserEvents($user)
|
|
{
|
|
if (!$GLOBALS['registry']->isAdmin()) {
|
|
throw new Horde_Exception_PermissionDenied();
|
|
}
|
|
|
|
try {
|
|
$shares = $GLOBALS['injector']
|
|
->getInstance('Kronolith_Shares')
|
|
->listShares($user, array('perm' => Horde_Perms::EDIT));
|
|
} catch (Horde_Share_Exception $e) {
|
|
Horde::log($shares, 'ERR');
|
|
throw new Kronolith_Exception($shares);
|
|
}
|
|
|
|
foreach (array_keys($shares) as $calendar) {
|
|
$driver = self::getDriver(null, $calendar);
|
|
$events = $driver->listEvents(null, null, array('cover_dates' => false));
|
|
$uids = array();
|
|
foreach ($events as $dayevents) {
|
|
foreach ($dayevents as $event) {
|
|
if (!$event->baseid) {
|
|
$uids[] = $event->uid;
|
|
}
|
|
}
|
|
}
|
|
foreach ($uids as $uid) {
|
|
$event = $driver->getByUID($uid, array($calendar));
|
|
if ($event->creator == $user) {
|
|
$driver->deleteEvent($event->id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TODO
|
|
*
|
|
* @param array $attendees
|
|
*
|
|
* @return Horde_Mail_Rfc822_List
|
|
*/
|
|
static public function getAttendeeEmailList($attendees)
|
|
{
|
|
$a_list = new Horde_Mail_Rfc822_List();
|
|
|
|
foreach ($attendees as $mail => $attendee) {
|
|
$tmp = new Horde_Mail_Rfc822_Address($mail);
|
|
if (!empty($attendee['name'])) {
|
|
$tmp->personal = $attendee['name'];
|
|
}
|
|
$a_list->add($tmp);
|
|
}
|
|
|
|
return $a_list;
|
|
}
|
|
|
|
/**
|
|
* Exports an event to a timeslice.
|
|
*
|
|
*
|
|
* @param Kronolith_Event $event An event.
|
|
* @param integer $type A job type ID.
|
|
* @param string $client A client ID.
|
|
*/
|
|
static public function toTimeslice(Kronolith_Event $event, $type, $client)
|
|
{
|
|
global $registry;
|
|
|
|
if (!$registry->hasMethod('time/recordTime')) {
|
|
throw new Kronolith_Exception();
|
|
}
|
|
|
|
$data = array(
|
|
'date' => $event->start,
|
|
'type' => $type,
|
|
'client' => $client,
|
|
'hours' => ($event->end->timestamp() - $event->start->timestamp()) / 3600,
|
|
'description' => $event->title,
|
|
'note' => $event->description
|
|
);
|
|
|
|
try {
|
|
$registry->time->recordTime($data);
|
|
} catch (Horde_Exception $e) {
|
|
throw new Kronolith_Exception($e->getMessage());
|
|
}
|
|
}
|
|
|
|
}
|