1<?php 2/** 3 * Nag application API. 4 * 5 * This file defines Horde's core API interface. Other core Horde libraries 6 * can interact with Horde through this API. 7 * 8 * See the enclosed file COPYING for license information (GPL). If you 9 * did not receive this file, see http://www.horde.org/licenses/gpl. 10 * 11 * @package Nag 12 */ 13 14/* Determine the base directories. */ 15if (!defined('NAG_BASE')) { 16 define('NAG_BASE', realpath(__DIR__ . '/..')); 17} 18 19if (!defined('HORDE_BASE')) { 20 /* If Horde does not live directly under the app directory, the HORDE_BASE 21 * constant should be defined in config/horde.local.php. */ 22 if (file_exists(NAG_BASE . '/config/horde.local.php')) { 23 include NAG_BASE . '/config/horde.local.php'; 24 } else { 25 define('HORDE_BASE', realpath(NAG_BASE . '/..')); 26 } 27} 28 29/* Load the Horde Framework core (needed to autoload 30 * Horde_Registry_Application::). */ 31require_once HORDE_BASE . '/lib/core.php'; 32 33use Sabre\CalDAV; 34 35class Nag_Application extends Horde_Registry_Application 36{ 37 /** 38 */ 39 public $features = array( 40 'smartmobileView' => true, 41 'modseq' => true, 42 ); 43 44 /** 45 */ 46 public $version = 'H5 (4.2.19)'; 47 48 /** 49 * Global variables defined: 50 * $nag_shares - TODO 51 */ 52 protected function _init() 53 { 54 // Set the timezone variable. 55 $GLOBALS['registry']->setTimeZone(); 56 57 /* For now, autoloading the Content_* classes depend on there being a 58 * registry entry for the 'content' application that contains at least 59 * the fileroot entry. */ 60 $GLOBALS['injector']->getInstance('Horde_Autoloader') 61 ->addClassPathMapper( 62 new Horde_Autoloader_ClassPathMapper_Prefix('/^Content_/', $GLOBALS['registry']->get('fileroot', 'content') . '/lib/')); 63 64 // Create a share instance. 65 $GLOBALS['nag_shares'] = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Share')->create(); 66 67 Nag::initialize(); 68 } 69 70 /** 71 */ 72 public function perms() 73 { 74 return array( 75 'max_tasks' => array( 76 'title' => _("Maximum Number of Tasks"), 77 'type' => 'int' 78 ) 79 ); 80 } 81 82 /** 83 * Generate links in the sidebar. 84 * 85 * @param Horde_Menu The menu object. 86 */ 87 public function menu($menu) 88 { 89 global $conf; 90 91 $menu->add(Horde::url('list.php'), _("_List Tasks"), 'nag-list', null, null, null, (basename($_SERVER['PHP_SELF']) == 'list.php' || strpos($_SERVER['PHP_SELF'], 'nag/index.php') !== false) ? 'current' : null); 92 93 /* Search. */ 94 $menu->add(Horde::url('search.php'), _("_Search"), 'nag-search'); 95 96 /* Import/Export. */ 97 if ($conf['menu']['import_export']) { 98 $menu->add(Horde::url('data.php'), _("_Import/Export"), 'horde-data'); 99 } 100 } 101 102 /** 103 * Add additional items to the sidebar. 104 * 105 * @param Horde_View_Sidebar $sidebar The sidebar object. 106 */ 107 public function sidebar($sidebar) 108 { 109 // @TODO: Implement an injector factory for this. 110 global $display_tasklists, $page_output, $prefs; 111 112 $perms = $GLOBALS['injector']->getInstance('Horde_Core_Perms'); 113 if (Nag::getDefaultTasklist(Horde_Perms::EDIT) && 114 ($perms->hasAppPermission('max_tasks') === true || 115 $perms->hasAppPermission('max_tasks') > Nag::countTasks())) { 116 $sidebar->addNewButton( 117 _("_New Task"), 118 Horde::url('task.php')->add('actionID', 'add_task')); 119 120 if ($GLOBALS['browser']->hasFeature('dom')) { 121 $page_output->addScriptFile('scriptaculous/effects.js', 'horde'); 122 $page_output->addScriptFile('redbox.js', 'horde'); 123 $blank = new Horde_Url(); 124 $sidebar->newExtra = $blank->link( 125 array_merge( 126 array('onclick' => 'RedBox.showInline(\'quickAddInfoPanel\'); $(\'quickText\').focus(); return false;'), 127 Horde::getAccessKeyAndTitle(_("_Quick Add"), false, true) 128 ) 129 ); 130 require_once NAG_TEMPLATES . '/quick.inc'; 131 } 132 } 133 134 $list = Horde::url('list.php'); 135 $edit = Horde::url('tasklists/edit.php'); 136 $user = $GLOBALS['registry']->getAuth(); 137 138 $sidebar->containers['my'] = array( 139 'header' => array( 140 'id' => 'nag-toggle-my', 141 'label' => _("My Task Lists"), 142 'collapsed' => false, 143 ), 144 ); 145 if (!$GLOBALS['prefs']->isLocked('default_tasklist')) { 146 $sidebar->containers['my']['header']['add'] = array( 147 'url' => Horde::url('tasklists/create.php'), 148 'label' => _("Create a new Task List"), 149 ); 150 } 151 if ($GLOBALS['registry']->isAdmin()) { 152 $sidebar->containers['system'] = array( 153 'header' => array( 154 'id' => 'nag-toggle-system', 155 'label' => _("System Task Lists"), 156 'collapsed' => true, 157 ), 158 ); 159 $sidebar->containers['system']['header']['add'] = array( 160 'url' => Horde::url('tasklists/create.php')->add('system', 1), 161 'label' => _("Create a new System Task List"), 162 ); 163 } 164 $sidebar->containers['shared'] = array( 165 'header' => array( 166 'id' => 'nag-toggle-shared', 167 'label' => _("Shared Task Lists"), 168 'collapsed' => true, 169 ), 170 ); 171 foreach (Nag::listTasklists(false, Horde_Perms::SHOW, false) as $name => $tasklist) { 172 $url = $list->add(array( 173 'display_tasklist' => $name, 174 'actionID' => in_array($name, $display_tasklists) 175 ? 'remove_displaylist' 176 : 'add_displaylist' 177 )); 178 $row = array( 179 'selected' => in_array($name, $display_tasklists), 180 'url' => $url, 181 'label' => Nag::getLabel($tasklist), 182 'color' => $tasklist->get('color') ?: '#dddddd', 183 'edit' => $edit->add('t', $tasklist->getName()), 184 'type' => 'checkbox', 185 ); 186 if ($GLOBALS['registry']->isAdmin() && 187 is_null($tasklist->get('owner'))) { 188 $sidebar->addRow($row, 'system'); 189 } elseif ($tasklist->get('owner') == $user) { 190 $sidebar->addRow($row, 'my'); 191 } else { 192 $sidebar->addRow($row, 'shared'); 193 } 194 } 195 } 196 197 /** 198 */ 199 public function hasPermission($permission, $allowed, $opts = array()) 200 { 201 if (is_array($allowed)) { 202 switch ($permission) { 203 case 'max_tasks': 204 $allowed = max($allowed); 205 break; 206 } 207 } 208 return $allowed; 209 } 210 211 /** 212 * Remove all data for the specified user. 213 * 214 * @param string $user The user to remove. 215 * @throws Nag_Exception 216 */ 217 public function removeUserData($user) 218 { 219 try { 220 $shares = $GLOBALS['nag_shares'] 221 ->listShares($user, array('attributes' => $user)); 222 } catch (Horde_Share_Exception $e) { 223 Horde::log($e, 'ERR'); 224 throw new Nag_Exception($e); 225 } 226 227 $error = false; 228 foreach ($shares as $share) { 229 $storage = $GLOBALS['injector']->getInstance('Nag_Factory_Driver')->create($share->getName()); 230 $result = $storage->deleteAll(); 231 try { 232 $GLOBALS['nag_shares']->removeShare($share); 233 } catch (Horde_Share_Exception $e) { 234 Horde::log($e, 'NOTICE'); 235 $error = true; 236 } 237 } 238 239 /* Now remove perms for this user from all other shares */ 240 try { 241 $shares = $GLOBALS['nag_shares']->listShares($user); 242 foreach ($shares as $share) { 243 $share->removeUser($user); 244 } 245 } catch (Horde_Share_Exception $e) { 246 Horde::log($e, 'NOTICE'); 247 $error = true; 248 } 249 250 if ($error) { 251 throw new Nag_Exception(sprintf(_("There was an error removing tasks for %s. Details have been logged."), $user)); 252 } 253 } 254 255 /* Alarm method. */ 256 257 /** 258 */ 259 public function listAlarms($time, $user = null) 260 { 261 if ((empty($user) || $user != $GLOBALS['registry']->getAuth()) && 262 !$GLOBALS['registry']->isAdmin()) { 263 264 throw new Horde_Exception_PermissionDenied(); 265 } 266 267 $group = $GLOBALS['injector']->getInstance('Horde_Group'); 268 $alarm_list = array(); 269 $tasklists = is_null($user) ? 270 array_keys($GLOBALS['nag_shares']->listAllShares()) : 271 $GLOBALS['display_tasklists']; 272 273 $alarms = Nag::listAlarms($time, $tasklists); 274 foreach ($alarms as $alarm) { 275 try { 276 $share = $GLOBALS['nag_shares']->getShare($alarm->tasklist); 277 } catch (Horde_Share_Exception $e) { 278 continue; 279 } 280 if (empty($user)) { 281 $users = $share->listUsers(Horde_Perms::READ); 282 $groups = $share->listGroups(Horde_Perms::READ); 283 foreach ($groups as $gid) { 284 $users = array_merge($users, $group->listUsers($gid)); 285 } 286 $users = array_unique($users); 287 } else { 288 $users = array($user); 289 } 290 foreach ($users as $alarm_user) { 291 $prefs = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Prefs')->create('nag', array( 292 'cache' => false, 293 'user' => $alarm_user 294 )); 295 $GLOBALS['registry']->setLanguageEnvironment($prefs->getValue('language')); 296 $alarm_list[] = $alarm->toAlarm($alarm_user, $prefs); 297 } 298 } 299 300 return $alarm_list; 301 } 302 303 304 /* Topbar method. */ 305 306 /** 307 */ 308 public function topbarCreate(Horde_Tree_Renderer_Base $tree, $parent = null, 309 array $params = array()) 310 { 311 global $registry; 312 313 switch ($params['id']) { 314 case 'menu': 315 $add = Horde::url('task.php', true)->add('actionID', 'add_task'); 316 317 $tree->addNode(array( 318 'id' => $parent . '__new', 319 'parent' => $parent, 320 'label' => _("New Task"), 321 'expanded' => false, 322 'params' => array( 323 'icon' => Horde_Themes::img('add.png'), 324 'url' => $add 325 ) 326 )); 327 328 $user = $registry->getAuth(); 329 foreach (Nag::listTasklists(false, Horde_Perms::SHOW, false) as $name => $tasklist) { 330 if (!$tasklist->hasPermission($user, Horde_Perms::EDIT)) { 331 continue; 332 } 333 $tree->addNode(array( 334 'id' => $parent . $name . '__new', 335 'parent' => $parent . '__new', 336 'label' => sprintf(_("in %s"), Nag::getLabel($tasklist)), 337 'expanded' => false, 338 'params' => array( 339 'icon' => Horde_Themes::img('add.png'), 340 'url' => $add->copy()->add('tasklist_id', $name) 341 ) 342 )); 343 } 344 345 $tree->addNode(array( 346 'id' => $parent . '__search', 347 'parent' => $parent, 348 'label' => _("Search"), 349 'expanded' => false, 350 'params' => array( 351 'icon' => Horde_Themes::img('search.png'), 352 'url' => Horde::url('search.php') 353 ) 354 )); 355 break; 356 } 357 } 358 359 /* Download data. */ 360 361 /** 362 * @throws Nag_Exception 363 */ 364 public function download(Horde_Variables $vars) 365 { 366 global $display_tasklists, $injector, $registry; 367 368 switch ($vars->actionID) { 369 case 'export': 370 $allowed = array_keys( 371 Nag::listTasklists(false, Horde_Perms::READ, false) 372 ); 373 $tasklists = $vars->get('exportList', $allowed); 374 if (!is_array($tasklists)) { 375 $tasklists = array($tasklists); 376 } 377 $tasklists = array_intersect($tasklists, $allowed); 378 379 /* Get the full, sorted task list. */ 380 $tasks = Nag::listTasks(array( 381 'tasklists' => $tasklists, 382 'completed' => $vars->exportTasks, 383 'include_tags' => true, 384 'include_history' => false) 385 ); 386 387 $tasks->reset(); 388 switch ($vars->exportID) { 389 case Horde_Data::EXPORT_CSV: 390 $data = array(); 391 392 while ($task = $tasks->each()) { 393 $task = $task->toHash(); 394 $task['desc'] = str_replace(',', '', $task['desc']); 395 $task['tags'] = implode(',', $task['tags']); 396 unset( 397 $task['complete_link'], 398 $task['delete_link'], 399 $task['edit_link'], 400 $task['parent'], 401 $task['task_id'], 402 $task['tasklist_id'], 403 $task['view_link'], 404 $task['recurrence'], 405 $task['methods'] 406 ); 407 foreach (array('start', 'due', 'completed_date') as $field) { 408 if (!empty($task[$field])) { 409 $date = new Horde_Date($task[$field]); 410 $task[$field] = $date->format('c'); 411 } 412 } 413 $data[] = $task; 414 } 415 416 $injector->getInstance('Horde_Core_Factory_Data')->create('Csv', array('cleanup' => array($this, 'cleanupData')))->exportFile(_("tasks.csv"), $data, true); 417 exit; 418 419 case Horde_Data::EXPORT_ICALENDAR: 420 $iCal = new Horde_Icalendar(); 421 $iCal->setAttribute( 422 'PRODID', 423 '-//The Horde Project//Nag ' . $registry->getVersion() . '//EN'); 424 while ($task = $tasks->each()) { 425 $iCal->addComponent($task->toiCalendar($iCal)); 426 } 427 428 return array( 429 'data' => $iCal->exportvCalendar(), 430 'name' => _("tasks.ics"), 431 'type' => 'text/calendar' 432 ); 433 } 434 } 435 } 436 437 /** 438 */ 439 public function cleanupData() 440 { 441 $GLOBALS['import_step'] = 1; 442 return Horde_Data::IMPORT_FILE; 443 } 444 445 /* DAV methods. */ 446 447 /** 448 */ 449 public function davGetCollections($user) 450 { 451 global $injector, $nag_shares, $registry; 452 453 $hordeUser = $registry->convertUsername($user, true); 454 $shares = $nag_shares->listShares($hordeUser); 455 $dav = $injector->getInstance('Horde_Dav_Storage'); 456 $tasklists = array(); 457 foreach ($shares as $id => $share) { 458 if ($user == '-system-' && $share->get('owner')) { 459 continue; 460 } 461 try { 462 $id = $dav->getExternalCollectionId($id, 'tasks') ?: $id; 463 } catch (Horde_Dav_Exception $e) { 464 } 465 $tasklists[] = array( 466 'id' => $id, 467 'uri' => $id, 468 '{' . CalDAV\Plugin::NS_CALENDARSERVER . '}shared-url' => 469 Nag::getUrl(Nag::DAV_CALDAV, $share), 470 'principaluri' => 'principals/' . $user, 471 '{http://sabredav.org/ns}owner-principal' => 472 'principals/' 473 . ($share->get('owner') 474 ? $registry->convertUsername($share->get('owner'), false) 475 : '-system-' 476 ), 477 '{DAV:}displayname' => Nag::getLabel($share), 478 '{urn:ietf:params:xml:ns:caldav}calendar-description' => 479 $share->get('desc'), 480 '{http://apple.com/ns/ical/}calendar-color' => 481 $share->get('color'), 482 '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new Sabre\CalDAV\Property\SupportedCalendarComponentSet(array('VTODO')), 483 '{http://sabredav.org/ns}read-only' => !$share->hasPermission($hordeUser, Horde_Perms::EDIT), 484 ); 485 } 486 return $tasklists; 487 } 488 489 /** 490 */ 491 public function davGetObjects($collection) 492 { 493 $dav = $GLOBALS['injector'] 494 ->getInstance('Horde_Dav_Storage'); 495 496 $internal = $dav->getInternalCollectionId($collection, 'tasks') ?: $collection; 497 if (!Nag::hasPermission($internal, Horde_Perms::READ)) { 498 throw new Nag_Exception("Task List does not exist or no permission to edit"); 499 } 500 501 $storage = $GLOBALS['injector'] 502 ->getInstance('Nag_Factory_Driver') 503 ->create($internal); 504 505 $storage->retrieve(); 506 $storage->tasks->reset(); 507 508 $tasks = array(); 509 while ($task = $storage->tasks->each()) { 510 $id = $task->id; 511 $modified = $this->_modified($internal, $task->uid); 512 try { 513 $id = $dav->getExternalObjectId($id, $internal) ?: $id . '.ics'; 514 } catch (Horde_Dav_Exception $e) { 515 } 516 $tasks[] = array( 517 'id' => $id, 518 'uri' => $id, 519 'lastmodified' => $modified, 520 'etag' => '"' . md5($task->id . '|' . $modified) . '"', 521 'calendarid' => $collection, 522 ); 523 } 524 525 return $tasks; 526 } 527 528 /** 529 */ 530 public function davGetObject($collection, $object) 531 { 532 $dav = $GLOBALS['injector'] 533 ->getInstance('Horde_Dav_Storage'); 534 535 $internal = $dav->getInternalCollectionId($collection, 'tasks') ?: $collection; 536 if (!Nag::hasPermission($internal, Horde_Perms::READ)) { 537 throw new Nag_Exception("Task List does not exist or no permission to edit"); 538 } 539 540 try { 541 $object = $dav->getInternalObjectId($object, $internal) ?: preg_replace('/\.ics$/', '', $object); 542 } catch (Horde_Dav_Exception $e) { 543 } 544 $task = Nag::getTask($internal, $object); 545 $id = $task->id; 546 $modified = $this->_modified($internal, $task->uid); 547 try { 548 $id = $dav->getExternalObjectId($id, $internal) ?: $id . '.ics'; 549 } catch (Horde_Dav_Exception $e) { 550 } 551 552 $share = $GLOBALS['nag_shares']->getShare($internal); 553 $ical = new Horde_Icalendar('2.0'); 554 $ical->setAttribute('X-WR-CALNAME', $share->get('name')); 555 $ical->addComponent($task->toiCalendar($ical)); 556 $data = $ical->exportvCalendar(); 557 558 return array( 559 'id' => $id, 560 'calendardata' => $data, 561 'uri' => $id, 562 'lastmodified' => $modified, 563 'etag' => '"' . md5($task->id . '|' . $modified) . '"', 564 'calendarid' => $collection, 565 'size' => strlen($data), 566 ); 567 } 568 569 /** 570 */ 571 public function davPutObject($collection, $object, $data) 572 { 573 $dav = $GLOBALS['injector'] 574 ->getInstance('Horde_Dav_Storage'); 575 576 $internal = $dav->getInternalCollectionId($collection, 'tasks') ?: $collection; 577 if (!Nag::hasPermission($internal, Horde_Perms::EDIT)) { 578 throw new Nag_Exception("Task List does not exist or no permission to edit"); 579 } 580 581 $ical = new Horde_Icalendar(); 582 if (!$ical->parsevCalendar($data)) { 583 throw new Nag_Exception(_("There was an error importing the iCalendar data.")); 584 } 585 586 $storage = $GLOBALS['injector'] 587 ->getInstance('Nag_Factory_Driver') 588 ->create($internal); 589 590 foreach ($ical->getComponents() as $content) { 591 if (!($content instanceof Horde_Icalendar_Vtodo)) { 592 continue; 593 } 594 595 $task = new Nag_Task(); 596 $task->fromiCalendar($content); 597 598 try { 599 try { 600 $existing_id = $dav->getInternalObjectId($object, $internal) 601 ?: preg_replace('/\.ics$/', '', $object); 602 } catch (Horde_Dav_Exception $e) { 603 $existing_id = $object; 604 } 605 $existing_task = Nag::getTask($internal, $existing_id); 606 /* Check if our task is newer then the existing - get the 607 * task's history. */ 608 $modified = $this->_modified($internal, $existing_task->uid); 609 try { 610 if (!empty($modified) && 611 $content->getAttribute('LAST-MODIFIED') < $modified) { 612 /* LAST-MODIFIED timestamp of existing entry is newer: 613 * don't replace it. */ 614 continue; 615 } 616 } catch (Horde_Icalendar_Exception $e) { 617 } 618 $task->owner = $existing_task->owner; 619 $storage->modify($existing_task->id, $task->toHash()); 620 } catch (Horde_Exception_NotFound $e) { 621 $hash = $task->toHash(); 622 $newTask = $storage->add($hash); 623 $dav->addObjectMap($newTask[0], $object, $internal); 624 } 625 } 626 } 627 628 /** 629 */ 630 public function davDeleteObject($collection, $object) 631 { 632 $dav = $GLOBALS['injector']->getInstance('Horde_Dav_Storage'); 633 634 $internal = $dav->getInternalCollectionId($collection, 'tasks') ?: $collection; 635 if (!Nag::hasPermission($internal, Horde_Perms::DELETE)) { 636 throw new Nag_Exception("Task List does not exist or no permission to delete"); 637 } 638 639 try { 640 $object = $dav->getInternalObjectId($object, $internal) 641 ?: preg_replace('/\.ics$/', '', $object); 642 } catch (Horde_Dav_Exception $e) { 643 } 644 $GLOBALS['injector'] 645 ->getInstance('Nag_Factory_Driver') 646 ->create($internal) 647 ->delete($object); 648 649 try { 650 $dav->deleteExternalObjectId($object, $internal); 651 } catch (Horde_Dav_Exception $e) { 652 } 653 } 654 655 /** 656 * Returns the last modification (or creation) date of a task. 657 * 658 * @param string $collection A task list ID. 659 * @param string $object A task UID. 660 * 661 * @return integer Timestamp of the last modification. 662 */ 663 protected function _modified($collection, $uid) 664 { 665 $history = $GLOBALS['injector'] 666 ->getInstance('Horde_History'); 667 $modified = $history->getActionTimestamp( 668 'nag:' . $collection . ':' . $uid, 669 'modify' 670 ); 671 if (!$modified) { 672 $modified = $history->getActionTimestamp( 673 'nag:' . $collection . ':' . $uid, 674 'add' 675 ); 676 } 677 return $modified; 678 } 679} 680