Files
server/usr/share/psa-horde/kronolith/lib/Kronolith.php
2026-01-07 20:52:11 +01:00

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());
}
}
}