Overview

Packages

  • Nag
  • None

Classes

  • Horde_Core_Ui_VarRenderer_Nag
  • Nag
  • Nag_Ajax_Application
  • Nag_Api
  • Nag_Driver
  • Nag_Driver_Kolab
  • Nag_Driver_Sql
  • Nag_Exception
  • Nag_Factory_Tasklists
  • Nag_Form_CreateTaskList
  • Nag_Form_DeleteTaskList
  • Nag_Form_EditTaskList
  • Nag_Form_Renderer_Task
  • Nag_Form_Task
  • Nag_Form_Type_NagAlarm
  • Nag_Form_Type_NagDue
  • Nag_Form_Type_NagMethod
  • Nag_Form_Type_NagStart
  • Nag_Task
  • Nag_Tasklists_Base
  • Nag_Tasklists_Default
  • Nag_Tasklists_Kolab
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * Nag external API interface.
   4:  *
   5:  * This file defines Nag's external API interface. Other applications can
   6:  * interact with Nag through this API.
   7:  *
   8:  * @package Nag
   9:  */
  10: class Nag_Api extends Horde_Registry_Api
  11: {
  12:     /**
  13:      * Links.
  14:      *
  15:      * @var array
  16:      */
  17:     public $links = array(
  18:         'show' => '%application%/view.php?tasklist=|tasklist|&task=|task|&uid=|uid|'
  19:     );
  20: 
  21:     /**
  22:      * Returns a number of defaults necessary for the ajax view.
  23:      *
  24:      * @return array  A hash with default values.
  25:      */
  26:     public function ajaxDefaults()
  27:     {
  28:         return array(
  29:             'URI_TASKLIST_EXPORT' => (string)Horde::url('data.php', true)->add(array('actionID' => 'export', 'exportTasks' => 1, 'exportID' => Horde_Data::EXPORT_ICALENDAR, 'exportList' => '')),
  30:             'default_tasklist' => Nag::getDefaultTasklist(Horde_Perms::EDIT),
  31:             'default_due' => (bool)$GLOBALS['prefs']->getValue('default_due'),
  32:             'default_due_days' => (int)$GLOBALS['prefs']->getValue('default_due_days'),
  33:             'default_due_time' => $GLOBALS['prefs']->getValue('default_due_time'),
  34:         );
  35:     }
  36: 
  37:     /**
  38:      * Retrieves the current user's task list from storage.
  39:      *
  40:      * This function will also sort the resulting list, if requested.
  41:      *
  42:      * @param string $sortby        The field by which to sort
  43:      *                              (NAG_SORT_PRIORITY, NAG_SORT_NAME
  44:      *                              NAG_SORT_DUE, NAG_SORT_COMPLETION).
  45:      * @param integer $sortdir      The direction by which to sort.
  46:      * @param string $altsortby     The secondary sort field.
  47:      * @param array $tasklists      An array of tasklist to display or
  48:      *                              null/empty to display taskslists
  49:      *                              $GLOBALS['display_tasklists'].
  50:      * @param string $completed     Which tasks to retrieve (all, incomplete,
  51:      *                              complete, future or future_incomplete).
  52:      * @param boolean $json         Retrieve the results of the tasks in
  53:      *                              'json format'.
  54:      *
  55:      * @return Nag_Task  A list of the requested tasks.
  56:      */
  57:     public function listTasks($sortby = null, $sortdir = null,
  58:                               $altsortby = null, $tasklists = null,
  59:                               $completed = null, $json = false)
  60:     {
  61:         $completedArray = array('incomplete' => Nag::VIEW_INCOMPLETE,
  62:                                 'all' => Nag::VIEW_ALL,
  63:                                 'complete' => Nag::VIEW_COMPLETE,
  64:                                 'future' => Nag::VIEW_FUTURE,
  65:                                 'future_incomplete' => Nag::VIEW_FUTURE_INCOMPLETE);
  66: 
  67:         if (!isset($sortby)) {
  68:             $sortby = $GLOBALS['prefs']->getValue('sortby');
  69:         }
  70:         if (!isset($sortdir)) {
  71:             $sortdir = $GLOBALS['prefs']->getValue('sortdir');
  72:         }
  73:         if (is_null($altsortby)) {
  74:             $altsortby =  $GLOBALS['prefs']->getValue('altsortby');
  75:         }
  76:         if (is_null($tasklists)) {
  77:             $tasklists = $GLOBALS['display_tasklists'];
  78:         }
  79:         if (is_null($completed) || !isset($completedArray[$completed])) {
  80:             $completed = $GLOBALS['prefs']->getValue('show_completed');
  81:         } else {
  82:             $completed = $completedArray[$completed];
  83:         }
  84: 
  85:         $tasks = Nag::listTasks($sortby, $sortdir, $altsortby, $tasklists, $completed);
  86:         $tasks->reset();
  87:         $list = array();
  88:         while ($task = $tasks->each()) {
  89:             $list[$task->id] = $json ? $task->toJson() : $task->toHash();
  90:         }
  91: 
  92:         return $list;
  93:     }
  94: 
  95:     /**
  96:      * Returns a list of task lists.
  97:      *
  98:      * @param boolean $owneronly   Only return tasklists that this user owns?
  99:      *                             Defaults to false.
 100:      * @param integer $permission  The permission to filter tasklists by.
 101:      *
 102:      * @return array  The task lists.
 103:      */
 104:     public function listTasklists($owneronly = false, $permission = Horde_Perms::SHOW)
 105:     {
 106:         return Nag::listTasklists($owneronly, $permission);
 107:     }
 108: 
 109:     /**
 110:      * Returns a task list.
 111:      *
 112:      * @since Nag 3.0.3
 113:      *
 114:      * @param string $name   A task list name.
 115:      *
 116:      * @return Horde_Share_Object  The task list.
 117:      */
 118:     public function getTasklist($name)
 119:     {
 120:         try {
 121:             $tasklist = $GLOBALS['nag_shares']->getShare($name);
 122:         } catch (Horde_Share_Exception $e) {
 123:             Horde::logMessage($e->getMessage(), 'ERR');
 124:             throw new Nag_Exception($e);
 125:         }
 126:         if (!$tasklist->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::READ)) {
 127:             throw new Horde_Exception_PermissionDenied(_("You are not allowed to retrieve this task list."));
 128:         }
 129:         return $tasklist;
 130:     }
 131: 
 132:     /**
 133:      * Adds a new task list.
 134:      *
 135:      * @param string $name        Task list name.
 136:      * @param string $description Task list description.
 137:      * @param string $color       Task list color.
 138:      *
 139:      * @return integer  The new tasklist's id.
 140:      */
 141:     public function addTasklist($name, $description = '', $color = '')
 142:     {
 143:         $tasklist = Nag::addTasklist(array('name' => $name, 'description' => $description, 'color' => $color));
 144:         return $tasklist->getName();
 145:     }
 146: 
 147:     /**
 148:      * Updates an existing task list.
 149:      *
 150:      * @param string $name   A task list name.
 151:      * @param array $info  Hash with task list information.
 152:      */
 153:     public static function updateTasklist($name, $info)
 154:     {
 155:         try {
 156:             $tasklist = $GLOBALS['nag_shares']->getShare($name);
 157:         } catch (Horde_Share_Exception $e) {
 158:             Horde::logMessage($e->getMessage(), 'ERR');
 159:             throw new Nag_Exception($e);
 160:         }
 161: 
 162:         return Nag::updateTasklist($tasklist, $info);
 163:     }
 164: 
 165:     /**
 166:      * Deletes a task list.
 167:      *
 168:      * @param string $id  A task list id.
 169:      */
 170:     public function deleteTasklist($id)
 171:     {
 172:         $tasklist = $GLOBALS['nag_shares']->getShare($id);
 173:         return Nag::deleteTasklist($tasklist);
 174:     }
 175: 
 176:     /**
 177:      * Returns the displayed task lists.
 178:      *
 179:      * @since Nag 3.0.3
 180:      *
 181:      * @return array  Displayed tasklists.
 182:      */
 183:     public function getDisplayedTasklists()
 184:     {
 185:         return $GLOBALS['display_tasklists'];
 186:     }
 187: 
 188:     /**
 189:      * Sets the displayed task lists.
 190:      *
 191:      * @since Nag 3.0.3
 192:      *
 193:      * @param array $list  Displayed tasklists.
 194:      */
 195:     public function setDisplayedTasklists($list)
 196:     {
 197:         $GLOBALS['display_tasklists'] = $list;
 198:         $GLOBALS['prefs']->setValue('display_tasklists', serialize($list));
 199:     }
 200: 
 201:     /**
 202:      * Returns the last modification timestamp of a given uid.
 203:      *
 204:      * @param string $uid      The uid to look for.
 205:      * @param string $tasklist The tasklist to look in.
 206:      *
 207:      * @return integer  The timestamp for the last modification of $uid.
 208:      */
 209:     public function modified($uid, $tasklist = null)
 210:     {
 211:         $modified = $this->getActionTimestamp($uid, 'modify', $tasklist);
 212:         if (empty($modified)) {
 213:             $modified = $this->getActionTimestamp($uid, 'add', $tasklist);
 214:         }
 215:         return $modified;
 216:     }
 217: 
 218:     /**
 219:      * Browse through Nag's object tree.
 220:      *
 221:      * @param string $path       The level of the tree to browse.
 222:      * @param array $properties  The item properties to return. Defaults to 'name',
 223:      *                           'icon', and 'browseable'.
 224:      *
 225:      * @return array  The contents of $path
 226:      */
 227:     public function browse($path = '', $properties = array())
 228:     {
 229:         global $registry;
 230: 
 231:         // Default properties.
 232:         if (!$properties) {
 233:             $properties = array('name', 'icon', 'browseable');
 234:         }
 235: 
 236:         if (substr($path, 0, 3) == 'nag') {
 237:             $path = substr($path, 3);
 238:         }
 239:         $path = trim($path, '/');
 240:         $parts = explode('/', $path);
 241: 
 242:         if (empty($path)) {
 243:             // This request is for a list of all users who have tasklists
 244:             // visible to the requesting user.
 245:             $tasklists = Nag::listTasklists(false, Horde_Perms::READ);
 246:             $owners = array();
 247:             foreach ($tasklists as $tasklist) {
 248:                 $owners[$tasklist->get('owner') ? $tasklist->get('owner') : '-system-'] = true;
 249:             }
 250: 
 251:             $results = array();
 252:             foreach (array_keys($owners) as $owner) {
 253:                 if (in_array('name', $properties)) {
 254:                     $results['nag/' . $owner]['name'] = $owner;
 255:                 }
 256:                 if (in_array('icon', $properties)) {
 257:                     $results['nag/' . $owner]['icon'] = Horde_Themes::img('user.png');
 258:                 }
 259:                 if (in_array('browseable', $properties)) {
 260:                     $results['nag/' . $owner]['browseable'] = true;
 261:                 }
 262:                 if (in_array('contenttype', $properties)) {
 263:                     $results['nag/' . $owner]['contenttype'] =
 264:                         'httpd/unix-directory';
 265:                 }
 266:                 if (in_array('contentlength', $properties)) {
 267:                     $results['nag/' . $owner]['contentlength'] = 0;
 268:                 }
 269:                 if (in_array('modified', $properties)) {
 270:                     $results['nag/' . $owner]['modified'] =
 271:                         $_SERVER['REQUEST_TIME'];
 272:                 }
 273:                 if (in_array('created', $properties)) {
 274:                     $results['nag/' . $owner]['created'] = 0;
 275:                 }
 276:             }
 277:             return $results;
 278: 
 279:         } elseif (count($parts) == 1) {
 280:             //
 281:             // This request is for all tasklists owned by the requested user
 282:             //
 283:             $tasklists = $GLOBALS['nag_shares']->listShares(
 284:                 $GLOBALS['registry']->getAuth(),
 285:                 array('perm' => Horde_Perms::SHOW,
 286:                       'attributes' => $parts[0]));
 287: 
 288:             // The last check returns all addressbooks for the requested user,
 289:             // but that does not mean the requesting user has access to them.
 290:             // Filter out those address books for which the requesting user has
 291:             // no access.
 292:             $tasklists = Nag::permissionsFilter($tasklists);
 293: 
 294:             $results = array();
 295:             foreach ($tasklists as $tasklistId => $tasklist) {
 296:                 $retpath = 'nag/' . $parts[0] . '/' . $tasklistId;
 297:                 if (in_array('name', $properties)) {
 298:                     $results[$retpath]['name'] = sprintf(_("Tasks from %s"), $tasklist->get('name'));
 299:                     $results[$retpath . '.ics']['name'] = $tasklist->get('name');
 300:                 }
 301:                 if (in_array('icon', $properties)) {
 302:                     $results[$retpath]['icon'] = Horde_Themes::img('nag.png');
 303:                     $results[$retpath . '.ics']['icon'] = Horde_Themes::img('mime/icalendar.png');
 304:                 }
 305:                 if (in_array('browseable', $properties)) {
 306:                     $results[$retpath]['browseable'] = $tasklist->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::READ);
 307:                     $results[$retpath . '.ics']['browseable'] = false;
 308:                 }
 309:                 if (in_array('contenttype', $properties)) {
 310:                     $results[$retpath]['contenttype'] = 'httpd/unix-directory';
 311:                     $results[$retpath . '.ics']['contenttype'] = 'text/calendar';
 312:                 }
 313:                 if (in_array('contentlength', $properties)) {
 314:                     $results[$retpath]['contentlength'] = 0;
 315:                     $results[$retpath . '.ics']['contentlength'] = strlen($this->exportTasklist($tasklistId, 'text/calendar'));
 316:                 }
 317:                 if (in_array('modified', $properties)) {
 318:                     // @TODO Find a way to get the actual modification times
 319:                     $results[$retpath]['modified'] = $_SERVER['REQUEST_TIME'];
 320:                     $results[$retpath . '.ics']['modified'] = $_SERVER['REQUEST_TIME'];
 321:                 }
 322:                 if (in_array('created', $properties)) {
 323:                     // @TODO Find a way to get the actual creation times
 324:                     $results[$retpath]['created'] = 0;
 325:                     $results[$retpath . '.ics']['created'] = 0;
 326:                 }
 327:             }
 328:             return $results;
 329: 
 330:         } elseif (count($parts) == 2 && substr($parts[1], -4) == '.ics') {
 331:             //
 332:             // This is a request for the entire tasklist in iCalendar format.
 333:             //
 334:             $tasklist = substr($parts[1], 0, -4);
 335:             if (!Nag::hasPermission($tasklist, Horde_Perms::READ)) {
 336:                 return PEAR::raiseError(_("Invalid tasklist file requested."), 404);
 337:             }
 338:             $ical_data = $this->exportTasklist($tasklist, 'text/calendar');
 339:             $result = array('data'          => $ical_data,
 340:                 'mimetype'      => 'text/calendar',
 341:                 'contentlength' => strlen($ical_data),
 342:                 'mtime'         => $_SERVER['REQUEST_TIME']);
 343: 
 344:             return $result;
 345: 
 346:         } elseif (count($parts) == 2) {
 347:             //
 348:             // This request is browsing into a specific tasklist.  Generate the list
 349:             // of items and represent them as files within the directory.
 350:             //
 351:             if (!Nag::hasPermission($parts[1], Horde_Perms::READ)) {
 352:                 return PEAR::raiseError(_("Invalid tasklist requested."), 404);
 353:             }
 354:             $storage = Nag_Driver::singleton($parts[1]);
 355:             try {
 356:                 $storage->retrieve();
 357:             } catch (Nag_Exception $e) {
 358:                  throw new Nag_Exception($e->getMessage, 500);
 359:             }
 360:             $icon = Horde_Themes::img('nag.png');
 361:             $results = array();
 362:             $storage->tasks->reset();
 363:             while ($task = $storage->tasks->each()) {
 364:                 $key = 'nag/' . $parts[0] . '/' . $parts[1] . '/' . $task->id;
 365:                 if (in_array('name', $properties)) {
 366:                     $results[$key]['name'] = $task->name;
 367:                 }
 368:                 if (in_array('icon', $properties)) {
 369:                     $results[$key]['icon'] = $icon;
 370:                 }
 371:                 if (in_array('browseable', $properties)) {
 372:                     $results[$key]['browseable'] = false;
 373:                 }
 374:                 if (in_array('contenttype', $properties)) {
 375:                     $results[$key]['contenttype'] = 'text/calendar';
 376:                 }
 377:                 if (in_array('contentlength', $properties)) {
 378:                     // FIXME:  This is a hack.  If the content length is longer
 379:                     // than the actual data then some WebDAV clients will report
 380:                     // an error when the file EOF is received.  Ideally we should
 381:                     // determine the actual size of the data and report it here, but
 382:                     // the performance hit may be prohibitive.  This requires
 383:                     // further investigation.
 384:                     $results[$key]['contentlength'] = 1;
 385:                 }
 386:                 if (in_array('modified', $properties)) {
 387:                     $results[$key]['modified'] = $this->modified($task->uid, $path);
 388:                 }
 389:                 if (in_array('created', $properties)) {
 390:                     $results[$key]['created'] = $this->getActionTimestamp($task->uid, 'add', $path);
 391:                 }
 392:             }
 393:             return $results;
 394:         } else {
 395:             //
 396:             // The only valid request left is for either a specific task item.
 397:             //
 398:             if (count($parts) == 3 &&
 399:                 Nag::hasPermission($parts[1], Horde_Perms::READ)) {
 400:                 //
 401:                 // This request is for a specific item within a given task list.
 402:                 //
 403:                 /* Create a Nag storage instance. */
 404:                 $storage = Nag_Driver::singleton($parts[1]);
 405:                 $storage->retrieve();
 406:                 try {
 407:                     $storage->get($parts[2]);
 408:                 } catch (Nag_Exception $e) {
 409:                     throw new Nag_Exception($e->getMessage(), 500);
 410:                 }
 411:                 $result = array(
 412:                     'data' => $this->export($task->uid, 'text/calendar'),
 413:                     'mimetype' => 'text/calendar');
 414:                 $modified = $this->modified($task->uid, $parts[1]);
 415:                 if (!empty($modified)) {
 416:                     $result['mtime'] = $modified;
 417:                 }
 418:                 return $result;
 419:             } elseif (count($parts) == 2 &&
 420:                 substr($parts[1], -4) == '.ics' &&
 421:                 Nag::hasPermission(substr($parts[1], 0, -4), Horde_Perms::READ)) {
 422: 
 423:                 // ??
 424: 
 425:             } else {
 426:                 //
 427:                 // All other requests are a 404: Not Found
 428:                 //
 429:                 return false;
 430:             }
 431:         }
 432:     }
 433: 
 434:     /**
 435:      * Saves a file into the Nag tree.
 436:      *
 437:      * @param string $path          The path where to PUT the file.
 438:      * @param string $content       The file content.
 439:      * @param string $content_type  The file's content type.
 440:      *
 441:      * @return array  The event UIDs
 442:      */
 443:     public function put($path, $content, $content_type)
 444:     {
 445:         if (substr($path, 0, 3) == 'nag') {
 446:             $path = substr($path, 3);
 447:         }
 448:         $path = trim($path, '/');
 449:         $parts = explode('/', $path);
 450: 
 451:         if (count($parts) == 2 &&
 452:             substr($parts[1], -4) == '.ics') {
 453: 
 454:                 // Workaround for WebDAV clients that are not smart enough to send
 455:                 // the right content type.  Assume text/calendar.
 456:                 if ($content_type == 'application/octet-stream') {
 457:                     $content_type = 'text/calendar';
 458:                 }
 459:                 $tasklist = substr($parts[1], 0, -4);
 460:             } elseif (count($parts) == 3) {
 461:                 $tasklist = $parts[1];
 462: 
 463:                 // Workaround for WebDAV clients that are not smart enough to send
 464:                 // the right content type.  Assume the same format we send individual
 465:                 // tasklist items: text/calendar
 466:                 if ($content_type == 'application/octet-stream') {
 467:                     $content_type = 'text/calendar';
 468:                 }
 469:             } else {
 470:                 throw new Nag_Exception(_("Invalid tasklist name supplied."), 403);
 471:             }
 472: 
 473:         if (!Nag::hasPermission($tasklist, Horde_Perms::EDIT)) {
 474:             // FIXME: Should we attempt to create a tasklist based on the filename
 475:             // in the case that the requested tasklist does not exist?
 476:             throw new Nag_Exception(_("Tasklist does not exist or no permission to edit"), 403);
 477:         }
 478: 
 479:         // Store all currently existings UIDs. Use this info to delete UIDs not
 480:         // present in $content after processing.
 481:         $ids = array();
 482:         $uids_remove = array_flip($this->listUids($tasklist));
 483: 
 484:         $storage = Nag_Driver::singleton($tasklist);
 485: 
 486:         switch ($content_type) {
 487:         case 'text/calendar':
 488:         case 'text/x-vcalendar':
 489:             $iCal = new Horde_Icalendar();
 490:             if (!($content instanceof Horde_Icalendar_Vtodo)) {
 491:                 if (!$iCal->parsevCalendar($content)) {
 492:                     throw new Nag_Exception(_("There was an error importing the iCalendar data."), 400);
 493:                 }
 494:             } else {
 495:                 $iCal->addComponent($content);
 496:             }
 497: 
 498:             foreach ($iCal->getComponents() as $content) {
 499:                 if (!($content instanceof Horde_Icalendar_Vtodo)) {
 500:                     continue;
 501:                 }
 502:                 $task = new Nag_Task();
 503:                 $task->fromiCalendar($content);
 504:                 $task->tasklist = $tasklist;
 505:                 $create = true;
 506:                 if (isset($task->uid)) {
 507:                     try {
 508:                         $existing = $storage->getByUID($task->uid);
 509:                         $create = false;
 510:                     } catch (Horde_Exception_NotFound $e) {
 511:                     }
 512:                 }
 513:                 if (!$create) {
 514:                     // Entry exists, remove from uids_remove list so we
 515:                     // won't delete in the end.
 516:                     if (isset($uids_remove[$task->uid])) {
 517:                         unset($uids_remove[$task->uid]);
 518:                     }
 519:                     if ($existing->private &&
 520:                         $existing->owner != $GLOBALS['registry']->getAuth()) {
 521:                         continue;
 522:                     }
 523:                     // Check if our task is newer then the existing - get
 524:                     // the task's history.
 525:                     $history = $GLOBALS['injector']->getInstance('Horde_History');
 526:                     $created = $modified = null;
 527:                     try {
 528:                         $log = $history->getHistory('nag:' . $tasklist . ':' . $task->uid);
 529:                         foreach ($log as $entry) {
 530:                             switch ($entry['action']) {
 531:                             case 'add':
 532:                                 $created = $entry['ts'];
 533:                                 break;
 534: 
 535:                             case 'modify':
 536:                                 $modified = $entry['ts'];
 537:                                 break;
 538:                             }
 539:                         }
 540:                     } catch (Exception $e) {}
 541:                     if (empty($modified) && !empty($add)) {
 542:                         $modified = $add;
 543:                     }
 544:                     if (!empty($modified) &&
 545:                         $modified >= $content->getAttribute('LAST-MODIFIED')) {
 546:                             // LAST-MODIFIED timestamp of existing entry
 547:                             // is newer: don't replace it.
 548:                             continue;
 549:                         }
 550: 
 551:                     // Don't change creator/owner.
 552:                     $owner = $existing->owner;
 553:                     $taskId = $existing->id;
 554:                     try {
 555:                         $storage->modify(
 556:                             $taskId,
 557:                             isset($task->name) ? $task->name : $existing->name,
 558:                             isset($task->desc) ? $task->desc : $existing->desc,
 559:                             isset($task->start) ? $task->start : $existing->start,
 560:                             isset($task->due) ? $task->due : $existing->due,
 561:                             isset($task->priority) ? $task->priority : $existing->priority,
 562:                             isset($task->estimate) ? $task->estimate : 0,
 563:                             isset($task->completed) ? (int)$task->completed : $existing->completed,
 564:                             isset($task->category) ? $task->category : $existing->category,
 565:                             isset($task->alarm) ? $task->alarm : $existing->alarm,
 566:                             isset($task->methods) ? $task->methods : $existing->methods,
 567:                             isset($task->parent_id) ? $task->parent_id : $existing->parent_id,
 568:                             isset($task->private) ? $task->private : $existing->private,
 569:                             $owner,
 570:                             isset($task->assignee) ? $task->assignee : $existing->assignee,
 571:                             isset($task->completed_date) ? $task->completed_date : $existing->completed_date
 572:                         );
 573:                     } catch (Nag_Exception $e) {
 574:                         throw new Nag_Exception($e->getMessage(), 500);
 575:                     }
 576:                     $ids[] = $task->uid;
 577:                 } else {
 578:                     try {
 579:                         $newTask = $storage->add(
 580:                             isset($task->name) ? $task->name : '',
 581:                             isset($task->desc) ? $task->desc : '',
 582:                             isset($task->start) ? $task->start : 0,
 583:                             isset($task->due) ? $task->due : 0,
 584:                             isset($task->priority) ? $task->priority : 3,
 585:                             isset($task->estimate) ? $task->estimate : 0,
 586:                             !empty($task->completed),
 587:                             isset($task->category) ? $task->category : '',
 588:                             isset($task->alarm) ? $task->alarm : 0,
 589:                             isset($task->methods) ? $task->methods : null,
 590:                             isset($task->uid) ? $task->uid : null,
 591:                             isset($task->parent_id) ? $task->parent_id : '',
 592:                             !empty($task->private),
 593:                             $GLOBALS['registry']->getAuth(),
 594:                             isset($task->assignee) ? $task->assignee : null);
 595:                     } catch (Nag_Exception $e) {
 596:                         throw new Nag_Exception($e->getMessage(), 500);
 597:                     }
 598:                     // use UID rather than ID
 599:                     $ids[] = $newTask[1];
 600:                 }
 601:             }
 602:             break;
 603: 
 604:         default:
 605:             throw new Nag_Exception(sprintf(_("Unsupported Content-Type: %s"), $content_type), 400);
 606:         }
 607: 
 608:         if (Nag::hasPermission($tasklist, Horde_Perms::DELETE)) {
 609:             foreach (array_keys($uids_remove) as $uid) {
 610:                 $this->delete($uid);
 611:             }
 612:         }
 613: 
 614:         return $ids;
 615:     }
 616: 
 617:     /**
 618:      * Deletes a file from the Nag tree.
 619:      *
 620:      * @param string $path  The path to the file.
 621:      *
 622:      * @return string  The event's UID
 623:      * @throws Nag_Exception
 624:      */
 625:     public function path_delete($path)
 626:     {
 627:         if (substr($path, 0, 3) == 'nag') {
 628:             $path = substr($path, 3);
 629:         }
 630:         $path = trim($path, '/');
 631:         $parts = explode('/', $path);
 632: 
 633:         if (count($parts) == 2) {
 634:             // @TODO Deny deleting of the entire tasklist for now.
 635:             // Allow users to delete tasklists but not create them via WebDAV will
 636:             // be more confusing than helpful.  They are, however, still able to
 637:             // delete individual task items within the tasklist folder.
 638:             throw Nag_Exception(_("Deleting entire tasklists is not supported."), 403);
 639:             // To re-enable the functionality just remove this if {} block.
 640:         }
 641: 
 642:         if (substr($parts[1], -4) == '.ics') {
 643:             $tasklistID = substr($parts[1], 0, -4);
 644:         } else {
 645:             $tasklistID = $parts[1];
 646:         }
 647: 
 648:         if (!(count($parts) == 2 || count($parts) == 3) ||
 649:             !Nag::hasPermission($tasklistID, Horde_Perms::DELETE)) {
 650: 
 651:             throw new Nag_Exception(_("Tasklist does not exist or no permission to delete"), 403);
 652:         }
 653: 
 654:         /* Create a Nag storage instance. */
 655:         try {
 656:             $storage = Nag_Driver::singleton($tasklistID);
 657:             $storage->retrieve();
 658:         } catch (Nag_Exception $e) {
 659:             throw new Nag_Exception(sprintf(_("Connection failed: %s"), $e->getMessage()), 500);
 660:         }
 661:         if (count($parts) == 3) {
 662:             // Delete just a single entry
 663:             return $storage->delete($parts[2]);
 664:         } else {
 665:             // Delete the entire task list
 666:             try {
 667:                 $storage->deleteAll();
 668:             } catch (Nag_Exception $e) {
 669:                 throw new Nag_Exception(sprintf(_("Unable to delete tasklist \"%s\": %s"), $tasklistID, $e->getMessage()), 500);
 670:             }
 671: 
 672:             // Remove share and all groups/permissions.
 673:             $share = $GLOBALS['nag_shares']->getShare($tasklistID);
 674:             try {
 675:                 $GLOBALS['nag_shares']->removeShare($share);
 676:             } catch (Horde_Share_Exception $e) {
 677:                 throw new Nag_Exception($e->getMessage());
 678:             }
 679:         }
 680:     }
 681: 
 682:     /**
 683:      * Returns an array of UIDs for all tasks that the current user is authorized
 684:      * to see.
 685:      *
 686:      * @param mixed $tasklists  The tasklist or an array of taskslists to list.
 687:      *
 688:      * @return array             An array of UIDs for all tasks
 689:      *                           the user can access.
 690:      *
 691:      * @throws Horde_Exception_PermissionDenied
 692:      * @throws Nag_Exception
 693:      */
 694:     public function listUids($tasklists = null)
 695:     {
 696:         if (!isset($GLOBALS['conf']['storage']['driver'])) {
 697:             throw new Nag_Exception(_("Not configured"));
 698:         }
 699: 
 700:         if (empty($tasklists)) {
 701:             $tasklists = Nag::getSyncLists();
 702:         } else {
 703:             if (!is_array($tasklists)) {
 704:                 $tasklists = array($tasklists);
 705:             }
 706:             foreach ($tasklists as $list) {
 707:                 if (!Nag::hasPermission($list, Horde_Perms::READ)) {
 708:                     throw new Horde_Exception_PermissionDenied();
 709:                 }
 710:             }
 711:         }
 712: 
 713:         $tasks = Nag::listTasks(null, null, null, $tasklists, 1);
 714:         $uids = array();
 715:         $tasks->reset();
 716:         while ($task = $tasks->each()) {
 717:             $uids[] = $task->uid;
 718:         }
 719: 
 720:         return $uids;
 721:     }
 722: 
 723:     /**
 724:      * Returns an array of UIDs for tasks that have had $action happen since
 725:      * $timestamp.
 726:      *
 727:      * @param string  $action     The action to check for - add, modify, or delete.
 728:      * @param integer $timestamp  The time to start the search.
 729:      * @param mixed   $tasklists  The tasklists to be used. If 'null', the
 730:      *                            user's default tasklist will be used.
 731:      * @param integer $end        The optional ending timestamp.
 732:      *
 733:      * @return array  An array of UIDs matching the action and time criteria.
 734:      *
 735:      * @throws Horde_History_Exception
 736:      * @throws InvalidArgumentException
 737:      */
 738:     public function listBy($action, $timestamp, $tasklist = null, $end = null)
 739:     {
 740:         if (empty($tasklist)) {
 741:             $tasklist = Nag::getSyncLists();
 742:             $results = array();
 743:             foreach ($tasklist as $list) {
 744:                 $results = array_merge($results, $this->listBy($action, $timestamp, $list, $end));
 745:             }
 746:             return $results;
 747:         }
 748: 
 749:         $filter = array(array('op' => '=', 'field' => 'action', 'value' => $action));
 750:         if (!empty($end)) {
 751:             $filter[] = array('op' => '<', 'field' => 'ts', 'value' => $end);
 752:         }
 753:         $histories = $GLOBALS['injector']
 754:             ->getInstance('Horde_History')
 755:             ->getByTimestamp('>', $timestamp, $filter, 'nag:' . $tasklist);
 756: 
 757:         // Strip leading nag:username:.
 758:         return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));
 759:     }
 760: 
 761:     /**
 762:      * Method for obtaining all server changes between two timestamps. Basically
 763:      * a wrapper around listBy(), but returns an array containing all adds,
 764:      * edits and deletions.
 765:      *
 766:      * @param integer $start             The starting timestamp
 767:      * @param integer $end               The ending timestamp.
 768:      *
 769:      * @return array  An hash with 'add', 'modify' and 'delete' arrays.
 770:      */
 771:     public function getChanges($start, $end)
 772:     {
 773:         return array(
 774:             'add' => $this->listBy('add', $start, null, $end),
 775:             'modify' => $this->listBy('modify', $start, null, $end),
 776:             'delete' => $this->listBy('delete', $start, null, $end));
 777:     }
 778: 
 779:     /**
 780:      * Returns the timestamp of an operation for a given uid an action.
 781:      *
 782:      * @param string $uid      The uid to look for.
 783:      * @param string $action   The action to check for - add, modify, or delete.
 784:      * @param string $tasklist The tasklist to be used. If 'null', the
 785:      *                         user's default tasklist will be used.
 786:      *
 787:      * @return integer  The timestamp for this action.
 788:      *
 789:      * @throws InvalidArgumentException
 790:      * @throws Horde_Exception_PermissionDenied
 791:      * @thorws Horde_History_Exception
 792:      */
 793:     public function getActionTimestamp($uid, $action, $tasklist = null)
 794:     {
 795:         if ($tasklist === null) {
 796:             $tasklist = Nag::getDefaultTasklist(Horde_Perms::READ);
 797:         } elseif (!Nag::hasPermission($tasklist, Horde_Perms::READ)) {
 798:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
 799:         }
 800: 
 801:         return $GLOBALS['injector']
 802:             ->getInstance('Horde_History')
 803:             ->getActionTimestamp('nag:' . $tasklist . ':' . $uid, $action);
 804:     }
 805: 
 806:     /**
 807:      * Imports one or more tasks represented in the specified content type.
 808:      *
 809:      * If a UID is present in the content and the task is already in the
 810:      * database, a replace is performed rather than an add.
 811:      *
 812:      * @param string $content      The content of the task.
 813:      * @param string $contentType  What format is the data in? Currently supports:
 814:      *                             text/calendar
 815:      *                             text/x-vcalendar
 816:      * @param string $tasklist     The tasklist into which the task will be
 817:      *                             imported.  If 'null', the user's default
 818:      *                             tasklist will be used.
 819:      *
 820:      * @return string  The new UID on one import, an array of UIDs on multiple imports,
 821:      */
 822:     public function import($content, $contentType, $tasklist = null)
 823:     {
 824:         if ($tasklist === null) {
 825:             $tasklist = Nag::getDefaultTasklist(Horde_Perms::EDIT);
 826:         } elseif (!Nag::hasPermission($tasklist, Horde_Perms::EDIT)) {
 827:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
 828:         }
 829: 
 830:         /* Create a Nag_Driver instance. */
 831:         $storage = Nag_Driver::singleton($tasklist);
 832: 
 833:         switch ($contentType) {
 834:         case 'text/x-vcalendar':
 835:         case 'text/calendar':
 836:         case 'text/x-vtodo':
 837:             $iCal = new Horde_Icalendar();
 838:             if (!($content instanceof Horde_Icalendar_Vtodo)) {
 839:                 if (!$iCal->parsevCalendar($content)) {
 840:                     throw new Nag_Exception(_("There was an error importing the iCalendar data."));
 841:                 }
 842:             } else {
 843:                 $iCal->addComponent($content);
 844:             }
 845: 
 846:             $components = $iCal->getComponents();
 847:             if (count($components) == 0) {
 848:                 throw new Nag_Exception(_("No iCalendar data was found."));
 849:             }
 850: 
 851:             $ids = array();
 852:             foreach ($components as $content) {
 853:                 if ($content instanceof Horde_Icalendar_Vtodo) {
 854:                     $task = new Nag_Task();
 855:                     $task->fromiCalendar($content);
 856:                     if (isset($task->uid)) {
 857:                         $existing = $storage->getByUID($task->uid);
 858:                         $taskId = $existing->id;
 859:                         $result = $storage->modify(
 860:                             $taskId,
 861:                             isset($task->name) ? $task->name : $existing->name,
 862:                             isset($task->desc) ? $task->desc : $existing->desc,
 863:                             isset($task->start) ? $task->start : $existing->start,
 864:                             isset($task->due) ? $task->due : $existing->due,
 865:                             isset($task->priority) ? $task->priority : $existing->priority,
 866:                             isset($task->estimate) ? $task->estimate : 0,
 867:                             isset($task->completed) ? (int)$task->completed : $existing->completed,
 868:                             isset($task->category) ? $task->category : $existing->category,
 869:                             isset($task->alarm) ? $task->alarm : $existing->alarm,
 870:                             isset($task->parent_id) ? $task->parent_id : $existing->parent_id,
 871:                             isset($task->private) ? $task->private : $existing->private,
 872:                             isset($task->owner) ? $task->owner : $existing->owner,
 873:                             isset($task->assignee) ? $task->assignee : $existing->assignee
 874:                         );
 875:                         $ids[] = $task->uid;
 876:                     } else {
 877:                         $newTask = $storage->add(
 878:                             isset($task->name) ? $task->name : '',
 879:                             isset($task->desc) ? $task->desc : '',
 880:                             isset($task->start) ? $task->start : 0,
 881:                             isset($task->due) ? $task->due : 0,
 882:                             isset($task->priority) ? $task->priority : 3,
 883:                             isset($task->estimate) ? $task->estimate : 0,
 884:                             !empty($task->completed),
 885:                             isset($task->category) ? $task->category : '',
 886:                             isset($task->alarm) ? $task->alarm : 0,
 887:                             isset($task->methods) ? $task->methods : null,
 888:                             isset($task->uid) ? $task->uid : null,
 889:                             isset($task->parent_id) ? $task->parent_id : '',
 890:                             !empty($task->private),
 891:                             $GLOBALS['registry']->getAuth(),
 892:                             isset($task->assignee) ? $task->assignee : null
 893:                         );
 894:                         // use UID rather than ID
 895:                         $ids[] = $newTask[1];
 896:                     }
 897:                 }
 898:             }
 899:             if (count($ids) == 0) {
 900:                 throw Nag_Exception(_("No iCalendar data was found."));
 901:             } else if (count($ids) == 1) {
 902:                 return $ids[0];
 903:             }
 904:             return $ids;
 905: 
 906:         case 'activesync':
 907:             $task = new Nag_Task();
 908:             $task->fromASTask($content);
 909:             $results = $storage->add(
 910:                 isset($task->name) ? $task->name : '',
 911:                 isset($task->desc) ? $task->desc : '',
 912:                 isset($task->start) ? $task->start : 0,
 913:                 isset($task->due) ? $task->due : 0,
 914:                 isset($task->priority) ? $task->priority : 3,
 915:                 isset($task->estimate) ? $task->estimate : 0,
 916:                 !empty($task->completed),
 917:                 isset($task->category) ? $task->category : '',
 918:                 isset($task->alarm) ? $task->alarm : 0,
 919:                 isset($task->methods) ? $task->methods : null,
 920:                 isset($task->uid) ? $task->uid : null,
 921:                 isset($task->parent_id) ? $task->parent_id : '',
 922:                 !empty($task->private),
 923:                 $GLOBALS['registry']->getAuth(),
 924:                 isset($task->assignee) ? $task->assignee : null
 925:             );
 926: 
 927:             /* array index 0 is id, 1 is uid */
 928:             return $results[1];
 929:         }
 930: 
 931:         throw new Nag_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
 932:     }
 933: 
 934:     /**
 935:      * Adds a task.
 936:      *
 937:      * @param array $task  A hash with overwriting task information.
 938:      *
 939:      * @throws Horde_Exception_PermissionDenied
 940:      */
 941:     public function addTask(array $task)
 942:     {
 943:         if (!$GLOBALS['registry']->isAdmin() &&
 944:             !Nag::hasPermission($task['tasklist'], Horde_Perms::EDIT)) {
 945:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
 946:         }
 947: 
 948:         $storage = Nag_Driver::singleton($task['tasklist']);
 949:         return $storage->add(
 950:             isset($task['name']) ? $task['name'] : '',
 951:             isset($task['desc']) ? $task['desc'] : '',
 952:             isset($task['start']) ? $task['start'] : 0,
 953:             isset($task['due']) ? $task['due'] : 0,
 954:             isset($task['priority']) ? $task['priority'] : 3,
 955:             isset($task['estimate']) ? $task['estimate'] : 0,
 956:             !empty($task['completed']),
 957:             isset($task['category']) ? $task['category'] : '',
 958:             isset($task['alarm']) ? $task['alarm'] : 0,
 959:             isset($task['methods']) ? $task['methods'] : null,
 960:             isset($task['uid']) ? $task['uid'] : null,
 961:             isset($task['parent_id']) ? $task['parent_id'] : '',
 962:             !empty($task['private']),
 963:             $GLOBALS['registry']->getAuth(),
 964:             isset($task['assignee']) ? $task['assignee'] : null
 965:         );
 966:     }
 967: 
 968:     /**
 969:      * Imports one or more tasks parsed from a string.
 970:      *
 971:      * @param string $text      The text to parse into
 972:      * @param string $tasklist  The tasklist into which the task will be
 973:      *                          imported.  If 'null', the user's default
 974:      *                          tasklist will be used.
 975:      *
 976:      * @return array  The UIDs of all tasks that were added.
 977:      * @throws Horde_Exception_PermissionDenied
 978:      */
 979:     public function quickAdd($text, $tasklist = null)
 980:     {
 981:         if ($tasklist === null) {
 982:             $tasklist = Nag::getDefaultTasklist(Horde_Perms::EDIT);
 983:         } elseif (!Nag::hasPermission($tasklist, Horde_Perms::EDIT)) {
 984:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
 985:         }
 986: 
 987:         return Nag::createTasksFromText($text, $tasklist);
 988:     }
 989: 
 990:     /**
 991:      * Toggles the task completion flag.
 992:      *
 993:      * @param string $task_id      The task ID.
 994:      * @param string $tasklist_id  The tasklist that contains the task.
 995:      */
 996:     public function toggleCompletion($task_id, $tasklist_id)
 997:     {
 998:         if (!Nag::hasPermission($tasklist_id, Horde_Perms::EDIT)) {
 999:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
1000:         }
1001: 
1002:         try {
1003:             $share = $GLOBALS['nag_shares']->getShare($tasklist_id);
1004:         } catch (Horde_Share_Exception $e) {
1005:             Horde::logMessage($e->getMessage(), 'ERR');
1006:             throw new Nag_Exception($e);
1007:         }
1008:         $task = Nag::getTask($tasklist_id, $task_id);
1009:         $task->completed = !$task->completed;
1010:         if ($task->completed) {
1011:             $task->completed_date = time();
1012:         } else {
1013:             $task->completed_date = null;
1014:         }
1015: 
1016:         return $task->save();
1017:     }
1018: 
1019:     /**
1020:      * Exports a task, identified by UID, in the requested content type.
1021:      *
1022:      * @param string $uid          Identify the task to export.
1023:      * @param string $contentType  What format should the data be in?
1024:      *                             A string with one of:
1025:      * <pre>
1026:      * text/calendar    - (VCALENDAR 2.0. Recommended as this is specified in
1027:      *                    rfc2445)
1028:      * text/x-vcalendar - (old VCALENDAR 1.0 format. Still in wide use)
1029:      * </pre>
1030:      *
1031:      * @return string  The requested data.
1032:      */
1033:     public function export($uid, $contentType)
1034:     {
1035:         $storage = Nag_Driver::singleton();
1036:         $task = $storage->getByUID($uid);
1037:         if (!Nag::hasPermission($task->tasklist, Horde_Perms::READ)) {
1038:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
1039:         }
1040: 
1041:         $version = '2.0';
1042:         switch ($contentType) {
1043:         case 'text/x-vcalendar':
1044:             $version = '1.0';
1045:         case 'text/calendar':
1046:             // Create the new iCalendar container.
1047:             $iCal = new Horde_Icalendar($version);
1048:             $iCal->setAttribute('PRODID', '-//The Horde Project//Nag ' . $GLOBALS['registry']->getVersion() . '//EN');
1049:             $iCal->setAttribute('METHOD', 'PUBLISH');
1050: 
1051:             // Create new vTodo object.
1052:             $vTodo = $task->toiCalendar($iCal);
1053:             $vTodo->setAttribute('VERSION', $version);
1054: 
1055:             $iCal->addComponent($vTodo);
1056: 
1057:             return $iCal->exportvCalendar();
1058:         case 'activesync':
1059:             return $task->toASTask();
1060:         default:
1061:             throw new Nag_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
1062:         }
1063:     }
1064: 
1065:     /**
1066:      * Returns a task object.
1067:      *
1068:      * @param string $tasklist  A tasklist id.
1069:      * @param string $id        A task id.
1070:      *
1071:      * @return Nag_Task  The matching task object.
1072:      */
1073:     public function getTask($tasklist, $id)
1074:     {
1075:         if (!Nag::hasPermission($tasklist, Horde_Perms::READ)) {
1076:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
1077:         }
1078: 
1079:         return Nag::getTask($tasklist, $id);
1080:     }
1081: 
1082:     /**
1083:      * Exports a tasklist in the requested content type.
1084:      *
1085:      * @param string $tasklist     The tasklist to export.
1086:      * @param string $contentType  What format should the data be in?
1087:      *                             A string with one of:
1088:      *                             <pre>
1089:      *                             text/calendar (VCALENDAR 2.0. Recommended as
1090:      *                                            this is specified in rfc2445)
1091:      *                             text/x-vcalendar (old VCALENDAR 1.0 format.
1092:      *                                              Still in wide use)
1093:      *                             </pre>
1094:      *
1095:      * @return string  The iCalendar representation of the tasklist.
1096:      */
1097:     public function exportTasklist($tasklist, $contentType)
1098:     {
1099:         if (!Nag::hasPermission($tasklist, Horde_Perms::READ)) {
1100:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
1101:         }
1102: 
1103:         $tasks = Nag::listTasks(null, null, null, array($tasklist), 1);
1104: 
1105:         $version = '2.0';
1106:         switch ($contentType) {
1107:         case 'text/x-vcalendar':
1108:             $version = '1.0';
1109:         case 'text/calendar':
1110:             $share = $GLOBALS['nag_shares']->getShare($tasklist);
1111: 
1112:             $iCal = new Horde_Icalendar($version);
1113:             $iCal->setAttribute('X-WR-CALNAME', $share->get('name'));
1114: 
1115:             $tasks->reset();
1116:             while ($task = $tasks->each()) {
1117:                 $iCal->addComponent($task->toiCalendar($iCal));
1118:             }
1119: 
1120:             return $iCal->exportvCalendar();
1121:         }
1122: 
1123:         throw new Nag_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
1124: 
1125:     }
1126: 
1127:     /**
1128:      * Deletes a task identified by UID.
1129:      *
1130:      * @param string|array $uid  Identify the task to delete, either a single UID
1131:      *                           or an array.
1132:      *
1133:      * @return boolean  Success or failure.
1134:      */
1135:     public function delete($uid)
1136:     {
1137:         // Handle an arrray of UIDs for convenience
1138:         if (is_array($uid)) {
1139:             foreach ($uid as $g) {
1140:                 $result = $this->delete($g);
1141:             }
1142: 
1143:             return true;
1144:         }
1145: 
1146:         $storage = Nag_Driver::singleton();
1147:         $task = $storage->getByUID($uid);
1148: 
1149:         if (!$GLOBALS['registry']->isAdmin() &&
1150:             !Nag::hasPermission($task->tasklist, Horde_Perms::DELETE)) {
1151: 
1152:              throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
1153:         }
1154: 
1155:         return $storage->delete($task->id);
1156:     }
1157: 
1158:     /**
1159:      * Deletes a task identified by tasklist and ID.
1160:      *
1161:      * @param string $tasklist  A tasklist id.
1162:      * @param string $id        A task id.
1163:      */
1164:     public function deleteTask($tasklist, $id)
1165:     {
1166:         if (!$GLOBALS['registry']->isAdmin() &&
1167:             !Nag::hasPermission($tasklist, Horde_Perms::DELETE)) {
1168: 
1169:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
1170:         }
1171: 
1172:         $storage = Nag_Driver::singleton($tasklist);
1173:         return $storage->delete($id);
1174:     }
1175: 
1176:     /**
1177:      * Replaces the task identified by UID with the content represented in the
1178:      * specified content type.
1179:      *
1180:      * If you want to replace multiple tasks with the UID specified in the
1181:      * VCALENDAR data, you may use $this->import instead. This automatically does a
1182:      * replace if existings UIDs are found.
1183:      *
1184:      *
1185:      * @param string $uid          Identify the task to replace.
1186:      * @param string $content      The content of the task.
1187:      * @param string $contentType  What format is the data in? Currently supports:
1188:      *                             - text/x-vcalendar
1189:      *                             - text/calendar
1190:      *
1191:      * @return boolean  Success or failure.
1192:      */
1193:     public function replace($uid, $content, $contentType)
1194:     {
1195:         $storage = Nag_Driver::singleton();
1196:         $existing = $storage->getByUID($uid);
1197:         $taskId = $existing->id;
1198:         if (!Nag::hasPermission($existing->tasklist, Horde_Perms::EDIT)) {
1199:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
1200:         }
1201: 
1202:         switch ($contentType) {
1203:         case 'text/calendar':
1204:         case 'text/x-vcalendar':
1205:             if (!($content instanceof Horde_Icalendar_Vtodo)) {
1206:                 $iCal = new Horde_Icalendar();
1207:                 if (!$iCal->parsevCalendar($content)) {
1208:                     throw new Nag_Exception(_("There was an error importing the iCalendar data."));
1209:                 }
1210: 
1211:                 $components = $iCal->getComponents();
1212:                 $component = null;
1213:                 foreach ($components as $content) {
1214:                     if ($content instanceof Horde_Icalendar_Vtodo) {
1215:                         if ($component !== null) {
1216:                             throw new Nag_Exception(_("Multiple iCalendar components found; only one vTodo is supported."));
1217:                         }
1218:                         $component = $content;
1219:                     }
1220: 
1221:                 }
1222:                 if ($component === null) {
1223:                     throw new Nag_Exception(_("No iCalendar data was found."));
1224:                 }
1225:             }
1226: 
1227:             $task = new Nag_Task();
1228:             $task->fromiCalendar($content);
1229:             $result = $storage->modify(
1230:                 $taskId,
1231:                 isset($task->name) ? $task->name : $existing->name,
1232:                 isset($task->desc) ? $task->desc : $existing->desc,
1233:                 isset($task->start) ? $task->start : $existing->start,
1234:                 isset($task->due) ? $task->due : $existing->due,
1235:                 isset($task->priority) ? $task->priority : $existing->priority,
1236:                 isset($task->estimate) ? $task->estimate : 0,
1237:                 isset($task->completed) ? (int)$task->completed : $existing->completed,
1238:                 isset($task->category) ? $task->category : $existing->category,
1239:                 isset($task->alarm) ? $task->alarm : $existing->alarm,
1240:                 isset($task->parent_id) ? $task->parent_id : $existing->parent_id,
1241:                 isset($task->private) ? $task->private : $existing->private,
1242:                 isset($task->owner) ? $task->owner : $existing->owner,
1243:                 isset($task->assignee) ? $task->assignee : $existing->assignee
1244:             );
1245:             break;
1246: 
1247:         case 'activesync':
1248:             $task = new Nag_Task();
1249:             $task->fromASTask($content);
1250:             $result = $storage->modify(
1251:                 $taskId,
1252:                 isset($task->name) ? $task->name : $existing->name,
1253:                 isset($task->desc) ? $task->desc : $existing->desc,
1254:                 isset($task->start) ? $task->start : $existing->start,
1255:                 isset($task->due) ? $task->due : $existing->due,
1256:                 isset($task->priority) ? $task->priority : $existing->priority,
1257:                 isset($task->estimate) ? $task->estimate : 0,
1258:                 isset($task->completed) ? (int)$task->completed : $existing->completed,
1259:                 isset($task->category) ? $task->category : $existing->category,
1260:                 isset($task->alarm) ? $task->alarm : $existing->alarm,
1261:                 isset($task->parent_id) ? $task->parent_id : $existing->parent_id,
1262:                 isset($task->private) ? $task->private : $existing->private,
1263:                 isset($task->owner) ? $task->owner : $existing->owner,
1264:                 isset($task->assignee) ? $task->assignee : $existing->assignee
1265:             );
1266:             break;
1267:         default:
1268:             throw new Nag_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
1269:         }
1270: 
1271:         return $result;
1272:     }
1273: 
1274:     /**
1275:      * Changes a task identified by tasklist and ID.
1276:      *
1277:      * @param string $tasklist  A tasklist id.
1278:      * @param string $id        A task id.
1279:      * @param array $task       A hash with overwriting task information.
1280:      */
1281:     public function updateTask($tasklist, $id, $task)
1282:     {
1283:         if (!$GLOBALS['registry']->isAdmin() &&
1284:             !Nag::hasPermission($tasklist, Horde_Perms::EDIT)) {
1285:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
1286:         }
1287: 
1288:         $storage = Nag_Driver::singleton($tasklist);
1289:         $existing = $storage->get($id);
1290: 
1291:         return $storage->modify(
1292:             $id,
1293:             isset($task['name']) ? $task['name'] : $existing->name,
1294:             isset($task['desc']) ? $task['desc'] : $existing->desc,
1295:             isset($task['start']) ? $task['start'] : $existing->start,
1296:             isset($task['due']) ? $task['due'] : $existing->due,
1297:             isset($task['priority']) ? $task['priority'] : $existing->priority,
1298:             isset($task['estimate']) ? $task['estimate'] : $existing->estimate,
1299:             isset($task['completed']) ? (int)$task['completed'] : $existing->completed,
1300:             isset($task['category']) ? $task['category'] : $existing->category,
1301:             isset($task['alarm']) ? $task['alarm'] : $existing->alarm,
1302:             isset($task['methods']) ? $task['methods'] : $existing->methods,
1303:             isset($task['parent_id']) ? $task['parent_id'] : $existing->parent_id,
1304:             isset($task['private']) ? $task['private'] : $existing->private,
1305:             isset($task['owner']) ? $task['owner'] : $existing->owner,
1306:             isset($task['assignee']) ? $task['assignee'] : $existing->assignee,
1307:             $task['completed'] && !$existing->completed ? date() : $existing->completed_date,
1308:             isset($task['tasklist']) ? $task['tasklist'] : $existing->tasklist
1309:         );
1310:     }
1311: 
1312:     /**
1313:      * Lists active tasks as cost objects.
1314:      *
1315:      * @todo Implement $criteria parameter.
1316:      *
1317:      * @param array $criteria   Filter attributes
1318:      */
1319:     public function listCostObjects($criteria)
1320:     {
1321:         $tasks = Nag::listTasks(null, null, null, null, 1);
1322:         $result = array();
1323:         $tasks->reset();
1324:         while ($task = $tasks->each()) {
1325:             $result[$task->id] = array('id' => $task->id,
1326:                 'active' => !$task->completed,
1327:                 'name' => $task->name);
1328:             if (!empty($task->estimate)) {
1329:                 $result[$task->id]['estimate'] = $task->estimate;
1330:             }
1331:         }
1332: 
1333:         if (count($result) == 0) {
1334:             return array();
1335:         } else {
1336:             return array(array('category' => _("Tasks"),
1337:                 'objects'  => array_values($result)));
1338:         }
1339:     }
1340: 
1341:     public function listTimeObjectCategories()
1342:     {
1343:         $categories = array();
1344:         $tasklists = Nag::listTasklists(false, Horde_Perms::SHOW | Horde_Perms::READ);
1345:         foreach ($tasklists as $tasklistId => $tasklist) {
1346:             $categories[$tasklistId] = $tasklist->get('name');
1347:         }
1348:         return $categories;
1349:     }
1350: 
1351:     /**
1352:      * Lists active tasks as time objects.
1353:      *
1354:      * @param array $categories  The time categories (from
1355:      *                           listTimeObjectCategories) to list.
1356:      * @param mixed $start       The start date of the period.
1357:      * @param mixed $end         The end date of the period.
1358:      */
1359:     public function listTimeObjects($categories, $start, $end)
1360:     {
1361:         $allowed_tasklists = Nag::listTasklists(false, Horde_Perms::READ);
1362:         foreach ($categories as $tasklist) {
1363:             if (!Nag::hasPermission($tasklist, Horde_Perms::READ)) {
1364:                 return PEAR::raiseError(_("Permission Denied"));
1365:             }
1366:         }
1367: 
1368:         $timeobjects = array();
1369:         $start = new Horde_Date($start);
1370:         $start_ts = $start->timestamp();
1371:         $end = new Horde_Date($end);
1372:         $end_ts = $end->timestamp();
1373: 
1374:         // List incomplete tasks.
1375:         $tasks = Nag::listTasks(null, null, null, $categories, 0);
1376:         $tasks->reset();
1377:         while ($task = $tasks->each()) {
1378:             // If there's no due date, it's not a time object.
1379:             if (!$task->due || $task->due + 1 < $start_ts || $task->due > $end_ts) {
1380:                 continue;
1381:             }
1382:             $due_date = date('Y-m-d\TH:i:s', $task->due);
1383:             $timeobjects[$task->id] = array(
1384:                 'id' => $task->id,
1385:                 'title' => $task->name,
1386:                 'description' => $task->desc,
1387:                 'start' => $due_date,
1388:                 'end' => $due_date,
1389:                 'category' => $task->category,
1390:                 'color' => $allowed_tasklists[$task->tasklist]->get('color'),
1391:                 'owner' => $allowed_tasklists[$task->tasklist]->get('owner'),
1392:                 'permissions' => $GLOBALS['nag_shares']->getPermissions($task->tasklist, $GLOBALS['registry']->getAuth()),
1393:                 'variable_length' => false,
1394:                 'params' => array(
1395:                     'task' => $task->id,
1396:                     'tasklist' => $task->tasklist,
1397:                 ),
1398:                 'link' => Horde::url('view.php', true)->add(array('tasklist' => $task->tasklist, 'task' => $task->id)),
1399:                 'edit_link' => Horde::url('task.php', true)->add(array('tasklist' => $task->tasklist, 'task' => $task->id, 'actionID' => 'modify_task')),
1400:                 'delete_link' => Horde::url('task.php', true)->add(array('tasklist' => $task->tasklist, 'task' => $task->id, 'actionID' => 'delete_task')),
1401:                 'ajax_link' => 'task:' . $task->tasklist . ':' . $task->id,
1402:             );
1403:         }
1404: 
1405:         return $timeobjects;
1406:     }
1407: 
1408:     /**
1409:      * Saves properties of a time object back to the task that it represents.
1410:      *
1411:      * At the moment only the title, description and due date are saved.
1412:      *
1413:      * @param array $timeobject  A time object hash.
1414:      * @throws Nag_Exception
1415:      */
1416:     public function saveTimeObject(array $timeobject)
1417:     {
1418:         $storage = Nag_Driver::singleton();
1419:         $existing = $storage->get($timeobject['id']);
1420:         if (!Nag::hasPermission($existing->tasklist, Horde_Perms::EDIT)) {
1421:             throw new Horde_Exception_PermissionDenied(_("Permission Denied"));
1422:         }
1423:         $storage = Nag_Driver::singleton($existing->tasklist);
1424:         if (isset($timeobject['start'])) {
1425:             $due = new Horde_Date($timeobject['start']);
1426:             $due = $due->timestamp();
1427:         } else {
1428:             $due = $existing->due;
1429:         }
1430: 
1431:         return $storage->modify(
1432:             $timeobject['id'],
1433:             isset($timeobject['title']) ? $timeobject['title'] : $existing->name,
1434:             isset($timeobject['description']) ? $timeobject['description'] : $existing->desc,
1435:             $existing->start,
1436:             $due,
1437:             $existing->priority,
1438:             $existing->estimate,
1439:             $existing->completed,
1440:             $existing->category,
1441:             $existing->alarm,
1442:             $existing->methods,
1443:             $existing->parent_id,
1444:             $existing->private,
1445:             $existing->owner,
1446:             $existing->assignee,
1447:             $existing->completed_date,
1448:             $existing->tasklist
1449:         );
1450:     }
1451: 
1452: }
1453: 
API documentation generated by ApiGen