1<?php
2/*********************************************************************
3    class.user.php
4
5    External end-user identification for osTicket
6
7    Peter Rotich <peter@osticket.com>
8    Jared Hancock <jared@osticket.com>
9    Copyright (c)  2006-2013 osTicket
10    http://www.osticket.com
11
12    Released under the GNU General Public License WITHOUT ANY WARRANTY.
13    See LICENSE.TXT for details.
14
15    vim: expandtab sw=4 ts=4 sts=4:
16**********************************************************************/
17require_once INCLUDE_DIR . 'class.orm.php';
18require_once INCLUDE_DIR . 'class.util.php';
19require_once INCLUDE_DIR . 'class.variable.php';
20require_once INCLUDE_DIR . 'class.search.php';
21require_once INCLUDE_DIR . 'class.organization.php';
22
23class UserEmailModel extends VerySimpleModel {
24    static $meta = array(
25        'table' => USER_EMAIL_TABLE,
26        'pk' => array('id'),
27        'joins' => array(
28            'user' => array(
29                'constraint' => array('user_id' => 'UserModel.id')
30            )
31        )
32    );
33
34    function __toString() {
35        return (string) $this->address;
36    }
37
38    static function getIdByEmail($email) {
39        $row = UserEmailModel::objects()
40            ->filter(array('address'=>$email))
41            ->values_flat('user_id')
42            ->first();
43
44        return $row ? $row[0] : 0;
45    }
46}
47
48class UserModel extends VerySimpleModel {
49    static $meta = array(
50        'table' => USER_TABLE,
51        'pk' => array('id'),
52        'select_related' => array('default_email', 'org', 'account'),
53        'joins' => array(
54            'emails' => array(
55                'reverse' => 'UserEmailModel.user',
56            ),
57            'tickets' => array(
58                'null' => true,
59                'reverse' => 'Ticket.user',
60            ),
61            'account' => array(
62                'list' => false,
63                'null' => true,
64                'reverse' => 'ClientAccount.user',
65            ),
66            'org' => array(
67                'null' => true,
68                'constraint' => array('org_id' => 'Organization.id')
69            ),
70            'default_email' => array(
71                'null' => true,
72                'constraint' => array('default_email_id' => 'UserEmailModel.id')
73            ),
74            'cdata' => array(
75                'constraint' => array('id' => 'UserCdata.user_id'),
76                'null' => true,
77            ),
78            'entries' => array(
79                'constraint' => array(
80                    'id' => 'DynamicFormEntry.object_id',
81                    "'U'" => 'DynamicFormEntry.object_type',
82                ),
83                'list' => true,
84            ),
85        )
86    );
87
88    const PRIMARY_ORG_CONTACT   = 0x0001;
89
90    const PERM_CREATE =     'user.create';
91    const PERM_EDIT =       'user.edit';
92    const PERM_DELETE =     'user.delete';
93    const PERM_MANAGE =     'user.manage';
94    const PERM_DIRECTORY =  'user.dir';
95
96    static protected $perms = array(
97        self::PERM_CREATE => array(
98            'title' => /* @trans */ 'Create',
99            'desc' => /* @trans */ 'Ability to add new users',
100            'primary' => true,
101        ),
102        self::PERM_EDIT => array(
103            'title' => /* @trans */ 'Edit',
104            'desc' => /* @trans */ 'Ability to manage user information',
105            'primary' => true,
106        ),
107        self::PERM_DELETE => array(
108            'title' => /* @trans */ 'Delete',
109            'desc' => /* @trans */ 'Ability to delete users',
110            'primary' => true,
111        ),
112        self::PERM_MANAGE => array(
113            'title' => /* @trans */ 'Manage Account',
114            'desc' => /* @trans */ 'Ability to manage active user accounts',
115            'primary' => true,
116        ),
117        self::PERM_DIRECTORY => array(
118            'title' => /* @trans */ 'User Directory',
119            'desc' => /* @trans */ 'Ability to access the user directory',
120            'primary' => true,
121        ),
122    );
123
124    function getId() {
125        return $this->id;
126    }
127
128    function getDefaultEmailAddress() {
129        return $this->getDefaultEmail()->address;
130    }
131
132    function getDefaultEmail() {
133        return $this->default_email;
134    }
135
136    function hasAccount() {
137        return !is_null($this->account);
138    }
139    function getAccount() {
140        return $this->account;
141    }
142
143    function getOrgId() {
144         return $this->get('org_id');
145    }
146
147    function getOrganization() {
148        return $this->org;
149    }
150
151    function setOrganization($org, $save=true) {
152
153        $this->set('org', $org);
154
155        if ($save)
156            $this->save();
157
158        return true;
159    }
160
161    public function setFlag($flag, $val) {
162        if ($val)
163            $this->status |= $flag;
164        else
165            $this->status &= ~$flag;
166    }
167
168    protected function hasStatus($flag) {
169        return $this->get('status') & $flag !== 0;
170    }
171
172    protected function clearStatus($flag) {
173        return $this->set('status', $this->get('status') & ~$flag);
174    }
175
176    protected function setStatus($flag) {
177        return $this->set('status', $this->get('status') | $flag);
178    }
179
180    function isPrimaryContact() {
181        return $this->hasStatus(User::PRIMARY_ORG_CONTACT);
182    }
183
184    function setPrimaryContact($flag) {
185        if ($flag)
186            $this->setStatus(User::PRIMARY_ORG_CONTACT);
187        else
188            $this->clearStatus(User::PRIMARY_ORG_CONTACT);
189    }
190
191    static function getPermissions() {
192        return self::$perms;
193    }
194}
195include_once INCLUDE_DIR.'class.role.php';
196RolePermission::register(/* @trans */ 'Users', UserModel::getPermissions());
197
198class UserCdata extends VerySimpleModel {
199    static $meta = array(
200        'table' => USER_CDATA_TABLE,
201        'pk' => array('user_id'),
202        'joins' => array(
203            'user' => array(
204                'constraint' => array('user_id' => 'UserModel.id'),
205            ),
206        ),
207    );
208}
209
210class User extends UserModel
211implements TemplateVariable, Searchable {
212
213    var $_email;
214    var $_entries;
215    var $_forms;
216    var $_queue;
217
218
219
220    static function fromVars($vars, $create=true, $update=false) {
221        // Try and lookup by email address
222        $user = static::lookupByEmail($vars['email']);
223        if (!$user && $create) {
224            $name = $vars['name'];
225            if (is_array($name))
226                $name = implode(', ', $name);
227            elseif (!$name)
228                list($name) = explode('@', $vars['email'], 2);
229
230            $user = new User(array(
231                'name' => Format::htmldecode(Format::sanitize($name, false)),
232                'created' => new SqlFunction('NOW'),
233                'updated' => new SqlFunction('NOW'),
234                //XXX: Do plain create once the cause
235                // of the detached emails is fixed.
236                'default_email' => UserEmail::ensure($vars['email'])
237            ));
238            // Is there an organization registered for this domain
239            list($mailbox, $domain) = explode('@', $vars['email'], 2);
240            if (isset($vars['org_id']))
241                $user->set('org_id', $vars['org_id']);
242            elseif ($org = Organization::forDomain($domain))
243                $user->setOrganization($org, false);
244
245            try {
246                $user->save(true);
247                $user->emails->add($user->default_email);
248                // Attach initial custom fields
249                $user->addDynamicData($vars);
250            }
251            catch (OrmException $e) {
252                return null;
253            }
254            $type = array('type' => 'created');
255            Signal::send('object.created', $user, $type);
256            Signal::send('user.created', $user);
257        }
258        elseif ($update) {
259            $errors = array();
260            $user->updateInfo($vars, $errors, true);
261        }
262
263        return $user;
264    }
265
266    static function fromForm($form, $create=true) {
267        global $thisstaff;
268
269        if(!$form) return null;
270
271        //Validate the form
272        $valid = true;
273        $filter = function($f) use ($thisstaff) {
274            return !isset($thisstaff) || $f->isRequiredForStaff() || $f->isVisibleToStaff();
275        };
276        if (!$form->isValid($filter))
277            $valid  = false;
278
279        //Make sure the email is not in-use
280        if (($field=$form->getField('email'))
281                && $field->getClean()
282                && User::lookup(array('emails__address'=>$field->getClean()))) {
283            $field->addError(__('Email is assigned to another user'));
284            $valid = false;
285        }
286
287        return $valid ? self::fromVars($form->getClean(), $create) : null;
288    }
289
290    function getEmail() {
291
292        if (!isset($this->_email))
293            $this->_email = new EmailAddress(sprintf('"%s" <%s>',
294                    addcslashes($this->getName(), '"'),
295                    $this->default_email->address));
296
297        return $this->_email;
298    }
299
300    function getAvatar($size=null) {
301        global $cfg;
302        $source = $cfg->getClientAvatarSource();
303        $avatar = $source->getAvatar($this);
304        if (isset($size))
305            $avatar->setSize($size);
306        return $avatar;
307    }
308
309    function getFullName() {
310        return $this->name;
311    }
312
313    function getPhoneNumber() {
314        foreach ($this->getDynamicData() as $e)
315            if ($a = $e->getAnswer('phone'))
316                return $a;
317    }
318
319    function getName() {
320        if (!$this->name)
321            list($name) = explode('@', $this->getDefaultEmailAddress(), 2);
322        else
323            $name = $this->name;
324        return new UsersName($name);
325    }
326
327    function getUpdateDate() {
328        return $this->updated;
329    }
330
331    function getCreateDate() {
332        return $this->created;
333    }
334
335    function getTimezone() {
336        global $cfg;
337
338        if (($acct = $this->getAccount()) && ($tz = $acct->getTimezone())) {
339            return $tz;
340        }
341        return $cfg->getDefaultTimezone();
342    }
343
344    function addForm($form, $sort=1, $data=null) {
345        $entry = $form->instanciate($sort, $data);
346        $entry->set('object_type', 'U');
347        $entry->set('object_id', $this->getId());
348        $entry->save();
349        return $entry;
350    }
351
352    function getLanguage($flags=false) {
353        if ($acct = $this->getAccount())
354            return $acct->getLanguage($flags);
355    }
356
357    function to_json() {
358
359        $info = array(
360                'id'  => $this->getId(),
361                'name' => Format::htmlchars($this->getName()),
362                'email' => (string) $this->getEmail(),
363                'phone' => (string) $this->getPhoneNumber());
364
365        return Format::json_encode($info);
366    }
367
368    function __toString() {
369        return $this->asVar();
370    }
371
372    function asVar() {
373        return (string) $this->getName();
374    }
375
376    function getVar($tag) {
377        $tag = mb_strtolower($tag);
378        foreach ($this->getDynamicData() as $e)
379            if ($a = $e->getAnswer($tag))
380                return $a;
381    }
382
383    static function getVarScope() {
384        $base = array(
385            'email' => array(
386                'class' => 'EmailAddress', 'desc' => __('Default email address')
387            ),
388            'name' => array(
389                'class' => 'PersonsName', 'desc' => 'User name, default format'
390            ),
391            'organization' => array('class' => 'Organization', 'desc' => __('Organization')),
392        );
393        $extra = VariableReplacer::compileFormScope(UserForm::getInstance());
394        return $base + $extra;
395    }
396
397    static function getSearchableFields() {
398        $base = array();
399        $uform = UserForm::getUserForm();
400        $base = array();
401        foreach ($uform->getFields() as $F) {
402            $fname = $F->get('name') ?: ('field_'.$F->get('id'));
403            # XXX: email in the model corresponds to `emails__address` ORM path
404            if ($fname == 'email')
405                $fname = 'emails__address';
406            if (!$F->hasData() || $F->isPresentationOnly())
407                continue;
408            if (!$F->isStorable())
409                $base[$fname] = $F;
410            else
411                $base["cdata__{$fname}"] = $F;
412        }
413        return $base;
414    }
415
416    static function supportsCustomData() {
417        return true;
418    }
419
420    function addDynamicData($data) {
421        return $this->addForm(UserForm::objects()->one(), 1, $data);
422    }
423
424    function getDynamicData($create=true) {
425        if (!isset($this->_entries)) {
426            $this->_entries = DynamicFormEntry::forObject($this->id, 'U')->all();
427            if (!$this->_entries && $create) {
428                $g = UserForm::getNewInstance();
429                $g->setClientId($this->id);
430                $g->save();
431                $this->_entries[] = $g;
432            }
433        }
434
435        return $this->_entries ?: array();
436    }
437
438    function getFilterData() {
439        $vars = array();
440        foreach ($this->getDynamicData() as $entry) {
441            $vars += $entry->getFilterData();
442
443            // Add in special `name` and `email` fields
444            if ($entry->getDynamicForm()->get('type') != 'U')
445                continue;
446
447            foreach (array('name', 'email') as $name) {
448                if ($f = $entry->getField($name))
449                    $vars['field.'.$f->get('id')] =
450                        $name == 'name' ? $this->getName() : $this->getEmail();
451            }
452        }
453
454        return $vars;
455    }
456
457    function getForms($data=null, $cb=null) {
458
459        if (!isset($this->_forms)) {
460            $this->_forms = array();
461            $cb = $cb ?: function ($f) use($data) { return ($data); };
462            foreach ($this->getDynamicData() as $entry) {
463                $entry->addMissingFields();
464                if(($form = $entry->getDynamicForm())
465                        && $form->get('type') == 'U' ) {
466
467                    foreach ($entry->getFields() as $f) {
468                        if ($f->get('name') == 'name' && !$cb($f))
469                            $f->value = $this->getFullName();
470                        elseif ($f->get('name') == 'email' && !$cb($f))
471                            $f->value = $this->getEmail();
472                    }
473                }
474
475                $this->_forms[] = $entry;
476            }
477        }
478
479        return $this->_forms;
480    }
481
482    function getAccountStatus() {
483
484        if (!($account=$this->getAccount()))
485            return __('Guest');
486
487        return (string) $account->getStatus();
488    }
489
490    function canSeeOrgTickets() {
491        return $this->org && (
492                $this->org->shareWithEverybody()
493            || ($this->isPrimaryContact() && $this->org->shareWithPrimaryContacts()));
494    }
495
496    function register($vars, &$errors) {
497
498        // user already registered?
499        if ($this->getAccount())
500            return true;
501
502        return UserAccount::register($this, $vars, $errors);
503    }
504
505    static function importCsv($stream, $defaults=array()) {
506        require_once INCLUDE_DIR . 'class.import.php';
507
508        $importer = new CsvImporter($stream);
509        $imported = 0;
510        try {
511            db_autocommit(false);
512            $records = $importer->importCsv(UserForm::getUserForm()->getFields(), $defaults);
513            foreach ($records as $data) {
514                if (!Validator::is_email($data['email']) || empty($data['name']))
515                    throw new ImportError('Both `name` and `email` fields are required');
516                if (!($user = static::fromVars($data, true, true)))
517                    throw new ImportError(sprintf(__('Unable to import user: %s'),
518                        print_r(Format::htmlchars($data), true)));
519                $imported++;
520            }
521            db_autocommit(true);
522        }
523        catch (Exception $ex) {
524            db_rollback();
525            return $ex->getMessage();
526        }
527        return $imported;
528    }
529
530    function importFromPost($stream, $extra=array()) {
531        if (!is_array($stream))
532            $stream = sprintf('name, email%s %s',PHP_EOL, $stream);
533
534        return User::importCsv($stream, $extra);
535    }
536
537    function updateInfo($vars, &$errors, $staff=false) {
538        $isEditable = function ($f) use($staff) {
539            return ($staff ? $f->isEditableToStaff() :
540                    $f->isEditableToUsers());
541        };
542        $valid = true;
543        $forms = $this->getForms($vars, $isEditable);
544        foreach ($forms as $entry) {
545            $entry->setSource($vars);
546            if ($staff && !$entry->isValidForStaff(true))
547                $valid = false;
548            elseif (!$staff && !$entry->isValidForClient(true))
549                $valid = false;
550            elseif ($entry->getDynamicForm()->get('type') == 'U'
551                    && ($f=$entry->getField('email'))
552                    && $isEditable($f)
553                    && $f->getClean()
554                    && ($u=User::lookup(array('emails__address'=>$f->getClean())))
555                    && $u->id != $this->getId()) {
556                $valid = false;
557                $f->addError(__('Email is assigned to another user'));
558            }
559
560            if (!$valid)
561                $errors = array_merge($errors, $entry->errors());
562        }
563
564
565        if (!$valid)
566            return false;
567
568        // Save the entries
569        foreach ($forms as $entry) {
570            $fields = $entry->getFields();
571            foreach ($fields as $field) {
572                $changes = $field->getChanges();
573                if ((is_array($changes) && $changes[0]) || $changes && !is_array($changes)) {
574                    $type = array('type' => 'edited', 'key' => $field->getLabel());
575                    Signal::send('object.edited', $this, $type);
576                }
577            }
578
579            if ($entry->getDynamicForm()->get('type') == 'U') {
580                //  Name field
581                if (($name = $entry->getField('name')) && $isEditable($name) ) {
582                    $name = $name->getClean();
583                    if (is_array($name))
584                        $name = implode(', ', $name);
585                    if ($this->name != $name) {
586                        $type = array('type' => 'edited', 'key' => 'Name');
587                        Signal::send('object.edited', $this, $type);
588                    }
589                    $this->name = $name;
590                }
591
592                // Email address field
593                if (($email = $entry->getField('email'))
594                        && $isEditable($email)) {
595                    if ($this->default_email->address != $email->getClean()) {
596                        $type = array('type' => 'edited', 'key' => 'Email');
597                        Signal::send('object.edited', $this, $type);
598                    }
599                    $this->default_email->address = $email->getClean();
600                    $this->default_email->save();
601                }
602            }
603
604            // DynamicFormEntry::saveAnswers returns the number of answers updated
605            if ($entry->saveAnswers($isEditable)) {
606                $this->updated = SqlFunction::NOW();
607            }
608        }
609
610        return $this->save();
611    }
612
613
614    function save($refetch=false) {
615        // Drop commas and reorganize the name without them
616        $parts = array_map('trim', explode(',', $this->name));
617        switch (count($parts)) {
618            case 2:
619                // Assume last, first --or-- last suff., first
620                $this->name = $parts[1].' '.$parts[0];
621                // XXX: Consider last, first suff.
622                break;
623            case 3:
624                // Assume last, first, suffix, write 'first last suffix'
625                $this->name = $parts[1].' '.$parts[0].' '.$parts[2];
626                break;
627        }
628
629        // Handle email addresses -- use the box name
630        if (Validator::is_email($this->name)) {
631            list($box, $domain) = explode('@', $this->name, 2);
632            if (strpos($box, '.') !== false)
633                $this->name = str_replace('.', ' ', $box);
634            else
635                $this->name = $box;
636            $this->name = mb_convert_case($this->name, MB_CASE_TITLE);
637        }
638
639        if (count($this->dirty)) //XXX: doesn't work??
640            $this->set('updated', new SqlFunction('NOW'));
641        return parent::save($refetch);
642    }
643
644    function delete() {
645        // Refuse to delete a user with tickets
646        if ($this->tickets->count())
647            return false;
648
649        // Delete account record (if any)
650        if ($this->getAccount())
651            $this->getAccount()->delete();
652
653        // Delete emails.
654        $this->emails->expunge();
655
656        // Drop dynamic data
657        foreach ($this->getDynamicData() as $entry) {
658            $entry->delete();
659        }
660
661        $type = array('type' => 'deleted');
662        Signal::send('object.deleted', $this, $type);
663
664        // Delete user
665        return parent::delete();
666    }
667
668    function deleteAllTickets() {
669        $status_id = TicketStatus::lookup(array('state' => 'deleted'));
670        foreach($this->tickets as $ticket) {
671            if (!$T = Ticket::lookup($ticket->getId()))
672                continue;
673            if (!$T->setStatus($status_id))
674                return false;
675        }
676        $this->tickets->reset();
677        return true;
678    }
679
680    static function lookupByEmail($email) {
681        return static::lookup(array('emails__address'=>$email));
682    }
683
684    static function getNameById($id) {
685        if ($user = static::lookup($id))
686            return $user->getName();
687    }
688
689    static function getLink($id) {
690        global $thisstaff;
691
692        if (!$id || !$thisstaff)
693            return false;
694
695        return ROOT_PATH . sprintf('scp/users.php?id=%s', $id);
696    }
697
698    function getTicketsQueue($collabs=true) {
699        global $thisstaff;
700
701        if (!$this->_queue) {
702            $email = $this->getDefaultEmailAddress();
703            $filter = [
704                ['user__id', 'equal', $this->getId()],
705            ];
706            if ($collabs)
707                $filter = [
708                    ['user__emails__address', 'equal', $email],
709                    ['thread__collaborators__user__emails__address', 'equal',  $email],
710                ];
711            $this->_queue = new AdhocSearch(array(
712                'id' => 'adhoc,uid'.$this->getId(),
713                'root' => 'T',
714                'staff_id' => $thisstaff->getId(),
715                'title' => $this->getName()
716            ));
717            $this->_queue->config = $filter;
718        }
719
720        return $this->_queue;
721    }
722}
723
724class EmailAddress
725implements TemplateVariable {
726    var $email;
727    var $address;
728    protected $_info;
729
730    function __construct($address) {
731        $this->_info = self::parse($address);
732        $this->email = sprintf('%s@%s',
733                $this->getMailbox(),
734                $this->getDomain());
735
736        if ($this->getName())
737            $this->address = sprintf('"%s" <%s>',
738                    $this->getName(),
739                    $this->email);
740    }
741
742    function __toString() {
743        return (string) $this->email;
744    }
745
746    function getVar($what) {
747
748        if (!$this->_info)
749            return '';
750
751        switch ($what) {
752        case 'host':
753        case 'domain':
754            return $this->_info->host;
755        case 'personal':
756            return trim($this->_info->personal, '"');
757        case 'mailbox':
758            return $this->_info->mailbox;
759        }
760    }
761
762    function getAddress() {
763        return $this->address ?: $this->email;
764    }
765
766    function getHost() {
767        return $this->getVar('host');
768    }
769
770    function getDomain() {
771        return $this->getHost();
772    }
773
774    function getName() {
775        return $this->getVar('personal');
776    }
777
778    function getMailbox() {
779        return $this->getVar('mailbox');
780    }
781
782    // Parse and email adddress (RFC822) into it's parts.
783    // @address - one address is expected
784    static function parse($address) {
785        require_once PEAR_DIR . 'Mail/RFC822.php';
786        require_once PEAR_DIR . 'PEAR.php';
787        if (($parts = Mail_RFC822::parseAddressList($address))
788                && !PEAR::isError($parts))
789            return current($parts);
790    }
791
792    static function getVarScope() {
793        return array(
794            'domain' => __('Domain'),
795            'mailbox' => __('Mailbox'),
796            'personal' => __('Personal name'),
797        );
798    }
799}
800
801class PersonsName
802implements TemplateVariable {
803    var $format;
804    var $parts;
805    var $name;
806
807    static $formats = array(
808        'first' => array(     /*@trans*/ "First", 'getFirst'),
809        'last' => array(      /*@trans*/ "Last", 'getLast'),
810        'full' => array(      /*@trans*/ "First Last", 'getFull'),
811        'legal' => array(     /*@trans*/ "First M. Last", 'getLegal'),
812        'lastfirst' => array( /*@trans*/ "Last, First", 'getLastFirst'),
813        'formal' => array(    /*@trans*/ "Mr. Last", 'getFormal'),
814        'short' => array(     /*@trans*/ "First L.", 'getShort'),
815        'shortformal' => array(/*@trans*/ "F. Last", 'getShortFormal'),
816        'complete' => array(  /*@trans*/ "Mr. First M. Last Sr.", 'getComplete'),
817        'original' => array(  /*@trans*/ '-- As Entered --', 'getOriginal'),
818    );
819
820    function __construct($name, $format=null) {
821        global $cfg;
822
823        if ($format && isset(static::$formats[$format]))
824            $this->format = $format;
825        else
826            $this->format = 'original';
827
828        if (!is_array($name)) {
829            $this->parts = static::splitName($name);
830            $this->name = $name;
831        }
832        else {
833            $this->parts = $name;
834            $this->name = implode(' ', $name);
835        }
836    }
837
838    function getFirst() {
839        return $this->parts['first'];
840    }
841
842    function getLast() {
843        return $this->parts['last'];
844    }
845
846    function getMiddle() {
847        return $this->parts['middle'];
848    }
849
850    function getFirstInitial() {
851        if ($this->parts['first'])
852            return mb_substr($this->parts['first'],0,1).'.';
853        return '';
854    }
855
856    function getMiddleInitial() {
857        if ($this->parts['middle'])
858            return mb_substr($this->parts['middle'],0,1).'.';
859        return '';
860    }
861
862    function getLastInitial() {
863        if ($this->parts['last'])
864            return mb_substr($this->parts['last'],0,1).'.';
865        return '';
866    }
867
868    function getFormal() {
869        return trim($this->parts['salutation'].' '.$this->parts['last']);
870    }
871
872    function getFull() {
873        return trim($this->parts['first'].' '.$this->parts['last']);
874    }
875
876    function getLegal() {
877        $parts = array(
878            $this->parts['first'],
879            $this->getMiddleInitial(),
880            $this->parts['last'],
881        );
882        return implode(' ', array_filter($parts));
883    }
884
885    function getComplete() {
886        $parts = array(
887            $this->parts['salutation'],
888            $this->parts['first'],
889            $this->getMiddleInitial(),
890            $this->parts['last'],
891            $this->parts['suffix']
892        );
893        return implode(' ', array_filter($parts));
894    }
895
896    function getLastFirst() {
897        $name = $this->parts['last'].', '.$this->parts['first'];
898        $name = trim($name, ', ');
899        if ($this->parts['suffix'])
900            $name .= ', '.$this->parts['suffix'];
901        return $name;
902    }
903
904    function getShort() {
905        return $this->parts['first'].' '.$this->getLastInitial();
906    }
907
908    function getShortFormal() {
909        return $this->getFirstInitial().' '.$this->parts['last'];
910    }
911
912    function getOriginal() {
913        return $this->name;
914    }
915
916    function getInitials() {
917        $names = array($this->parts['first']);
918        $names = array_merge($names, explode(' ', $this->parts['middle']));
919        $names[] = $this->parts['last'];
920        $initials = '';
921        foreach (array_filter($names) as $n)
922            $initials .= mb_substr($n,0,1);
923        return mb_convert_case($initials, MB_CASE_UPPER);
924    }
925
926    function getName() {
927        return $this;
928    }
929
930    function getNameFormats($user, $type) {
931      $nameFormats = array();
932
933      foreach (PersonsName::allFormats() as $format => $func) {
934          $nameFormats[$type . '.name.' . $format] = $user->getName()->$func[1]();
935      }
936
937      return $nameFormats;
938    }
939
940    function asVar() {
941        return $this->__toString();
942    }
943
944    static function getVarScope() {
945        $formats = array();
946        foreach (static::$formats as $name=>$info) {
947            if (in_array($name, array('original', 'complete')))
948                continue;
949            $formats[$name] = $info[0];
950        }
951        return $formats;
952    }
953
954    function __toString() {
955
956        @list(, $func) = static::$formats[$this->format];
957        if (!$func) $func = 'getFull';
958
959        return (string) call_user_func(array($this, $func));
960    }
961
962    static function allFormats() {
963        return static::$formats;
964    }
965
966    /**
967     * Thanks, http://stackoverflow.com/a/14420217
968     */
969    static function splitName($name) {
970        $results = array();
971
972        $r = explode(' ', $name);
973        $size = count($r);
974
975        //check if name is bad format (ex: J.Everybody), and fix them
976        if($size==1 && mb_strpos($r[0], '.') !== false)
977        {
978            $r = explode('.', $name);
979            $size = count($r);
980        }
981
982        //check first for period, assume salutation if so
983        if (mb_strpos($r[0], '.') === false)
984        {
985            $results['salutation'] = '';
986            $results['first'] = $r[0];
987        }
988        else
989        {
990            $results['salutation'] = $r[0];
991            $results['first'] = $r[1];
992        }
993
994        //check last for period, assume suffix if so
995        if (mb_strpos($r[$size - 1], '.') === false)
996        {
997            $results['suffix'] = '';
998        }
999        else
1000        {
1001            $results['suffix'] = $r[$size - 1];
1002        }
1003
1004        //combine remains into last
1005        $start = ($results['salutation']) ? 2 : 1;
1006        $end = ($results['suffix']) ? $size - 2 : $size - 1;
1007
1008        $middle = array();
1009        for ($i = $start; $i <= $end; $i++)
1010        {
1011            $middle[] = $r[$i];
1012        }
1013        if (count($middle) > 1) {
1014            $results['last'] = array_pop($middle);
1015            $results['middle'] = implode(' ', $middle);
1016        }
1017        else {
1018            $results['last'] = $middle[0];
1019            $results['middle'] = '';
1020        }
1021
1022        return $results;
1023    }
1024
1025}
1026
1027class AgentsName extends PersonsName {
1028    function __construct($name, $format=null) {
1029        global $cfg;
1030
1031        if (!$format && $cfg)
1032            $format = $cfg->getAgentNameFormat();
1033
1034        parent::__construct($name, $format);
1035    }
1036}
1037
1038class UsersName extends PersonsName {
1039    function __construct($name, $format=null) {
1040        global $cfg;
1041        if (!$format && $cfg)
1042            $format = $cfg->getClientNameFormat();
1043
1044        parent::__construct($name, $format);
1045    }
1046}
1047
1048
1049class UserEmail extends UserEmailModel {
1050    static function ensure($address) {
1051        $email = static::lookup(array('address'=>$address));
1052        if (!$email) {
1053            $email = new static(array('address'=>$address));
1054            $email->save();
1055        }
1056        return $email;
1057    }
1058}
1059
1060
1061class UserAccount extends VerySimpleModel {
1062    static $meta = array(
1063        'table' => USER_ACCOUNT_TABLE,
1064        'pk' => array('id'),
1065        'joins' => array(
1066            'user' => array(
1067                'null' => false,
1068                'constraint' => array('user_id' => 'User.id')
1069            ),
1070        ),
1071    );
1072
1073    const LANG_MAILOUTS = 1;            // Language preference for mailouts
1074
1075    var $_status;
1076    var $_extra;
1077
1078    function getStatus() {
1079        if (!isset($this->_status))
1080            $this->_status = new UserAccountStatus($this->get('status'));
1081        return $this->_status;
1082    }
1083
1084    function statusChanged($flag, $var) {
1085        if (($this->hasStatus($flag) && !$var) ||
1086            (!$this->hasStatus($flag) && $var))
1087                return true;
1088    }
1089
1090    protected function hasStatus($flag) {
1091        return $this->getStatus()->check($flag);
1092    }
1093
1094    protected function clearStatus($flag) {
1095        return $this->set('status', $this->get('status') & ~$flag);
1096    }
1097
1098    protected function setStatus($flag) {
1099        return $this->set('status', $this->get('status') | $flag);
1100    }
1101
1102    function confirm() {
1103        $this->setStatus(UserAccountStatus::CONFIRMED);
1104        return $this->save();
1105    }
1106
1107    function isConfirmed() {
1108        return $this->getStatus()->isConfirmed();
1109    }
1110
1111    function lock() {
1112        $this->setStatus(UserAccountStatus::LOCKED);
1113        return $this->save();
1114    }
1115
1116    function unlock() {
1117        $this->clearStatus(UserAccountStatus::LOCKED);
1118        return $this->save();
1119    }
1120
1121    function isLocked() {
1122        return $this->getStatus()->isLocked();
1123    }
1124
1125    function forcePasswdReset() {
1126        $this->setStatus(UserAccountStatus::REQUIRE_PASSWD_RESET);
1127        return $this->save();
1128    }
1129
1130    function isPasswdResetForced() {
1131        return $this->hasStatus(UserAccountStatus::REQUIRE_PASSWD_RESET);
1132    }
1133
1134    function isPasswdResetEnabled() {
1135        return !$this->hasStatus(UserAccountStatus::FORBID_PASSWD_RESET);
1136    }
1137
1138    function getInfo() {
1139        return $this->ht;
1140    }
1141
1142    function getId() {
1143        return $this->get('id');
1144    }
1145
1146    function getUserId() {
1147        return $this->get('user_id');
1148    }
1149
1150    function getUser() {
1151        return $this->user;
1152    }
1153
1154    function getUserName() {
1155        return $this->getUser()->getName();
1156    }
1157
1158    function getExtraAttr($attr=false, $default=null) {
1159        if (!isset($this->_extra))
1160            $this->_extra = JsonDataParser::decode($this->get('extra', ''));
1161
1162        return $attr ? (@$this->_extra[$attr] ?: $default) : $this->_extra;
1163    }
1164
1165    function setExtraAttr($attr, $value) {
1166        $this->getExtraAttr();
1167        $this->_extra[$attr] = $value;
1168    }
1169
1170    /**
1171     * Function: getLanguage
1172     *
1173     * Returns the language preference for the user or false if no
1174     * preference is defined. False indicates the browser indicated
1175     * preference should be used. For requests apart from browser requests,
1176     * the last language preference of the browser is set in the
1177     * 'browser_lang' extra attribute upon logins. Send the LANG_MAILOUTS
1178     * flag to also consider this saved value. Such is useful when sending
1179     * the user a message (such as an email), and the user's browser
1180     * preference is not available in the HTTP request.
1181     *
1182     * Parameters:
1183     * $flags - (int) Send UserAccount::LANG_MAILOUTS if the user's
1184     *      last-known browser preference should be considered. Normally
1185     *      only the user's saved language preference is considered.
1186     *
1187     * Returns:
1188     * Current or last-known language preference or false if no language
1189     * preference is currently set or known.
1190     */
1191    function getLanguage($flags=false) {
1192        $lang = $this->get('lang', false);
1193        if (!$lang && ($flags & UserAccount::LANG_MAILOUTS))
1194            $lang = $this->getExtraAttr('browser_lang', false);
1195
1196        return $lang;
1197    }
1198
1199    function getTimezone() {
1200        return $this->timezone;
1201    }
1202
1203    function save($refetch=false) {
1204        // Serialize the extra column on demand
1205        if (isset($this->_extra)) {
1206            $this->extra = JsonDataEncoder::encode($this->_extra);
1207        }
1208        return parent::save($refetch);
1209    }
1210
1211    function hasPassword() {
1212        return (bool) $this->get('passwd');
1213    }
1214
1215    function sendResetEmail() {
1216        return static::sendUnlockEmail('pwreset-client') === true;
1217    }
1218
1219    function sendConfirmEmail() {
1220        return static::sendUnlockEmail('registration-client') === true;
1221    }
1222
1223    function setPassword($new) {
1224        $this->set('passwd', Passwd::hash($new));
1225        // Clean sessions
1226        Signal::send('auth.clean', $this->getUser());
1227    }
1228
1229    protected function sendUnlockEmail($template) {
1230        global $ost, $cfg;
1231
1232        $token = Misc::randCode(48); // 290-bits
1233
1234        $email = $cfg->getDefaultEmail();
1235        $content = Page::lookupByType($template);
1236
1237        if (!$email ||  !$content)
1238            return new BaseError(sprintf(_S('%s: Unable to retrieve template'),
1239                $template));
1240
1241        $vars = array(
1242            'url' => $ost->getConfig()->getBaseUrl(),
1243            'token' => $token,
1244            'user' => $this->getUser(),
1245            'recipient' => $this->getUser(),
1246            'link' => sprintf(
1247                "%s/pwreset.php?token=%s",
1248                $ost->getConfig()->getBaseUrl(),
1249                $token),
1250        );
1251        $vars['reset_link'] = &$vars['link'];
1252
1253        $info = array('email' => $email, 'vars' => &$vars, 'log'=>true);
1254        Signal::send('auth.pwreset.email', $this->getUser(), $info);
1255
1256        $lang = $this->getLanguage(UserAccount::LANG_MAILOUTS);
1257        $msg = $ost->replaceTemplateVariables(array(
1258            'subj' => $content->getLocalName($lang),
1259            'body' => $content->getLocalBody($lang),
1260        ), $vars);
1261
1262        $_config = new Config('pwreset');
1263        $_config->set($vars['token'], 'c'.$this->getUser()->getId());
1264
1265        $email->send($this->getUser()->getEmail(),
1266            Format::striptags($msg['subj']), $msg['body']);
1267
1268        return true;
1269    }
1270
1271    function __toString() {
1272        return (string) $this->getStatus();
1273    }
1274
1275    /*
1276     * Updates may be done by Staff or by the User if registration
1277     * options are set to Public
1278     */
1279    function update($vars, &$errors) {
1280        // TODO: Make sure the username is unique
1281
1282        // Timezone selection is not required. System default is a valid
1283        // fallback
1284
1285        // Changing password?
1286        if ($vars['passwd1'] || $vars['passwd2']) {
1287            if (!$vars['passwd1'])
1288                $errors['passwd1'] = __('New password is required');
1289            else {
1290                try {
1291                    self::checkPassword($vars['passwd1']);
1292                } catch (BadPassword $ex) {
1293                    $errors['passwd1'] =  $ex->getMessage();
1294                }
1295            }
1296        }
1297
1298        // Make sure the username is not an email.
1299        if ($vars['username'] && Validator::is_email($vars['username']))
1300            $errors['username'] =
1301                __('Users can always sign in with their email address');
1302
1303        if ($errors) return false;
1304
1305        //flags
1306        $pwreset = $this->statusChanged(UserAccountStatus::REQUIRE_PASSWD_RESET, $vars['pwreset-flag']);
1307        $locked = $this->statusChanged(UserAccountStatus::LOCKED, $vars['locked-flag']);
1308        $forbidPwChange = $this->statusChanged(UserAccountStatus::FORBID_PASSWD_RESET, $vars['forbid-pwchange-flag']);
1309
1310        $info = $this->getInfo();
1311        foreach ($vars as $key => $value) {
1312            if (($key != 'id' && $info[$key] && $info[$key] != $value) || ($pwreset && $key == 'pwreset-flag' ||
1313                    $locked && $key == 'locked-flag' || $forbidPwChange && $key == 'forbid-pwchange-flag')) {
1314                $type = array('type' => 'edited', 'key' => $key);
1315                Signal::send('object.edited', $this, $type);
1316            }
1317        }
1318
1319        $this->set('timezone', $vars['timezone']);
1320        $this->set('username', $vars['username']);
1321
1322        if ($vars['passwd1']) {
1323            $this->setPassword($vars['passwd1']);
1324            $this->setStatus(UserAccountStatus::CONFIRMED);
1325            $type = array('type' => 'edited', 'key' => 'password');
1326            Signal::send('object.edited', $this, $type);
1327        }
1328
1329        // Set flags
1330        foreach (array(
1331                'pwreset-flag' => UserAccountStatus::REQUIRE_PASSWD_RESET,
1332                'locked-flag' => UserAccountStatus::LOCKED,
1333                'forbid-pwchange-flag' => UserAccountStatus::FORBID_PASSWD_RESET
1334        ) as $ck=>$flag) {
1335            if ($vars[$ck])
1336                $this->setStatus($flag);
1337            else {
1338                if (($pwreset && $ck == 'pwreset-flag') || ($locked && $ck == 'locked-flag') ||
1339                    ($forbidPwChange && $ck == 'forbid-pwchange-flag')) {
1340                        $type = array('type' => 'edited', 'key' => $ck);
1341                        Signal::send('object.edited', $this, $type);
1342                }
1343                $this->clearStatus($flag);
1344            }
1345        }
1346
1347        return $this->save(true);
1348    }
1349
1350    static function createForUser($user, $defaults=false) {
1351        $acct = new static(array('user_id'=>$user->getId()));
1352        if ($defaults && is_array($defaults)) {
1353            foreach ($defaults as $k => $v)
1354                $acct->set($k, $v);
1355        }
1356        return $acct;
1357    }
1358
1359    static function lookupByUsername($username) {
1360        if (Validator::is_email($username))
1361            $user = static::lookup(array('user__emails__address' => $username));
1362        elseif (Validator::is_userid($username))
1363            $user = static::lookup(array('username' => $username));
1364
1365        return $user;
1366    }
1367
1368    static function register($user, $vars, &$errors) {
1369
1370        if (!$user || !$vars)
1371            return false;
1372
1373        //Require temp password.
1374        if ((!$vars['backend'] || $vars['backend'] != 'client')
1375                && !isset($vars['sendemail'])) {
1376            if (!$vars['passwd1'])
1377                $errors['passwd1'] = 'Temporary password required';
1378            elseif ($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2']))
1379                $errors['passwd2'] = 'Passwords do not match';
1380            else {
1381                try {
1382                    self::checkPassword($vars['passwd1']);
1383                } catch (BadPassword $ex) {
1384                    $errors['passwd1'] =  $ex->getMessage();
1385                }
1386            }
1387        }
1388
1389        if ($errors) return false;
1390
1391        $account = new UserAccount(array(
1392            'user_id' => $user->getId(),
1393            'timezone' => $vars['timezone'],
1394            'backend' => $vars['backend'],
1395        ));
1396
1397        if ($vars['username'] && strcasecmp($vars['username'], $user->getEmail()))
1398            $account->set('username', $vars['username']);
1399
1400        if ($vars['passwd1'] && !$vars['sendemail']) {
1401            $account->set('passwd', Passwd::hash($vars['passwd1']));
1402            $account->setStatus(UserAccountStatus::CONFIRMED);
1403            if ($vars['pwreset-flag'])
1404                $account->setStatus(UserAccountStatus::REQUIRE_PASSWD_RESET);
1405            if ($vars['forbid-pwreset-flag'])
1406                $account->setStatus(UserAccountStatus::FORBID_PASSWD_RESET);
1407        }
1408        elseif ($vars['backend'] && $vars['backend'] != 'client') {
1409            // Auto confirm remote accounts
1410            $account->setStatus(UserAccountStatus::CONFIRMED);
1411        }
1412
1413        $account->save(true);
1414
1415        if (!$account->isConfirmed() && $vars['sendemail'])
1416            $account->sendConfirmEmail();
1417
1418        return $account;
1419    }
1420
1421    static function checkPassword($new, $current=null) {
1422        osTicketClientAuthentication::checkPassword($new, $current);
1423    }
1424
1425}
1426
1427class UserAccountStatus {
1428
1429    var $flag;
1430
1431    const CONFIRMED             = 0x0001;
1432    const LOCKED                = 0x0002;
1433    const REQUIRE_PASSWD_RESET  = 0x0004;
1434    const FORBID_PASSWD_RESET   = 0x0008;
1435
1436    function __construct($flag) {
1437        $this->flag = $flag;
1438    }
1439
1440    function check($flag) {
1441        return 0 !== ($this->flag & $flag);
1442    }
1443
1444    function isLocked() {
1445        return $this->check(self::LOCKED);
1446    }
1447
1448    function isConfirmed() {
1449        return $this->check(self::CONFIRMED);
1450    }
1451
1452    function __toString() {
1453
1454        if ($this->isLocked())
1455            return __('Locked (Administrative)');
1456
1457        if (!$this->isConfirmed())
1458            return __('Locked (Pending Activation)');
1459
1460        // ... Other flags here (password reset, etc).
1461
1462        return __('Active (Registered)');
1463    }
1464}
1465
1466/*
1467 *  Generic user list.
1468 */
1469class UserList extends MailingList {
1470
1471   function add($user) {
1472        if (!$user instanceof ITicketUser)
1473            throw new InvalidArgumentException('User expected');
1474
1475        return parent::add($user);
1476    }
1477}
1478
1479?>
1480