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