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