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