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

1539 lines
57 KiB
PHP

<?php
/**
* Nag external API interface.
*
* This file defines Nag's external API interface. Other applications can
* interact with Nag through this API.
*
* @package Nag
*/
class Nag_Api extends Horde_Registry_Api
{
/**
* Links.
*
* @var array
*/
protected $_links = array(
'show' => '%application%/view.php?tasklist=|tasklist|&task=|task|&uid=|uid|'
);
/**
* Returns a number of defaults necessary for the ajax view.
*
* @return array A hash with default values.
*/
public function ajaxDefaults()
{
return array(
'URI_TASKLIST_EXPORT' => str_replace(
array('%23', '%2523', '%7B', '%257B', '%7D', '%257D'),
array('#', '#', '{', '{', '}', '}'),
strval($GLOBALS['registry']->downloadUrl('#{tasklist}.ics', array('actionID' => 'export', 'exportTasks' => 1, 'exportID' => Horde_Data::EXPORT_ICALENDAR, 'exportList' => '#{tasklist}'))->setRaw(true))),
'default_tasklist' => Nag::getDefaultTasklist(Horde_Perms::EDIT),
'default_due' => (bool)$GLOBALS['prefs']->getValue('default_due'),
'default_due_days' => (int)$GLOBALS['prefs']->getValue('default_due_days'),
'default_due_time' => $GLOBALS['prefs']->getValue('default_due_time'),
'prefs_url' => strval($GLOBALS['registry']->getServiceLink('prefs', 'nag')->setRaw(true)),
);
}
/**
* 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.
* DEFAULT: show_completed pref is used.
* - 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.
* - include_tags: (boolean) Autoload all tags.
* DEFAULT: false (Tags are lazy loaded as needed.)
* - json: (boolean) Return data as JSON.
* DEFAULT: false (Data is returned as Nag_Task)
* - tasklists: (array) An array of tasklists to include.
* DEFAULT: Use $GLOBALS['display_tasklists'];
*
* @return array An array of the requested tasks.
*/
public function listTasks(array $options = array())
{
global $prefs;
$completedArray = array(
'incomplete' => Nag::VIEW_INCOMPLETE,
'all' => Nag::VIEW_ALL,
'complete' => Nag::VIEW_COMPLETE,
'future' => Nag::VIEW_FUTURE,
'future_incomplete' => Nag::VIEW_FUTURE_INCOMPLETE);
// Prevent null tasklists value from obscuring the default value.
if (array_key_exists('tasklists', $options) && empty($options['tasklists'])) {
unset($options['tasklists']);
}
if (is_null($options['completed']) || !isset($completedArray[$options['completed']])) {
$options['completed'] = $prefs->getValue('show_completed');
} else {
$options['completed'] = $completedArray[$options['completed']];
}
$options = array_merge(
array(
'sortby' => $prefs->getValue('sortby'),
'sortdir' => $prefs->getValue('sortdir'),
'altsortby' => $prefs->getValue('altsortby'),
'tasklists' => $GLOBALS['display_tasklists'],
'include_tags' => false,
'external' => false,
'json' => false
),
$options
);
$tasks = Nag::listTasks($options);
$tasks->reset();
$list = array();
while ($task = $tasks->each()) {
$list[$task->id] = $options['json'] ? $task->toJson() : $task->toHash();
}
return $list;
}
/**
* Returns a list of task lists.
*
* @param boolean $owneronly Only return tasklists that this user owns?
* Defaults to false.
* @param integer $permission The permission to filter tasklists by.
* @param boolean $smart Include smart tasklists in results.
*
* @return array The task lists.
*/
public function listTasklists($owneronly = false, $permission = Horde_Perms::SHOW, $smart = true)
{
return Nag::listTasklists($owneronly, $permission, $smart);
}
/**
* Returns a task list.
*
* @param string $name A task list name.
*
* @return Horde_Share_Object The task list.
*/
public function getTasklist($name)
{
try {
$tasklist = $GLOBALS['nag_shares']->getShare($name);
} catch (Horde_Share_Exception $e) {
Horde::log($e->getMessage(), 'ERR');
throw new Nag_Exception($e);
}
if (!$tasklist->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::READ)) {
throw new Horde_Exception_PermissionDenied(_("You are not allowed to retrieve this task list."));
}
return $tasklist;
}
/**
* Adds a new task list.
*
* @param string $name Task list name.
* @param string $description Task list description.
* @param string $color Task list color.
* @param array $params Any addtional parameters needed. @since 4.2.1
* - synchronize: (boolean) If true, add task list to the list of
* task lists to syncronize.
* DEFAULT: false (do not add to the list).
*
* @return string The new tasklist's id.
*/
public function addTasklist($name, $description = '', $color = '', array $params = array())
{
$tasklist = Nag::addTasklist(array('name' => $name, 'description' => $description, 'color' => $color));
$name = $tasklist->getName();
if (!empty($params['synchronize'])) {
$sync = @unserialize($GLOBALS['prefs']->getValue('sync_lists'));
$sync[] = $name;
$GLOBALS['prefs']->setValue('sync_lists', serialize($sync));
}
return $name;
}
/**
* Updates an existing task list.
*
* @param string $name A task list name.
* @param array $info Hash with task list information.
*/
public static function updateTasklist($name, $info)
{
try {
$tasklist = $GLOBALS['nag_shares']->getShare($name);
} catch (Horde_Share_Exception $e) {
Horde::log($e->getMessage(), 'ERR');
throw new Nag_Exception($e);
}
return Nag::updateTasklist($tasklist, $info);
}
/**
* Deletes a task list.
*
* @param string $id A task list id.
*/
public function deleteTasklist($id)
{
$tasklist = $GLOBALS['nag_shares']->getShare($id);
return Nag::deleteTasklist($tasklist);
}
/**
* Returns the displayed task lists.
*
* @return array Displayed tasklists.
*/
public function getDisplayedTasklists()
{
return $GLOBALS['display_tasklists'];
}
/**
* Sets the displayed task lists.
*
* @param array $list Displayed tasklists.
*/
public function setDisplayedTasklists($list)
{
$GLOBALS['display_tasklists'] = $list;
$GLOBALS['prefs']->setValue('display_tasklists', serialize($list));
}
/**
* Returns the last modification timestamp of a given uid.
*
* @param string $uid The uid to look for.
* @param string $tasklist The tasklist to look in.
*
* @return integer The timestamp for the last modification of $uid.
*/
public function modified($uid, $tasklist = null)
{
$modified = $this->getActionTimestamp($uid, 'modify', $tasklist);
if (empty($modified)) {
$modified = $this->getActionTimestamp($uid, 'add', $tasklist);
}
return $modified;
}
/**
* Browse through Nag's object tree.
*
* @param string $path The level of the tree to browse.
* @param array $properties The item properties to return. Defaults to 'name',
* 'icon', and 'browseable'.
*
* @return array The contents of $path
*/
public function browse($path = '', $properties = array())
{
global $injector, $nag_shares, $registry;
// Default properties.
if (!$properties) {
$properties = array('name', 'icon', 'browseable');
}
if (substr($path, 0, 3) == 'nag') {
$path = substr($path, 3);
}
$path = trim($path, '/');
$parts = explode('/', $path);
$currentUser = $registry->getAuth();
if (empty($path)) {
// This request is for a list of all users who have tasklists
// visible to the requesting user.
$tasklists = Nag::listTasklists(false, Horde_Perms::READ);
$owners = array();
foreach ($tasklists as $tasklist) {
$owners[$tasklist->get('owner') ? $registry->convertUsername($tasklist->get('owner'), false) : '-system-'] = $tasklist->get('owner') ?: '-system-';
}
$results = array();
foreach ($owners as $externalOwner => $internalOwner) {
if (in_array('name', $properties)) {
$results['nag/' . $externalOwner]['name'] = $injector
->getInstance('Horde_Core_Factory_Identity')
->create($internalOwner)
->getName();
}
if (in_array('icon', $properties)) {
$results['nag/' . $externalOwner]['icon'] = Horde_Themes::img('user.png');
}
if (in_array('browseable', $properties)) {
$results['nag/' . $externalOwner]['browseable'] = true;
}
if (in_array('read-only', $properties)) {
$results['nag/' . $externalOwner]['read-only'] = true;
}
}
return $results;
} elseif (count($parts) == 1) {
// This request is for all tasklists owned by the requested user
$owner = $parts[0] == '-system-' ? '' : $registry->convertUsername($parts[0], true);
$tasklists = $nag_shares->listShares(
$currentUser,
array('perm' => Horde_Perms::SHOW,
'attributes' => $owner));
$results = array();
foreach ($tasklists as $tasklistId => $tasklist) {
if ($parts[0] == '-system-' && $tasklist->get('owner')) {
continue;
}
$retpath = 'nag/' . $parts[0] . '/' . $tasklistId;
if (in_array('name', $properties)) {
$results[$retpath]['name'] = sprintf(_("Tasks from %s"), Nag::getLabel($tasklist));
$results[$retpath . '.ics']['name'] = Nag::getLabel($tasklist);
}
if (in_array('displayname', $properties)) {
$results[$retpath]['displayname'] = Nag::getLabel($tasklist);
$results[$retpath . '.ics']['displayname'] = Nag::getLabel($tasklist) . '.ics';
}
if (in_array('owner', $properties)) {
$results[$retpath]['owner'] = $results[$retpath . '.ics']['owner'] = $tasklist->get('owner') ? $registry->convertUsername($tasklist->get('owner'), false) : '-system-';
}
if (in_array('icon', $properties)) {
$results[$retpath]['icon'] = Horde_Themes::img('nag.png');
$results[$retpath . '.ics']['icon'] = Horde_Themes::img('mime/icalendar.png');
}
if (in_array('browseable', $properties)) {
$results[$retpath]['browseable'] = $tasklist->hasPermission($currentUser, Horde_Perms::READ);
$results[$retpath . '.ics']['browseable'] = false;
}
if (in_array('read-only', $properties)) {
$results[$retpath]['read-only'] = $results[$retpath . '.ics']['read-only'] = !$tasklist->hasPermission($currentUser, Horde_Perms::EDIT);
}
if (in_array('contenttype', $properties)) {
$results[$retpath . '.ics']['contenttype'] = 'text/calendar';
}
}
return $results;
} elseif (count($parts) == 2 && substr($parts[1], -4) == '.ics') {
//
// This is a request for the entire tasklist in iCalendar format.
//
$tasklist = substr($parts[1], 0, -4);
if (!Nag::hasPermission($tasklist, Horde_Perms::READ)) {
throw new Nag_Exception(_("Invalid task list file requested."), 404);
}
$ical_data = $this->exportTasklist($tasklist, 'text/calendar');
return array(
'data' => $ical_data,
'mimetype' => 'text/calendar',
'contentlength' => strlen($ical_data),
'mtime' => $_SERVER['REQUEST_TIME']
);
} elseif (count($parts) == 2) {
//
// This request is browsing into a specific tasklist. Generate the
// list of items and represent them as files within the directory.
//
try {
$tasklist = $nag_shares->getShare($parts[1]);
} catch (Horde_Exception_NotFound $e) {
throw new Nag_Exception(_("Invalid task list requested."), 404);
} catch (Horde_Share_Exception $e) {
throw new Nag_Exception($e->getMessage, 500);
}
if (!$tasklist->hasPermission($currentUser, Horde_Perms::READ)) {
throw new Nag_Exception(_("Invalid task list requested."), 404);
}
$storage = $injector->getInstance('Nag_Factory_Driver')->create($parts[1]);
try {
$storage->retrieve();
} catch (Nag_Exception $e) {
throw new Nag_Exception($e->getMessage, 500);
}
$icon = Horde_Themes::img('nag.png');
$owner = $tasklist->get('owner')
? $registry->convertUsername($tasklist->get('owner'), false)
: '-system-';
$results = array();
$storage->tasks->reset();
while ($task = $storage->tasks->each()) {
$key = 'nag/' . $parts[0] . '/' . $parts[1] . '/' . $task->id;
if (in_array('name', $properties)) {
$results[$key]['name'] = $task->name;
}
if (in_array('owner', $properties)) {
$results[$key]['owner'] = $owner;
}
if (in_array('icon', $properties)) {
$results[$key]['icon'] = $icon;
}
if (in_array('browseable', $properties)) {
$results[$key]['browseable'] = false;
}
if (in_array('read-only', $properties)) {
$results[$key]['read-only'] = !$tasklist->hasPermission($currentUser, Horde_Perms::EDIT);
}
if (in_array('contenttype', $properties)) {
$results[$key]['contenttype'] = 'text/calendar';
}
if (in_array('modified', $properties)) {
$results[$key]['modified'] = $this->modified($task->uid, $parts[1]);
}
if (in_array('created', $properties)) {
$results[$key]['created'] = $this->getActionTimestamp($task->uid, 'add', $parts[1]);
}
}
return $results;
} else {
//
// The only valid request left is for either a specific task item.
//
if (count($parts) == 3 &&
Nag::hasPermission($parts[1], Horde_Perms::READ)) {
//
// This request is for a specific item within a given task list.
//
/* Create a Nag storage instance. */
$storage = $injector->getInstance('Nag_Factory_Driver')
->create($parts[1]);
$storage->retrieve();
try {
$task = $storage->get($parts[2]);
} catch (Nag_Exception $e) {
throw new Nag_Exception($e->getMessage(), 500);
}
$result = array(
'data' => $this->export($task->uid, 'text/calendar'),
'mimetype' => 'text/calendar');
$modified = $this->modified($task->uid, $parts[1]);
if (!empty($modified)) {
$result['mtime'] = $modified;
}
return $result;
} else {
//
// All other requests are a 404: Not Found
//
return false;
}
}
}
/**
* Saves a file into the Nag tree.
*
* @param string $path The path where to PUT the file.
* @param string $content The file content.
* @param string $content_type The file's content type.
*
* @return array The event UIDs
*/
public function put($path, $content, $content_type)
{
if (substr($path, 0, 3) == 'nag') {
$path = substr($path, 3);
}
$path = trim($path, '/');
$parts = explode('/', $path);
if (count($parts) == 2 &&
substr($parts[1], -4) == '.ics') {
// Workaround for WebDAV clients that are not smart enough to send
// the right content type. Assume text/calendar.
if ($content_type == 'application/octet-stream') {
$content_type = 'text/calendar';
}
$tasklist = substr($parts[1], 0, -4);
} elseif (count($parts) == 3) {
$tasklist = $parts[1];
// Workaround for WebDAV clients that are not smart enough to send
// the right content type. Assume the same format we send
// individual tasklist items: text/calendar
if ($content_type == 'application/octet-stream') {
$content_type = 'text/calendar';
}
} else {
throw new Nag_Exception(_("Invalid task list name supplied."), 403);
}
if (!Nag::hasPermission($tasklist, Horde_Perms::EDIT)) {
// FIXME: Should we attempt to create a tasklist based on the
// filename in the case that the requested tasklist does not exist?
throw new Nag_Exception(_("Task list does not exist or no permission to edit"), 403);
}
// Store all currently existings UIDs. Use this info to delete UIDs not
// present in $content after processing.
$ids = array();
if (count($parts) == 2) {
$uids_remove = array_flip($this->listUids($tasklist));
} else {
$uids_remove = array();
}
$storage = $GLOBALS['injector']->getInstance('Nag_Factory_Driver')->create($tasklist);
switch ($content_type) {
case 'text/calendar':
case 'text/x-vcalendar':
$iCal = new Horde_Icalendar();
if (!($content instanceof Horde_Icalendar_Vtodo)) {
if (!$iCal->parsevCalendar($content)) {
throw new Nag_Exception(_("There was an error importing the iCalendar data."), 400);
}
} else {
$iCal->addComponent($content);
}
foreach ($iCal->getComponents() as $content) {
if (!($content instanceof Horde_Icalendar_Vtodo)) {
continue;
}
$task = new Nag_Task();
$task->fromiCalendar($content);
$task->tasklist = $tasklist;
$create = true;
if (isset($task->uid)) {
try {
$existing = $storage->getByUID($task->uid);
$create = false;
} catch (Horde_Exception_NotFound $e) {
}
}
if (!$create) {
// Entry exists, remove from uids_remove list so we
// won't delete in the end.
unset($uids_remove[$task->uid]);
if ($existing->private &&
$existing->owner != $GLOBALS['registry']->getAuth()) {
continue;
}
// Check if our task is newer then the existing - get
// the task's history.
$history = $GLOBALS['injector']->getInstance('Horde_History');
$created = $modified = null;
try {
$log = $history->getHistory('nag:' . $tasklist . ':' . $task->uid);
foreach ($log as $entry) {
switch ($entry['action']) {
case 'add':
$created = $entry['ts'];
break;
case 'modify':
$modified = $entry['ts'];
break;
}
}
} catch (Exception $e) {
}
if (empty($modified) && !empty($add)) {
$modified = $add;
}
if (!empty($modified) &&
$modified >= $content->getAttribute('LAST-MODIFIED')) {
// LAST-MODIFIED timestamp of existing entry
// is newer: don't replace it.
continue;
}
// Don't change creator/owner.
$task->owner = $existing->owner;
try {
$storage->modify($existing->id, $task->toHash());
} catch (Nag_Exception $e) {
throw new Nag_Exception($e->getMessage(), 500);
}
$ids[] = $task->uid;
} else {
try {
$newTask = $storage->add($task->toHash());
} catch (Nag_Exception $e) {
throw new Nag_Exception($e->getMessage(), 500);
}
// use UID rather than ID
$ids[] = $newTask[1];
}
}
break;
default:
throw new Nag_Exception(sprintf(_("Unsupported Content-Type: %s"), $content_type), 400);
}
if (Nag::hasPermission($tasklist, Horde_Perms::DELETE)) {
foreach (array_keys($uids_remove) as $uid) {
$this->delete($uid);
}
}
return $ids;
}
/**
* Deletes a file from the Nag tree.
*
* @param string $path The path to the file.
*
* @return string The event's UID
* @throws Nag_Exception
*/
public function path_delete($path)
{
if (substr($path, 0, 3) == 'nag') {
$path = substr($path, 3);
}
$path = trim($path, '/');
$parts = explode('/', $path);
if (count($parts) == 2) {
// @TODO Deny deleting of the entire tasklist for now.
// Allow users to delete tasklists but not create them via WebDAV
// will be more confusing than helpful. They are, however, still
// able to delete individual task items within the tasklist folder.
throw Nag_Exception(_("Deleting entire task lists is not supported."), 403);
// To re-enable the functionality just remove this if {} block.
}
if (substr($parts[1], -4) == '.ics') {
$tasklistID = substr($parts[1], 0, -4);
} else {
$tasklistID = $parts[1];
}
if (!(count($parts) == 2 || count($parts) == 3) ||
!Nag::hasPermission($tasklistID, Horde_Perms::DELETE)) {
throw new Nag_Exception(_("Task list does not exist or no permission to delete"), 403);
}
/* Create a Nag storage instance. */
try {
$storage = $GLOBALS['injector']->getInstance('Nag_Factory_Driver')->create($tasklistID);
$storage->retrieve();
} catch (Nag_Exception $e) {
throw new Nag_Exception(sprintf(_("Connection failed: %s"), $e->getMessage()), 500);
}
if (count($parts) == 3) {
// Delete just a single entry
return $storage->delete($parts[2]);
} else {
// Delete the entire task list
try {
$storage->deleteAll();
} catch (Nag_Exception $e) {
throw new Nag_Exception(sprintf(_("Unable to delete task list \"%s\": %s"), $tasklistID, $e->getMessage()), 500);
}
// Remove share and all groups/permissions.
$share = $GLOBALS['nag_shares']->getShare($tasklistID);
try {
$GLOBALS['nag_shares']->removeShare($share);
} catch (Horde_Share_Exception $e) {
throw new Nag_Exception($e->getMessage());
}
}
}
/**
* Returns an array of UIDs for all tasks that the current user is authorized
* to see.
*
* @param mixed $tasklists The tasklist or an array of taskslists to list.
*
* @return array An array of UIDs for all tasks
* the user can access.
*
* @throws Horde_Exception_PermissionDenied
* @throws Nag_Exception
*/
public function listUids($tasklists = null)
{
if (!isset($GLOBALS['conf']['storage']['driver'])) {
throw new Nag_Exception(_("Not configured"));
}
if (empty($tasklists)) {
$tasklists = Nag::getSyncLists();
} else {
if (!is_array($tasklists)) {
$tasklists = array($tasklists);
}
foreach ($tasklists as $list) {
if (!Nag::hasPermission($list, Horde_Perms::READ)) {
throw new Horde_Exception_PermissionDenied();
}
}
}
$tasks = Nag::listTasks(array(
'tasklists' => $tasklists,
'completed' => Nag::VIEW_ALL,
'include_history' => false)
);
$uids = array();
$tasks->reset();
while ($task = $tasks->each()) {
$uids[] = $task->uid;
}
return $uids;
}
/**
* Returns an array of UIDs for tasks that have had $action happen since
* $timestamp.
*
* @param string $action The action to check for - add, modify, or delete.
* @param integer $timestamp The time to start the search.
* @param mixed $tasklists The tasklists to be used. If 'null', the
* user's default tasklist will be used.
* @param integer $end The optional ending timestamp.
* @param boolean $isModSeq If true, $timestamp and $end are modification
* sequences and not timestamps. @since 4.1.1
*
* @return array An array of UIDs matching the action and time criteria.
*
* @throws Horde_History_Exception
* @throws InvalidArgumentException
*/
public function listBy($action, $timestamp, $tasklist = null, $end = null, $isModSeq = false)
{
if (empty($tasklist)) {
$tasklist = Nag::getSyncLists();
$results = array();
foreach ($tasklist as $list) {
$results = array_merge($results, $this->listBy($action, $timestamp, $list, $end, $isModSeq));
}
return $results;
}
$filter = array(array('op' => '=', 'field' => 'action', 'value' => $action));
if (!empty($end) && !$isModSeq) {
$filter[] = array('op' => '<', 'field' => 'ts', 'value' => $end);
}
if (!$isModSeq) {
$histories = $GLOBALS['injector']
->getInstance('Horde_History')
->getByTimestamp('>', $timestamp, $filter, 'nag:' . $tasklist);
} else {
$histories = $GLOBALS['injector']
->getInstance('Horde_History')
->getByModSeq($timestamp, $end, $filter, 'nag:' . $tasklist);
}
// Strip leading nag:username:.
return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));
}
/**
* Method for obtaining all server changes between two timestamps. Basically
* a wrapper around listBy(), but returns an array containing all adds,
* edits and deletions.
*
* @param integer $start The starting timestamp
* @param integer $end The ending timestamp.
* @param boolean $isModSeq If true, $timestamp and $end are
* modification sequences and not
* timestamps. @since 4.1.1
* @param string|array $tasklists The sources to check. @since 4.2.0
*
* @return array An hash with 'add', 'modify' and 'delete' arrays.
*/
public function getChanges($start, $end, $isModSeq = false, $tasklists = null)
{
return array(
'add' => $this->listBy('add', $start, $tasklists, $end, $isModSeq),
'modify' => $this->listBy('modify', $start, $tasklists, $end, $isModSeq),
'delete' => $this->listBy('delete', $start, $tasklists, $end, $isModSeq));
}
/**
* Return all changes occuring between the specified modification
* sequences.
*
* @param integer $start The starting modseq.
* @param integer $end The ending modseq.
* @param string|array $tasklists The sources to check. @since 4.2.0
*
* @return array The changes @see getChanges()
* @since 4.1.1
*/
public function getChangesByModSeq($start, $end, $tasklists = null)
{
return $this->getChanges($start, $end, true, $tasklists);
}
/**
* Returns the timestamp of an operation for a given uid an action.
*
* @param string $uid The uid to look for.
* @param string $action The action to check for - add, modify, or delete.
* @param string $tasklist The tasklist to be used. If 'null', the
* user's default tasklist will be used.
* @param boolean $modSeq Request a modification sequence instead of a
* timestamp. @since 4.1.1
*
* @return integer The timestamp for this action.
*
* @throws InvalidArgumentException
* @throws Horde_Exception_PermissionDenied
* @thorws Horde_History_Exception
*/
public function getActionTimestamp($uid, $action, $tasklist = null, $modSeq = false)
{
if ($tasklist === null) {
$tasklist = Nag::getDefaultTasklist(Horde_Perms::READ);
} elseif (!Nag::hasPermission($tasklist, Horde_Perms::READ)) {
throw new Horde_Exception_PermissionDenied();
}
if (!$modSeq) {
return $GLOBALS['injector']
->getInstance('Horde_History')
->getActionTimestamp('nag:' . $tasklist . ':' . $uid, $action);
} else {
return $GLOBALS['injector']
->getInstance('Horde_History')
->getActionModSeq('nag:' . $tasklist . ':' . $uid, $action);
}
}
/**
* Return the largest modification sequence from the history backend.
*
* @param string $id Limit the check to this tasklist. @since 4.2.0
*
* @return integer The modseq.
* @since 4.1.1
*/
public function getHighestModSeq($id = null)
{
$parent = 'nag';
if (!empty($id)) {
$parent .= ':' . $id;
}
return $GLOBALS['injector']->getInstance('Horde_History')->getHighestModSeq($parent);
}
/**
* Imports one or more tasks represented in the specified content type.
*
* If a UID is present in the content and the task is already in the
* database, a replace is performed rather than an add.
*
* @param string $content The content of the task.
* @param string $contentType What format is the data in? Currently supports:
* text/calendar
* text/x-vcalendar
* @param string $tasklist The tasklist into which the task will be
* imported. If 'null', the user's default
* tasklist will be used.
*
* @return string The new UID on one import, an array of UIDs on multiple imports,
*/
public function import($content, $contentType, $tasklist = null)
{
if ($tasklist === null) {
$tasklist = Nag::getDefaultTasklist(Horde_Perms::EDIT);
} elseif (!Nag::hasPermission($tasklist, Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
/* Create a Nag_Driver instance. */
$storage = $GLOBALS['injector']->getInstance('Nag_Factory_Driver')->create($tasklist);
switch ($contentType) {
case 'text/x-vcalendar':
case 'text/calendar':
case 'text/x-vtodo':
$iCal = new Horde_Icalendar();
if (!($content instanceof Horde_Icalendar_Vtodo)) {
if (!$iCal->parsevCalendar($content)) {
throw new Nag_Exception(_("There was an error importing the iCalendar data."));
}
} else {
$iCal->addComponent($content);
}
$components = $iCal->getComponents();
if (count($components) == 0) {
throw new Nag_Exception(_("No iCalendar data was found."));
}
$ids = array();
foreach ($components as $content) {
if ($content instanceof Horde_Icalendar_Vtodo) {
$task = new Nag_Task($storage);
$task->fromiCalendar($content);
if (isset($task->uid)) {
try {
$existing = $storage->getByUID($task->uid);
$task->owner = $existing->owner;
$storage->modify($existing->id, $task->toHash());
} catch ( Horde_Exception_NotFound $e ) {
$storage->add($task->toHash());
}
$ids[] = $task->uid;
} else {
$hash = $task->toHash();
unset($hash['uid']);
$newTask = $storage->add($hash);
// use UID rather than ID
$ids[] = $newTask[1];
}
}
}
if (count($ids) == 0) {
throw new Nag_Exception(_("No iCalendar data was found."));
} else if (count($ids) == 1) {
return $ids[0];
}
return $ids;
case 'activesync':
$task = new Nag_Task();
$task->fromASTask($content);
$hash = $task->toHash();
unset($hash['uid']);
$results = $storage->add($hash);
/* array index 0 is id, 1 is uid */
return $results[1];
}
throw new Nag_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
}
/**
* Adds a task.
*
* @param array $task A hash with task information.
*
* @throws Horde_Exception_PermissionDenied
*/
public function addTask(array $task)
{
if (!$GLOBALS['registry']->isAdmin() &&
!Nag::hasPermission($task['tasklist'], Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
$storage = $GLOBALS['injector']->getInstance('Nag_Factory_Driver')->create($task['tasklist']);
return $storage->add($task);
}
/**
* 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.
* @throws Horde_Exception_PermissionDenied
*/
public function quickAdd($text, $tasklist = null)
{
if ($tasklist === null) {
$tasklist = Nag::getDefaultTasklist(Horde_Perms::EDIT);
} elseif (!Nag::hasPermission($tasklist, Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
return Nag::createTasksFromText($text, $tasklist);
}
/**
* Toggles the task completion flag.
*
* @param string $task_id The task ID.
* @param string $tasklist_id The tasklist that contains the task.
*
* @return boolean|string True if the task has been toggled, a due date if
* there are still incomplete recurrences, otherwise
* false.
*/
public function toggleCompletion($task_id, $tasklist_id)
{
if (!Nag::hasPermission($tasklist_id, Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
try {
$share = $GLOBALS['nag_shares']->getShare($tasklist_id);
} catch (Horde_Share_Exception $e) {
Horde::log($e->getMessage(), 'ERR');
throw new Nag_Exception($e);
}
$task = Nag::getTask($tasklist_id, $task_id);
$completed = $task->completed;
try {
$task->toggleComplete();
} catch (Nag_Exception $e) {
Horde::log($e->getMessage(), 'DEBUG');
return false;
}
$task->save();
$due = $task->getNextDue();
if ($task->completed == $completed) {
if ($due) {
return $due->toJson();
}
return false;
}
return true;
}
/**
* Exports a task, identified by UID, in the requested content type.
*
* @param string $uid Identify the task to export.
* @param string $contentType What format should the data be in?
* A string with one of:
* - text/calendar: iCalendar 2.0. Recommended as this is specified in
* RFC 2445.
* - text/x-vcalendar: vCalendar 1.0 format. Still in wide use.
* - activesync: Horde_ActiveSync_Message_Task.
* - raw: Nag_Task.
* @param array $options Any additional options for the exporter.
*
* @return string The requested data.
*/
public function export($uid, $contentType, array $options = array())
{
$task = $GLOBALS['injector']
->getInstance('Nag_Factory_Driver')
->create('')
->getByUID($uid);
if (!Nag::hasPermission($task->tasklist, Horde_Perms::READ)) {
throw new Horde_Exception_PermissionDenied();
}
$version = '2.0';
switch ($contentType) {
case 'text/x-vcalendar':
$version = '1.0';
case 'text/calendar':
// Create the new iCalendar container.
$iCal = new Horde_Icalendar($version);
$iCal->setAttribute('PRODID', '-//The Horde Project//Nag ' . $GLOBALS['registry']->getVersion() . '//EN');
$iCal->setAttribute('METHOD', 'PUBLISH');
// Create new vTodo object.
$vTodo = $task->toiCalendar($iCal);
$vTodo->setAttribute('VERSION', $version);
$iCal->addComponent($vTodo);
return $iCal->exportvCalendar();
case 'activesync':
return $task->toASTask($options);
case 'raw':
return $task;
default:
throw new Nag_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
}
}
/**
* Returns a task object.
*
* @param string $tasklist A tasklist id.
* @param string $id A task id.
*
* @return Nag_Task The matching task object.
*/
public function getTask($tasklist, $id)
{
if (!Nag::hasPermission($tasklist, Horde_Perms::READ)) {
throw new Horde_Exception_PermissionDenied();
}
return Nag::getTask($tasklist, $id);
}
/**
* Exports a tasklist in the requested content type.
*
* @param string $tasklist The tasklist to export.
* @param string $contentType What format should the data be in?
* A string with one of:
* <pre>
* text/calendar (VCALENDAR 2.0. Recommended as
* this is specified in rfc2445)
* text/x-vcalendar (old VCALENDAR 1.0 format.
* Still in wide use)
* </pre>
*
* @return string The iCalendar representation of the tasklist.
*/
public function exportTasklist($tasklist, $contentType)
{
if (!Nag::hasPermission($tasklist, Horde_Perms::READ)) {
throw new Horde_Exception_PermissionDenied();
}
$tasks = Nag::listTasks(array(
'tasklists' => array($tasklist),
'completed' => Nag::VIEW_ALL,
'external' => false,
'include_tags' => true));
$version = '2.0';
switch ($contentType) {
case 'text/x-vcalendar':
$version = '1.0';
case 'text/calendar':
$share = $GLOBALS['nag_shares']->getShare($tasklist);
$iCal = new Horde_Icalendar($version);
$iCal->setAttribute('X-WR-CALNAME', $share->get('name'));
$tasks->reset();
while ($task = $tasks->each()) {
$iCal->addComponent($task->toiCalendar($iCal));
}
return $iCal->exportvCalendar();
}
throw new Nag_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
}
/**
* Deletes a task identified by UID.
*
* @param string|array $uid Identify the task to delete, either a single UID
* or an array.
*
* @return boolean Success or failure.
*/
public function delete($uid)
{
// Handle an arrray of UIDs for convenience
if (is_array($uid)) {
foreach ($uid as $g) {
$result = $this->delete($g);
}
return true;
}
$factory = $GLOBALS['injector']->getInstance('Nag_Factory_Driver');
$task = $factory->create('')->getByUID($uid);
if (!$GLOBALS['registry']->isAdmin() &&
!Nag::hasPermission($task->tasklist, Horde_Perms::DELETE)) {
throw new Horde_Exception_PermissionDenied();
}
return $factory->create($task->tasklist)->delete($task->id);
}
/**
* Deletes a task identified by tasklist and ID.
*
* @param string $tasklist A tasklist id.
* @param string $id A task id.
*/
public function deleteTask($tasklist, $id)
{
if (!$GLOBALS['registry']->isAdmin() &&
!Nag::hasPermission($tasklist, Horde_Perms::DELETE)) {
throw new Horde_Exception_PermissionDenied();
}
$storage = $GLOBALS['injector']->getInstance('Nag_Factory_Driver')->create($tasklist);
return $storage->delete($id);
}
/**
* Replaces the task identified by UID with the content represented in the
* specified content type.
*
* If you want to replace multiple tasks with the UID specified in the
* VCALENDAR data, you may use $this->import instead. This automatically does a
* replace if existings UIDs are found.
*
*
* @param string $uid Identify the task to replace.
* @param string $content The content of the task.
* @param string $contentType What format is the data in? Currently supports:
* - text/x-vcalendar
* - text/calendar
*
* @return boolean Success or failure.
*/
public function replace($uid, $content, $contentType)
{
$factory = $GLOBALS['injector']->getInstance('Nag_Factory_Driver');
$existing = $factory->create('')->getByUID($uid);
$taskId = $existing->id;
$owner = $existing->owner;
if (!Nag::hasPermission($existing->tasklist, Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
switch ($contentType) {
case 'text/calendar':
case 'text/x-vcalendar':
if (!($content instanceof Horde_Icalendar_Vtodo)) {
$iCal = new Horde_Icalendar();
if (!$iCal->parsevCalendar($content)) {
throw new Nag_Exception(_("There was an error importing the iCalendar data."));
}
$components = $iCal->getComponents();
$component = null;
foreach ($components as $content) {
if ($content instanceof Horde_Icalendar_Vtodo) {
if ($component !== null) {
throw new Nag_Exception(_("Multiple iCalendar components found; only one vTodo is supported."));
}
$component = $content;
}
}
if ($component === null) {
throw new Nag_Exception(_("No iCalendar data was found."));
}
}
$task = new Nag_Task();
$task->fromiCalendar($content);
$task->owner = $owner;
$factory->create($existing->tasklist)->modify($taskId, $task->toHash());
break;
case 'activesync':
$task = new Nag_Task();
$task->fromASTask($content);
$task->owner = $owner;
$task->uid = $uid;
$factory->create($existing->tasklist)->modify($taskId, $task->toHash());
break;
default:
throw new Nag_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
}
return $result;
}
/**
* Changes a task identified by tasklist and ID.
*
* @param string $tasklist A tasklist id.
* @param string $id A task id.
* @param array $task A hash with overwriting task information.
*/
public function updateTask($tasklist, $id, $task)
{
if (!$GLOBALS['registry']->isAdmin() &&
!Nag::hasPermission($tasklist, Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
$storage = $GLOBALS['injector']->getInstance('Nag_Factory_Driver')->create($tasklist);
$existing = $storage->get($id);
$task['owner'] = $existing->owner;
return $storage->modify($id, $task);
}
/**
* Lists active tasks as cost objects.
*
* @todo Implement $criteria parameter.
*
* @param array $criteria Filter attributes
*/
public function listCostObjects($criteria)
{
$tasks = Nag::listTasks(array(
'completed' => Nag::VIEW_ALL,
'include_history' => false)
);
$result = array();
$tasks->reset();
$last_week = $_SERVER['REQUEST_TIME'] - 7 * 86400;
while ($task = $tasks->each()) {
if (($task->completed && $task->completed_date < $last_week) ||
($task->start && $task->start > $_SERVER['REQUEST_TIME'])) {
continue;
}
$result[$task->id] = array(
'id' => $task->id,
'active' => !$task->completed,
'name' => $task->name
);
for ($parent = $task->parent; $parent->parent; $parent = $parent->parent) {
$result[$task->id]['name'] = $parent->name . ': '
. $result[$task->id]['name'];
}
if (!empty($task->estimate)) {
$result[$task->id]['estimate'] = $task->estimate;
}
}
if (count($result) == 0) {
return array();
} else {
return array(array('category' => _("Tasks"),
'objects' => array_values($result)));
}
}
public function listTimeObjectCategories()
{
$categories = array();
$tasklists = Nag::listTasklists(false, Horde_Perms::SHOW | Horde_Perms::READ);
foreach ($tasklists as $tasklistId => $tasklist) {
$categories[$tasklistId] = array('title' => Nag::getLabel($tasklist), 'type' => 'share');
}
return $categories;
}
/**
* Lists active tasks as time objects.
*
* @param array $categories The time categories (from
* listTimeObjectCategories) to list.
* @param mixed $start The start date of the period.
* @param mixed $end The end date of the period.
*/
public function listTimeObjects($categories, $start, $end)
{
$allowed_tasklists = Nag::listTasklists(false, Horde_Perms::READ);
foreach ($categories as $tasklist) {
if (!isset($allowed_tasklists[$tasklist])) {
throw new Horde_Exception_PermissionDenied();
}
}
$timeobjects = array();
$start = new Horde_Date($start);
$start_ts = $start->timestamp();
$end = new Horde_Date($end);
$end_ts = $end->timestamp();
// List incomplete tasks.
$tasks = Nag::listTasks(array(
'tasklists' => $categories,
'completed' => Nag::VIEW_FUTURE_INCOMPLETE,
'include_history' => false)
);
$tasks->reset();
while ($task = $tasks->each()) {
// If there's no due date, it's not a time object.
if (!$task->due ||
$task->due > $end_ts ||
(!$task->recurs() && $task->due + 1 < $start_ts) ||
($task->recurs() && $task->recurrence->getRecurEnd() &&
$task->recurrence->getRecurEnd()->timestamp() + 1 < $start_ts)) {
continue;
}
$due_date = date('Y-m-d\TH:i:s', $task->due);
$recurrence = null;
if ($task->recurs()) {
$recurrence = array(
'type' => $task->recurrence->getRecurType(),
'interval' => $task->recurrence->getRecurInterval(),
'end' => $task->recurrence->getRecurEnd(),
'count' => $task->recurrence->getRecurCount(),
'days' => $task->recurrence->getRecurOnDays(),
'exceptions' => $task->recurrence->getExceptions(),
'completions' => $task->recurrence->getCompletions());
}
$timeobjects[$task->id] = array(
'id' => $task->id,
'title' => $task->name,
'description' => $task->desc,
'start' => $due_date,
'end' => $due_date,
'recurrence' => $recurrence,
'color' => $allowed_tasklists[$task->tasklist]->get('color'),
'owner' => $allowed_tasklists[$task->tasklist]->get('owner'),
'permissions' => $GLOBALS['nag_shares']->getPermissions($task->tasklist, $GLOBALS['registry']->getAuth()),
'variable_length' => false,
'params' => array(
'task' => $task->id,
'tasklist' => $task->tasklist,
),
'link' => Horde::url('view.php', true)->add(array('tasklist' => $task->tasklist, 'task' => $task->id)),
'edit_link' => Horde::url('task.php', true)->add(array('tasklist' => $task->tasklist, 'task' => $task->id, 'actionID' => 'modify_task')),
'delete_link' => Horde::url('task.php', true)->add(array('tasklist' => $task->tasklist, 'task' => $task->id, 'actionID' => 'delete_task')),
'ajax_link' => 'task:' . $task->tasklist . ':' . $task->id
);
}
return $timeobjects;
}
/**
* Saves properties of a time object back to the task that it represents.
*
* At the moment only the title, description and due date are saved.
*
* @param array $timeobject A time object hash.
* @throws Nag_Exception
*/
public function saveTimeObject(array $timeobject)
{
if (!Nag::hasPermission($timeobject['params']['tasklist'], Horde_Perms::EDIT)) {
throw new Horde_Exception_PermissionDenied();
}
$storage = $GLOBALS['injector']
->getInstance('Nag_Factory_Driver')
->create($timeobject['params']['tasklist']);
$existing = $storage->get($timeobject['id']);
$info = array();
if (isset($timeobject['start'])) {
$info['due'] = new Horde_Date($timeobject['start']);
$info['due'] = $info['due']->timestamp();
}
if (isset($timeobject['title'])) {
$info['name'] = $timeobject['title'];
}
if (isset($timeobject['description'])) {
$info['desc'] = $timeobject['description'];
}
$storage->modify($timeobject['id'], $info);
}
/**
* Returns a list of available sources.
*
* @param boolean $writeable If true, limits to writeable sources.
* @param boolean $sync_only Only include synchable address books.
*
* @return array An array of the available sources. Keys are source IDs,
* values are source titles.
* @since 4.2.0
*/
public function sources($writeable = false, $sync_only = false)
{
$out = array();
foreach (Nag::listTasklists(false, $writeable ? Horde_Perms::EDIT : Horde_Perms::READ, false) as $key => $val) {
$out[$key] = $val->get('name');
}
if ($sync_only) {
$syncable = Nag::getSyncLists();
$out = array_intersect_key($out, array_flip($syncable));
}
return $out;
}
/**
* Retrieve the UID for the current user's default tasklist.
*
* @return string UID.
* @since 4.2.0
*/
public function getDefaultShare()
{
return Nag::getDefaultTasklist(Horde_Perms::EDIT);
}
/**
* Retrieve the list of used tag_names, tag_ids and the total number
* of resources that are linked to that tag.
*
* @param array $tags An optional array of tag_ids. If omitted, all tags
* will be included.
*
* @return array An array containing tag_name, and total
*/
public function listTagInfo($tags = null, $user = null)
{
return $GLOBALS['injector']->getInstance('Nag_Tagger')
->getTagInfo($tags, 500, null, $user);
}
/**
* SearchTags API:
* Returns an application-agnostic array (useful for when doing a tag search
* across multiple applications)
*
* The 'raw' results array can be returned instead by setting $raw = true.
*
* @param array $names An array of tag_names to search for.
* @param integer $max The maximum number of resources to return.
* @param integer $from The number of the resource to start with.
* @param string $resource_type The resource type [bookmark, '']
* @param string $user Restrict results to resources owned by $user.
* @param boolean $raw Return the raw data?
*
* @return array An array of results:
* <pre>
* 'title' - The title for this resource.
* 'desc' - A terse description of this resource.
* 'view_url' - The URL to view this resource.
* 'app' - The Horde application this resource belongs to.
* 'icon' - URL to an image.
* </pre>
*/
public function searchTags($names, $max = 10, $from = 0,
$resource_type = '', $user = null, $raw = false)
{
// TODO: $max, $from, $resource_type not honored
global $injector, $registry;
$results = $injector
->getInstance('Nag_Tagger')
->search(
$names,
array('user' => $user));
// Check for error or if we requested the raw data array.
if ($raw) {
return $results;
}
$return = array();
$redirectUrl = Horde::url('redirect.php');
foreach ($results as $task_id) {
try {
$task = $injector->getInstance('Nag_Factory_Driver')
->create(null)
->getByUID($task_id);
$return[] = array(
'title' => $task->name,
'desc' => $task->description,
'view_url' => $redirectUrl->add('b', $task->id),
'app' => 'nag'
);
} catch (Exception $e) {
}
}
return $return;
}
}