Elgg  Version 2.2
 All Classes Namespaces Files Functions Variables Pages
PersistentLoginService.php
Go to the documentation of this file.
1 <?php
2 namespace Elgg;
27 
38  public function __construct(
39  Database $db,
41  \ElggCrypto $crypto,
42  array $cookie_config,
43  $cookie_token,
44  $time = null) {
45  $this->db = $db;
46  $this->session = $session;
47  $this->crypto = $crypto;
48  $this->cookie_config = $cookie_config;
49  $this->cookie_token = $cookie_token;
50 
51  $prefix = $this->db->getTablePrefix();
52  $this->table = "{$prefix}users_remember_me_cookies";
53  $this->time = is_numeric($time) ? (int)$time : time();
54  }
55 
63  public function makeLoginPersistent(\ElggUser $user) {
64  $token = $this->generateToken();
65  $hash = $this->hashToken($token);
66 
67  $this->storeHash($user, $hash);
68  $this->setCookie($token);
69  $this->setSession($token);
70  }
71 
77  public function removePersistentLogin() {
78  if ($this->cookie_token) {
79  $client_hash = $this->hashToken($this->cookie_token);
80  $this->removeHash($client_hash);
81  }
82 
83  $this->setCookie("");
84  $this->setSession("");
85  }
86 
95  public function handlePasswordChange(\ElggUser $subject, \ElggUser $modifier = null) {
96  $this->removeAllHashes($subject);
97  if (!$modifier || ($modifier->guid !== $subject->guid) || !$this->cookie_token) {
98  return;
99  }
100 
101  $this->makeLoginPersistent($modifier);
102  }
103 
110  public function bootSession() {
111  if (!$this->cookie_token) {
112  return null;
113  }
114 
115  // is this token good?
116  $cookie_hash = $this->hashToken($this->cookie_token);
117  $user = $this->getUserFromHash($cookie_hash);
118  if ($user) {
119  $this->setSession($this->cookie_token);
120  // note: if the token is legacy, we don't both replacing it here because
121  // it will be replaced during the next request boot
122  return $user;
123  } else {
124  if ($this->isLegacyToken($this->cookie_token)) {
125  // may be attempt to brute force legacy low-entropy tokens
126  call_user_func($this->_callable_sleep, 1);
127  }
128  $this->setCookie('');
129  }
130  }
131 
139  public function replaceLegacyToken(\ElggUser $logged_in_user) {
140  if (!$this->cookie_token || !$this->isLegacyToken($this->cookie_token)) {
141  return;
142  }
143 
144  // replace user's old weaker-entropy code with new one
145  $this->removeHash($this->hashToken($this->cookie_token));
146  $this->makeLoginPersistent($logged_in_user);
147  }
148 
156  public function getUserFromHash($hash) {
157  if (!$hash) {
158  return null;
159  }
160 
161  $hash = $this->db->sanitizeString($hash);
162  $query = "SELECT guid FROM {$this->table} WHERE code = '$hash'";
163  try {
164  $user_row = $this->db->getDataRow($query);
165  } catch (\DatabaseException $e) {
166  return $this->handleDbException($e);
167  }
168  if (!$user_row) {
169  return null;
170  }
171 
172  $user = call_user_func($this->_callable_get_user, $user_row->guid);
173  return $user ? $user : null;
174  }
175 
184  protected function storeHash(\ElggUser $user, $hash) {
185  // This prevents inserting the same hash twice, which seems to be happening in some rare cases
186  // and for unknown reasons. See https://github.com/Elgg/Elgg/issues/8104
187  $this->removeHash($hash);
188 
189  $time = time();
190  $hash = $this->db->sanitizeString($hash);
191 
192  $query = "
193  INSERT INTO {$this->table} (code, guid, timestamp)
194  VALUES ('$hash', {$user->guid}, $time)
195  ";
196  try {
197  $this->db->insertData($query);
198  } catch (\DatabaseException $e) {
199  $this->handleDbException($e);
200  }
201  }
202 
209  protected function removeHash($hash) {
210  $hash = $this->db->sanitizeString($hash);
211 
212  $query = "DELETE FROM {$this->table} WHERE code = '$hash'";
213  try {
214  $this->db->deleteData($query);
215  } catch (\DatabaseException $e) {
216  $this->handleDbException($e);
217  }
218  }
219 
231  if (false !== strpos($exception->getMessage(), "users_remember_me_cookies' doesn't exist")) {
232  // schema has not been updated so we swallow this exception
233  return $default;
234  } else {
235  throw $exception;
236  }
237  }
238 
246  protected function removeAllHashes(\ElggUser $user) {
247  $query = "DELETE FROM {$this->table} WHERE guid = '{$user->guid}'";
248  try {
249  $this->db->deleteData($query);
250  } catch (\DatabaseException $e) {
251  $this->handleDbException($e);
252  }
253  }
254 
262  protected function hashToken($token) {
263  // note: with user passwords, you'd want legit password hashing, but since these are randomly
264  // generated and long tokens, rainbow tables aren't any help.
265  return md5($token);
266  }
267 
275  protected function setCookie($token) {
276  $cookie = new \ElggCookie($this->cookie_config['name']);
277  foreach (array('expire', 'path', 'domain', 'secure', 'httponly') as $key) {
278  $cookie->$key = $this->cookie_config[$key];
279  }
280  $cookie->value = $token;
281  if (!$token) {
282  $cookie->expire = $this->time - (86400 * 30);
283  }
284  call_user_func($this->_callable_elgg_set_cookie, $cookie);
285  }
286 
294  protected function setSession($token) {
295  if ($token) {
296  $this->session->set('code', $token);
297  } else {
298  $this->session->remove('code');
299  }
300  }
301 
310  protected function generateToken() {
311  return 'z' . $this->crypto->getRandomString(31);
312  }
313 
321  protected function isLegacyToken($token) {
322  return (isset($token[0]) && $token[0] !== 'z');
323  }
324 
328  protected $db;
329 
333  protected $table;
334 
338  protected $cookie_config;
339 
343  protected $cookie_token;
344 
348  protected $session;
349 
353  protected $crypto;
354 
358  protected $time;
359 
364  public $_callable_get_user = 'get_user';
365 
370  public $_callable_elgg_set_cookie = 'elgg_set_cookie';
371 
376  public $_callable_sleep = 'sleep';
377 }
378 
handleDbException(\DatabaseException $exception, $default=null)
Swallow a schema not upgraded exception, otherwise rethrow it.
$subject
Definition: exceptions.php:25
removeHash($hash)
Remove a hash from the DB.
makeLoginPersistent(\ElggUser $user)
Make the user's login persistent.
removeAllHashes(\ElggUser $user)
Remove all the hashes associated with a user.
$e
Definition: metadata.php:12
bootSession()
Boot the persistent login session, possibly returning the user who should be silently logged in...
__construct(Database $db,\ElggSession $session,\ElggCrypto $crypto, array $cookie_config, $cookie_token, $time=null)
Constructor.
$default
Definition: checkbox.php:35
storeHash(\ElggUser $user, $hash)
Store a hash in the DB.
setCookie($token)
Store the token in the client cookie (or remove the cookie)
getUserFromHash($hash)
Find a user with the given hash.
$exception
isLegacyToken($token)
Is the given token a legacy MD5 hash?
$key
Definition: summary.php:34
$user
Definition: ban.php:13
$token
table
Definition: admin.css.php:59
generateToken()
Generate a random token (base 64 URL)
hashToken($token)
Create a hash from the token.
removePersistentLogin()
Remove the persisted login token from client and server.
setSession($token)
Store the token in the session (or remove it from the session)
handlePasswordChange(\ElggUser $subject,\ElggUser $modifier=null)
Handle a password change.
$session
Definition: login.php:9
replaceLegacyToken(\ElggUser $logged_in_user)
Replace the user's token if it's a legacy hexadecimal token.