1<?php 2/********************************************************************* 3 class.staff.php 4 5 Everything about staff. 6 7 Peter Rotich <peter@osticket.com> 8 Copyright (c) 2006-2013 osTicket 9 http://www.osticket.com 10 11 Released under the GNU General Public License WITHOUT ANY WARRANTY. 12 See LICENSE.TXT for details. 13 14 vim: expandtab sw=4 ts=4 sts=4: 15**********************************************************************/ 16include_once(INCLUDE_DIR.'class.ticket.php'); 17include_once(INCLUDE_DIR.'class.dept.php'); 18include_once(INCLUDE_DIR.'class.error.php'); 19include_once(INCLUDE_DIR.'class.team.php'); 20include_once(INCLUDE_DIR.'class.role.php'); 21include_once(INCLUDE_DIR.'class.passwd.php'); 22include_once(INCLUDE_DIR.'class.user.php'); 23include_once(INCLUDE_DIR.'class.auth.php'); 24 25class Staff extends VerySimpleModel 26implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable { 27 28 static $meta = array( 29 'table' => STAFF_TABLE, 30 'pk' => array('staff_id'), 31 'joins' => array( 32 'dept' => array( 33 'constraint' => array('dept_id' => 'Dept.id'), 34 ), 35 'role' => array( 36 'constraint' => array('role_id' => 'Role.id'), 37 ), 38 'dept_access' => array( 39 'reverse' => 'StaffDeptAccess.staff', 40 ), 41 'teams' => array( 42 'reverse' => 'TeamMember.staff', 43 ), 44 ), 45 ); 46 47 var $authkey; 48 var $departments; 49 var $stats = array(); 50 var $_extra; 51 var $passwd_change; 52 var $_roles = null; 53 var $_teams = null; 54 var $_config = null; 55 var $_perm; 56 57 const PERM_STAFF = 'visibility.agents'; 58 59 static protected $perms = array( 60 self::PERM_STAFF => array( 61 'title' => /* @trans */ 'Agent', 62 'desc' => /* @trans */ 'Ability to see Agents in all Departments', 63 'primary' => true, 64 ), 65 ); 66 67 function __onload() { 68 } 69 70 function get($field, $default=false) { 71 72 // Check primary fields 73 try { 74 return parent::get($field, $default); 75 } catch (Exception $e) {} 76 77 // Autoload config if not loaded already 78 if (!isset($this->_config)) 79 $this->getConfig(); 80 81 if (isset($this->_config[$field])) 82 return $this->_config[$field]; 83 } 84 85 function getConfigObj() { 86 return new Config('staff.'.$this->getId()); 87 } 88 89 function getConfig() { 90 91 if (!isset($this->_config) && $this->getId()) { 92 $_config = new Config('staff.'.$this->getId(), 93 // Defaults 94 array( 95 'default_from_name' => '', 96 'datetime_format' => '', 97 'thread_view_order' => '', 98 'default_ticket_queue_id' => 0, 99 'reply_redirect' => 'Ticket', 100 'img_att_view' => 'download', 101 'editor_spacing' => 'double', 102 )); 103 $this->_config = $_config->getInfo(); 104 } 105 106 return $this->_config; 107 } 108 109 function updateConfig($vars) { 110 $config = $this->getConfigObj(); 111 $config->updateAll($vars); 112 $this->_config = null; 113 } 114 115 function __toString() { 116 return (string) $this->getName(); 117 } 118 119 function asVar() { 120 return $this->__toString(); 121 } 122 123 static function getVarScope() { 124 return array( 125 'dept' => array('class' => 'Dept', 'desc' => __('Department')), 126 'email' => __('Email Address'), 127 'name' => array( 128 'class' => 'PersonsName', 'desc' => __('Agent name'), 129 ), 130 'mobile' => __('Mobile Number'), 131 'phone' => __('Phone Number'), 132 'signature' => __('Signature'), 133 'timezone' => "Agent's configured timezone", 134 'username' => 'Access username', 135 ); 136 } 137 138 function getVar($tag) { 139 switch ($tag) { 140 case 'mobile': 141 return Format::phone($this->ht['mobile']); 142 case 'phone': 143 return Format::phone($this->ht['phone']); 144 } 145 } 146 147 static function getSearchableFields() { 148 return array( 149 'email' => new TextboxField(array( 150 'label' => __('Email Address'), 151 )), 152 ); 153 } 154 155 static function supportsCustomData() { 156 return false; 157 } 158 159 function getHashtable() { 160 $base = $this->ht; 161 unset($base['teams']); 162 unset($base['dept_access']); 163 164 if ($this->getConfig()) 165 $base += $this->getConfig(); 166 167 return $base; 168 } 169 170 function getInfo() { 171 return $this->getHashtable(); 172 } 173 174 // AuthenticatedUser implementation... 175 // TODO: Move to an abstract class that extends Staff 176 function getUserType() { 177 return 'staff'; 178 } 179 180 function getAuthBackend() { 181 list($bk, ) = explode(':', $this->getAuthKey()); 182 183 // If administering a user other than yourself, fallback to the 184 // agent's declared backend, if any 185 if (!$bk && $this->backend) 186 $bk = $this->backend; 187 188 return StaffAuthenticationBackend::getBackend($bk); 189 } 190 191 function get2FABackendId() { 192 return $this->default_2fa; 193 } 194 195 function get2FABackend() { 196 return Staff2FABackend::getBackend($this->get2FABackendId()); 197 } 198 199 // gets configured backends 200 function get2FAConfig($id) { 201 $config = $this->getConfig(); 202 return isset($config[$id]) ? 203 JsonDataParser::decode($config[$id]) : array(); 204 } 205 206 function setAuthKey($key) { 207 $this->authkey = $key; 208 } 209 210 function getAuthKey() { 211 return $this->authkey; 212 } 213 214 // logOut the user 215 function logOut() { 216 217 if ($bk = $this->getAuthBackend()) 218 return $bk->signOut($this); 219 220 return false; 221 } 222 223 /*compares user password*/ 224 function check_passwd($password, $autoupdate=true) { 225 226 /*bcrypt based password match*/ 227 if(Passwd::cmp($password, $this->getPasswd())) 228 return true; 229 230 //Fall back to MD5 231 if(!$password || strcmp($this->getPasswd(), MD5($password))) 232 return false; 233 234 //Password is a MD5 hash: rehash it (if enabled) otherwise force passwd change. 235 $this->passwd = Passwd::hash($password); 236 237 if(!$autoupdate || !$this->save()) 238 $this->forcePasswdRest(); 239 240 return true; 241 } 242 243 function cmp_passwd($password) { 244 return $this->check_passwd($password, false); 245 } 246 247 function hasPassword() { 248 return (bool) $this->passwd; 249 } 250 251 function forcePasswdRest() { 252 $this->change_passwd = 1; 253 return $this->save(); 254 } 255 256 function getPasswdResetTimestamp() { 257 if (!isset($this->passwd_change)) { 258 // WE have to patch info here to support upgrading from old versions. 259 if (isset($this->passwdreset) && $this->passwdreset) 260 $this->passwd_change = strtotime($this->passwdreset); 261 elseif (isset($this->added) && $this->added) 262 $this->passwd_change = strtotime($this->added); 263 elseif (isset($this->created) && $this->created) 264 $this->passwd_change = strtotime($this->created); 265 } 266 267 return $this->passwd_change; 268 } 269 270 static function checkPassword($new, $current=null) { 271 osTicketStaffAuthentication::checkPassword($new, $current); 272 } 273 274 function setPassword($new, $current=false) { 275 global $thisstaff; 276 277 // Allow the backend to update the password. This is the preferred 278 // method as it allows for integration with password policies and 279 // also allows for remotely updating the password where possible and 280 // supported. 281 if (!($bk = $this->getAuthBackend()) 282 || !$bk instanceof AuthBackend 283 ) { 284 // Fallback to osTicket authentication token udpates 285 $bk = new osTicketStaffAuthentication(); 286 } 287 288 // And now for the magic 289 if (!$bk->supportsPasswordChange()) { 290 throw new PasswordUpdateFailed( 291 __('Authentication backend does not support password updates')); 292 } 293 // Backend should throw PasswordUpdateFailed directly 294 $rv = $bk->setPassword($this, $new, $current); 295 296 // Successfully updated authentication tokens 297 $this->change_passwd = 0; 298 $this->cancelResetTokens(); 299 $this->passwdreset = SqlFunction::NOW(); 300 301 // Clean sessions 302 Signal::send('auth.clean', $this, $thisstaff); 303 304 return $rv; 305 } 306 307 function canAccess($something) { 308 if ($something instanceof RestrictedAccess) 309 return $something->checkStaffPerm($this); 310 311 return true; 312 } 313 314 function getRefreshRate() { 315 return $this->auto_refresh_rate; 316 } 317 318 function getPageLimit() { 319 return $this->max_page_size; 320 } 321 322 function getId() { 323 return $this->staff_id; 324 } 325 function getUserId() { 326 return $this->getId(); 327 } 328 329 function getEmail() { 330 return $this->email; 331 } 332 333 function getAvatar($size=null) { 334 global $cfg; 335 $source = $cfg->getStaffAvatarSource(); 336 $avatar = $source->getAvatar($this); 337 if (isset($size)) 338 $avatar->setSize($size); 339 return $avatar; 340 } 341 342 function getUserName() { 343 return $this->username; 344 } 345 346 function getPasswd() { 347 return $this->passwd; 348 } 349 350 function getName() { 351 return new AgentsName(array('first' => $this->ht['firstname'], 'last' => $this->ht['lastname'])); 352 } 353 354 function getAvatarAndName() { 355 return $this->getAvatar().Format::htmlchars((string) $this->getName()); 356 } 357 358 function getFirstName() { 359 return $this->firstname; 360 } 361 362 function getLastName() { 363 return $this->lastname; 364 } 365 366 function getSignature() { 367 return $this->signature; 368 } 369 370 function getDefaultTicketQueueId() { 371 return $this->default_ticket_queue_id; 372 } 373 374 function getDefaultSignatureType() { 375 return $this->default_signature_type; 376 } 377 378 function getReplyFromNameType() { 379 return $this->default_from_name; 380 } 381 382 function getDefaultPaperSize() { 383 return $this->default_paper_size; 384 } 385 386 function getReplyRedirect() { 387 return $this->reply_redirect; 388 } 389 390 function getImageAttachmentView() { 391 return $this->img_att_view; 392 } 393 394 function editorSpacing() { 395 return $this->editor_spacing; 396 } 397 398 function forcePasswdChange() { 399 return $this->change_passwd; 400 } 401 402 function force2faConfig() { 403 global $cfg; 404 405 $id = $this->get2FABackendId(); 406 $config = $this->get2FAConfig($id); 407 408 //2fa is required and 409 //1. agent doesn't have default_2fa or 410 //2. agent has default_2fa, but that default_2fa is not configured 411 return ($cfg->require2FAForAgents() && !$id || ($id && empty($config))); 412 } 413 414 function getDepartments() { 415 // TODO: Cache this in the agent's session as it is unlikely to 416 // change while logged in 417 418 if (!isset($this->departments)) { 419 420 // Departments the staff is "allowed" to access... 421 // based on the group they belong to + user's primary dept + user's managed depts. 422 $sql='SELECT DISTINCT d.id FROM '.STAFF_TABLE.' s ' 423 .' LEFT JOIN '.STAFF_DEPT_TABLE.' g ON (s.staff_id=g.staff_id) ' 424 .' INNER JOIN '.DEPT_TABLE.' d ON (LOCATE(CONCAT("/", s.dept_id, "/"), d.path) OR d.manager_id=s.staff_id OR LOCATE(CONCAT("/", g.dept_id, "/"), d.path)) ' 425 .' WHERE s.staff_id='.db_input($this->getId()); 426 $depts = array(); 427 if (($res=db_query($sql)) && db_num_rows($res)) { 428 while(list($id)=db_fetch_row($res)) 429 $depts[] = (int) $id; 430 } 431 432 /* ORM method — about 2.0ms slower 433 $q = Q::any(array( 434 'path__contains' => '/'.$this->dept_id.'/', 435 'manager_id' => $this->getId(), 436 )); 437 // Add in extended access 438 foreach ($this->dept_access->depts->values_flat('dept_id') as $row) { 439 // Skip primary dept 440 if ($row[0] == $this->dept_id) 441 continue; 442 $q->add(new Q(array('path__contains'=>'/'.$row[0].'/'))); 443 } 444 445 $dept_ids = Dept::objects() 446 ->filter($q) 447 ->distinct('id') 448 ->values_flat('id'); 449 450 foreach ($dept_ids as $row) 451 $depts[] = $row[0]; 452 */ 453 454 $this->departments = $depts; 455 } 456 457 return $this->departments; 458 } 459 460 function getDepts() { 461 return $this->getDepartments(); 462 } 463 464 function getDepartmentNames($activeonly=false) { 465 $depts = Dept::getDepartments(array('activeonly' => $activeonly)); 466 467 //filter out departments the agent does not have access to 468 if (!$this->hasPerm(Dept::PERM_DEPT) && $staffDepts = $this->getDepts()) { 469 foreach ($depts as $id => $name) { 470 if (!in_array($id, $staffDepts)) 471 unset($depts[$id]); 472 } 473 } 474 475 return $depts; 476 } 477 478 function getTopicNames($publicOnly=false, $disabled=false) { 479 $allInfo = !$this->hasPerm(Dept::PERM_DEPT) ? true : false; 480 $topics = Topic::getHelpTopics($publicOnly, $disabled, true, array(), $allInfo); 481 $topicsClean = array(); 482 483 if (!$this->hasPerm(Dept::PERM_DEPT) && $staffDepts = $this->getDepts()) { 484 foreach ($topics as $id => $info) { 485 if ($info['pid']) { 486 $childDeptId = $info['dept_id']; 487 //show child if public, has access to topic dept_id, or no dept at all 488 if ($info['public'] || !$childDeptId || ($childDeptId && in_array($childDeptId, $staffDepts))) 489 $topicsClean[$id] = $info['topic']; 490 $parent = Topic::lookup($info['pid']); 491 if (!$parent->isPublic() && $parent->getDeptId() && !in_array($parent->getDeptId(), $staffDepts)) { 492 //hide child if parent topic is private and no access to parent topic dept_id 493 unset($topicsClean[$id]); 494 } 495 } elseif ($info['public']) { 496 //show all public topics 497 $topicsClean[$id] = $info['topic']; 498 } else { 499 //show private topics if access to topic dept_id or no dept at all 500 if ($info['dept_id'] && in_array($info['dept_id'], $staffDepts) || !$info['dept_id']) 501 $topicsClean[$id] = $info['topic']; 502 } 503 } 504 505 return $topicsClean; 506 } 507 508 return $topics; 509 } 510 511 function getManagedDepartments() { 512 513 return ($depts=Dept::getDepartments( 514 array('manager' => $this->getId()) 515 ))?array_keys($depts):array(); 516 } 517 518 function getDeptId() { 519 return $this->dept_id; 520 } 521 522 function getDept() { 523 return $this->dept; 524 } 525 526 function setDepartmentId($dept_id, $eavesdrop=false) { 527 // Grant access to the current department 528 $old = $this->dept_id; 529 if ($eavesdrop) { 530 $da = new StaffDeptAccess(array( 531 'dept_id' => $old, 532 'role_id' => $this->role_id, 533 )); 534 $da->setAlerts(true); 535 $this->dept_access->add($da); 536 } 537 538 // Drop extended access to new department 539 $this->dept_id = $dept_id; 540 if ($da = $this->dept_access->findFirst(array( 541 'dept_id' => $dept_id)) 542 ) { 543 $this->dept_access->remove($da); 544 } 545 546 $this->save(); 547 } 548 549 function usePrimaryRoleOnAssignment() { 550 return $this->getExtraAttr('def_assn_role', true); 551 } 552 553 function getLanguage() { 554 return (isset($this->lang)) ? $this->lang : false; 555 } 556 557 function getTimezone() { 558 if (isset($this->timezone)) 559 return $this->timezone; 560 } 561 562 function getLocale() { 563 //XXX: isset is required here to avoid possible crash when upgrading 564 // installation where locale column doesn't exist yet. 565 return isset($this->locale) ? $this->locale : 0; 566 } 567 568 function getRoles() { 569 if (!isset($this->_roles)) { 570 $this->_roles = array($this->dept_id => $this->role); 571 foreach($this->dept_access as $da) 572 $this->_roles[$da->dept_id] = $da->role; 573 } 574 575 return $this->_roles; 576 } 577 578 function getRole($dept=null, $assigned=false) { 579 580 if (is_null($dept)) 581 return $this->role; 582 583 if (is_numeric($dept)) 584 $deptId = $dept; 585 elseif($dept instanceof Dept) 586 $deptId = $dept->getId(); 587 else 588 return null; 589 590 $roles = $this->getRoles(); 591 if (isset($roles[$deptId])) 592 return $roles[$deptId]; 593 594 // Default to primary role. 595 if ($assigned && $this->usePrimaryRoleOnAssignment()) 596 return $this->role; 597 598 // Ticket Create & View only access 599 $perms = JSONDataEncoder::encode(array( 600 Ticket::PERM_CREATE => 1)); 601 return new Role(array('permissions' => $perms)); 602 } 603 604 function hasPerm($perm, $global=true) { 605 if ($global) 606 return $this->getPermission()->has($perm); 607 if ($this->getRole()->hasPerm($perm)) 608 return true; 609 foreach ($this->dept_access as $da) 610 if ($da->role->hasPerm($perm)) 611 return true; 612 return false; 613 } 614 615 function canSearchEverything() { 616 return $this->hasPerm(SearchBackend::PERM_EVERYTHING); 617 } 618 619 function canManageTickets() { 620 return $this->hasPerm(Ticket::PERM_DELETE, false) 621 || $this->hasPerm(Ticket::PERM_TRANSFER, false) 622 || $this->hasPerm(Ticket::PERM_ASSIGN, false) 623 || $this->hasPerm(Ticket::PERM_CLOSE, false); 624 } 625 626 function isManager($dept=null) { 627 return (($dept=$dept?:$this->getDept()) && $dept->getManagerId()==$this->getId()); 628 } 629 630 function isStaff() { 631 return TRUE; 632 } 633 634 function isActive() { 635 return $this->isactive; 636 } 637 638 function getStatus() { 639 return $this->isActive() ? __('Active') : __('Locked'); 640 } 641 642 function isVisible() { 643 return $this->isvisible; 644 } 645 646 function onVacation() { 647 return $this->onvacation; 648 } 649 650 function isAvailable() { 651 return ($this->isActive() && !$this->onVacation()); 652 } 653 654 function showAssignedOnly() { 655 return $this->assigned_only; 656 } 657 658 function isAccessLimited() { 659 return $this->showAssignedOnly(); 660 } 661 662 function isAdmin() { 663 return $this->isadmin; 664 } 665 666 function isTeamMember($teamId) { 667 return ($teamId && in_array($teamId, $this->getTeams())); 668 } 669 670 function canAccessDept($dept) { 671 672 if (!$dept instanceof Dept) 673 return false; 674 675 return (!$this->isAccessLimited() 676 && in_array($dept->getId(), $this->getDepts())); 677 } 678 679 function getTeams() { 680 681 if (!isset($this->_teams)) { 682 $this->_teams = array(); 683 foreach ($this->teams as $team) 684 $this->_teams[] = (int) $team->team_id; 685 } 686 687 return $this->_teams; 688 } 689 690 function getTicketsVisibility($exclude_archived=false) { 691 // -- Open and assigned to me 692 $assigned = Q::any(array( 693 'staff_id' => $this->getId(), 694 )); 695 $assigned->add(array('thread__referrals__agent__staff_id' => $this->getId())); 696 $childRefAgent = Q::all(new Q(array('child_thread__object_type' => 'C', 697 'child_thread__referrals__agent__staff_id' => $this->getId()))); 698 $assigned->add($childRefAgent); 699 // -- Open and assigned to a team of mine 700 if (($teams = array_filter($this->getTeams()))) { 701 $assigned->add(array('team_id__in' => $teams)); 702 $assigned->add(array('thread__referrals__team__team_id__in' => $teams)); 703 $childRefTeam = Q::all(new Q(array('child_thread__object_type' => 'C', 704 'child_thread__referrals__team__team_id__in' => $teams))); 705 $assigned->add($childRefTeam); 706 } 707 $visibility = Q::any(new Q(array('status__state'=>'open', $assigned))); 708 // -- If access is limited to assigned only, return assigned 709 if ($this->isAccessLimited()) 710 return $visibility; 711 // -- Routed to a department of mine 712 if (($depts=$this->getDepts()) && count($depts)) { 713 $in_dept = Q::any(array( 714 'dept_id__in' => $depts, 715 'thread__referrals__dept__id__in' => $depts, 716 )); 717 if ($exclude_archived) { 718 $in_dept = Q::all(array( 719 'status__state__in' => ['open', 'closed'], 720 $in_dept, 721 )); 722 } 723 $visibility->add($in_dept); 724 $childRefDept = Q::all(new Q(array('child_thread__object_type' => 'C', 725 'child_thread__referrals__dept__id__in' => $depts))); 726 $visibility->add($childRefDept); 727 } 728 return $visibility; 729 } 730 731 function applyVisibility($query, $exclude_archived=false) { 732 return $query->filter($this->getTicketsVisibility($exclude_archived)); 733 } 734 735 function applyDeptVisibility($qs) { 736 // Restrict agents based on visibility of the assigner 737 if (!$this->hasPerm(Staff::PERM_STAFF) 738 && ($depts=$this->getDepts())) { 739 return $qs->filter(Q::any(array( 740 'dept_id__in' => $depts, 741 'dept_access__dept_id__in' => $depts, 742 ))); 743 } 744 return $qs; 745 } 746 747 /* stats */ 748 function resetStats() { 749 $this->stats = array(); 750 } 751 752 function getTasksStats() { 753 754 if (!$this->stats['tasks']) 755 $this->stats['tasks'] = Task::getStaffStats($this); 756 757 return $this->stats['tasks']; 758 } 759 760 function getNumAssignedTasks() { 761 return ($stats=$this->getTasksStats()) ? $stats['assigned'] : 0; 762 } 763 764 function getNumClosedTasks() { 765 return ($stats=$this->getTasksStats()) ? $stats['closed'] : 0; 766 } 767 768 function getExtraAttr($attr=false, $default=null) { 769 if (!isset($this->_extra) && isset($this->extra)) 770 $this->_extra = JsonDataParser::decode($this->extra); 771 772 return $attr 773 ? (isset($this->_extra[$attr]) ? $this->_extra[$attr] : $default) 774 : $this->_extra; 775 } 776 777 function setExtraAttr($attr, $value, $commit=true) { 778 $this->getExtraAttr(); 779 $this->_extra[$attr] = $value; 780 $this->extra = JsonDataEncoder::encode($this->_extra); 781 782 if ($commit) { 783 $this->save(); 784 } 785 } 786 787 function getPermission() { 788 if (!isset($this->_perm)) { 789 $this->_perm = new RolePermission($this->permissions); 790 } 791 return $this->_perm; 792 } 793 794 function getPermissionInfo() { 795 return $this->getPermission()->getInfo(); 796 } 797 798 function onLogin($bk) { 799 // Update last apparent language preference 800 $this->setExtraAttr('browser_lang', 801 Internationalization::getCurrentLanguage(), 802 false); 803 804 $this->lastlogin = SqlFunction::NOW(); 805 $this->save(); 806 } 807 808 //Staff profile update...unfortunately we have to separate it from admin update to avoid potential issues 809 function updateProfile($vars, &$errors) { 810 global $cfg; 811 812 $vars['firstname']=Format::striptags($vars['firstname']); 813 $vars['lastname']=Format::striptags($vars['lastname']); 814 815 if (isset($this->staff_id) && $this->getId() != $vars['id']) 816 $errors['err']=__('Internal error occurred'); 817 818 if(!$vars['firstname']) 819 $errors['firstname']=__('First name is required'); 820 821 if(!$vars['lastname']) 822 $errors['lastname']=__('Last name is required'); 823 824 if(!$vars['email'] || !Validator::is_valid_email($vars['email'])) 825 $errors['email']=__('Valid email is required'); 826 elseif(Email::getIdByEmail($vars['email'])) 827 $errors['email']=__('Already in-use as system email'); 828 elseif (($uid=static::getIdByEmail($vars['email'])) 829 && (!isset($this->staff_id) || $uid!=$this->getId())) 830 $errors['email']=__('Email already in use by another agent'); 831 832 if($vars['phone'] && !Validator::is_phone($vars['phone'])) 833 $errors['phone']=__('Valid phone number is required'); 834 835 if($vars['mobile'] && !Validator::is_phone($vars['mobile'])) 836 $errors['mobile']=__('Valid phone number is required'); 837 838 if($vars['default_signature_type']=='mine' && !$vars['signature']) 839 $errors['default_signature_type'] = __("You don't have a signature"); 840 841 // Update the user's password if requested 842 if ($vars['passwd1']) { 843 try { 844 $this->setPassword($vars['passwd1'], $vars['cpasswd']); 845 } 846 catch (BadPassword $ex) { 847 $errors['passwd1'] = $ex->getMessage(); 848 } 849 catch (PasswordUpdateFailed $ex) { 850 // TODO: Add a warning banner or crash the update 851 } 852 } 853 854 $vars['onvacation'] = isset($vars['onvacation']) ? 1 : 0; 855 $this->firstname = $vars['firstname']; 856 $this->lastname = $vars['lastname']; 857 $this->email = $vars['email']; 858 $this->phone = Format::phone($vars['phone']); 859 $this->phone_ext = $vars['phone_ext']; 860 $this->mobile = Format::phone($vars['mobile']); 861 $this->signature = Format::sanitize($vars['signature']); 862 $this->timezone = $vars['timezone']; 863 $this->locale = $vars['locale']; 864 $this->max_page_size = $vars['max_page_size']; 865 $this->auto_refresh_rate = $vars['auto_refresh_rate']; 866 $this->default_signature_type = $vars['default_signature_type']; 867 $this->default_paper_size = $vars['default_paper_size']; 868 $this->lang = $vars['lang']; 869 $this->onvacation = $vars['onvacation']; 870 871 if (isset($vars['avatar_code'])) 872 $this->setExtraAttr('avatar', $vars['avatar_code']); 873 874 if ($errors) 875 return false; 876 877 $_SESSION['::lang'] = null; 878 TextDomain::configureForUser($this); 879 880 // Update the config information 881 $_config = new Config('staff.'.$this->getId()); 882 $_config->updateAll(array( 883 'datetime_format' => $vars['datetime_format'], 884 'default_from_name' => $vars['default_from_name'], 885 'default_2fa' => $vars['default_2fa'], 886 'thread_view_order' => $vars['thread_view_order'], 887 'default_ticket_queue_id' => $vars['default_ticket_queue_id'], 888 'reply_redirect' => ($vars['reply_redirect'] == 'Queue') ? 'Queue' : 'Ticket', 889 'img_att_view' => ($vars['img_att_view'] == 'inline') ? 'inline' : 'download', 890 'editor_spacing' => ($vars['editor_spacing'] == 'double') ? 'double' : 'single' 891 ) 892 ); 893 $this->_config = $_config->getInfo(); 894 895 return $this->save(); 896 } 897 898 function updateTeams($membership, &$errors) { 899 $dropped = array(); 900 foreach ($this->teams as $TM) 901 $dropped[$TM->team_id] = 1; 902 903 reset($membership); 904 while(list(, list($team_id, $alerts)) = each($membership)) { 905 $member = $this->teams->findFirst(array('team_id' => $team_id)); 906 if (!$member) { 907 $this->teams->add($member = new TeamMember(array( 908 'team_id' => $team_id, 909 ))); 910 } 911 $member->setAlerts($alerts); 912 if (!$errors) 913 $member->save(); 914 unset($dropped[$member->team_id]); 915 } 916 if (!$errors && $dropped) { 917 $member = $this->teams 918 ->filter(array('team_id__in' => array_keys($dropped))) 919 ->delete(); 920 $this->teams->reset(); 921 } 922 return true; 923 } 924 925 function delete() { 926 global $thisstaff; 927 928 if (!$thisstaff || $this->getId() == $thisstaff->getId()) 929 return false; 930 931 if (!parent::delete()) 932 return false; 933 934 $type = array('type' => 'deleted'); 935 Signal::send('object.deleted', $this, $type); 936 937 // DO SOME HOUSE CLEANING 938 //Move remove any ticket assignments...TODO: send alert to Dept. manager? 939 Ticket::objects() 940 ->filter(array('staff_id' => $this->getId())) 941 ->update(array('staff_id' => 0)); 942 943 //Update the poster and clear staff_id on ticket thread table. 944 ThreadEntry::objects() 945 ->filter(array('staff_id' => $this->getId())) 946 ->update(array( 947 'staff_id' => 0, 948 'poster' => $this->getName()->getOriginal(), 949 )); 950 951 // Cleanup Team membership table. 952 TeamMember::objects() 953 ->filter(array('staff_id'=>$this->getId())) 954 ->delete(); 955 956 // Cleanup staff dept access 957 StaffDeptAccess::objects() 958 ->filter(array('staff_id'=>$this->getId())) 959 ->delete(); 960 961 return true; 962 } 963 964 /**** Static functions ********/ 965 static function lookup($var) { 966 if (is_array($var)) 967 return parent::lookup($var); 968 elseif (is_numeric($var)) 969 return parent::lookup(array('staff_id' => (int) $var)); 970 elseif (Validator::is_email($var)) 971 return parent::lookup(array('email' => $var)); 972 elseif (is_string($var) && Validator::is_username($var)) 973 return parent::lookup(array('username' => (string) $var)); 974 else 975 return null; 976 } 977 978 static function getStaffMembers($criteria=array()) { 979 global $cfg; 980 981 $members = static::objects(); 982 983 if (isset($criteria['available'])) { 984 $members = $members->filter(array( 985 'onvacation' => 0, 986 'isactive' => 1, 987 )); 988 } 989 990 // Restrict agents based on visibility of the assigner 991 if (($staff=$criteria['staff'])) 992 $members = $staff->applyDeptVisibility($members); 993 994 $members = self::nsort($members); 995 996 $users=array(); 997 foreach ($members as $M) { 998 $users[$M->getId()] = $M->getName(); 999 } 1000 1001 return $users; 1002 } 1003 1004 static function getAvailableStaffMembers() { 1005 return self::getStaffMembers(array('available'=>true)); 1006 } 1007 1008 //returns agents in departments this agent has access to 1009 function getDeptAgents($criteria=array()) { 1010 $agents = static::objects() 1011 ->distinct('staff_id') 1012 ->select_related('dept') 1013 ->select_related('dept_access'); 1014 1015 $agents = $this->applyDeptVisibility($agents); 1016 1017 if (isset($criteria['available'])) { 1018 $agents = $agents->filter(array( 1019 'onvacation' => 0, 1020 'isactive' => 1, 1021 )); 1022 } 1023 1024 $agents = self::nsort($agents); 1025 1026 if (isset($criteria['namesOnly'])) { 1027 $clean = array(); 1028 foreach ($agents as $a) { 1029 $clean[$a->getId()] = $a->getName(); 1030 } 1031 return $clean; 1032 } 1033 1034 return $agents; 1035 } 1036 1037 static function getsortby($path='', $format=null) { 1038 global $cfg; 1039 1040 $format = $format ?: $cfg->getAgentNameFormat(); 1041 switch ($format) { 1042 case 'last': 1043 case 'lastfirst': 1044 case 'legal': 1045 $fields = array("{$path}lastname", "{$path}firstname"); 1046 break; 1047 default: 1048 $fields = array("${path}firstname", "${path}lastname"); 1049 } 1050 1051 return $fields; 1052 } 1053 1054 static function nsort(QuerySet $qs, $path='', $format=null) { 1055 $fields = self::getsortby($path, $format); 1056 $qs->order_by($fields); 1057 return $qs; 1058 } 1059 1060 static function getIdByUsername($username) { 1061 $row = static::objects()->filter(array('username' => $username)) 1062 ->values_flat('staff_id')->first(); 1063 return $row ? $row[0] : 0; 1064 } 1065 1066 static function getIdByEmail($email) { 1067 $row = static::objects()->filter(array('email' => $email)) 1068 ->values_flat('staff_id')->first(); 1069 return $row ? $row[0] : 0; 1070 } 1071 1072 1073 static function create($vars=false) { 1074 $staff = new static($vars); 1075 $staff->created = SqlFunction::NOW(); 1076 return $staff; 1077 } 1078 1079 function cancelResetTokens() { 1080 // TODO: Drop password-reset tokens from the config table for 1081 // this user id 1082 $sql = 'DELETE FROM '.CONFIG_TABLE.' WHERE `namespace`="pwreset" 1083 AND `value`='.db_input($this->getId()); 1084 db_query($sql, false); 1085 unset($_SESSION['_staff']['reset-token']); 1086 } 1087 1088 function sendResetEmail($template='pwreset-staff', $log=true) { 1089 global $ost, $cfg; 1090 1091 $content = Page::lookupByType($template); 1092 $token = Misc::randCode(48); // 290-bits 1093 1094 if (!$content) 1095 return new BaseError(/* @trans */ 'Unable to retrieve password reset email template'); 1096 1097 $vars = array( 1098 'url' => $ost->getConfig()->getBaseUrl(), 1099 'token' => $token, 1100 'staff' => $this, 1101 'recipient' => $this, 1102 'reset_link' => sprintf( 1103 "%s/scp/pwreset.php?token=%s", 1104 $ost->getConfig()->getBaseUrl(), 1105 $token), 1106 ); 1107 $vars['link'] = &$vars['reset_link']; 1108 1109 if (!($email = $cfg->getAlertEmail())) 1110 $email = $cfg->getDefaultEmail(); 1111 1112 $info = array('email' => $email, 'vars' => &$vars, 'log'=>$log); 1113 Signal::send('auth.pwreset.email', $this, $info); 1114 1115 if ($info['log']) 1116 $ost->logWarning(_S('Agent Password Reset'), sprintf( 1117 _S('Password reset was attempted for agent: %1$s<br><br> 1118 Requested-User-Id: %2$s<br> 1119 Source-Ip: %3$s<br> 1120 Email-Sent-To: %4$s<br> 1121 Email-Sent-Via: %5$s'), 1122 $this->getName(), 1123 $_POST['userid'], 1124 $_SERVER['REMOTE_ADDR'], 1125 $this->getEmail(), 1126 $email->getEmail() 1127 ), false); 1128 1129 $lang = $this->lang ?: $this->getExtraAttr('browser_lang'); 1130 $msg = $ost->replaceTemplateVariables(array( 1131 'subj' => $content->getLocalName($lang), 1132 'body' => $content->getLocalBody($lang), 1133 ), $vars); 1134 1135 $_config = new Config('pwreset'); 1136 $_config->set($vars['token'], $this->getId()); 1137 1138 $email->send($this->getEmail(), Format::striptags($msg['subj']), 1139 $msg['body']); 1140 } 1141 1142 static function importCsv($stream, $defaults=array(), $callback=false) { 1143 require_once INCLUDE_DIR . 'class.import.php'; 1144 1145 $importer = new CsvImporter($stream); 1146 $imported = 0; 1147 $fields = array( 1148 'firstname' => new TextboxField(array( 1149 'label' => __('First Name'), 1150 )), 1151 'lastname' => new TextboxField(array( 1152 'label' => __('Last Name'), 1153 )), 1154 'email' => new TextboxField(array( 1155 'label' => __('Email Address'), 1156 'configuration' => array( 1157 'validator' => 'email', 1158 ), 1159 )), 1160 'username' => new TextboxField(array( 1161 'label' => __('Username'), 1162 'validators' => function($self, $value) { 1163 if (!Validator::is_username($value)) 1164 $self->addError('Not a valid username'); 1165 }, 1166 )), 1167 ); 1168 $form = new SimpleForm($fields); 1169 1170 try { 1171 db_autocommit(false); 1172 $errors = array(); 1173 $records = $importer->importCsv($form->getFields(), $defaults); 1174 foreach ($records as $data) { 1175 if (!isset($data['email']) || !isset($data['username'])) 1176 throw new ImportError('Both `username` and `email` fields are required'); 1177 1178 if ($agent = self::lookup(array('username' => $data['username']))) { 1179 // TODO: Update the user 1180 } 1181 elseif ($agent = self::create($data, $errors)) { 1182 if ($callback) 1183 $callback($agent, $data); 1184 $agent->save(); 1185 } 1186 else { 1187 throw new ImportError(sprintf(__('Unable to import (%s): %s'), 1188 Format::htmlchars($data['username']), 1189 print_r(Format::htmlchars($errors), true) 1190 )); 1191 } 1192 $imported++; 1193 } 1194 db_autocommit(true); 1195 } 1196 catch (Exception $ex) { 1197 db_rollback(); 1198 return $ex->getMessage(); 1199 } 1200 return $imported; 1201 } 1202 1203 function save($refetch=false) { 1204 if ($this->dirty) 1205 $this->updated = SqlFunction::NOW(); 1206 return parent::save($refetch || $this->dirty); 1207 } 1208 1209 function update($vars, &$errors) { 1210 $vars['username']=Format::striptags($vars['username']); 1211 $vars['firstname']=Format::striptags($vars['firstname']); 1212 $vars['lastname']=Format::striptags($vars['lastname']); 1213 1214 if (isset($this->staff_id) && $this->getId() != $vars['id']) 1215 $errors['err']=__('Internal error occurred'); 1216 1217 if(!$vars['firstname']) 1218 $errors['firstname']=__('First name required'); 1219 if(!$vars['lastname']) 1220 $errors['lastname']=__('Last name required'); 1221 1222 $error = ''; 1223 if(!$vars['username'] || !Validator::is_username($vars['username'], $error)) 1224 $errors['username']=($error) ? $error : __('Username is required'); 1225 elseif (($uid=static::getIdByUsername($vars['username'])) 1226 && (!isset($this->staff_id) || $uid!=$this->getId())) 1227 $errors['username']=__('Username already in use'); 1228 1229 if(!$vars['email'] || !Validator::is_valid_email($vars['email'])) 1230 $errors['email']=__('Valid email is required'); 1231 elseif(Email::getIdByEmail($vars['email'])) 1232 $errors['email']=__('Already in use system email'); 1233 elseif (($uid=static::getIdByEmail($vars['email'])) 1234 && (!isset($this->staff_id) || $uid!=$this->getId())) 1235 $errors['email']=__('Email already in use by another agent'); 1236 1237 if($vars['phone'] && !Validator::is_phone($vars['phone'])) 1238 $errors['phone']=__('Valid phone number is required'); 1239 1240 if($vars['mobile'] && !Validator::is_phone($vars['mobile'])) 1241 $errors['mobile']=__('Valid phone number is required'); 1242 1243 if(!$vars['dept_id']) 1244 $errors['dept_id']=__('Department is required'); 1245 if(!$vars['role_id']) 1246 $errors['role_id']=__('Role for primary department is required'); 1247 1248 $dept = Dept::lookup($vars['dept_id']); 1249 if($dept && !$dept->isActive()) 1250 $errors['dept_id'] = sprintf(__('%s selected must be active'), __('Department')); 1251 1252 // Ensure we will still have an administrator with access 1253 if ($vars['isadmin'] !== '1' || $vars['islocked'] === '1') { 1254 $sql = 'select count(*), max(staff_id) from '.STAFF_TABLE 1255 .' WHERE isadmin=1 and isactive=1'; 1256 if (($res = db_query($sql)) 1257 && (list($count, $sid) = db_fetch_row($res))) { 1258 if ($count == 1 && $sid == $uid) { 1259 $errors['isadmin'] = __( 1260 'Cowardly refusing to remove or lock out the only active administrator' 1261 ); 1262 } 1263 } 1264 } 1265 1266 // Update the local permissions 1267 $this->updatePerms($vars['perms'], $errors); 1268 1269 //checkboxes 1270 $vars['isadmin'] = isset($vars['isadmin']) ? 1 : 0; 1271 $vars['islocked'] = isset($vars['islocked']) ? 0 : 1; 1272 $vars['isvisible'] = isset($vars['isvisible']) ? 1 : 0; 1273 $vars['onvacation'] = isset($vars['onvacation']) ? 1 : 0; 1274 $vars['assigned_only'] = isset($vars['assigned_only']) ? 1 : 0; 1275 1276 $this->isadmin = $vars['isadmin']; 1277 $this->isactive = $vars['islocked']; 1278 $this->isvisible = $vars['isvisible']; 1279 $this->onvacation = $vars['onvacation']; 1280 $this->assigned_only = $vars['assigned_only']; 1281 $this->role_id = $vars['role_id']; 1282 $this->username = $vars['username']; 1283 $this->firstname = $vars['firstname']; 1284 $this->lastname = $vars['lastname']; 1285 $this->email = $vars['email']; 1286 $this->backend = $vars['backend']; 1287 $this->phone = Format::phone($vars['phone']); 1288 $this->phone_ext = $vars['phone_ext']; 1289 $this->mobile = Format::phone($vars['mobile']); 1290 $this->notes = Format::sanitize($vars['notes']); 1291 1292 // Set staff password if exists 1293 if (!$vars['welcome_email'] && $vars['passwd1']) { 1294 $this->setPassword($vars['passwd1'], null); 1295 $this->change_passwd = $vars['change_passwd'] ? 1 : 0; 1296 } 1297 1298 if ($errors) 1299 return false; 1300 1301 if ($this->save()) { 1302 // Update some things for ::updateAccess to inspect 1303 $this->setDepartmentId($vars['dept_id']); 1304 1305 // Format access update as [array(dept_id, role_id, alerts?)] 1306 $access = array(); 1307 if (isset($vars['dept_access'])) { 1308 foreach (@$vars['dept_access'] as $dept_id) { 1309 $access[] = array($dept_id, $vars['dept_access_role'][$dept_id], 1310 @$vars['dept_access_alerts'][$dept_id]); 1311 } 1312 } 1313 $this->updateAccess($access, $errors); 1314 $this->setExtraAttr('def_assn_role', 1315 isset($vars['assign_use_pri_role']), true); 1316 1317 // Format team membership as [array(team_id, alerts?)] 1318 $teams = array(); 1319 if (isset($vars['teams'])) { 1320 foreach (@$vars['teams'] as $team_id) { 1321 $teams[] = array($team_id, @$vars['team_alerts'][$team_id]); 1322 } 1323 } 1324 $this->updateTeams($teams, $errors); 1325 1326 if ($vars['welcome_email']) 1327 $this->sendResetEmail('registration-staff', false); 1328 return true; 1329 } 1330 1331 if (isset($this->staff_id)) { 1332 $errors['err']=sprintf(__('Unable to update %s.'), __('this agent')) 1333 .' '.__('Internal error occurred'); 1334 } else { 1335 $errors['err']=sprintf(__('Unable to create %s.'), __('this agent')) 1336 .' '.__('Internal error occurred'); 1337 } 1338 return false; 1339 } 1340 1341 /** 1342 * Parameters: 1343 * $access - (<array($dept_id, $role_id, $alerts)>) a list of the complete, 1344 * extended access for this agent. Any the agent currently has, which 1345 * is not listed will be removed. 1346 * $errors - (<array>) list of error messages from the process, which will 1347 * be indexed by the dept_id number. 1348 */ 1349 function updateAccess($access, &$errors) { 1350 reset($access); 1351 $dropped = array(); 1352 foreach ($this->dept_access as $DA) 1353 $dropped[$DA->dept_id] = 1; 1354 while (list(, list($dept_id, $role_id, $alerts)) = each($access)) { 1355 unset($dropped[$dept_id]); 1356 if (!$role_id || !Role::lookup($role_id)) 1357 $errors['dept_access'][$dept_id] = __('Select a valid role'); 1358 if (!$dept_id || !($dept=Dept::lookup($dept_id))) 1359 $errors['dept_access'][$dept_id] = __('Select a valid department'); 1360 if ($dept_id == $this->getDeptId()) 1361 $errors['dept_access'][$dept_id] = sprintf(__('Agent already has access to %s'), __('this department')); 1362 $da = $this->dept_access->findFirst(array('dept_id' => $dept_id)); 1363 if (!isset($da)) { 1364 $da = new StaffDeptAccess(array( 1365 'dept_id' => $dept_id, 'role_id' => $role_id 1366 )); 1367 $this->dept_access->add($da); 1368 $type = array('type' => 'edited', 1369 'key' => sprintf('%s Department Access Added', $dept->getName())); 1370 Signal::send('object.edited', $this, $type); 1371 } 1372 else { 1373 $da->role_id = $role_id; 1374 } 1375 $da->setAlerts($alerts); 1376 if (!$errors) 1377 $da->save(); 1378 } 1379 if (!$errors && $dropped) { 1380 $this->dept_access 1381 ->filter(array('dept_id__in' => array_keys($dropped))) 1382 ->delete(); 1383 $this->dept_access->reset(); 1384 foreach (array_keys($dropped) as $dept_id) { 1385 $deptName = Dept::getNameById($dept_id); 1386 $type = array('type' => 'edited', 1387 'key' => sprintf('%s Department Access Removed', $deptName)); 1388 Signal::send('object.edited', $this, $type); 1389 } 1390 } 1391 return !$errors; 1392 } 1393 1394 function updatePerms($vars, &$errors=array()) { 1395 if (!$vars) { 1396 $this->permissions = ''; 1397 return; 1398 } 1399 $permissions = $this->getPermission(); 1400 foreach ($vars as $k => $val) { 1401 if (!$permissions->exists($val)) { 1402 $type = array('type' => 'edited', 'key' => $val); 1403 Signal::send('object.edited', $this, $type); 1404 } 1405 } 1406 1407 foreach (RolePermission::allPermissions() as $g => $perms) { 1408 foreach ($perms as $k => $v) { 1409 if (!in_array($k, $vars) && $permissions->exists($k)) { 1410 $type = array('type' => 'edited', 'key' => $k); 1411 Signal::send('object.edited', $this, $type); 1412 } 1413 $permissions->set($k, in_array($k, $vars) ? 1 : 0); 1414 } 1415 } 1416 $this->permissions = $permissions->toJson(); 1417 return true; 1418 } 1419 1420 static function export($criteria=null, $filename='') { 1421 include_once(INCLUDE_DIR.'class.error.php'); 1422 1423 $agents = Staff::objects(); 1424 // Sort based on name formating 1425 $agents = self::nsort($agents); 1426 Export::agents($agents, $filename); 1427 } 1428 1429 static function getPermissions() { 1430 return self::$perms; 1431 } 1432 1433} 1434RolePermission::register(/* @trans */ 'Miscellaneous', Staff::getPermissions()); 1435 1436interface RestrictedAccess { 1437 function checkStaffPerm($staff); 1438} 1439 1440class StaffDeptAccess extends VerySimpleModel { 1441 static $meta = array( 1442 'table' => STAFF_DEPT_TABLE, 1443 'pk' => array('staff_id', 'dept_id'), 1444 'select_related' => array('dept', 'role'), 1445 'joins' => array( 1446 'dept' => array( 1447 'constraint' => array('dept_id' => 'Dept.id'), 1448 ), 1449 'staff' => array( 1450 'constraint' => array('staff_id' => 'Staff.staff_id'), 1451 ), 1452 'role' => array( 1453 'constraint' => array('role_id' => 'Role.id'), 1454 ), 1455 ), 1456 ); 1457 1458 const FLAG_ALERTS = 0x0001; 1459 1460 function isAlertsEnabled() { 1461 return $this->flags & self::FLAG_ALERTS != 0; 1462 } 1463 1464 function setFlag($flag, $value) { 1465 if ($value) 1466 $this->flags |= $flag; 1467 else 1468 $this->flags &= ~$flag; 1469 } 1470 1471 function setAlerts($value) { 1472 $this->setFlag(self::FLAG_ALERTS, $value); 1473 } 1474} 1475 1476/** 1477 * This form is used to administratively change the password. The 1478 * ChangePasswordForm is used for an agent to change their own password. 1479 */ 1480class PasswordResetForm 1481extends AbstractForm { 1482 function buildFields() { 1483 return array( 1484 'welcome_email' => new BooleanField(array( 1485 'default' => true, 1486 'configuration' => array( 1487 'desc' => __('Send the agent a password reset email'), 1488 ), 1489 )), 1490 'passwd1' => new PasswordField(array( 1491 'placeholder' => __('New Password'), 1492 'required' => true, 1493 'configuration' => array( 1494 'classes' => 'span12', 1495 ), 1496 'visibility' => new VisibilityConstraint( 1497 new Q(array('welcome_email' => false)), 1498 VisibilityConstraint::HIDDEN 1499 ), 1500 'validator' => '', 1501 'validators' => function($self, $v) { 1502 try { 1503 Staff::checkPassword($v, null); 1504 } catch (BadPassword $ex) { 1505 $self->addError($ex->getMessage()); 1506 } 1507 }, 1508 )), 1509 'passwd2' => new PasswordField(array( 1510 'placeholder' => __('Confirm Password'), 1511 'required' => true, 1512 'configuration' => array( 1513 'classes' => 'span12', 1514 ), 1515 'visibility' => new VisibilityConstraint( 1516 new Q(array('welcome_email' => false)), 1517 VisibilityConstraint::HIDDEN 1518 ), 1519 'validator' => '', 1520 'validators' => function($self, $v) { 1521 try { 1522 Staff::checkPassword($v, null); 1523 } catch (BadPassword $ex) { 1524 $self->addError($ex->getMessage()); 1525 } 1526 }, 1527 )), 1528 'change_passwd' => new BooleanField(array( 1529 'default' => true, 1530 'configuration' => array( 1531 'desc' => __('Require password change at next login'), 1532 'classes' => 'form footer', 1533 ), 1534 'visibility' => new VisibilityConstraint( 1535 new Q(array('welcome_email' => false)), 1536 VisibilityConstraint::HIDDEN 1537 ), 1538 )), 1539 ); 1540 } 1541 1542 function validate($clean) { 1543 if ($clean['passwd1'] != $clean['passwd2']) 1544 $this->getField('passwd1')->addError(__('Passwords do not match')); 1545 } 1546} 1547 1548class PasswordChangeForm 1549extends AbstractForm { 1550 function buildFields() { 1551 $fields = array( 1552 'current' => new PasswordField(array( 1553 'placeholder' => __('Current Password'), 1554 'required' => true, 1555 'configuration' => array( 1556 'autofocus' => true, 1557 ), 1558 'validator' => 'noop', 1559 )), 1560 'passwd1' => new PasswordField(array( 1561 'label' => __('Enter a new password'), 1562 'placeholder' => __('New Password'), 1563 'required' => true, 1564 'validator' => '', 1565 'validators' => function($self, $v) { 1566 try { 1567 Staff::checkPassword($v, null); 1568 } catch (BadPassword $ex) { 1569 $self->addError($ex->getMessage()); 1570 } 1571 }, 1572 )), 1573 'passwd2' => new PasswordField(array( 1574 'placeholder' => __('Confirm Password'), 1575 'required' => true, 1576 'validator' => '', 1577 'validators' => function($self, $v) { 1578 try { 1579 Staff::checkPassword($v, null); 1580 } catch (BadPassword $ex) { 1581 $self->addError($ex->getMessage()); 1582 } 1583 }, 1584 )), 1585 ); 1586 1587 // When using the password reset system, the current password is not 1588 // required for agents. 1589 if (isset($_SESSION['_staff']['reset-token'])) { 1590 unset($fields['current']); 1591 $fields['passwd1']->set('configuration', array('autofocus' => true)); 1592 } 1593 else { 1594 $fields['passwd1']->set('layout', 1595 new GridFluidCell(12, array('style' => 'padding-top: 20px')) 1596 ); 1597 } 1598 return $fields; 1599 } 1600 1601 function getInstructions() { 1602 return __('Confirm your current password and enter a new password to continue'); 1603 } 1604 1605 function validate($clean) { 1606 if ($clean['passwd1'] != $clean['passwd2']) 1607 $this->getField('passwd1')->addError(__('Passwords do not match')); 1608 } 1609} 1610 1611class ResetAgentPermissionsForm 1612extends AbstractForm { 1613 function buildFields() { 1614 $permissions = array(); 1615 foreach (RolePermission::allPermissions() as $g => $perms) { 1616 foreach ($perms as $k => $v) { 1617 if (!$v['primary']) 1618 continue; 1619 $permissions[$g][$k] = "{$v['title']} — {$v['desc']}"; 1620 } 1621 } 1622 return array( 1623 'clone' => new ChoiceField(array( 1624 'default' => 0, 1625 'choices' => 1626 array(0 => '— '.__('Clone an existing agent').' —') 1627 + Staff::getStaffMembers(), 1628 'configuration' => array( 1629 'classes' => 'span12', 1630 ), 1631 )), 1632 'perms' => new ChoiceField(array( 1633 'choices' => $permissions, 1634 'widget' => 'TabbedBoxChoicesWidget', 1635 'configuration' => array( 1636 'multiple' => true, 1637 ), 1638 )), 1639 ); 1640 } 1641 1642 function getClean($validate = true) { 1643 $clean = parent::getClean(); 1644 // Index permissions as ['ticket.edit' => 1] 1645 $clean['perms'] = array_keys($clean['perms']); 1646 return $clean; 1647 } 1648 1649 function render($staff=true, $title=false, $options=array()) { 1650 return parent::render($staff, $title, $options + array('template' => 'dynamic-form-simple.tmpl.php')); 1651 } 1652} 1653 1654class ChangeDepartmentForm 1655extends AbstractForm { 1656 function buildFields() { 1657 return array( 1658 'dept_id' => new ChoiceField(array( 1659 'default' => 0, 1660 'required' => true, 1661 'label' => __('Primary Department'), 1662 'choices' => 1663 array(0 => '— '.__('Primary Department').' —') 1664 + Dept::getDepartments(), 1665 'configuration' => array( 1666 'classes' => 'span12', 1667 ), 1668 )), 1669 'role_id' => new ChoiceField(array( 1670 'default' => 0, 1671 'required' => true, 1672 'label' => __('Primary Role'), 1673 'choices' => 1674 array(0 => '— '.__('Corresponding Role').' —') 1675 + Role::getRoles(), 1676 'configuration' => array( 1677 'classes' => 'span12', 1678 ), 1679 )), 1680 'eavesdrop' => new BooleanField(array( 1681 'configuration' => array( 1682 'desc' => __('Maintain access to current primary department'), 1683 'classes' => 'form footer', 1684 ), 1685 )), 1686 // alerts? 1687 ); 1688 } 1689 1690 function getInstructions() { 1691 return __('Change the primary department and primary role of the selected agents'); 1692 } 1693 1694 function getClean($validate = true) { 1695 $clean = parent::getClean(); 1696 $clean['eavesdrop'] = $clean['eavesdrop'] ? 1 : 0; 1697 return $clean; 1698 } 1699 1700 function render($staff=true, $title=false, $options=array()) { 1701 return parent::render($staff, $title, $options + array('template' => 'dynamic-form-simple.tmpl.php')); 1702 } 1703} 1704 1705class StaffQuickAddForm 1706extends AbstractForm { 1707 static $layout = 'GridFormLayout'; 1708 1709 function buildFields() { 1710 global $cfg; 1711 1712 return array( 1713 'firstname' => new TextboxField(array( 1714 'required' => true, 1715 'configuration' => array( 1716 'placeholder' => __("First Name"), 1717 'autofocus' => true, 1718 ), 1719 'layout' => new GridFluidCell(6), 1720 )), 1721 'lastname' => new TextboxField(array( 1722 'required' => true, 1723 'configuration' => array( 1724 'placeholder' => __("Last Name"), 1725 ), 1726 'layout' => new GridFluidCell(6), 1727 )), 1728 'email' => new TextboxField(array( 1729 'required' => true, 1730 'configuration' => array( 1731 'validator' => 'email', 1732 'placeholder' => __('Email Address — e.g. me@mycompany.com'), 1733 'length' => 128, 1734 'autocomplete' => 'email', 1735 ), 1736 )), 1737 'dept_id' => new ChoiceField(array( 1738 'label' => __('Department'), 1739 'required' => true, 1740 'choices' => Dept::getDepartments(), 1741 'default' => $cfg->getDefaultDeptId(), 1742 'layout' => new GridFluidCell(6), 1743 )), 1744 'role_id' => new ChoiceField(array( 1745 'label' => __('Primary Role'), 1746 'required' => true, 1747 'choices' => Role::getRoles(), 1748 'layout' => new GridFluidCell(6), 1749 )), 1750 'isadmin' => new BooleanField(array( 1751 'label' => __('Account Type'), 1752 'configuration' => array( 1753 'desc' => __('Agent has access to the admin panel'), 1754 ), 1755 'layout' => new GridFluidCell(6), 1756 )), 1757 'welcome_email' => new BooleanField(array( 1758 'configuration' => array( 1759 'desc' => __('Send a welcome email with login information'), 1760 ), 1761 'default' => true, 1762 'layout' => new GridFluidCell(12, array('style' => 'padding-top: 50px')), 1763 )), 1764 'passwd1' => new PasswordField(array( 1765 'required' => true, 1766 'configuration' => array( 1767 'placeholder' => __("Temporary Password"), 1768 'autocomplete' => 'new-password', 1769 ), 1770 'validator' => '', 1771 'validators' => function($self, $v) { 1772 try { 1773 Staff::checkPassword($v, null); 1774 } catch (BadPassword $ex) { 1775 $self->addError($ex->getMessage()); 1776 } 1777 }, 1778 'visibility' => new VisibilityConstraint( 1779 new Q(array('welcome_email' => false)) 1780 ), 1781 'layout' => new GridFluidCell(6), 1782 )), 1783 'passwd2' => new PasswordField(array( 1784 'required' => true, 1785 'configuration' => array( 1786 'placeholder' => __("Confirm Password"), 1787 'autocomplete' => 'new-password', 1788 ), 1789 'visibility' => new VisibilityConstraint( 1790 new Q(array('welcome_email' => false)) 1791 ), 1792 'layout' => new GridFluidCell(6), 1793 )), 1794 // TODO: Add role_id drop-down 1795 ); 1796 } 1797 1798 function getClean($validate = true) { 1799 $clean = parent::getClean(); 1800 list($clean['username'],) = preg_split('/[^\w.-]/u', $clean['email'], 2); 1801 if (mb_strlen($clean['username']) < 3 || Staff::lookup($clean['username'])) 1802 $clean['username'] = mb_strtolower($clean['firstname']); 1803 1804 1805 // Inherit default dept's role as primary role 1806 $clean['assign_use_pri_role'] = true; 1807 1808 // Default permissions 1809 $clean['perms'] = array( 1810 User::PERM_CREATE, 1811 User::PERM_EDIT, 1812 User::PERM_DELETE, 1813 User::PERM_MANAGE, 1814 User::PERM_DIRECTORY, 1815 Organization::PERM_CREATE, 1816 Organization::PERM_EDIT, 1817 Organization::PERM_DELETE, 1818 FAQ::PERM_MANAGE, 1819 Dept::PERM_DEPT, 1820 Staff::PERM_STAFF, 1821 ); 1822 return $clean; 1823 } 1824} 1825