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