PageRenderTime 55ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/include/class.client.php

https://gitlab.com/billyprice1/osTicket
PHP | 481 lines | 331 code | 89 blank | 61 comment | 63 complexity | 740d9c58b8797c6e45374f169203bb05 MD5 | raw file
  1. <?php
  2. /*********************************************************************
  3. class.client.php
  4. Handles everything about EndUser
  5. Peter Rotich <peter@osticket.com>
  6. Copyright (c) 2006-2013 osTicket
  7. http://www.osticket.com
  8. Released under the GNU General Public License WITHOUT ANY WARRANTY.
  9. See LICENSE.TXT for details.
  10. vim: expandtab sw=4 ts=4 sts=4:
  11. **********************************************************************/
  12. require_once INCLUDE_DIR.'class.user.php';
  13. abstract class TicketUser
  14. implements EmailContact, ITicketUser, TemplateVariable {
  15. static private $token_regex = '/^(?P<type>\w{1})(?P<algo>\d+)x(?P<hash>.*)$/i';
  16. protected $user;
  17. protected $_guest = false;
  18. function __construct($user) {
  19. $this->user = $user;
  20. }
  21. function __call($name, $args) {
  22. global $cfg;
  23. $rv = null;
  24. if($this->user && is_callable(array($this->user, $name)))
  25. $rv = $args
  26. ? call_user_func_array(array($this->user, $name), $args)
  27. : call_user_func(array($this->user, $name));
  28. return $rv ?: false;
  29. }
  30. // Required for Internationalization::getCurrentLanguage() in templates
  31. function getLanguage() {
  32. return $this->user->getLanguage();
  33. }
  34. static function getVarScope() {
  35. return array(
  36. 'email' => __('Email Address'),
  37. 'name' => array('class' => 'PersonsName', 'desc' => __('Full Name')),
  38. 'ticket_link' => __('Link to view the ticket'),
  39. );
  40. }
  41. function getVar($tag) {
  42. global $cfg;
  43. switch (strtolower($tag)) {
  44. case 'ticket_link':
  45. $qstr = array();
  46. if ($cfg && $cfg->isAuthTokenEnabled()
  47. && ($ticket=$this->getTicket()))
  48. $qstr['auth'] = $ticket->getAuthToken($this);
  49. return sprintf('%s/view.php?%s',
  50. $cfg->getBaseUrl(),
  51. Http::build_query($qstr, false)
  52. );
  53. break;
  54. }
  55. }
  56. function getId() { return ($this->user) ? $this->user->getId() : null; }
  57. function getEmail() { return ($this->user) ? $this->user->getEmail() : null; }
  58. static function lookupByToken($token) {
  59. //Expecting well formatted token see getAuthToken routine for details.
  60. $matches = array();
  61. if (!preg_match(static::$token_regex, $token, $matches))
  62. return null;
  63. //Unpack the user and ticket ids
  64. $matches +=unpack('Vuid/Vtid',
  65. Base32::decode(strtolower(substr($matches['hash'], 0, 13))));
  66. $user = null;
  67. if (!($ticket = Ticket::lookup($matches['tid'])))
  68. // Require a ticket for now
  69. return null;
  70. switch ($matches['type']) {
  71. case 'c': //Collaborator c
  72. if (($user = Collaborator::lookup($matches['uid']))
  73. && $user->getTicketId() != $matches['tid'])
  74. $user = null;
  75. break;
  76. case 'o': //Ticket owner
  77. if (($user = $ticket->getOwner())
  78. && $user->getId() != $matches['uid']) {
  79. $user = null;
  80. }
  81. break;
  82. }
  83. if (!$user
  84. || !$user instanceof ITicketUser
  85. || strcasecmp($ticket->getAuthToken($user, $matches['algo']), $token))
  86. return false;
  87. return $user;
  88. }
  89. static function lookupByEmail($email) {
  90. if (!($user=User::lookup(array('emails__address' => $email))))
  91. return null;
  92. return new EndUser($user);
  93. }
  94. function isOwner() {
  95. return $this instanceof TicketOwner;
  96. }
  97. function flagGuest() {
  98. $this->_guest = true;
  99. }
  100. function isGuest() {
  101. return $this->_guest;
  102. }
  103. function getUserId() {
  104. return $this->user->getId();
  105. }
  106. abstract function getTicketId();
  107. abstract function getTicket();
  108. }
  109. class TicketOwner extends TicketUser {
  110. protected $ticket;
  111. function __construct($user, $ticket) {
  112. parent::__construct($user);
  113. $this->ticket = $ticket;
  114. }
  115. function getTicket() {
  116. return $this->ticket;
  117. }
  118. function getTicketId() {
  119. return $this->ticket->getId();
  120. }
  121. }
  122. /*
  123. * Decorator class for authenticated user
  124. *
  125. */
  126. class EndUser extends BaseAuthenticatedUser {
  127. protected $user;
  128. protected $_account = false;
  129. protected $_stats;
  130. protected $topic_stats;
  131. function __construct($user) {
  132. $this->user = $user;
  133. }
  134. /*
  135. * Delegate calls to the user
  136. */
  137. function __call($name, $args) {
  138. if(!$this->user
  139. || !is_callable(array($this->user, $name)))
  140. return $this->getVar(substr($name, 3));
  141. return $args
  142. ? call_user_func_array(array($this->user, $name), $args)
  143. : call_user_func(array($this->user, $name));
  144. }
  145. function getVar($tag) {
  146. $u = $this;
  147. // Traverse the $user properties of all nested user objects to get
  148. // to the User instance with the custom data
  149. while (isset($u->user)) {
  150. $u = $u->user;
  151. if (method_exists($u, 'getVar')) {
  152. if ($rv = $u->getVar($tag))
  153. return $rv;
  154. }
  155. }
  156. }
  157. function getId() {
  158. //We ONLY care about user ID at the ticket level
  159. if ($this->user instanceof Collaborator)
  160. return $this->user->getUserId();
  161. elseif ($this->user)
  162. return $this->user->getId();
  163. return false;
  164. }
  165. function getUserName() {
  166. //XXX: Revisit when real usernames are introduced or when email
  167. // requirement is removed.
  168. return $this->user->getEmail();
  169. }
  170. function getUserType() {
  171. return $this->isOwner() ? 'owner' : 'collaborator';
  172. }
  173. function getAuthBackend() {
  174. list($authkey,) = explode(':', $this->getAuthKey());
  175. return UserAuthenticationBackend::getBackend($authkey);
  176. }
  177. function getTicketStats() {
  178. if (!isset($this->_stats))
  179. $this->_stats = $this->getStats();
  180. return $this->_stats;
  181. }
  182. function getNumTickets($forMyOrg=false, $state=false) {
  183. $stats = $this->getTicketStats();
  184. $count = 0;
  185. $section = $forMyOrg ? 'myorg' : 'mine';
  186. foreach ($stats[$section] as $row) {
  187. if ($state && $row['status__state'] != $state)
  188. continue;
  189. $count += $row['count'];
  190. }
  191. return $count;
  192. }
  193. function getNumOpenTickets($forMyOrg=false) {
  194. return $this->getNumTickets($forMyOrg, 'open') ?: 0;
  195. }
  196. function getNumClosedTickets($forMyOrg=false) {
  197. return $this->getNumTickets($forMyOrg, 'closed') ?: 0;
  198. }
  199. function getNumTopicTickets($topic_id, $forMyOrg=false) {
  200. $stats = $this->getTicketStats();
  201. $section = $forMyOrg ? 'myorg' : 'mine';
  202. if (!isset($this->topic_stats)) {
  203. $this->topic_stats = array();
  204. foreach ($stats[$section] as $row) {
  205. $this->topic_stats[$row['topic_id']] += $row['count'];
  206. }
  207. }
  208. return $this->topic_stats[$topic_id];
  209. }
  210. function getNumTopicTicketsInState($topic_id, $state=false, $forMyOrg=false) {
  211. $stats = $this->getTicketStats();
  212. $count = 0;
  213. $section = $forMyOrg ? 'myorg' : 'mine';
  214. foreach ($stats[$section] as $row) {
  215. if ($topic_id != $row['topic_id'])
  216. continue;
  217. if ($state && $state != $row['status__state'])
  218. continue;
  219. $count += $row['count'];
  220. }
  221. return $count;
  222. }
  223. function getNumOrganizationTickets() {
  224. return $this->getNumTickets(true);
  225. }
  226. function getNumOpenOrganizationTickets() {
  227. return $this->getNumTickets(true, 'open');
  228. }
  229. function getNumClosedOrganizationTickets() {
  230. return $this->getNumTickets(true, 'closed');
  231. }
  232. function getAccount() {
  233. if ($this->_account === false)
  234. $this->_account =
  235. ClientAccount::lookup(array('user_id'=>$this->getId()));
  236. return $this->_account;
  237. }
  238. function getLanguage($flags=false) {
  239. if ($acct = $this->getAccount())
  240. return $acct->getLanguage($flags);
  241. }
  242. private function getStats() {
  243. $basic = Ticket::objects()
  244. ->annotate(array('count' => SqlAggregate::COUNT('ticket_id')))
  245. ->values('status__state', 'topic_id')
  246. ->distinct('status_id', 'topic_id');
  247. // Share tickets among the organization for owners only
  248. $mine = clone $basic;
  249. $collab = clone $basic;
  250. $mine->filter(array(
  251. 'user_id' => $this->getId(),
  252. ));
  253. // Also add collaborator tickets to the list. This may seem ugly;
  254. // but the general rule for SQL is that a single query can only use
  255. // one index. Therefore, to scan two indexes (by user_id and
  256. // thread.collaborators.user_id), we need two queries. A union will
  257. // help out with that.
  258. $mine->union($collab->filter(array(
  259. 'thread__collaborators__user_id' => $this->getId(),
  260. Q::not(array('user_id' => $this->getId()))
  261. )));
  262. if ($orgid = $this->getOrgId()) {
  263. // Also generate a separate query for all the tickets owned by
  264. // either my organization or ones that I'm collaborating on
  265. // which are not part of the organization.
  266. $myorg = clone $basic;
  267. $myorg->values('user__org_id');
  268. $collab = clone $myorg;
  269. $myorg->filter(array('user__org_id' => $orgid));
  270. $myorg->union($collab->filter(array(
  271. 'thread__collaborators__user_id' => $this->getId(),
  272. Q::not(array('user__org_id' => $orgid))
  273. )));
  274. }
  275. return array('mine' => $mine, 'myorg' => $myorg);
  276. }
  277. function onLogin($bk) {
  278. if ($account = $this->getAccount())
  279. $account->onLogin($bk);
  280. }
  281. }
  282. class ClientAccount extends UserAccount {
  283. function checkPassword($password, $autoupdate=true) {
  284. /*bcrypt based password match*/
  285. if(Passwd::cmp($password, $this->get('passwd')))
  286. return true;
  287. //Fall back to MD5
  288. if(!$password || strcmp($this->get('passwd'), MD5($password)))
  289. return false;
  290. //Password is a MD5 hash: rehash it (if enabled) otherwise force passwd change.
  291. if ($autoupdate)
  292. $this->set('passwd', Passwd::hash($password));
  293. if (!$autoupdate || !$this->save())
  294. $this->forcePasswdReset();
  295. return true;
  296. }
  297. function hasCurrentPassword($password) {
  298. return $this->checkPassword($password, false);
  299. }
  300. function cancelResetTokens() {
  301. // TODO: Drop password-reset tokens from the config table for
  302. // this user id
  303. $sql = 'DELETE FROM '.CONFIG_TABLE.' WHERE `namespace`="pwreset"
  304. AND `value`='.db_input('c'.$this->getUserId());
  305. if (!db_query($sql, false))
  306. return false;
  307. unset($_SESSION['_client']['reset-token']);
  308. }
  309. function onLogin($bk) {
  310. $this->setExtraAttr('browser_lang',
  311. Internationalization::getCurrentLanguage());
  312. $this->save();
  313. }
  314. function update($vars, &$errors) {
  315. global $cfg;
  316. // FIXME: Updates by agents should go through UserAccount::update()
  317. global $thisstaff;
  318. if ($thisstaff)
  319. return parent::update($vars, $errors);
  320. $rtoken = $_SESSION['_client']['reset-token'];
  321. if ($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd'] || $rtoken) {
  322. if (!$vars['passwd1'])
  323. $errors['passwd1']=__('New password is required');
  324. elseif ($vars['passwd1'] && strlen($vars['passwd1'])<6)
  325. $errors['passwd1']=__('Password must be at least 6 characters');
  326. elseif ($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2']))
  327. $errors['passwd2']=__('Passwords do not match');
  328. if ($rtoken) {
  329. $_config = new Config('pwreset');
  330. if ($_config->get($rtoken) != 'c'.$this->getUserId())
  331. $errors['err'] =
  332. __('Invalid reset token. Logout and try again');
  333. elseif (!($ts = $_config->lastModified($rtoken))
  334. && ($cfg->getPwResetWindow() < (time() - strtotime($ts))))
  335. $errors['err'] =
  336. __('Invalid reset token. Logout and try again');
  337. }
  338. elseif ($this->get('passwd')) {
  339. if (!$vars['cpasswd'])
  340. $errors['cpasswd']=__('Current password is required');
  341. elseif (!$this->hasCurrentPassword($vars['cpasswd']))
  342. $errors['cpasswd']=__('Invalid current password!');
  343. elseif (!strcasecmp($vars['passwd1'], $vars['cpasswd']))
  344. $errors['passwd1']=__('New password MUST be different from the current password!');
  345. }
  346. }
  347. // Timezone selection is not required. System default is a valid
  348. // fallback
  349. if ($errors) return false;
  350. $this->set('timezone', $vars['timezone']);
  351. $this->set('dst', isset($vars['dst']) ? 1 : 0);
  352. // Change language
  353. $this->set('lang', $vars['lang'] ?: null);
  354. Internationalization::setCurrentLanguage(null);
  355. TextDomain::configureForUser($this);
  356. if ($vars['backend']) {
  357. $this->set('backend', $vars['backend']);
  358. if ($vars['username'])
  359. $this->set('username', $vars['username']);
  360. }
  361. if ($vars['passwd1']) {
  362. $this->set('passwd', Passwd::hash($vars['passwd1']));
  363. $info = array('password' => $vars['passwd1']);
  364. Signal::send('auth.pwchange', $this->getUser(), $info);
  365. $this->cancelResetTokens();
  366. $this->clearStatus(UserAccountStatus::REQUIRE_PASSWD_RESET);
  367. }
  368. return $this->save();
  369. }
  370. }
  371. // Used by the email system
  372. interface EmailContact {
  373. // function getId()
  374. // function getName()
  375. // function getEmail()
  376. }
  377. interface ITicketUser {
  378. /* PHP 5.3 < 5.3.8 will crash with some abstract inheritance issue
  379. * ------------------------------------------------------------
  380. function isOwner();
  381. function flagGuest();
  382. function isGuest();
  383. function getUserId();
  384. function getTicketId();
  385. function getTicket();
  386. */
  387. }
  388. ?>