account activation and password recovery implemented
[living-lab-site.git] / application / models / users_model.php
1 <?php
2
3 /**
4  * Class Users_model models user information from DB
5  * 
6  * @category    Model
7  * @author              calinburloiu
8  *
9  */
10 class Users_model extends CI_Model {
11         public $db = NULL;
12
13         public function __construct()
14         {
15                 parent::__construct();
16
17                 if ($this->db === NULL)
18                 {
19                         $this->load->library('singleton_db');
20                         $this->db = $this->singleton_db->connect();
21                 }
22         }
23
24         /**
25          * Check authentication credentials. $username can be username or e-mail.
26          * 
27          * @param string $username
28          * @param string $password
29          * @return mixed can return FALSE if authentication failed, a DB row as an
30          * associative array if authentication was succesful or an associative
31          * array with LDAP user information if authentication with LDAP was
32          * successful but the user logged in for the first time and it does not
33          * have an entry in `users` table yet. The key 'auth_src' distinguishes
34          * which associative array was returned:
35          * <ul>
36          *   <li>'internal' or 'ldap': a DB row</li>
37          *   <li>'ldap_first_time': LDAP user information</li>
38          * </ul>
39          */
40         public function login($username, $password)
41         {
42                 $this->load->helper('email');
43                 
44                 // User logs with e-mail address.
45                 if (! valid_email($username))
46                         $cond_user = "username = '$username'";
47                 else
48                         $cond_user = "email = '$username'";
49                 
50                 $enc_password = sha1($password);
51                 
52                 // TODO select only required fields.
53                 $query = $this->db->query("SELECT u.*, a.activation_code
54                         FROM `users` u LEFT JOIN `users_unactivated` a ON (u.id = a.user_id)
55                         WHERE $cond_user
56                                 AND (auth_src = 'ldap' OR password = '$enc_password')");
57                 
58                 // It is possible that the user has a LDAP account but he's
59                 // authenticating here for the first time so it does not have an entry
60                 // in `users` table.
61                 if ($query->num_rows() === 0)
62                 {
63                         $ldap_userdata = $this->ldap_login($username, $password);
64                         if ($ldap_userdata === FALSE)
65                                 return FALSE;
66                         $userdata = $this->convert_ldap_userdata($ldap_userdata);
67                         $this->register($userdata);
68                         
69                         $user = $this->login($username, $password);
70                         $user['import'] = TRUE;
71                         return $user;
72                 }
73                 
74                 $user = $query->row_array();
75                 
76                 // Authenticate with LDAP.
77                 if ($user['auth_src'] == 'ldap'
78                                 && ! $this->ldap_login($username, $password))
79                         return FALSE; 
80                 
81                 // Update last login time.
82                 $this->db->query("UPDATE `users`
83                         SET last_login = UTC_TIMESTAMP()
84                         WHERE username = '$username'");
85                 
86                 // If we are here internal authentication has successful.
87                 return $user;
88         }
89         
90         /**
91          * Converts an array returned by LDAP login to an array which contains
92          * user data ready to be used in `users` DB.
93          * 
94          * @param array $ldap_userdata
95          * @return array
96          */
97         public function convert_ldap_userdata($ldap_userdata)
98         {
99                 $userdata['username'] = $ldap_userdata['uid'][0];
100                 $userdata['email'] = $ldap_userdata['mail'][0];
101                 $userdata['first_name'] = $ldap_userdata['givenname'][0];
102                 $userdata['last_name'] = $ldap_userdata['sn'][0];
103                 
104                 $userdata['auth_src'] = 'ldap';
105                 
106                 return $userdata;
107         }
108         
109         /**
110         * Login with LDAP.
111         *
112         * @param string $username
113         * @param string $password
114         * @return boolean
115         * @author  Alex Herișanu, Răzvan Deaconescu, Călin-Andrei Burloiu
116         */
117         public function ldap_login($username, $password)
118         {
119                 $this->config->load('ldap');
120                 
121                 // First connection: binding.
122                 // TODO exception
123                 $ds = ldap_connect($this->config->item('ldap_server')) or die("Can't connect to ldap server.\n");
124                 if (!@ldap_bind($ds, $this->config->item('ldap_bind_user'),
125                         $this->config->item('ldap_bind_password'))) 
126                 {
127                         ldap_close($ds);
128                         die("Can't connect to ".$this->config->item('ldap_server')."\n");
129                         return FALSE;
130                 }
131                 $sr = ldap_search($ds, "dc=cs,dc=curs,dc=pub,dc=ro", "(uid=" . $username . ")");
132                 if (ldap_count_entries($ds, $sr) > 1)
133                 die("Multiple entries with the same uid in LDAP database??");
134                 if (ldap_count_entries($ds, $sr) < 1) {
135                         ldap_close($ds);
136                         return FALSE;
137                 }
138                 
139                 $info = ldap_get_entries($ds, $sr);
140                 $dn = $info[0]["dn"];
141                 ldap_close($ds);
142                 
143                 // Second connection: connect with user's credentials.
144                 $ds = ldap_connect($this->config->item('ldap_server')) or die("Can't connect to ldap server\n");
145                 if (!@ldap_bind($ds, $dn, $password) or $password == '') {
146                         ldap_close($ds);
147                         return FALSE;
148                 }
149                 
150                 // Verifify if DN belongs to the requested OU.
151                 $info[0]['ou_ok'] = $this->ldap_dn_belongs_ou( $dn, $this->config->item('ldap_req_ou') );
152                 
153                 // Set authentication source.
154                 $info[0]['auth_src'] = 'ldap_first_time';
155                 
156                 return $info[0];
157         }
158         
159         /**
160         * Verify if a user belongs to a group.
161         * 
162         * @param string $dn = "ou=Student,ou=People..."
163         * @param array $ou = array ("Student", etc
164         * @return TRUE or FALSE
165         * @author  Răzvan Herișanu, Răzvan Deaconescu, Călin-Andrei Burloiu
166         */
167         public function ldap_dn_belongs_ou($dn, $ou)
168         {
169                 if (!is_array($ou))
170                         $ou = array ($ou);
171                 
172                 $founded = FALSE;
173                 $words = explode(',', $dn);
174                 foreach ($words as $c) {
175                         $parts = explode("=", $c);
176                         $key = $parts[0];
177                         $value = $parts[1];
178                 
179                         if (strtolower($key) == "ou" && in_array($value, $ou) )
180                                 $founded = TRUE;
181                 }
182                 
183                 return $founded;
184         }
185         
186         /**
187          * Adds a new user to DB.
188          * Do not add join_date and last_login column, they will be automatically
189          * added.
190          * 
191          * @param array $data   corresponds to DB columns
192          */
193         public function register($data)
194         {
195                 $this->load->helper('array');
196                 
197                 // TODO verify mandatory data existance
198                 
199                 // Process data.
200                 if (isset($data['password']))
201                         $data['password'] = sha1($data['password']);
202                 // TODO picture data: save, convert, make it thumbnail
203                 
204                 $cols = '';
205                 $vals = '';
206                 foreach ($data as $col=> $val)
207                 {
208                         $cols .= "$col, ";
209                         if (is_int($val))
210                                 $vals .= "$val, ";
211                         else
212                                 $vals .= "'$val', ";
213                 }
214                 $cols = substr($cols, 0, -2);
215                 $vals = substr($vals, 0, -2);
216                 
217                 $query = $this->db->query("INSERT INTO `users`
218                         ($cols, registration_date, last_login)
219                         VALUES ($vals, utc_timestamp(), utc_timestamp())");
220                 
221                 if ($query === FALSE)
222                         return FALSE;
223                 
224                 // If registered with internal authentication it needs to activate
225                 // the account.
226                 $activation_code = Users_model::gen_activation_code($data['username']);
227                 $user_id = $this->get_user_id($data['username']);
228                 $query = $this->db->query("INSERT INTO `users_unactivated`
229                         (user_id, activation_code)
230                         VALUES ($user_id, '$activation_code')");
231                 $this->send_activation_email($user_id, $data['email'],
232                         $activation_code, $data['username']);
233                 
234                 // TODO exception on failure
235                 return $query;
236         }
237         
238         public function get_user_id($username)
239         {
240                 $query = $this->db->query("SELECT id FROM `users`
241                         WHERE username = '$username'");
242                 
243                 if ($query->num_rows() === 0)
244                         return FALSE;
245                 
246                 return $query->row()->id;
247         }
248         
249         // TODO cleanup account activation
250         public function cleanup_account_activation()
251         {
252                 
253         }
254         
255         /**
256          * Activated an account for an user having $user_id with $activation_code.
257          * 
258          * @param int $user_id
259          * @param string $activation_code       hexa 16 characters string
260          * @return returns TRUE if activation was successful and FALSE otherwise
261          */
262         public function activate_account($user_id, $activation_code)
263         {
264                 $query = $this->db->query("SELECT * FROM `users_unactivated`
265                         WHERE user_id = $user_id
266                                 AND activation_code = '$activation_code'");
267                 
268                 if ($query->num_rows() === 0)
269                         return FALSE;
270                 
271                 $this->db->query("DELETE FROM `users_unactivated`
272                         WHERE user_id = $user_id");
273                 
274                 return TRUE;
275         }
276         
277         public function send_activation_email($user_id, $email = NULL,
278                         $activation_code = NULL, $username = NULL)
279         {
280                 if (!$activation_code || !$email || !$username)
281                 {
282                         if (!$email)
283                                 $cols = 'email, ';
284                         else
285                                 $cols = '';
286                         
287                         $userdata = $this->get_userdata($user_id,
288                                         $cols. "a.activation_code, username");
289                         $activation_code =& $userdata['activation_code'];
290                         
291                         if (!$email)
292                                 $email =& $userdata['email'];
293                         $username =& $userdata['username'];
294                 }
295                 
296                 if ($activation_code === NULL)
297                         return TRUE;
298                 
299                 $subject = '['. $this->config->item('site_name')
300                                 . '] Account Activation';
301                 $activation_url =
302                                 site_url("user/activate/$user_id/code/$activation_code"); 
303                 $msg = sprintf($this->lang->line('user_activation_email_content'),
304                         $username, $this->config->item('site_name'), site_url(),
305                         $activation_url, $activation_code);
306                 $headers = "From: ". $this->config->item('noreply_email');
307                 
308                 return mail($email, $subject, $msg, $headers);
309         }
310         
311         public function recover_password($username, $email)
312         {
313                 $userdata = $this->get_userdata($username, 'email, username, id');
314                 
315                 if (strcmp($userdata['email'], $email) !== 0)
316                         return FALSE;
317                 
318                 $recovered_password = Users_model::gen_password();
319                 
320                 $this->set_userdata(intval($userdata['id']), array('password'=> 
321                                 $recovered_password));
322                 
323                 $subject = '['. $this->config->item('site_name')
324                 . '] Password Recovery';
325                 $msg = sprintf($this->lang->line('user_password_recovery_email_content'),
326                         $username, $this->config->item('site_name'), site_url(),
327                         $recovered_password);
328                 $headers = "From: ". $this->config->item('noreply_email');
329                 
330                 mail($email, $subject, $msg, $headers);
331                 
332                 return TRUE;
333         }
334         
335         /**
336          * Returns data from `users` table. If $user is int it is used as an
337          * id, if it is string it is used as an username.
338          * 
339          * @param mixed $user
340          * @param string $table_cols    (optional) string with comma separated
341          * `users` table column names. Use a.activation_code to check user's
342          * account activation_code. If this value is NULL than the account is
343          * active.
344          * @return array        associative array with userdata from DB
345          */
346         public function get_userdata($user, $table_cols = '*')
347         {
348                 if (is_int($user))
349                         $cond = "id = $user";
350                 else
351                         $cond = "username = '$user'";
352                 
353                 $query = $this->db->query("SELECT $table_cols
354                         FROM `users` u LEFT JOIN `users_unactivated` a
355                                 ON (u.id = a.user_id)
356                         WHERE $cond");
357                 
358                 if ($query->num_rows() === 0)
359                         return FALSE;
360                 
361                 return $query->row_array();
362         }
363         
364         /**
365          * Modifies data from `users` table for user with $user_id.
366          * 
367          * @param int $user_id
368          * @param array $data   key-value pairs with columns and new values to be
369          * modified
370          * @return boolean      returns TRUE on success and FALSE otherwise
371          */
372         public function set_userdata($user_id, $data)
373         {
374                 // TODO verify mandatory data existance
375                 
376                 // Process data.
377                 if (isset($data['password']))
378                         $data['password'] = sha1($data['password']);
379                 // TODO picture data: save, convert, make it thumbnail
380                 
381                 $set = '';
382                 foreach ($data as $col => $val)
383                 {
384                         if (is_int($val))
385                                 $set .= "$col = $val, ";
386                         else
387                                 $set .= "$col = '$val', ";
388                 }
389                 $set = substr($set, 0, -2);
390                 
391                 $query_str = "UPDATE `users`
392                         SET $set WHERE id = $user_id";
393                 //echo "<p>$query_str</p>";
394                 $query = $this->db->query($query_str);
395                 
396                 // TODO exception
397                 return $query;
398         }
399         
400         public static function gen_activation_code($str = '')
401         {
402                 $ci =& get_instance();
403                 
404                 $activation_code = substr(
405                         sha1(''. $str. $ci->config->item('encryption_key')
406                                 . mt_rand()),
407                         0,
408                         16);
409                 
410                 return $activation_code;
411         }
412         
413         public static function gen_password()
414         {
415                 $ci =& get_instance();
416                 $length = 16;
417                 $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,.?!_-';
418                 $len_chars = strlen($chars);
419                 $enc_key = $ci->config->item('encryption_key');
420                 $len_enc_key = strlen($enc_key);
421                 $password = '';
422                 
423                 for ($p = 0; $p < $length; $p++) 
424                 {
425                         $i = (mt_rand(1, 100) * ord($enc_key[ mt_rand(0, $len_enc_key-1) ]))
426                                 % $len_chars;
427                         $password .= $chars[$i];
428                 }
429                 
430                 return $password;
431         } 
432         
433         public static function roles_to_string($roles)
434         {
435                 $ci =& get_instance();
436                 $ci->lang->load('user');
437                 
438                 if ($roles == USER_ROLE_STANDARD)
439                         return $ci->lang->line('user_role_standard');
440                 else
441                 {
442                         $str_roles = '';
443                         
444                         if ($roles & USER_ROLE_ADMIN)
445                                 $str_roles .= $ci->lang->line('user_role_admin') . '; ';
446                 }
447                 
448                 return $str_roles;
449         }
450 }
451
452 /* End of file users_model.php */
453 /* Location: ./application/models/users_model.php */