Newer
Older
<?php
/*********************************************************************
class.user.php
External end-user identification for osTicket
Peter Rotich <peter@osticket.com>
Jared Hancock <jared@osticket.com>
Copyright (c) 2006-2013 osTicket
http://www.osticket.com
Released under the GNU General Public License WITHOUT ANY WARRANTY.
See LICENSE.TXT for details.
vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
require_once INCLUDE_DIR . 'class.orm.php';
require_once INCLUDE_DIR . 'class.util.php';
require_once INCLUDE_DIR . 'class.variable.php';
require_once INCLUDE_DIR . 'class.search.php';
require_once INCLUDE_DIR . 'class.organization.php';
class UserEmailModel extends VerySimpleModel {
static $meta = array(
'table' => USER_EMAIL_TABLE,
'pk' => array('id'),
'joins' => array(
'user' => array(
'constraint' => array('user_id' => 'UserModel.id')
)
)
);
static function getIdByEmail($email) {
$row = UserEmailModel::objects()
->filter(array('address'=>$email))
->values_flat('user_id')
->first();
return $row ? $row[0] : 0;
}
}
class UserModel extends VerySimpleModel {
static $meta = array(
'table' => USER_TABLE,
'pk' => array('id'),
'select_related' => array('default_email', 'org', 'account'),
'joins' => array(
'emails' => array(
'reverse' => 'UserEmailModel.user',
'constraint' => array('org_id' => 'Organization.id')
),
'default_email' => array(
'null' => true,
'constraint' => array('default_email_id' => 'UserEmailModel.id')
),
'cdata' => array(
'constraint' => array('id' => 'UserCdata.user_id'),
'null' => true,
),
'entries' => array(
'constraint' => array(
'id' => 'DynamicFormEntry.object_id',
"'U'" => 'DynamicFormEntry.object_type',
),
const PRIMARY_ORG_CONTACT = 0x0001;
const PERM_CREATE = 'user.create';
const PERM_EDIT = 'user.edit';
const PERM_DELETE = 'user.delete';
const PERM_MANAGE = 'user.manage';
const PERM_DIRECTORY = 'user.dir';
static protected $perms = array(
self::PERM_CREATE => array(
'title' => /* @trans */ 'Create',
'desc' => /* @trans */ 'Ability to add new users',
),
self::PERM_EDIT => array(
'title' => /* @trans */ 'Edit',
'desc' => /* @trans */ 'Ability to manage user information',
),
self::PERM_DELETE => array(
'title' => /* @trans */ 'Delete',
'desc' => /* @trans */ 'Ability to delete users',
),
self::PERM_MANAGE => array(
'title' => /* @trans */ 'Manage Account',
'desc' => /* @trans */ 'Ability to manage active user accounts',
),
self::PERM_DIRECTORY => array(
'title' => /* @trans */ 'User Directory',
'desc' => /* @trans */ 'Ability to access the user directory',
function getId() {
return $this->id;
}
function getDefaultEmailAddress() {
return $this->getDefaultEmail()->address;
}
function getDefaultEmail() {
return $this->default_email;
}
function hasAccount() {
return !is_null($this->account);
}
function getAccount() {
return $this->account;
}
function getOrgId() {
return $this->get('org_id');
}
function getOrganization() {
return $this->org;
}
function setOrganization($org, $save=true) {
return true;
}
public function setFlag($flag, $val) {
if ($val)
$this->status |= $flag;
else
$this->status &= ~$flag;
}
protected function hasStatus($flag) {
return $this->get('status') & $flag !== 0;
}
protected function clearStatus($flag) {
return $this->set('status', $this->get('status') & ~$flag);
}
protected function setStatus($flag) {
return $this->set('status', $this->get('status') | $flag);
}
function isPrimaryContact() {
return $this->hasStatus(User::PRIMARY_ORG_CONTACT);
}
function setPrimaryContact($flag) {
if ($flag)
$this->setStatus(User::PRIMARY_ORG_CONTACT);
else
$this->clearStatus(User::PRIMARY_ORG_CONTACT);
}
static function getPermissions() {
return self::$perms;
}
include_once INCLUDE_DIR.'class.role.php';
RolePermission::register(/* @trans */ 'Users', UserModel::getPermissions());
class UserCdata extends VerySimpleModel {
static $meta = array(
'joins' => array(
'user' => array(
'constraint' => array('user_id' => 'UserModel.id'),
),
),
class User extends UserModel
implements TemplateVariable, Searchable {
static function fromVars($vars, $create=true, $update=false) {
// Try and lookup by email address
$user = static::lookupByEmail($vars['email']);
if (!$user && $create) {
if (is_array($name))
$name = implode(', ', $name);
elseif (!$name)
list($name) = explode('@', $vars['email'], 2);
$user = new User(array(
'name' => Format::htmldecode(Format::sanitize($name, false)),
'created' => new SqlFunction('NOW'),
'updated' => new SqlFunction('NOW'),
//XXX: Do plain create once the cause
// of the detached emails is fixed.
'default_email' => UserEmail::ensure($vars['email'])
// Is there an organization registered for this domain
list($mailbox, $domain) = explode('@', $vars['email'], 2);
if (isset($vars['org_id']))
$user->set('org_id', $vars['org_id']);
elseif ($org = Organization::forDomain($domain))
$user->setOrganization($org, false);
try {
$user->save(true);
$user->emails->add($user->default_email);
// Attach initial custom fields
$user->addDynamicData($vars);
}
catch (OrmException $e) {
return null;
}
Signal::send('user.created', $user);
$errors = array();
$user->updateInfo($vars, $errors, true);
static function fromForm($form, $create=true) {
if(!$form) return null;
//Validate the form
$valid = true;
$filter = function($f) use ($thisstaff) {
return !isset($thisstaff) || $f->isRequiredForStaff();
};
if (!$form->isValid($filter))
$valid = false;
//Make sure the email is not in-use
if (($field=$form->getField('email'))
&& $field->getClean()
&& User::lookup(array('emails__address'=>$field->getClean()))) {
$field->addError(__('Email is assigned to another user'));
return $valid ? self::fromVars($form->getClean(), $create) : null;
if (!isset($this->_email))
$this->_email = new EmailAddress(sprintf('"%s" <%s>',
$this->getName(),
$this->default_email->address));
return $this->_email;
function getAvatar($size=null) {
global $cfg;
$source = $cfg->getClientAvatarSource();
$avatar = $source->getAvatar($this);
if (isset($size))
$avatar->setSize($size);
return $avatar;
function getFullName() {
return $this->name;
}
function getPhoneNumber() {
foreach ($this->getDynamicData() as $e)
if ($a = $e->getAnswer('phone'))
return $a;
}
if (!$this->name)
list($name) = explode('@', $this->getDefaultEmailAddress(), 2);
else
$name = $this->name;
function getUpdateDate() {
return $this->updated;
}
function getCreateDate() {
return $this->created;
}
function getTimezone() {
global $cfg;
if (($acct = $this->getAccount()) && ($tz = $acct->getTimezone())) {
return $tz;
}
return $cfg->getDefaultTimezone();
}
function addForm($form, $sort=1, $data=null) {
$entry = $form->instanciate($sort, $data);
$entry->set('object_type', 'U');
$entry->set('object_id', $this->getId());
$entry->save();
return $entry;
function getLanguage($flags=false) {
if ($acct = $this->getAccount())
return $acct->getLanguage($flags);
function to_json() {
$info = array(
'id' => $this->getId(),
'name' => Format::htmlchars($this->getName()),
'email' => (string) $this->getEmail(),
'phone' => (string) $this->getPhoneNumber());
return JsonDataEncoder::encode($info);
}
function __toString() {
return $this->asVar();
}
function asVar() {
return (string) $this->getName();
}
function getVar($tag) {
$tag = mb_strtolower($tag);
foreach ($this->getDynamicData() as $e)
if ($a = $e->getAnswer($tag))
return $a;
static function getVarScope() {
$base = array(
'email' => array(
'class' => 'EmailAddress', 'desc' => __('Default email address')
),
'name' => array(
'class' => 'PersonsName', 'desc' => 'User name, default format'
),
'organization' => array('class' => 'Organization', 'desc' => __('Organization')),
);
$extra = VariableReplacer::compileFormScope(UserForm::getInstance());
return $base + $extra;
}
static function getSearchableFields() {
$uform = UserForm::getUserForm();
foreach ($uform->getFields() as $F) {
$fname = $F->get('name') ?: ('field_'.$F->get('id'));
# XXX: email in the model corresponds to `emails__address` ORM path
if ($fname == 'email')
$fname = 'emails__address';
if (!$F->hasData() || $F->isPresentationOnly())
continue;
if (!$F->isStorable())
$base[$fname] = $F;
else
$base["cdata__{$fname}"] = $F;
}
return $base;
}
static function supportsCustomData() {
return true;
}
return $this->addForm(UserForm::objects()->one(), 1, $data);
function getDynamicData($create=true) {
if (!isset($this->_entries)) {
$this->_entries = DynamicFormEntry::forObject($this->id, 'U')->all();
if (!$this->_entries && $create) {
$g = UserForm::getNewInstance();
$g->setClientId($this->id);
$this->_entries[] = $g;
}
return $this->_entries ?: array();
function getFilterData() {
$vars = array();
foreach ($this->getDynamicData() as $entry) {
$vars += $entry->getFilterData();
// Add in special `name` and `email` fields
if ($entry->getDynamicForm()->get('type') != 'U')
continue;
foreach (array('name', 'email') as $name) {
$vars['field.'.$f->get('id')] =
$name == 'name' ? $this->getName() : $this->getEmail();
return $vars;
}
if (!isset($this->_forms)) {
$this->_forms = array();
foreach ($this->getDynamicData() as $entry) {
$entry->addMissingFields();
&& ($form = $entry->getDynamicForm())
foreach ($entry->getFields() as $f) {
if ($f->get('name') == 'name')
$f->value = $this->getFullName();
elseif ($f->get('name') == 'email')
$f->value = $this->getEmail();
}
}
function canSeeOrgTickets() {
return $this->org && (
$this->org->shareWithEverybody()
|| ($this->isPrimaryContact() && $this->org->shareWithPrimaryContacts()));
}
function register($vars, &$errors) {
// user already registered?
if ($this->getAccount())
return true;
return UserAccount::register($this, $vars, $errors);
static function importCsv($stream, $defaults=array()) {
require_once INCLUDE_DIR . 'class.import.php';
$importer = new CsvImporter($stream);
$imported = 0;
try {
db_autocommit(false);
$records = $importer->importCsv(UserForm::getUserForm()->getFields(), $defaults);
foreach ($records as $data) {
if (!Validator::is_email($data['email']) || empty($data['name']))
throw new ImportError('Both `name` and `email` fields are required');
if (!($user = static::fromVars($data, true, true)))
throw new ImportError(sprintf(__('Unable to import user: %s'),
db_rollback();
return $ex->getMessage();
function importFromPost($stream, $extra=array()) {
return User::importCsv($stream, $extra);
}
function updateInfo($vars, &$errors, $staff=false) {
$forms = $this->getForms($vars);
foreach ($forms as $entry) {
$entry->setSource($vars);
if ($staff && !$entry->isValidForStaff())
elseif (!$staff && !$entry->isValidForClient())
elseif ($entry->getDynamicForm()->get('type') == 'U'
&& ($f=$entry->getField('email'))
&& $f->getClean()
&& ($u=User::lookup(array('emails__address'=>$f->getClean())))
&& $u->id != $this->getId()) {
$f->addError(__('Email is assigned to another user'));
if (!$valid)
$errors = array_merge($errors, $entry->errors());
if ($entry->getDynamicForm()->get('type') == 'U') {
// Name field
if (($name = $entry->getField('name'))) {
$name = $name->getClean();
if (is_array($name))
$name = implode(', ', $name);
$this->name = $name;
// Email address field
if (($email = $entry->getField('email'))) {
// DynamicFormEntry::save returns the number of answers updated
if ($entry->save()) {
$this->updated = SqlFunction::NOW();
}
return $this->save();
function save($refetch=false) {
// Drop commas and reorganize the name without them
$parts = array_map('trim', explode(',', $this->name));
switch (count($parts)) {
case 2:
// Assume last, first --or-- last suff., first
$this->name = $parts[1].' '.$parts[0];
// XXX: Consider last, first suff.
break;
case 3:
// Assume last, first, suffix, write 'first last suffix'
$this->name = $parts[1].' '.$parts[0].' '.$parts[2];
break;
}
// Handle email addresses -- use the box name
if (Validator::is_email($this->name)) {
list($box, $domain) = explode('@', $this->name, 2);
if (strpos($box, '.') !== false)
$this->name = str_replace('.', ' ', $box);
else
$this->name = $box;
$this->name = mb_convert_case($this->name, MB_CASE_TITLE);
}
$this->set('updated', new SqlFunction('NOW'));
return parent::save($refetch);
}
// Refuse to delete a user with tickets
if ($this->tickets->count())
// Delete account record (if any)
if ($this->getAccount())
$this->getAccount()->delete();
// Delete emails.
$this->emails->expunge();
foreach ($this->getDynamicData() as $entry) {
$entry->delete();
function deleteAllTickets() {
$deleted = TicketStatus::lookup(array('state' => 'deleted'));
foreach($this->tickets as $ticket) {
if (!$T = Ticket::lookup($ticket->getId()))
continue;
if (!$T->setStatus($deleted))
return false;
}
$this->tickets->reset();
return true;
}
static function lookupByEmail($email) {
return static::lookup(array('emails__address'=>$email));
static function getNameById($id) {
if ($user = static::lookup($id))
return $user->getName();
}
static function getLink($id) {
global $thisstaff;
if (!$id || !$thisstaff)
return false;
return ROOT_PATH . sprintf('scp/users.php?id=%s', $id);
class EmailAddress
implements TemplateVariable {
var $address;
function __construct($address) {
$this->_info = self::parse($address);
$this->email = sprintf('%s@%s',
$this->getMailbox(),
$this->getDomain());
if ($this->getName())
}
function __toString() {
}
function getVar($what) {
return '';
switch ($what) {
case 'domain':
case 'personal':
case 'mailbox':
}
}
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
function getAddress() {
return $this->address ?: $this->email;
}
function getHost() {
return $this->getVar('host');
}
function getDomain() {
return $this->getHost();
}
function getName() {
return $this->getVar('personal');
}
function getMailbox() {
return $this->getVar('mailbox');
}
// Parse and email adddress (RFC822) into it's parts.
// @address - one address is expected
static function parse($address) {
require_once PEAR_DIR . 'Mail/RFC822.php';
require_once PEAR_DIR . 'PEAR.php';
if (($parts = Mail_RFC822::parseAddressList($address))
&& !PEAR::isError($parts))
return current($parts);
}
static function getVarScope() {
return array(
'domain' => __('Domain'),
'mailbox' => __('Mailbox'),
'personal' => __('Personal name'),
);
}
}
class PersonsName
implements TemplateVariable {
var $parts;
var $name;
static $formats = array(
'first' => array( /*@trans*/ "First", 'getFirst'),
'last' => array( /*@trans*/ "Last", 'getLast'),
'full' => array( /*@trans*/ "First Last", 'getFull'),
'legal' => array( /*@trans*/ "First M. Last", 'getLegal'),
'lastfirst' => array( /*@trans*/ "Last, First", 'getLastFirst'),
'formal' => array( /*@trans*/ "Mr. Last", 'getFormal'),
'short' => array( /*@trans*/ "First L.", 'getShort'),
'shortformal' => array(/*@trans*/ "F. Last", 'getShortFormal'),
'complete' => array( /*@trans*/ "Mr. First M. Last Sr.", 'getComplete'),
'original' => array( /*@trans*/ '-- As Entered --', 'getOriginal'),
function __construct($name, $format=null) {
global $cfg;
if ($format && isset(static::$formats[$format]))
else
$this->format = 'original';
if (!is_array($name)) {
$this->parts = static::splitName($name);
$this->name = $name;
}
else {
$this->parts = $name;
$this->name = implode(' ', $name);
}
}
function getFirst() {
return $this->parts['first'];
}
function getLast() {
return $this->parts['last'];
}
function getMiddle() {
return $this->parts['middle'];
}
function getMiddleInitial() {
return mb_substr($this->parts['middle'],0,1).'.';
}
function getFormal() {
return trim($this->parts['salutation'].' '.$this->parts['last']);
}
function getFull() {
return trim($this->parts['first'].' '.$this->parts['last']);
}
function getLegal() {
$parts = array(
$this->parts['first'],
mb_substr($this->parts['middle'],0,1),
$this->parts['last'],
);
if ($parts[1]) $parts[1] .= '.';
return implode(' ', array_filter($parts));
}
$parts = array(
$this->parts['salutation'],
$this->parts['first'],
mb_substr($this->parts['middle'],0,1),
$this->parts['last'],
$this->parts['suffix']
);
if ($parts[2]) $parts[2] .= '.';
return implode(' ', array_filter($parts));
}
function getLastFirst() {
$name = $this->parts['last'].', '.$this->parts['first'];
if ($this->parts['suffix'])
$name .= ', '.$this->parts['suffix'];
return $name;
function getShort() {
return $this->parts['first'].' '.mb_substr($this->parts['last'],0,1).'.';
}
function getShortFormal() {
return mb_substr($this->parts['first'],0,1).'. '.$this->parts['last'];
}
function getOriginal() {
return $this->name;
}
function getInitials() {
$names = array($this->parts['first']);
$names = array_merge($names, explode(' ', $this->parts['middle']));
$names[] = $this->parts['last'];
$initials = '';
foreach (array_filter($names) as $n)
$initials .= mb_substr($n,0,1);
return mb_convert_case($initials, MB_CASE_UPPER);
}
function getName() {
return $this;
}
function getNameFormats($user, $type) {
$nameFormats = array();
foreach (PersonsName::allFormats() as $format => $func) {
$nameFormats[$type . '.name.' . $format] = $user->getName()->$func[1]();
}
return $nameFormats;
}
function asVar() {
return $this->__toString();
}
static function getVarScope() {
$formats = array();
foreach (static::$formats as $name=>$info) {
if (in_array($name, array('original', 'complete')))
continue;
$formats[$name] = $info[0];
}
return $formats;
}
if (!$func) $func = 'getFull';
return (string) call_user_func(array($this, $func));
}
static function allFormats() {
return static::$formats;
}
/**
* Thanks, http://stackoverflow.com/a/14420217
*/
static function splitName($name) {
$results = array();
$r = explode(' ', $name);
$size = count($r);
if($size==1 && mb_strpos($r[0], '.') !== false)
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
//check first for period, assume salutation if so
if (mb_strpos($r[0], '.') === false)
{
$results['salutation'] = '';
$results['first'] = $r[0];
}
else
{
$results['salutation'] = $r[0];
$results['first'] = $r[1];
}
//check last for period, assume suffix if so
if (mb_strpos($r[$size - 1], '.') === false)
{
$results['suffix'] = '';
}
else
{
$results['suffix'] = $r[$size - 1];
}
//combine remains into last
$start = ($results['salutation']) ? 2 : 1;
$end = ($results['suffix']) ? $size - 2 : $size - 1;
for ($i = $start; $i <= $end; $i++)
{
$middle[] = $r[$i];
}
if (count($middle) > 1) {
$results['last'] = array_pop($middle);
$results['middle'] = implode(' ', $middle);
}
else {
$results['last'] = $middle[0];
$results['middle'] = '';
}
return $results;
}
}
class AgentsName extends PersonsName {
function __construct($name, $format=null) {
global $cfg;
if (!$format && $cfg)
$format = $cfg->getAgentNameFormat();
parent::__construct($name, $format);
}
}
class UsersName extends PersonsName {
function __construct($name, $format=null) {
global $cfg;
if (!$format && $cfg)
$format = $cfg->getClientNameFormat();
parent::__construct($name, $format);
}
}
class UserEmail extends UserEmailModel {
static function ensure($address) {
$email = static::lookup(array('address'=>$address));
if (!$email) {
$email = new static(array('address'=>$address));
$email->save();
}
return $email;
}
}
class UserAccount extends VerySimpleModel {
static $meta = array(
'table' => USER_ACCOUNT_TABLE,
'pk' => array('id'),
'joins' => array(
'user' => array(
'null' => false,
'constraint' => array('user_id' => 'User.id')