1<?php
2/**
3 * Copyright 2001-2002 Robert E. Coyle <robertecoyle@hotmail.com>
4 * Copyright 2001-2017 Horde LLC (http://www.horde.org/)
5 *
6 * See the enclosed file LICENSE for license information (BSD). If you
7 * did not receive this file, see http://www.horde.org/licenses/bsdl.php.
8 *
9 * @package Whups
10 */
11
12/**
13 * The Whups class provides functionality that all of Whups needs, or that
14 * should be encapsulated from other parts of the Whups system.
15 *
16 * @author  Robert E. Coyle <robertecoyle@hotmail.com>
17 * @author  Jan Schneider <jan@horde.org>
18 * @package Whups
19 */
20class Whups
21{
22    /**
23     * Path to ticket attachments in the VFS.
24     */
25    const VFS_ATTACH_PATH = '.horde/whups/attachments';
26
27    /**
28     * The current sort field.
29     *
30     * @see sortBy()
31     * @var string
32     */
33    static protected $_sortBy;
34
35    /**
36     * The current sort direction.
37     *
38     * @see sortDir()
39     * @var integer
40     */
41    static protected $_sortDir;
42
43    /**
44     * Cached list of user information.
45     *
46     * @see getUserAttributes()
47     * @var array
48     */
49    static protected $_users = array();
50
51    /**
52     * All available form field types including all type information
53     * from the Horde_Form classes.
54     *
55     * @see fieldTypes()
56     * @var array
57     */
58    static protected $_fieldTypes = array();
59
60    /**
61     * URL factory.
62     *
63     * @param string $controller       The controller to link to, one of
64     *                                 'queue', 'ticket', 'ticket_rss',
65     *                                 'ticket_action', 'query', 'query_rss'.
66     * @param array|string $data       URL data, depending on the controller.
67     * @param boolean $full            @see Horde::url()
68     * @param integer $append_session  @see Horde::url()
69     *
70     * @return Horde_Url  The generated URL.
71     */
72    static public function urlFor($controller, $data, $full = false,
73                                  $append_session = 0)
74    {
75        $rewrite = isset($GLOBALS['conf']['urls']['pretty']) &&
76            $GLOBALS['conf']['urls']['pretty'] == 'rewrite';
77
78        switch ($controller) {
79        case 'queue':
80            if ($rewrite) {
81                if (is_array($data)) {
82                    if (empty($data['slug'])) {
83                        $slug = (int)$data['id'];
84                    } else {
85                        $slug = $data['slug'];
86                    }
87                } else {
88                    $slug = (int)$data;
89                }
90                return Horde::url('queue/' . $slug, $full, $append_session);
91            } else {
92                if (is_array($data)) {
93                    $id = $data['id'];
94                } else {
95                    $id = $data;
96                }
97                return Horde::url('queue/?id=' . $id, $full, $append_session);
98            }
99            break;
100
101        case 'ticket':
102            $id = (int)$data;
103            if ($rewrite) {
104                return Horde::url('ticket/' . $id, $full, $append_session);
105            } else {
106                return Horde::url('ticket/?id=' . $id, $full, $append_session);
107            }
108            break;
109
110        case 'ticket_rss':
111            $id = (int)$data;
112            if ($rewrite) {
113                return Horde::url('ticket/' . $id . '/rss', $full, $append_session);
114            } else {
115                return Horde::url('ticket/rss.php?id=' . $id, $full, $append_session);
116            }
117            break;
118
119        case 'ticket_action':
120            list($controller, $id) = $data;
121            if ($rewrite) {
122                return Horde::url('ticket/' . $id . '/' . $controller, $full, $append_session = 0);
123            } else {
124                return Horde::url('ticket/' . $controller . '.php?id=' . $id, $full, $append_session = 0);
125            }
126
127        case 'query':
128        case 'query_rss':
129            if ($rewrite) {
130                if (is_array($data)) {
131                    if (isset($data['slug'])) {
132                        $slug = $data['slug'];
133                    } else {
134                        $slug = $data['id'];
135                    }
136                } else {
137                    $slug = (int)$data;
138                }
139                $url = 'query/' . $slug;
140                if ($controller == 'query_rss') {
141                    $url .= '/rss';
142                }
143                return Horde::url($url, $full, $append_session);
144            } else {
145                if (is_array($data)) {
146                    if (isset($data['slug'])) {
147                        $param = array('slug' => $data['slug']);
148                    } else {
149                        $param = array('query' => $data['id']);
150                    }
151                } else {
152                    $param = array('query' => $data);
153                }
154                $url = $controller == 'query' ? 'query/run.php' : 'query/rss.php';
155                return Horde::url($url, $full, $append_session)->add($param);
156            }
157            break;
158        }
159    }
160
161    /**
162     * Sorts tickets by requested direction and fields.
163     *
164     * @param array $tickets  The list of tickets to sort.
165     * @param string $by      The field to sort by. If omitted, obtain from
166     *                        preferences.
167     * @param string $dir     The direction to sort. If omitted, obtain from
168     *                        preferences.
169     */
170    static public function sortTickets(&$tickets, $by = null, $dir = null)
171    {
172        if (is_null($by)) {
173            $by = $GLOBALS['prefs']->getValue('sortby');
174        }
175        if (is_null($dir)) {
176            $dir = $GLOBALS['prefs']->getValue('sortdir');
177        }
178
179        self::sortBy($by);
180        self::sortDir($dir);
181
182        // Do some prep for sorting.
183        $tickets = array_map(array('Whups', '_prepareSort'), $tickets);
184
185        usort($tickets, array('Whups', '_sort'));
186    }
187
188    /**
189     * Sets or returns the current sort field.
190     *
191     * @param string $b  The field to sort by.
192     *
193     * @return string  If $b is null, returns the previously set value.
194     */
195    static public function sortBy($b = null)
196    {
197        if (!is_null($b)) {
198            self::$_sortBy = $b;
199        } else {
200            return self::$_sortBy;
201        }
202    }
203
204    /**
205     * Sets or returns the current sort direction.
206     *
207     * @param integer $d  The direction to sort by.
208     *
209     * @return integer  If $d is null, returns the previously set value.
210     */
211    static public function sortDir($d = null)
212    {
213        if (!is_null($d)) {
214            self::$_sortDir = $d;
215        } else {
216            return self::$_sortDir;
217        }
218    }
219
220    /**
221     * Helper method to prepare an array of tickets for sorting.
222     *
223     * Adds a sort_by key to each ticket array, with values lowercased. Used as
224     * a callback to array_map().
225     *
226     * @param array $ticket  The ticket array to prepare.
227     *
228     * @return array  The altered $ticket array
229     */
230    static protected function _prepareSort(array $ticket)
231    {
232        $by = self::sortBy();
233        $ticket['sort_by'] = array();
234        if (is_array($by)) {
235            foreach ($by as $field) {
236                if (!isset($ticket[$field])) {
237                    $ticket['sort_by'][$field] = '';
238                } else {
239                    $ticket['sort_by'][$field] = Horde_String::lower($ticket[$field], true, 'UTF-8');
240                }
241            }
242        } else {
243            if (!isset($ticket[$by])) {
244                $ticket['sort_by'][$by] = '';
245            } elseif (is_array($ticket[$by])) {
246                natcasesort($ticket[$by]);
247                $ticket['sort_by'][$by] = implode('', $ticket[$by]);
248            } else {
249                $ticket['sort_by'][$by] = Horde_String::lower($ticket[$by], true, 'UTF-8');
250            }
251        }
252        return $ticket;
253    }
254
255    /**
256     * Helper method to sort an array of tickets.
257     *
258     * Used as callback to usort().
259     *
260     * @param array $a         The first ticket to compare.
261     * @param array $b         The second ticket to compare.
262     * @param string $sortby   The field to sort by. If null, uses the field
263     *                         from self::sortBy().
264     * @param string $sortdir  The direction to sort. If null, uses the value
265     *                         from self::sortDir().
266     *
267     * @return integer
268     */
269    static protected function _sort($a, $b, $sortby = null, $sortdir = null)
270    {
271        if (is_null($sortby)) {
272            $sortby = self::$_sortBy;
273        }
274        if (is_null($sortdir)) {
275            $sortdir = self::$_sortDir;
276        }
277
278        if (is_array($sortby)) {
279            if (!isset($a[$sortby[0]])) {
280                $a[$sortby[0]] = null;
281            }
282            if (!isset($b[$sortby[0]])) {
283                $b[$sortby[0]] = null;
284            }
285
286            if (!count($sortby)) {
287                return 0;
288            }
289            if ($a['sort_by'][$sortby[0]] > $b['sort_by'][$sortby[0]]) {
290                return $sortdir[0] ? -1 : 1;
291            }
292            if ($a['sort_by'][$sortby[0]] === $b['sort_by'][$sortby[0]]) {
293                array_shift($sortby);
294                array_shift($sortdir);
295                return self::_sort($a, $b, $sortby, $sortdir);
296            }
297            return $sortdir[0] ? 1 : -1;
298        }
299
300        $a_val = isset($a['sort_by'][$sortby]) ? $a['sort_by'][$sortby] : null;
301        $b_val = isset($b['sort_by'][$sortby]) ? $b['sort_by'][$sortby] : null;
302
303        // Take care of the simplest case first
304        if ($a_val === $b_val) {
305            return 0;
306        }
307
308        if ((is_numeric($a_val) || is_null($a_val)) &&
309            (is_numeric($b_val) || is_null($b_val))) {
310            // Numeric comparison
311            return (int)($sortdir ? ($b_val > $a_val) : ($a_val > $b_val));
312        }
313
314        // Some special case sorting
315        if (is_array($a_val) || is_array($b_val)) {
316            $a_val = implode('', $a_val);
317            $b_val = implode('', $b_val);
318        }
319
320        // String comparison
321        return $sortdir ? strcoll($b_val, $a_val) : strcoll($a_val, $b_val);
322    }
323
324    /**
325     * Returns a new or the current CAPTCHA string.
326     *
327     * @param boolean $new  If true, a new CAPTCHA is created and returned.
328     *                      The current, to-be-confirmed string otherwise.
329     *
330     * @return string  A CAPTCHA string.
331     */
332    static public function getCAPTCHA($new = false)
333    {
334        global $session;
335
336        if ($new || !$session->get('whups', 'captcha')) {
337            $captcha = '';
338            for ($i = 0; $i < 5; ++$i) {
339                $captcha .= chr(rand(65, 90));
340            }
341            $session->set('whups', 'captcha', $captcha);
342        }
343
344        return $session->get('whups', 'captcha');
345    }
346
347    /**
348     * Lists all templates of a given type.
349     *
350     * @param string $type  The kind of template ('searchresults', etc.) to
351     *                      list.
352     *
353     * @return array  All templates of the requested type.
354     */
355    static public function listTemplates($type)
356    {
357        $templates = array();
358
359        $_templates = Horde::loadConfiguration('templates.php', '_templates', 'whups');
360        foreach ($_templates as $name => $info) {
361            if ($info['type'] == $type) {
362                $templates[$name] = $info['name'];
363            }
364        }
365
366        return $templates;
367    }
368
369    /**
370     * Returns the current ticket.
371     *
372     * Uses the 'id' request variable to determine what to look for. Will
373     * redirect to the default view if the ticket isn't found or if permissions
374     * checks fail.
375     *
376     * @return Whups_Ticket  The current ticket.
377     */
378    static public function getCurrentTicket()
379    {
380        $default = Horde::url($GLOBALS['prefs']->getValue('whups_default_view') . '.php', true);
381        $id = Horde_Util::getFormData('searchfield');
382        if (empty($id)) {
383            $id = Horde_Util::getFormData('id');
384        }
385        $id = preg_replace('|\D|', '', $id);
386        if (!$id) {
387            $GLOBALS['notification']->push(_("Invalid Ticket Id"), 'horde.error');
388            $default->redirect();
389        }
390
391        try {
392            return Whups_Ticket::makeTicket($id);
393        } catch (Whups_Exception $e) {
394            if ($e->code === 0) {
395                // No permissions to this ticket.
396                $GLOBALS['notification']->push($e->getMessage(), 'horde.warning');
397                $default->redirect();
398            }
399        } catch (Exception $e) {
400            $GLOBALS['notification']->push($e);
401            $default->redirect();
402        }
403    }
404
405    /**
406     * Adds topbar search to page
407     */
408    static public function addTopbarSearch()
409    {
410        $topbar = $GLOBALS['injector']->getInstance('Horde_View_Topbar');
411        $topbar->search = true;
412        $topbar->searchAction = Horde::url('ticket');
413        $topbar->searchLabel =  $GLOBALS['session']->get('whups', 'search') ?: _("Ticket #Id");
414    }
415
416    /**
417     * Returns the tabs for navigating between ticket actions.
418     */
419    static public function getTicketTabs(&$vars, $id)
420    {
421        $tabs = new Horde_Core_Ui_Tabs(null, $vars);
422        $queue = Whups_Ticket::makeTicket($id)->get('queue');
423
424        $tabs->addTab(_("_History"), self::urlFor('ticket', $id), 'history');
425        $tabs->addTab(_("_Attachments"), self::urlFor('ticket_action', array('attachments', $id)), 'attachments');
426        if (self::hasPermission($queue, 'queue', 'update')) {
427            $tabs->addTab(_("_Update"),
428                          self::urlFor('ticket_action', array('update', $id)),
429                          'update');
430        } else {
431            $tabs->addTab(_("_Comment"),
432                          self::urlFor('ticket_action', array('comment', $id)),
433                          'comment');
434        }
435        $tabs->addTab(_("_Watch"),
436                      self::urlFor('ticket_action', array('watch', $id)),
437                      'watch');
438        if (self::hasPermission($queue, 'queue', Horde_Perms::DELETE)) {
439            $tabs->addTab(_("S_et Queue"),
440                          self::urlFor('ticket_action', array('queue', $id)),
441                          'queue');
442        }
443        if (self::hasPermission($queue, 'queue', 'update')) {
444            $tabs->addTab(_("Set _Type"),
445                          self::urlFor('ticket_action', array('type', $id)),
446                          'type');
447        }
448        if (self::hasPermission($queue, 'queue', Horde_Perms::DELETE)) {
449            $tabs->addTab(_("_Delete"),
450                          self::urlFor('ticket_action', array('delete', $id)),
451                          'delete');
452        }
453
454        return $tabs;
455    }
456
457    /**
458     * Returns whether a user has a certain permission on a single resource.
459     *
460     * @param mixed $in                   A single resource to check.
461     * @param string $filter              The kind of resource specified in
462     *                                    $in, currently only 'queue'.
463     * @param string|integer $permission  A permission, either 'assign' or
464     *                                    'update', 'requester', or one of the
465     *                                    PERM_* constants.
466     * @param string $user                A user name.
467     *
468     * @return boolean  True if the user has the specified permission.
469     */
470    static public function hasPermission($in, $filter, $permission, $user = null)
471    {
472        if (is_null($user)) {
473            $user = $GLOBALS['registry']->getAuth();
474        }
475
476        if ($permission == 'update' ||
477            $permission == 'assign' ||
478            $permission == 'requester') {
479            $admin_perm = Horde_Perms::EDIT;
480        } else {
481            $admin_perm = $permission;
482        }
483
484        $admin = $GLOBALS['registry']->isAdmin(array('permission' => 'whups:admin', 'permlevel' => $admin_perm, 'user' => $user));
485        $perms = $GLOBALS['injector']->getInstance('Horde_Perms');
486
487        switch ($filter) {
488        case 'queue':
489            if ($admin) {
490                return true;
491            }
492            switch ($permission) {
493            case Horde_Perms::SHOW:
494            case Horde_Perms::READ:
495            case Horde_Perms::EDIT:
496            case Horde_Perms::DELETE:
497                if ($perms->hasPermission('whups:queues:' . $in, $user,
498                                          $permission)) {
499                    return true;
500                }
501                break;
502
503            default:
504                if ($perms->exists('whups:queues:' . $in . ':' . $permission)) {
505                    if (($permission == 'update' ||
506                         $permission == 'assign' ||
507                         $permission == 'requester') &&
508                        $perms->getPermissions(
509                            'whups:queues:' . $in . ':' . $permission, $user)) {
510                        return true;
511                    }
512                } else {
513                    // If the sub-permission doesn't exist, use the queue
514                    // permission at an EDIT level and lock out guests.
515                    if ($permission != 'requester' &&
516                        $GLOBALS['registry']->getAuth() &&
517                        $perms->hasPermission('whups:queues:' . $in, $user,
518                                              Horde_Perms::EDIT)) {
519                        return true;
520                    }
521                }
522                break;
523            }
524            break;
525        }
526
527        return false;
528    }
529
530    /**
531     * Filters a list of resources based on whether a user has certain
532     * permissions on it.
533     *
534     * @param array $in            A list of resources to check.
535     * @param string $filter       The kind of resource specified in $in,
536     *                             one of 'queue', 'queue_id', 'reply', or
537     *                             'comment'.
538     * @param integer $permission  A permission, one of the PERM_* constants.
539     * @param string $user         A user name.
540     * @param string $creator      The creator of an object in the resource,
541     *                             e.g. a ticket creator.
542     *
543     * @return array  The list of resources matching the permission criteria.
544     */
545    static public function permissionsFilter($in, $filter,
546                                             $permission = Horde_Perms::READ,
547                                             $user = null, $creator = null)
548    {
549        if (is_null($user)) {
550            $user = $GLOBALS['registry']->getAuth();
551        }
552
553        $admin = $GLOBALS['registry']->isAdmin(array('permission' => 'whups:admin', 'permlevel' => $permission, 'user' => $user));
554        $perms = $GLOBALS['injector']->getInstance('Horde_Perms');
555        $out = array();
556
557        switch ($filter) {
558        case 'queue':
559            if ($admin) {
560                return $in;
561            }
562            foreach ($in as $queueID => $name) {
563                if (!$perms->exists('whups:queues:' . $queueID) ||
564                    $perms->hasPermission('whups:queues:' . $queueID, $user,
565                                          $permission, $creator)) {
566                    $out[$queueID] = $name;
567                }
568            }
569            break;
570
571        case 'queue_id':
572            if ($admin) {
573                return $in;
574            }
575            foreach ($in as $queueID) {
576                if (!$perms->exists('whups:queues:' . $queueID) ||
577                    $perms->hasPermission('whups:queues:' . $queueID, $user,
578                                          $permission, $creator)) {
579                    $out[] = $queueID;
580                }
581            }
582            break;
583
584        case 'reply':
585            if ($admin) {
586                return $in;
587            }
588            foreach ($in as $replyID => $name) {
589                if (!$perms->exists('whups:replies:' . $replyID) ||
590                    $perms->hasPermission('whups:replies:' . $replyID,
591                                          $user, $permission, $creator)) {
592                    $out[$replyID] = $name;
593                }
594            }
595            break;
596
597        case 'comment':
598            foreach ($in as $key => $row) {
599                foreach ($row as $rkey => $rval) {
600                    if ($rkey != 'changes') {
601                        $out[$key][$rkey] = $rval;
602                        continue;
603                    }
604                    foreach ($rval as $i => $change) {
605                        if ($change['type'] != 'comment' ||
606                            !$perms->exists('whups:comments:' . $change['value'])) {
607                            $out[$key][$rkey][$i] = $change;
608                            if (isset($change['comment'])) {
609                                $out[$key]['comment_text'] = $change['comment'];
610                            }
611                        } elseif ($perms->exists('whups:comments:' . $change['value'])) {
612                            $change['private'] = true;
613                            $out[$key][$rkey][$i] = $change;
614                            if (isset($change['comment'])) {
615                                if ($admin ||
616                                    $perms->hasPermission('whups:comments:' . $change['value'],
617                                                          $user, Horde_Perms::READ, $creator)) {
618                                    $out[$key]['comment_text'] = $change['comment'];
619                                } else {
620                                    $out[$key][$rkey][$i]['comment'] = _("[Hidden]");
621                                }
622                            }
623                        }
624                    }
625                }
626            }
627            break;
628
629        default:
630            $out = $in;
631            break;
632        }
633
634        return $out;
635    }
636
637    /**
638     * Builds a list of criteria for Whups_Driver#getTicketsByProperties() that
639     * match a certain user.
640     *
641     * Merges the user's groups with the user name.
642     *
643     * @param string $user  A user name.
644     *
645     * @return array  A list of criteria that would match the user.
646     */
647    static public function getOwnerCriteria($user)
648    {
649        $criteria = array('user:' . $user);
650        $mygroups = $GLOBALS['injector']
651            ->getInstance('Horde_Group')
652            ->listGroups($GLOBALS['registry']->getAuth());
653        foreach ($mygroups as $id => $group) {
654            $criteria[] = 'group:' . $id;
655        }
656        return $criteria;
657    }
658
659    /**
660     * Returns a hash with user information.
661     *
662     * @param string $user  A (Whups) user name, defaults to the current user.
663     *
664     * @return array  An information hash with 'user', 'name', 'email', and
665     *                'type' values.
666     */
667    static public function getUserAttributes($user = null)
668    {
669        if (is_null($user)) {
670            $user = $GLOBALS['registry']->getAuth();
671        }
672        if (empty($user)) {
673            return array('type' => 'user',
674                         'user' => '',
675                         'name' => '',
676                         'email' => '');
677        }
678
679        if (isset(self::$_users[$user])) {
680            return self::$_users[$user];
681        }
682
683        if (strpos($user, ':') !== false) {
684            list($type, $user) = explode(':', $user, 2);
685        } else {
686            $type = 'user';
687        }
688
689        // Default this; some of the cases below might change it.
690        self::$_users[$user]['user'] = $user;
691        self::$_users[$user]['type'] = $type;
692
693        switch ($type) {
694        case 'user':
695            if (substr($user, 0, 2) == '**') {
696                unset(self::$_users[$user]);
697                $user = substr($user, 2);
698
699                self::$_users[$user]['user'] = $user;
700                self::$_users[$user]['name'] = '';
701                self::$_users[$user]['email'] = '';
702
703                $addr_ob = new Horde_Mail_Rfc822_Address($user);
704                if ($addr_ob->valid) {
705                    self::$_users[$user]['name'] = is_null($addr_ob->personal)
706                        ? ''
707                        : $addr_ob->personal;
708                    self::$_users[$user]['email'] = $addr_ob->bare_address;
709                }
710            } elseif ($user < 0) {
711                global $whups_driver;
712
713                self::$_users[$user]['user'] = '';
714                self::$_users[$user]['name'] = '';
715                self::$_users[$user]['email'] = $whups_driver->getGuestEmail($user);
716
717                $addr_ob = new Horde_Mail_Rfc822_Address(self::$_users[$user]['email']);
718                if ($addr_ob->valid) {
719                    self::$_users[$user]['name'] = is_null($addr_ob->personal)
720                        ? ''
721                        : $addr_ob->personal;
722                    self::$_users[$user]['email'] = $addr_ob->bare_address;
723                }
724            } else {
725                $identity = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Identity')->create($user);
726
727                self::$_users[$user]['name'] = $identity->getName();
728                self::$_users[$user]['email'] = $identity->getDefaultFromAddress();
729            }
730            break;
731
732        case 'group':
733            try {
734                $group = $GLOBALS['injector']
735                    ->getInstance('Horde_Group')
736                    ->getData($user);
737                self::$_users[$user]['user'] = $group['name'];
738                self::$_users[$user]['name'] = $group['name'];
739                self::$_users[$user]['email'] = $group['email'];
740            } catch (Horde_Exception $e) {
741                self::$_users['user']['name'] = '';
742                self::$_users['user']['email'] = '';
743            }
744            break;
745        }
746
747        return self::$_users[$user];
748    }
749
750    /**
751     * Returns a user string from the user's name and email address.
752     *
753     * @param string|array $user  A user name or a hash as returned from
754     *                            {@link self::getUserAttributes()}.
755     * @param boolean $showemail  Whether to include the email address.
756     * @param boolean $showname   Whether to include the full name.
757     * @param boolean $html       Whether to "prettify" the result. If true,
758     *                            email addresses are obscured, the result is
759     *                            escaped for HTML output, and a group icon
760     *                            might be added.
761     */
762    static public function formatUser($user = null, $showemail = true,
763                                      $showname = true, $html = false)
764    {
765        if (!is_null($user) && empty($user)) {
766            return '';
767        }
768
769        if (is_array($user)) {
770            $details = $user;
771        } else {
772            $details = self::getUserAttributes($user);
773        }
774        if (!empty($details['name'])) {
775            $name = $details['name'];
776        } else {
777            $name = $details['user'];
778        }
779        if (($showemail || empty($name) || !$showname) &&
780            !empty($details['email'])) {
781            if ($html && strpos($details['email'], '@') !== false) {
782                $details['email'] = str_replace(array('@', '.'),
783                                                array(' (at) ', ' (dot) '),
784                                                $details['email']);
785            }
786
787            if (!empty($name) && $showname) {
788                $addrOb = new Horde_Mail_Rfc822_Address($details['email']);
789                $addrOb->personal = $name;
790                $name = strval($addrOb);
791            } else {
792                $name = $details['email'];
793            }
794        }
795
796        if ($html) {
797            $name = htmlspecialchars($name);
798            if ($details['type'] == 'group') {
799                $name = Horde::img('group.png',
800                                   !empty($details['name'])
801                                   ? $details['name']
802                                   : $details['user'])
803                    . $name;
804            }
805        }
806
807        return $name;
808    }
809
810    /**
811     * Formats a ticket property for a tabular ticket listing.
812     *
813     * @param array $info    A ticket information hash.
814     * @param string $value  The column/property to format.
815     *
816     * @return string  The formatted property.
817     */
818    static public function formatColumn($info, $value)
819    {
820        $url = self::urlFor('ticket', $info['id']);
821        $thevalue = isset($info[$value]) ? $info[$value] : '';
822
823        if ($value == 'timestamp' || $value == 'due' ||
824            substr($value, 0, 5) == 'date_') {
825            require_once 'Horde/Form/Type.php';
826            $thevalue = Horde_Form_Type_date::getFormattedTime(
827                $thevalue,
828                $GLOBALS['prefs']->getValue('report_time_format'),
829                false);
830        } elseif ($value == 'user_id_requester') {
831            $thevalue = $info['requester_formatted'];
832        } elseif ($value == 'id' || $value == 'summary') {
833            $thevalue = Horde::link($url) . '<strong>' . htmlspecialchars($thevalue) . '</strong></a>';
834        } elseif ($value == 'owners') {
835            if (!empty($info['owners_formatted'])) {
836                $thevalue = implode(', ', $info['owners_formatted']);
837            }
838        }
839
840        return $thevalue;
841    }
842
843    /**
844     * Returns the set of columns and their associated parameter from the
845     * backend that should be displayed to the user.
846     *
847     * The results can depend on the current user preferences, which search
848     * function was executed, and the $columns parameter.
849     *
850     * @param integer $search_type  The type of search that was executed.
851     *                              Currently only 'block' is supported.
852     * @param array $columns        The columns to return, overriding the
853     *                              defaults for some $search_type.
854     */
855    static public function getSearchResultColumns($search_type = null,
856                                                  $columns = null)
857    {
858        $all = array(
859            _("Id")        => 'id',
860            _("Summary")   => 'summary',
861            _("State")     => 'state_name',
862            _("Type")      => 'type_name',
863            _("Priority")  => 'priority_name',
864            _("Queue")     => 'queue_name',
865            _("Requester") => 'user_id_requester',
866            _("Owners")    => 'owners',
867            _("Created")   => 'timestamp',
868            _("Updated")   => 'date_updated',
869            _("Assigned")  => 'date_assigned',
870            _("Due")       => 'due',
871            _("Resolved")  => 'date_resolved',
872        );
873
874        if ($search_type != 'block') {
875            return $all;
876        }
877
878        if (is_null($columns)) {
879            $columns = array('summary', 'priority_name', 'state_name');
880        }
881
882        $result = array(_("Id") => 'id');
883        foreach ($columns as $param) {
884            if (($label = array_search($param, $all)) !== false) {
885                $result[$label] = $param;
886            }
887        }
888
889        return $result;
890    }
891
892    /**
893     * Sends reminders, one email per user.
894     *
895     * @param Horde_Variables $vars  The selection criteria:
896     *                               - 'id' (integer) for individual tickets
897     *                               - 'queue' (integer) for tickets of a queue.
898     *                                 - 'category' (array) for ticket
899     *                                   categories, defaults to unresolved
900     *                                   tickets.
901     *                               - 'unassigned' (boolean) for unassigned
902     *                                 tickets.
903     *
904     * @throws Whups_Exception
905     */
906    static public function sendReminders($vars)
907    {
908        global $whups_driver;
909
910        if ($vars->get('id')) {
911            $info = array('id' => $vars->get('id'));
912        } elseif ($vars->get('queue')) {
913            $info['queue'] = $vars->get('queue');
914            if ($vars->get('category')) {
915                $info['category'] = $vars->get('category');
916            } else {
917                // Make sure that resolved tickets aren't returned.
918                $info['category'] = array('unconfirmed', 'new', 'assigned');
919            }
920        } else {
921            throw new Whups_Exception(_("You must select at least one queue to send reminders for."));
922        }
923
924        $tickets = $whups_driver->getTicketsByProperties($info);
925        self::sortTickets($tickets);
926        if (!count($tickets)) {
927            throw new Whups_Exception(_("No tickets matched your search criteria."));
928        }
929
930        $unassigned = $vars->get('unassigned');
931        $remind = array();
932        foreach ($tickets as $info) {
933            $info['link'] = self::urlFor('ticket', $info['id'], true, -1);
934            $owners = $whups_driver->getOwners($info['id']);
935            if (!empty($owners)) {
936                foreach (reset($owners) as $owner) {
937                    $remind[$owner][] = $info;
938                }
939            } elseif (!empty($unassigned)) {
940                $remind['**' . $unassigned][] = $info;
941            }
942        }
943
944        /* Build message template. */
945        $view = new Horde_View(array('templatePath' => WHUPS_BASE . '/config'));
946        $view->date = strftime($GLOBALS['prefs']->getValue('date_format'));
947
948        /* Get queue specific notification message text, if available. */
949        $message_file = WHUPS_BASE . '/config/reminder_email.plain';
950        if (file_exists($message_file . '.local.php')) {
951            $message_file .= '.local.php';
952        } else {
953            $message_file .= '.php';
954        }
955        $message_file = basename($message_file);
956
957        foreach ($remind as $user => $utickets) {
958            if (empty($user) || !count($utickets)) {
959                continue;
960            }
961            $view->tickets = $utickets;
962            $subject = _("Reminder: Your open tickets");
963            $whups_driver->mail(array('recipients' => array($user => 'owner'),
964                                      'subject' => $subject,
965                                      'view' => $view,
966                                      'template' => $message_file,
967                                      'from' => $user));
968        }
969    }
970
971    /**
972     * Returns attachment information hashes from the VFS backend.
973     *
974     * @param integer $ticket  A ticket ID.
975     * @param string $name     An attachment name.
976     *
977     * @return array  If $name is empty a list of all attachments' information
978     *                hashes, otherwise only the hash for the attachment of
979     *                that name.
980     *
981     * @throws Whups_Exception if the VFS object cannot be created.
982     */
983    static public function getAttachments($ticket, $name = null)
984    {
985        if (empty($GLOBALS['conf']['vfs']['type'])) {
986            return;
987        }
988
989        try {
990            $vfs = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Vfs')->create();
991        } catch (Horde_Vfs_Exception $e) {
992            throw new Whups_Exception($e);
993        }
994
995        if (!$vfs->isFolder(self::VFS_ATTACH_PATH, $ticket)) {
996            return;
997        }
998
999        try {
1000            $files = $vfs->listFolder(self::VFS_ATTACH_PATH . '/' . $ticket);
1001        } catch (Horde_Vfs_Exception $e) {
1002            $files = array();
1003        }
1004        if (is_null($name)) {
1005            return $files;
1006        }
1007        foreach ($files as $file) {
1008            if ($file['name'] == $name) {
1009                return $file;
1010            }
1011        }
1012    }
1013
1014    /**
1015     * Returns the links to view, download, and delete an attachment.
1016     *
1017     * @param integer $ticket  A ticket ID.
1018     * @param string $file     An attachment name.
1019     * @param integer $queue   The ticket's queue ID.
1020     *
1021     * @return array  List of URLs.
1022     */
1023    static public function attachmentUrl($ticket, $file, $queue)
1024    {
1025        global $injector, $registry;
1026
1027        $links = array();
1028
1029        // Can we view the attachment online?
1030        $mime_part = new Horde_Mime_Part();
1031        $mime_part->setType(Horde_Mime_Magic::extToMime($file['type']));
1032        $viewer = $injector->getInstance('Horde_Core_Factory_MimeViewer')
1033            ->create($mime_part);
1034        if ($viewer && !($viewer instanceof Horde_Mime_Viewer_Default)) {
1035            $links['view'] = Horde::url('view.php')
1036                ->add(array(
1037                    'actionID' => 'view_file',
1038                    'type' => $file['type'],
1039                    'file' => $file['name'],
1040                    'ticket' => $ticket
1041                ))
1042                ->link(array('title' => $file['name'], 'target' => '_blank'))
1043                . $file['name'] . '</a>';
1044        } else {
1045            $links['view'] = $file['name'];
1046        }
1047
1048        // We can always download attachments.
1049        $url_params = array('actionID' => 'download_file',
1050                            'file' => $file['name'],
1051                            'ticket' => $ticket);
1052        $links['download'] =
1053            $registry->downloadUrl($file['name'], $url_params)->link(array(
1054                'title' => $file['name']
1055            ))
1056            . Horde::img('download.png', _("Download")) . '</a>';
1057
1058        // Admins can delete attachments.
1059        if (self::hasPermission($queue, 'queue', Horde_Perms::DELETE)) {
1060            $links['delete'] = Horde::url('ticket/delete_attachment.php')
1061                ->add(
1062                    array(
1063                        'file' => $file['name'],
1064                        'id' => $ticket,
1065                        'url' => Horde::signUrl(Horde::selfUrl(true, false, true))
1066                    )
1067                )
1068                ->link(array(
1069                    'title' => sprintf(_("Delete %s"), $file['name']),
1070                    'onclick' => 'return window.confirm(\''
1071                        . addslashes(
1072                            sprintf(_("Permanently delete %s?"), $file['name'])
1073                        )
1074                        . '\');'
1075                ))
1076                . Horde::img(
1077                    'delete.png',
1078                    sprintf(_("Delete %s"), $file['name'])
1079                )
1080                . '</a>';
1081        }
1082
1083        return $links;
1084    }
1085
1086    /**
1087     * Returns formatted owner names of a ticket.
1088     *
1089     * @param integer $ticket    A ticket id. Only used if $owners is null.
1090     * @param boolean $showmail  Should we include the email address in the
1091     *                           output?
1092     * @param boolean $showname  Should we include the name in the output?
1093     * @param array $owners      An array of owners as returned from
1094     *                           Whups_Driver::getOwners() to be formatted. If
1095     *                           this is provided, they are used instead of
1096     *                           the owners from $ticket.
1097     *
1098     * @return string  The formatted owner string.
1099     */
1100    static public function getOwners($ticket, $showemail = true,
1101                                     $showname = true, $owners = null)
1102    {
1103        if (is_null($owners)) {
1104            $owners = $GLOBALS['whups_driver']->getOwners($ticket);
1105        }
1106
1107        $results = array();
1108        if ($owners) {
1109            foreach (reset($owners) as $owner) {
1110                $results[] = self::formatUser($owner, $showemail, $showname);
1111            }
1112        }
1113        return implode(', ', $results);
1114    }
1115
1116    /**
1117     * Returns all available form field types including all type information
1118     * from the Horde_Form classes.
1119     *
1120     * @todo Doesn't work with autoloading.
1121     *
1122     * @return array  The full field types array.
1123     */
1124    static public function fieldTypes()
1125    {
1126        if (!empty(self::$_fieldTypes)) {
1127            return self::$_fieldTypes;
1128        }
1129
1130        /* Fetch all declared classes. */
1131        $classes = get_declared_classes();
1132
1133        /* Filter for the Horde_Form_Type classes. */
1134        $blacklist = array(
1135            'addresslink', 'captcha', 'description', 'figlet', 'header',
1136            'image', 'invalid', 'spacer'
1137        );
1138        foreach ($classes as $class) {
1139            if (stripos($class, 'horde_form_type_') !== false) {
1140                $field_type = substr($class, 16);
1141                /* Don't bother including the types that cannot be handled
1142                 * usefully. */
1143                if (in_array($field_type, $blacklist)) {
1144                    continue;
1145                }
1146                self::$_fieldTypes[$field_type] = @call_user_func(
1147                    array('Horde_Form_Type_' . $field_type, 'about'));
1148            }
1149        }
1150
1151        return self::$_fieldTypes;
1152    }
1153
1154    /**
1155     * Returns the available field type names from the Horde_Form classes.
1156     *
1157     * @return array  A hash The with available field types and names.
1158     */
1159    static public function fieldTypeNames()
1160    {
1161        /* Fetch the field type information from the Horde_Form classes. */
1162        $fields = self::fieldTypes();
1163
1164        /* Strip out the name element from the array. */
1165        $available_fields = array();
1166        foreach ($fields as $field_type => $info) {
1167            $available_fields[$field_type] = $info['name'];
1168        }
1169
1170        /* Sort for display purposes. */
1171        asort($available_fields);
1172
1173        return $available_fields;
1174    }
1175
1176    /**
1177     * Returns the parameters for a certain Horde_Form field type.
1178     *
1179     * @param string $field_type  A field type.
1180     *
1181     * @return array  A list of field type parameters.
1182     */
1183    static public function fieldTypeParams($field_type)
1184    {
1185        $fields = self::fieldTypes();
1186
1187        return isset($fields[$field_type]['params'])
1188            ? $fields[$field_type]['params']
1189            : array();
1190    }
1191
1192    /**
1193     * Returns the parameters necessary to run an address search.
1194     *
1195     * @return array  An array with two keys: 'sources' and 'fields'.
1196     */
1197    static public function getAddressbookSearchParams()
1198    {
1199        $src = json_decode($GLOBALS['prefs']->getValue('search_sources'));
1200        if (!is_array($src)) {
1201            $src = array();
1202        }
1203
1204        $fields = json_decode($GLOBALS['prefs']->getValue('search_fields'), true);
1205        if (!is_array($fields)) {
1206            $fields = array();
1207        }
1208
1209        return array(
1210            'fields' => $fields,
1211            'sources' => $src
1212        );
1213    }
1214
1215    static public function addFeedLink()
1216    {
1217        $GLOBALS['page_output']->addLinkTag(array(
1218            'href' => Horde::url('opensearch.php', true, -1),
1219            'rel' => 'search',
1220            'type' => 'application/opensearchdescription+xml',
1221            'title' => $GLOBALS['registry']->get('name') . ' (' . Horde::url('', true) . ')'
1222        ));
1223    }
1224
1225}
1226