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

1734 lines
58 KiB
PHP

<?php
/**
* Nag Base Class.
*
* See the enclosed file COPYING for license information (GPL). If you
* did not receive this file, see http://www.horde.org/licenses/gpl.
*
* @author Jon Parise <jon@horde.org>
* @author Chuck Hagenbuch <chuck@horde.org>
* @author Jan Schneider <jan@horde.org>
* @package Nag
*/
class Nag
{
/**
* Sort by task name.
*/
const SORT_NAME = 'name';
/**
* Sort by priority.
*/
const SORT_PRIORITY = 'priority';
/**
* Sort by due date.
*/
const SORT_DUE = 'due';
/**
* Sort by start date.
*/
const SORT_START = 'start';
/**
* Sort by completion.
*/
const SORT_COMPLETION = 'completed';
/**
* Sort by owner.
*/
const SORT_OWNER = 'tasklist';
/**
* Sort by estimate.
*/
const SORT_ESTIMATE = 'estimate';
/**
* Sort by assignee.
*/
const SORT_ASSIGNEE = 'assignee';
/**
* Sort in ascending order.
*/
const SORT_ASCEND = 0;
/**
* Sort in descending order.
*/
const SORT_DESCEND = 1;
/**
* Incomplete tasks
*/
const VIEW_INCOMPLETE = 0;
/**
* All tasks
*/
const VIEW_ALL = 1;
/**
* Complete tasks
*/
const VIEW_COMPLETE = 2;
/**
* Future tasks
*/
const VIEW_FUTURE = 3;
/**
* Future and incompleted tasks
*/
const VIEW_FUTURE_INCOMPLETE = 4;
/**
* WebDAV task list.
*/
const DAV_WEBDAV = 1;
/**
* CalDAV task list.
*/
const DAV_CALDAV = 2;
/**
* CalDAV principal.
*/
const DAV_ACCOUNT = 3;
/**
*
* @param integer $seconds
*
* @return string
*/
static public function secondsToString($seconds)
{
$hours = floor($seconds / 3600);
$minutes = ($seconds / 60) % 60;
if ($hours > 1) {
if ($minutes == 0) {
return sprintf(_("%d hours"), $hours);
} elseif ($minutes == 1) {
return sprintf(_("%d hours, %d minute"), $hours, $minutes);
} else {
return sprintf(_("%d hours, %d minutes"), $hours, $minutes);
}
} elseif ($hours == 1) {
if ($minutes == 0) {
return sprintf(_("%d hour"), $hours);
} elseif ($minutes == 1) {
return sprintf(_("%d hour, %d minute"), $hours, $minutes);
} else {
return sprintf(_("%d hour, %d minutes"), $hours, $minutes);
}
} else {
if ($minutes == 0) {
return _("no time");
} elseif ($minutes == 1) {
return sprintf(_("%d minute"), $minutes);
} else {
return sprintf(_("%d minutes"), $minutes);
}
}
}
/**
* 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.
*
* @return Horde_Date The parsed date.
* @throws Horde_Date_Exception
*/
static public function parseDate($date, $withtime = true)
{
// strptime() is not available on Windows.
if (!function_exists('strptime')) {
return new Horde_Date($date);
}
// 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 = $GLOBALS['prefs']->getValue('date_format_mini');
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);
}
}
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']));
}
/**
* Retrieves the current user's task list from storage.
*
* This function will also sort the resulting list, if requested.
*
* @param arary $options Options array:
* - altsortby: (string) The secondary sort field. Same values as sortdir.
* DEFAULT: altsortby pref is used.
* - completed: (integer) Which task to retrieve. A Nag::VIEW_* constant.
* DEFAULT: show_completed pref is used.
* - external: (boolean) Whether to include tasks from other applications
* too.
* DEFAULT: true.
* - include_history: (boolean) Autoload created/modified data from
* Horde_History.
* DEFAULT: true (Automatically load history data).
* - include_tags: (boolean) Autoload all tags.
* DEFAULT: false (Tags are lazy loaded as needed.)
* - sortby: (string) A Nag::SORT_* constant for the field to sort by.
* DEFAULT: sortby pref is used.
* - sortdir: (string) Direction of sort. NAG::SORT_ASCEND or
* NAG::SORT_DESCEND.
* DEFAULT: sortdir pref is used.
* - tasklists: (array) An array of tasklists to include.
* DEFAULT: Use $GLOBALS['display_tasklists'];
*
* @return Nag_Task A list of the requested tasks.
*/
static public function listTasks(array $options = array())
{
global $prefs, $registry;
// Prevent null tasklists value from obscuring the default value.
if (array_key_exists('tasklists', $options) && empty($options['tasklists'])) {
unset($options['tasklists']);
}
$options = array_merge(
array(
'sortby' => $prefs->getValue('sortby'),
'sortdir' => $prefs->getValue('sortdir'),
'altsortby' => $prefs->getValue('altsortby'),
'tasklists' => $GLOBALS['display_tasklists'],
'completed' => $prefs->getValue('show_completed'),
'include_tags' => false,
'external' => true,
'include_history' => true
),
$options
);
if (!is_array($options['tasklists'])) {
$options['tasklists'] = array($options['tasklists']);
}
$tasks = new Nag_Task();
foreach ($options['tasklists'] as $tasklist) {
$storage = $GLOBALS['injector']
->getInstance('Nag_Factory_Driver')
->create($tasklist);
// Retrieve the tasklist from storage.
$storage->retrieve($options['completed'], $options['include_history']);
$tasks->mergeChildren($storage->tasks->children);
}
// Process all tasks.
$tasks->process();
if ($options['external'] &&
($apps = @unserialize($prefs->getValue('show_external'))) &&
is_array($apps)) {
foreach ($apps as $app) {
// We look for registered apis that support listAs(taskHash).
if ($app == 'nag' ||
!$registry->hasMethod('getListTypes', $app)) {
continue;
}
try {
$types = $registry->callByPackage($app, 'getListTypes');
} catch (Horde_Exception $e) {
continue;
}
if (empty($types['taskHash'])) {
continue;
}
try {
$newtasks = $registry->callByPackage($app, 'listAs', array('taskHash'));
foreach ($newtasks as $task) {
if (!isset($task['priority'])) {
$task['priority'] = 3;
}
$task['tasklist_id'] = '**EXTERNAL**';
$task['tasklist_name'] = $registry->get('name', $app);
$task = new Nag_Task(null, $task);
if (($options['completed'] == Nag::VIEW_INCOMPLETE &&
($task->completed ||
$task->start > $_SERVER['REQUEST_TIME'])) ||
($options['completed'] == Nag::VIEW_COMPLETE &&
!$task->completed) ||
($options['completed'] == Nag::VIEW_FUTURE &&
($task->completed ||
!$task->start ||
$task->start < $_SERVER['REQUEST_TIME'])) ||
($options['completed'] == Nag::VIEW_FUTURE_INCOMPLETE &&
$task->completed)) {
continue;
}
$tasks->add($task);
}
} catch (Horde_Exception $e) {
Horde::log($e);
}
}
}
// Sort the array.
$tasks->sort($options['sortby'], $options['sortdir'], $options['altsortby']);
// Preload tags if requested.
if ($options['include_tags']) {
$tasks->loadTags();
}
return $tasks;
}
/**
* Returns a single task.
*
* @param string $tasklist A tasklist.
* @param string $task A task id.
*
* @return Nag_Task The task hash.
*/
static public function getTask($tasklist, $task)
{
$storage = $GLOBALS['injector']
->getInstance('Nag_Factory_Driver')
->create($tasklist);
$task = $storage->get($task);
$task->process();
return $task;
}
/**
* Returns the number of taks in task lists that the current user owns.
*
* @return integer The number of tasks that the user owns.
*/
static public function countTasks()
{
static $count;
if (isset($count)) {
return $count;
}
$tasklists = self::listTasklists(true, Horde_Perms::ALL);
$count = 0;
foreach (array_keys($tasklists) as $tasklist) {
/* Create a Nag storage instance. */
$storage = $GLOBALS['injector']
->getInstance('Nag_Factory_Driver')
->create($tasklist);
$storage->retrieve();
/* Retrieve the task list from storage. */
$count += $storage->tasks->count();
}
return $count;
}
/**
* Imports one or more tasks parsed from a string.
*
* @param string $text The text to parse into
* @param string $tasklist The tasklist into which the task will be
* imported. If 'null', the user's default
* tasklist will be used.
*
* @return array The UIDs of all tasks that were added.
*/
static public function createTasksFromText($text, $tasklist = null)
{
if ($tasklist === null) {
$tasklist = self::getDefaultTasklist(Horde_Perms::EDIT);
} elseif (!self::hasPermission($tasklist, Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
$storage = $GLOBALS['injector']
->getInstance('Nag_Factory_Driver')
->create($tasklist);
$dateParser = Horde_Date_Parser::factory(
array('locale' => $GLOBALS['prefs']->getValue('language')) );
$quickParser = new Nag_QuickParser();
$tasks = $quickParser->parse($text);
$uids = array();
foreach ($tasks as &$task) {
if (!is_array($task)) {
$name = $task;
$task = array($name);
}
$r = $dateParser->parse($task[0], array('return' => 'result'));
if ($d = $r->guess()) {
$name = $r->untaggedText();
$due = $d->timestamp();
} else {
$name = $task[0];
$due = 0;
}
// Look for tags to be added in the text.
$pattern = '/#\w+/';
$tags = array();
if (preg_match_all($pattern, $name, $results)) {
$tags = $results[0];
$name = str_replace($tags, '', $name);
$tags = array_map(function($x) { return substr($x, -(strlen($x) - 1)); }, $tags);
} else {
$tags = '';
}
if (isset($task['parent'])) {
$newTask = $storage->add(array('name' => $name, 'due' => $due, 'parent' => $tasks[$task['parent']]['id'], 'tags' => $tags));
} else {
$newTask = $storage->add(array('name' => $name, 'due' => $due, 'tags' => $tags));
}
$uids[] = $newTask[1];
$task['id'] = $newTask[0];
}
return $uids;
}
/**
* Returns all the alarms active right on $date.
*
* @param integer $date The unix epoch time to check for alarms.
* @param array $tasklists An array of tasklists
*
* @return array An array of Nag_Task objects with alarms active on $date.
*/
static public function listAlarms($date, array $tasklists = null)
{
if (is_null($tasklists)) {
$tasklists = $GLOBALS['display_tasklists'];
}
$tasks = array();
foreach ($tasklists as $tasklist) {
/* Create a Nag storage instance. */
$storage = $GLOBALS['injector']
->getInstance('Nag_Factory_Driver')
->create($tasklist);
/* Retrieve the alarms for the task list. */
$newtasks = $storage->listAlarms($date);
/* Don't show an alarm for complete tasks. */
foreach ($newtasks as $taskID => $task) {
if (!empty($task->completed)) {
unset($newtasks[$taskID]);
}
}
$tasks = array_merge($tasks, $newtasks);
}
return $tasks;
}
/**
* Lists all task lists a user has access to.
*
* @param boolean $owneronly Only return task lists that this user owns?
* Defaults to false.
* @param integer $permission The permission to filter task lists by.
* @param boolean $smart Include SmartLists in the results.
*
* @return array The task lists.
*/
static public function listTasklists($owneronly = false,
$permission = Horde_Perms::SHOW,
$smart = true)
{
if ($owneronly && !$GLOBALS['registry']->getAuth()) {
return array();
}
$att = array();
if ($owneronly) {
$att = array('owner' => $GLOBALS['registry']->getAuth());
}
if (!$smart) {
$att['issmart'] = 0;
}
try {
$tasklists = $GLOBALS['nag_shares']->listShares(
$GLOBALS['registry']->getAuth(),
array('perm' => $permission,
'attributes' => $att,
'sort_by' => 'name'));
// Must explicitly add system shares if we are an admin since
// it's possible for an admin user to have no explicit perms
// added to the share.
if ($GLOBALS['registry']->isAdmin()) {
$tasklists = array_merge(
$tasklists,
$GLOBALS['nag_shares']->listSystemShares()
);
}
} catch (Horde_Share_Exception $e) {
Horde::log($e->getMessage(), 'ERR');
return array();
}
if ($owneronly) {
return $tasklists;
}
$display_tasklists = @unserialize($GLOBALS['prefs']->getValue('display_tasklists'));
if (is_array($display_tasklists)) {
foreach ($display_tasklists as $id) {
try {
$tasklist = $GLOBALS['nag_shares']->getShare($id);
if ($tasklist->hasPermission($GLOBALS['registry']->getAuth(), $permission)) {
$tasklists[$id] = $tasklist;
}
} catch (Horde_Exception_NotFound $e) {
} catch (Horde_Share_Exception $e) {
Horde::log($e);
return array();
}
}
}
return $tasklists;
}
/**
* Returns whether the current user has certain permissions on a tasklist.
*
* @param string $tasklist A tasklist id.
* @param integer $perm A Horde_Perms permission mask.
*
* @return boolean True if the current user has the requested permissions.
*/
static public function hasPermission($tasklist, $perm)
{
try {
$share = $GLOBALS['nag_shares']->getShare($tasklist);
if (!$share->hasPermission($GLOBALS['registry']->getAuth(), $perm)) {
throw new Horde_Exception_NotFound();
}
} catch (Horde_Exception_NotFound $e) {
return false;
}
return true;
}
/**
* Returns the default tasklist for the current user at the specified
* permissions level.
*
* @param integer $permission Horde_Perms constant for permission level
* required.
*
* @return string The default tasklist or null if none.
*/
static public function getDefaultTasklist($permission = Horde_Perms::SHOW)
{
$tasklists = self::listTasklists(false, $permission);
$default_tasklist = $GLOBALS['prefs']->getValue('default_tasklist');
if (isset($tasklists[$default_tasklist])) {
return $default_tasklist;
}
$default_tasklist = $GLOBALS['injector']
->getInstance('Nag_Factory_Tasklists')
->create()
->getDefaultShare();
if (!isset($tasklists[$default_tasklist])) {
reset($tasklists);
$default_tasklist = key($tasklists);
}
$GLOBALS['prefs']->setValue('default_tasklist', $default_tasklist);
return $default_tasklist;
}
/**
* Creates a new share.
*
* @param array $info Hash with tasklist information.
* @param boolean $display Add the new tasklist to display_tasklists
*
* @return Horde_Share The new share.
*/
static public function addTasklist(array $info, $display = true)
{
try {
$tasklist = $GLOBALS['nag_shares']->newShare(
$GLOBALS['registry']->getAuth(),
strval(new Horde_Support_Randomid()), $info['name']);
$tasklist->set('color', $info['color']);
$tasklist->set('desc', $info['description']);
if (!empty($info['system'])) {
$tasklist->set('owner', null);
}
// Smartlist
if (!empty($info['search'])) {
$tasklist->set('search', $info['search']);
$tasklist->set('issmart', 1);
}
$GLOBALS['nag_shares']->addShare($tasklist);
} catch (Horde_Share_Exception $e) {
throw new Nag_Exception($e);
}
if ($display) {
$GLOBALS['display_tasklists'][] = $tasklist->getName();
$GLOBALS['prefs']->setValue('display_tasklists', serialize($GLOBALS['display_tasklists']));
}
return $tasklist;
}
/**
* Updates an existing share.
*
* @param Horde_Share_Object $tasklist The share to update.
* @param array $info Hash with task list information.
*
* @throws Horde_Exception_PermissionDenied
* @throws Nag_Exception
*/
static public function updateTasklist(Horde_Share_Object $tasklist, array $info)
{
if (!$GLOBALS['registry']->getAuth() ||
($tasklist->get('owner') != $GLOBALS['registry']->getAuth() &&
(!is_null($tasklist->get('owner')) || !$GLOBALS['registry']->isAdmin()))) {
throw new Horde_Exception_PermissionDenied(_("You are not allowed to change this task list."));
}
$tasklist->set('name', $info['name']);
$tasklist->set('color', $info['color']);
$tasklist->set('desc', $info['description']);
$tasklist->set('owner', empty($info['system']) ? $GLOBALS['registry']->getAuth() : null);
if ($tasklist->get('issmart')) {
if (empty($info['search'])) {
throw new Nag_Exception(_("Missing valid search criteria"));
}
$tasklist->set('search', $info['search']);
}
try {
$tasklist->save();
} catch (Horde_Share_Exception $e) {
throw new Nag_Exception(sprintf(_("Unable to save task list \"%s\": %s"), $info['name'], $e->getMessage()));
}
}
/**
* Deletes a task list.
*
* @param Horde_Share_Object $tasklist The task list to delete.
*
* @throws Nag_Exception
* @throws Horde_Exception_PermissionDenied
*/
static public function deleteTasklist(Horde_Share_Object $tasklist)
{
if (!$GLOBALS['registry']->getAuth() ||
($tasklist->get('owner') != $GLOBALS['registry']->getAuth() &&
(!is_null($tasklist->get('owner')) || !$GLOBALS['registry']->isAdmin()))) {
throw new Horde_Exception_PermissionDenied(_("You are not allowed to delete this task list."));
}
// Delete the task list.
$storage = &$GLOBALS['injector']->getInstance('Nag_Factory_Driver')->create($tasklist->getName());
$result = $storage->deleteAll();
// Remove share and all groups/permissions.
try {
$GLOBALS['nag_shares']->removeShare($tasklist);
} catch (Horde_Share_Exception $e) {
throw new Nag_Exception($e);
}
}
/**
* Returns the label to be used for a task list.
*
* Attaches the owner name of shared task lists if necessary.
*
* @param Horde_Share_Object A task list.
*
* @return string The task list's label.
*/
public static function getLabel($tasklist)
{
$label = $tasklist->get('name');
if ($tasklist->get('owner') &&
$tasklist->get('owner') != $GLOBALS['registry']->getAuth()) {
$label .= ' [' . $GLOBALS['registry']->convertUsername($tasklist->get('owner'), false) . ']';
}
return $label;
}
/**
* Returns a DAV URL to be used for a task list.
*
* @param integer $type A Nag::DAV_* constant.
* @param Horde_Share_Object A task list.
*
* @return string The task list's URL.
* @throws Horde_Exception
*/
public static function getUrl($type, $tasklist)
{
global $conf, $injector, $registry;
$url = $registry->get('webroot', 'horde');
$rewrite = isset($conf['urls']['pretty']) &&
$conf['urls']['pretty'] == 'rewrite';
switch ($type) {
case Nag::DAV_WEBDAV:
if ($rewrite) {
$url .= '/rpc/nag/';
} else {
$url .= '/rpc.php/nag/';
}
$url = Horde::url($url, true, -1)
. ($tasklist->get('owner')
? $registry->convertUsername($tasklist->get('owner'), false)
: '-system-')
. '/' . $tasklist->getName() . '.ics';
break;
case Nag::DAV_CALDAV:
if ($rewrite) {
$url .= '/rpc/calendars/';
} else {
$url .= '/rpc.php/calendars/';
}
$url = Horde::url($url, true, -1)
. $registry->convertUsername($registry->getAuth(), false)
. '/'
. $injector->getInstance('Horde_Dav_Storage')
->getExternalCollectionId($tasklist->getName(), 'tasks')
. '/';
break;
case Nag::DAV_ACCOUNT:
if ($rewrite) {
$url .= '/rpc/';
} else {
$url .= '/rpc.php/';
}
$url = Horde::url($url, true, -1)
. 'principals/' . $registry->convertUsername($registry->getAuth(), false) . '/';
break;
}
return $url;
}
/**
* 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;
}
/**
* Builds the HTML for a priority selection widget.
*
* @param string $name The name of the widget.
* @param integer $selected The default selected priority.
*
* @return string The HTML <select> widget.
*/
static public function buildPriorityWidget($name, $selected = -1)
{
$descs = array(1 => _("(highest)"), 5 => _("(lowest)"));
$html = "<select id=\"$name\" name=\"$name\">";
for ($priority = 1; $priority <= 5; $priority++) {
$html .= "<option value=\"$priority\"";
$html .= ($priority == $selected) ? ' selected="selected">' : '>';
$html .= $priority . ' ' . @$descs[$priority] . '</option>';
}
$html .= "</select>\n";
return $html;
}
/**
* Builds the HTML for a checkbox widget.
*
* @param string $name The name of the widget.
* @param integer $checked The default checkbox state.
*
* @return string HTML for a checkbox representing the completion state.
*/
static public function buildCheckboxWidget($name, $checked = 0)
{
$name = htmlspecialchars($name);
return "<input type=\"checkbox\" id=\"$name\" name=\"$name\"" .
($checked ? ' checked="checked"' : '') . ' />';
}
/**
* Formats the given Unix-style date string.
*
* @param string $unixdate The Unix-style date value to format.
* @param boolean $hours Whether to add hours.
*
* @return string The formatted due date string.
*/
static public function formatDate($unixdate = '', $hours = true)
{
global $prefs;
if (empty($unixdate)) {
return '';
}
$date = strftime($prefs->getValue('date_format'), $unixdate);
if (!$hours) {
return $date;
}
return sprintf(_("%s at %s"),
$date,
strftime($prefs->getValue('twentyFour') ? '%H:%M' : '%I:%M %p', $unixdate));
}
/**
* Returns the string representation of the given completion status.
*
* @param integer $completed The completion value.
*
* @return string The HTML representation of $completed.
*/
static public function formatCompletion($completed)
{
return $completed ?
Horde::img('checked.png', _("Completed")) :
Horde::img('unchecked.png', _("Not Completed"));
}
/**
* Returns a colored representation of a priority.
*
* @param integer $priority The priority level.
*
* @return string The HTML representation of $priority.
*/
static public function formatPriority($priority)
{
return '<span class="pri-' . (int)$priority . '">' . (int)$priority .
'</span>';
}
/**
* Returns the string matching the given alarm value.
*
* @param integer $value The alarm value in minutes.
*
* @return string The formatted alarm string.
*/
static public function formatAlarm($value)
{
if ($value) {
if ($value % 10080 == 0) {
$alarm_value = $value / 10080;
$alarm_unit = _("Week(s)");
} elseif ($value % 1440 == 0) {
$alarm_value = $value / 1440;
$alarm_unit = _("Day(s)");
} elseif ($value % 60 == 0) {
$alarm_value = $value / 60;
$alarm_unit = _("Hour(s)");
} else {
$alarm_value = $value;
$alarm_unit = _("Minute(s)");
}
$alarm_text = "$alarm_value $alarm_unit";
} else {
$alarm_text = _("None");
}
return $alarm_text;
}
/**
* Returns the full name and a compose to message an assignee.
*
* @param string $assignee The assignee's user name.
* @param boolean $link Whether to link to an email compose screen.
*
* @return string The formatted assignee name.
*/
static public function formatAssignee($assignee, $link = false)
{
if (!strlen($assignee)) {
return '';
}
$identity = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Identity')->create($assignee);
$fullname = $identity->getValue('fullname');
if (!strlen($fullname)) {
$fullname = $assignee;
}
$email = $identity->getValue('from_addr');
if ($link && !empty($email) &&
$GLOBALS['registry']->hasMethod('mail/compose')) {
return Horde::link($GLOBALS['registry']->call(
'mail/compose',
array(array('to' => $email))))
. htmlspecialchars($fullname . ' <' . $email . '>')
. '</a>';
}
return htmlspecialchars($fullname);
}
/**
* Initial app setup code.
*/
static public function initialize()
{
/* Store the request timestamp if it's not already present. */
if (!isset($_SERVER['REQUEST_TIME'])) {
$_SERVER['REQUEST_TIME'] = time();
}
// Update the preference for what task lists to display. If the user
// doesn't have any selected task lists for view then fall back to
// some available list.
$GLOBALS['display_tasklists'] = @unserialize($GLOBALS['prefs']->getValue('display_tasklists'));
if (!$GLOBALS['display_tasklists']) {
$GLOBALS['display_tasklists'] = array();
}
if (($actionID = Horde_Util::getFormData('actionID')) !== null) {
$tasklistId = Horde_Util::getFormData('display_tasklist');
switch ($actionID) {
case 'add_displaylist':
if (!in_array($tasklistId, $GLOBALS['display_tasklists'])) {
$GLOBALS['display_tasklists'][] = $tasklistId;
}
break;
case 'remove_displaylist':
if (in_array($tasklistId, $GLOBALS['display_tasklists'])) {
$key = array_search($tasklistId, $GLOBALS['display_tasklists']);
unset($GLOBALS['display_tasklists'][$key]);
}
}
}
// Make sure all task lists exist now, to save on checking later.
$_temp = $GLOBALS['display_tasklists'];
$GLOBALS['all_tasklists'] = self::listTasklists();
$GLOBALS['display_tasklists'] = array();
foreach ($_temp as $id) {
if (isset($GLOBALS['all_tasklists'][$id])) {
$GLOBALS['display_tasklists'][] = $id;
}
}
/* All tasklists for guests. */
if (!count($GLOBALS['display_tasklists']) &&
!$GLOBALS['registry']->getAuth()) {
$GLOBALS['display_tasklists'] = array_keys($GLOBALS['all_tasklists']);
}
$tasklists = $GLOBALS['injector']->getInstance('Nag_Factory_Tasklists')
->create();
if (($new_default = $tasklists->ensureDefaultShare()) !== null) {
$GLOBALS['display_tasklists'][] = $new_default;
$GLOBALS['prefs']->setValue('default_tasklist', $new_default);
}
$GLOBALS['prefs']->setValue('display_tasklists', serialize($GLOBALS['display_tasklists']));
}
/**
* Trigger notifications.
*/
static public function status()
{
global $notification;
if (empty($GLOBALS['conf']['alarms']['driver'])) {
// Get any alarms in the next hour.
try {
$alarmList = self::listAlarms($_SERVER['REQUEST_TIME']);
$messages = array();
foreach ($alarmList as $task) {
$differential = $task->due - $_SERVER['REQUEST_TIME'];
$key = $differential;
while (isset($messages[$key])) {
$key++;
}
if ($differential >= -60 && $differential < 60) {
$messages[$key] = array(sprintf(_("%s is due now."), $task->name), 'horde.alarm');
} elseif ($differential >= 60) {
$messages[$key] = array(sprintf(_("%s is due in %s"), $task->name,
self::secondsToString($differential)), 'horde.alarm');
}
}
ksort($messages);
foreach ($messages as $message) {
$notification->push($message[0], $message[1]);
}
} catch (Nag_Exception $e) {
Horde::log($e, 'ERR');
$notification->push($e->getMessage(), 'horde.error');
}
}
// Check here for guest task lists so that we don't get multiple
// messages after redirects, etc.
if (!$GLOBALS['registry']->getAuth() && !count(self::listTasklists())) {
$notification->push(_("No task lists are available to guests."));
}
// Display all notifications.
$notification->notify(array('listeners' => 'status'));
}
/**
* Sends email notifications that a task has been added, edited, or
* deleted to users that want such notifications.
*
* @param string $action The event action. One of "add", "edit", or
* "delete".
* @param Nag_Task $task The changed task.
* @param Nag_Task $old_task The original task if $action is "edit".
*
* @throws Nag_Exception
*/
static public function sendNotification($action, $task, $old_task = null)
{
if (!in_array($action, array('add', 'edit', 'delete'))) {
throw new Nag_Exception('Unknown event action: ' . $action);
}
try {
$share = $GLOBALS['nag_shares']->getShare($task->tasklist);
} catch (Horde_Share_Exception $e) {
Horde::log($e->getMessage(), 'ERR');
throw new Nag_Exception($e);
}
$groups = $GLOBALS['injector']->getInstance('Horde_Group');
$recipients = array();
$identity = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Identity')->create();
$from = $identity->getDefaultFromAddress(true);
$owner = $share->get('owner');
if (strlen($owner)) {
$recipients[$owner] = self::_notificationPref($owner, 'owner');
}
foreach ($share->listUsers(Horde_Perms::READ) as $user) {
if (empty($recipients[$user])) {
$recipients[$user] = self::_notificationPref($user, 'read', $task->tasklist);
}
}
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])) {
$recipients[$user] = self::_notificationPref($user, 'read', $task->tasklist);
}
}
}
$addresses = array();
foreach ($recipients as $user => $vals) {
if (!$vals) {
continue;
}
$identity = $GLOBALS['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']])) {
$addresses[$vals['lang']][$vals['tf']][$vals['df']] = array();
}
$tmp = new Horde_Mail_Rfc822_Address($email);
$tmp->personal = $identity->getValue('fullname');
$addresses[$vals['lang']][$vals['tf']][$vals['df']][] = strval($tmp);
}
if (!$addresses) {
return;
}
$mail = new Horde_Mime_Mail(array(
'User-Agent' => 'Nag ' . $GLOBALS['registry']->getVersion(),
'Precedence' => 'bulk',
'Auto-Submitted' => 'auto-generated',
'From' => $from));
foreach ($addresses as $lang => $twentyFour) {
$GLOBALS['registry']->setLanguageEnvironment($lang);
$view_link = Horde::url('view.php', true)->add(array(
'tasklist' => $task->tasklist,
'task' => $task->id
))->setRaw(true);
switch ($action) {
case 'add':
$subject = _("Task added:");
$notification_message = _("You requested to be notified when tasks are added to your task lists.")
. "\n\n"
. ($task->due
? _("The task \"%s\" has been added to task list \"%s\", with a due date of: %s.")
: _("The task \"%s\" has been added to task list \"%s\"."))
. "\n"
. str_replace('%', '%%', $view_link);
break;
case 'edit':
$subject = _("Task modified:");
$notification_message = _("You requested to be notified when tasks are edited on your task lists.")
. "\n\n"
. _("The task \"%s\" has been edited on task list \"%s\".")
. "\n"
. str_replace('%', '%%', $view_link)
. "\n\n"
. _("Changes made for this task:");
if ($old_task->name != $task->name) {
$notification_message .= "\n - "
. sprintf(_("Changed name from \"%s\" to \"%s\""),
$old_task->name, $task->name);
}
if ($old_task->tasklist != $task->tasklist) {
$old_share = $GLOBALS['nag_shares']->getShare($old_task->tasklist);
$notification_message .= "\n - "
. sprintf(_("Changed task list from \"%s\" to \"%s\""),
Nag::getLabel($old_share), Nag::getLabel($share));
}
if ($old_task->parent_id != $task->parent_id) {
$old_parent = $old_task->getParent();
try {
$parent = $task->getParent();
$notification_message .= "\n - "
. sprintf(_("Changed parent task from \"%s\" to \"%s\""),
$old_parent ? $old_parent->name : _("no parent"),
$parent ? $parent->name : _("no parent"));
} catch (Nag_Exception $e) {
}
}
if ($old_task->assignee != $task->assignee) {
$identity = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Identity')->create($old_task->assignee);
$old_name = $identity->getValue('fullname');
if (!strlen($old_name)) {
$old_name = $old_task->assignee;
}
$identity = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Identity')->create($task->assignee);
$new_name = $identity->getValue('fullname');
if (!strlen($new_name)) {
$new_name = $new_task->assignee;
}
$notification_message .= "\n - "
. sprintf(_("Changed assignee from \"%s\" to \"%s\""),
$old_name, $new_name);
}
if ($old_task->private != $task->private) {
$notification_message .= "\n - "
. ($task->private ? _("Turned privacy on") : _("Turned privacy off"));
}
if ($old_task->due != $task->due) {
$notification_message .= "\n - "
. sprintf(_("Changed due date from %s to %s"),
$old_task->due ? self::formatDate($old_task->due) : _("no due date"),
$task->due ? self::formatDate($task->due) : _("no due date"));
}
if ($old_task->start != $task->start) {
$notification_message .= "\n - "
. sprintf(_("Changed start date from %s to %s"),
$old_task->start ? self::formatDate($old_task->start) : _("no start date"),
$task->start ? self::formatDate($task->start) : _("no start date"));
}
if ($old_task->alarm != $task->alarm) {
$notification_message .= "\n - "
. sprintf(_("Changed alarm from %s to %s"),
self::formatAlarm($old_task->alarm), self::formatAlarm($task->alarm));
}
if ($old_task->priority != $task->priority) {
$notification_message .= "\n - "
. sprintf(_("Changed priority from %s to %s"),
$old_task->priority, $task->priority);
}
if ($old_task->estimate != $task->estimate) {
$notification_message .= "\n - "
. sprintf(_("Changed estimate from %s to %s"),
$old_task->estimate, $task->estimate);
}
if ($old_task->completed != $task->completed) {
$notification_message .= "\n - "
. sprintf(_("Changed completion from %s to %s"),
$old_task->completed ? _("completed") : _("not completed"),
$task->completed ? _("completed") : _("not completed"));
}
if ($old_task->desc != $task->desc) {
$notification_message .= "\n - " . _("Changed description");
}
break;
case 'delete':
$subject = _("Task deleted:");
$notification_message =
_("You requested to be notified when tasks are deleted from your task lists.")
. "\n\n"
. _("The task \"%s\" has been deleted from task list \"%s\".");
break;
}
$mail->addHeader('Subject', $subject . ' ' . $task->name);
foreach ($twentyFour as $tf => $dateFormat) {
foreach ($dateFormat as $df => $df_recipients) {
$message = sprintf($notification_message,
$task->name,
Nag::getLabel($share),
$task->due ? strftime($df, $task->due) . ' ' . date($tf ? 'H:i' : 'h:ia', $task->due) : '');
if (strlen(trim($task->desc))) {
$message .= "\n\n" . _("Task description:") . "\n\n" . $task->desc;
}
$mail->setBody($message);
$mail->clearRecipients();
$mail->addRecipients($df_recipients);
Horde::log(sprintf('Sending event notifications for %s to %s', $task->name, implode(', ', $df_recipients)), 'INFO');
$mail->send($GLOBALS['injector']->getInstance('Horde_Mail'));
}
}
}
}
/**
* 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;
}
/**
* Returns the real name, if available, of a user.
*
* @param string $uid The userid of the user to retrieve
*
* @return string The fullname of the 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 whether a user wants email notifications for a tasklist.
*
* @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
* tasklists he owns. If "read", the method checks
* if the user wants notifications for all
* tasklists he has read access to, or only for
* shown tasklists and the specified tasklist is
* currently shown.
* @param string $tasklist The name of the tasklist if mode is "read".
*
* @return boolean True if the user wants notifications for the tasklist.
*/
static protected function _notificationPref($user, $mode, $tasklist = null)
{
$prefs = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Prefs')->create('nag', array(
'cache' => false,
'user' => $user
));
$vals = array('lang' => $prefs->getValue('language'),
'tf' => $prefs->getValue('twentyFour'),
'df' => $prefs->getValue('date_format'));
if ($prefs->getValue('task_notification_exclude_self') &&
$user == $GLOBALS['registry']->getAuth()) {
return false;
}
$notification = $prefs->getValue('task_notification');
switch ($notification) {
case 'owner':
return $mode == 'owner' ? $vals : false;
case 'read':
return $mode == 'read' ? $vals : false;
case 'show':
if ($mode == 'read') {
$display_tasklists = unserialize($prefs->getValue('display_tasklists'));
return in_array($tasklist, $display_tasklists) ? $vals : false;
}
}
return false;
}
/**
* Comparison function for sorting tasks by create date (not currently used
* as it would require accessing Horde_History for each task) and id.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer 1 if task one is greater, -1 if task two is greater;
* 0 if they are equal (though no tasks should ever be
* equal in this comparison).
*/
static public function _sortByIdentity($a, $b)
{
return strcmp($a->id, $b->id);
}
/**
* Comparison function for sorting tasks by priority.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer 1 if task one is greater, -1 if task two is greater;
* 0 if they are equal.
*/
static public function _sortByPriority($a, $b)
{
if ($a->priority == $b->priority) {
return self::_sortByIdentity($a, $b);
}
return ($a->priority > $b->priority) ? 1 : -1;
}
/**
* Comparison function for reverse sorting tasks by priority.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer -1 if task one is greater, 1 if task two is greater;
* 0 if they are equal.
*/
static public function _rsortByPriority($a, $b)
{
return self::_sortByPriority($b, $a);
}
/**
* Comparison function for sorting tasks by name.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer 1 if task one is greater, -1 if task two is greater;
* 0 if they are equal.
*/
static public function _sortByName($a, $b)
{
return strcasecmp($a->name, $b->name);
}
/**
* Comparison function for reverse sorting tasks by name.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer -1 if task one is greater, 1 if task two is greater;
* 0 if they are equal.
*/
static public function _rsortByName($a, $b)
{
return strcasecmp($b->name, $a->name);
}
/**
* Comparison function for sorting tasks by assignee.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer 1 if task one is greater, -1 if task two is greater;
* 0 if they are equal.
*/
static public function _sortByAssignee($a, $b)
{
return strcasecmp($a->assignee, $b->assignee);
}
/**
* Comparison function for reverse sorting tasks by assignee.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer -1 if task one is greater, 1 if task two is greater;
* 0 if they are equal.
*/
static public function _rsortByAssignee($a, $b)
{
return strcasecmp($b->assignee, $a->assignee);
}
/**
* Comparison function for sorting tasks by assignee.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer 1 if task one is greater, -1 if task two is greater;
* 0 if they are equal.
*/
static public function _sortByEstimate($a, $b)
{
$a_est = $a->estimation();
$b_est = $b->estimation();
if ($a_est == $b_est) {
return self::_sortByIdentity($a, $b);
}
return ($a_est > $b_est) ? 1 : -1;
}
/**
* Comparison function for reverse sorting tasks by name.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer -1 if task one is greater, 1 if task two is greater;
* 0 if they are equal.
*/
static public function _rsortByEstimate($a, $b)
{
return self::_sortByEstimate($b, $a);
}
/**
* Comparison function for sorting tasks by due date.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer 1 if task one is greater, -1 if task two is greater;
* 0 if they are equal.
*/
static public function _sortByDue($a, $b)
{
$a_due = $a->getNextDue();
$b_due = $b->getNextDue();
if (!$a_due && !$b_due) {
return self::_sortByName($a, $b);
}
// Treat empty due dates as farthest into the future.
if (!$a_due) {
return 1;
}
if (!$b_due) {
return -1;
}
if ($a_due->equals($b_due)) {
return self::_sortByName($a, $b);
}
return ($a_due->after($b_due)) ? 1 : -1;
}
/**
* Comparison function for reverse sorting tasks by due date.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer -1 if task one is greater, 1 if task two is greater,
* 0 if they are equal.
*/
static public function _rsortByDue($a, $b)
{
return self::_sortByDue($b, $a);
}
/**
* Comparison function for sorting tasks by start date.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer 1 if task one is greater, -1 if task two is greater;
* 0 if they are equal.
*/
static public function _sortByStart($a, $b)
{
if ($a->start == $b->start) {
return self::_sortByIdentity($a, $b);
}
// Treat empty start dates as farthest into the future.
if ($a->start == 0) {
return 1;
}
if ($b->start == 0) {
return -1;
}
return ($a->start > $b->start) ? 1 : -1;
}
/**
* Comparison function for reverse sorting tasks by start date.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer -1 if task one is greater, 1 if task two is greater,
* 0 if they are equal.
*/
static public function _rsortByStart($a, $b)
{
return self::_sortByStart($b, $a);
}
/**
* Comparison function for sorting tasks by completion status.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer 1 if task one is greater, -1 if task two is greater;
* 0 if they are equal.
*/
static public function _sortByCompletion($a, $b)
{
if ($a->completed == $b->completed) {
return self::_sortByIdentity($a, $b);
}
return ($a->completed > $b->completed) ? -1 : 1;
}
/**
* Comparison function for reverse sorting tasks by completion status.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer -1 if task one is greater, 1 if task two is greater;
* 0 if they are equal.
*/
static public function _rsortByCompletion($a, $b)
{
return self::_sortByCompletion($b, $a);
}
/**
* Comparison function for sorting tasks by owner.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer 1 if task one is greater, -1 if task two is greater;
* 0 if they are equal.
*/
static public function _sortByOwner($a, $b)
{
$diff = strcasecmp(self::_getOwner($a), self::_getOwner($b));
if ($diff == 0) {
return self::_sortByIdentity($a, $b);
} else {
return $diff;
}
}
/**
* Comparison function for reverse sorting tasks by owner.
*
* @param array $a Task one.
* @param array $b Task two.
*
* @return integer -1 if task one is greater, 1 if task two is greater;
* 0 if they are equal.
*/
static public function _rsortByOwner($a, $b)
{
return self::_sortByOwner($b, $a);
}
/**
* Returns the owner of a task.
*
* @param Nag_Task $task A task.
*
* @return string The task's owner.
*/
static protected function _getOwner($task)
{
if ($task->tasklist == '**EXTERNAL**') {
return $GLOBALS['registry']->getAuth();
}
$share = $GLOBALS['nag_shares']->getShare($task->tasklist);
$owner = $task->tasklist;
if ($owner != $share->get('owner')) {
$owner = $share->get('name');
}
return $owner;
}
/**
* Returns the tasklists that should be used for syncing.
*
* @return array An array of task list ids
*/
static public function getSyncLists()
{
$cs = unserialize($GLOBALS['prefs']->getValue('sync_lists'));
// Bug #14585 Filter out erroneous null values.
$cs = array_filter($cs);
if (!empty($cs)) {
// Have a pref, make sure it's still available
$lists = self::listTasklists(false, Horde_Perms::DELETE);
$cscopy = array_flip($cs);
foreach ($cs as $c) {
if (empty($lists[$c])) {
unset($cscopy[$c]);
}
}
// Have at least one
if (count($cscopy)) {
return array_flip($cscopy);
}
}
if ($cs = self::getDefaultTasklist(Horde_Perms::EDIT)) {
return array($cs);
}
return array();
}
}