make sure we've ran our own random seeder before using mt_rand
[squirrelmail.git] / plugins / change_password / backend / peardb.php
CommitLineData
d032df69 1<?php
4b4abf93 2
d032df69 3/**
4 * Change password PearDB backend
5 *
4b5049de 6 * @copyright &copy; 2005-2007 The SquirrelMail Project Team
4b4abf93 7 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
d032df69 8 * @version $Id$
9 * @package plugins
10 * @subpackage change_password
11 */
12
202bcbcc 13/** load Pear DB.
14 * Global is needed because library must be loaded before configuration
d032df69 15 * in order to use DB constants.
16 */
17global $cpw_peardb_detect;
18$cpw_peardb_detect=@include_once('DB.php');
19
20/** declare configuration globals */
202bcbcc 21global $cpw_peardb_dsn, $cpw_peardb_connect_opts, $cpw_peardb_table,
d032df69 22 $cpw_peardb_uid_field, $cpw_peardb_domain_field, $cpw_peardb_passwd_field,
23 $cpw_peardb_crypted_passwd, $cpw_peardb_debug;
24
25/**
26 * Connection DSN.
27 * Any format supported by peardb
28 * @global mixed $cpw_peardb_dsn
29 */
30$cpw_peardb_dsn='';
31
32/**
33 * Pear DB connection options
34 * @global array $cpw_peardb_connect_opts
35 */
36$cpw_peardb_connect_opts=array();
37
38/**
39 * Table that stores user information
40 * @global string $cpw_peardb_table
41 */
42$cpw_peardb_table='';
43
44/**
45 * Field that stores user name
46 * @global string $cpw_peardb_uid_field
47 */
48$cpw_peardb_uid_field='userid';
49
50/**
51 * Field that stores domain part of username
52 * @global string $cpw_peardb_domain_field
53 */
54$cpw_peardb_domain_field='';
55
56/**
57 * Field that stores password
58 * @global string $cpw_peardb_passwd_field
59 */
60$cpw_peardb_passwd_field='password';
61
62/**
63 * Passwords are plaintext or encrypted
64 * @global boolean $cpw_peardb_crypted_passwd
65 */
66$cpw_peardb_crypted_passwd=false;
67
68/**
69 * Controls output debugging errors
70 * Error messages might contain login and password information.
71 * Don't enable on production systems.
72 * @global boolean $cpw_peardb_debug
73 */
74$cpw_peardb_debug=false;
75
76/** configuration overrides */
77if ( isset($cpw_peardb) && is_array($cpw_peardb) && !empty($cpw_peardb) ) {
78 if (isset($cpw_peardb['dsn']))
79 $cpw_peardb_dsn=$cpw_peardb['dsn'];
80 if (isset($cpw_peardb['connect_opts']))
b437888c 81 $cpw_peardb_connect_opts=$cpw_peardb['connect_opts'];
d032df69 82 if (isset($cpw_peardb['table']))
83 $cpw_peardb_table=$cpw_peardb['table'];
84 if (isset($cpw_peardb['uid_field']))
85 $cpw_peardb_uid_field=$cpw_peardb['uid_field'];
86 if (isset($cpw_peardb['domain_field']))
87 $cpw_peardb_domain_field=$cpw_peardb['domain_field'];
88 if (isset($cpw_peardb['password_field']))
89 $cpw_peardb_passwd_field=$cpw_peardb['password_field'];
90 if (isset($cpw_peardb['crypted_passwd']))
91 $cpw_peardb_crypted_passwd=true;
92 if (isset($cpw_peardb['debug']))
93 $cpw_peardb_debug=$cpw_peardb['debug'];
94}
95
96/**
97 * Define here the name of your password changing function.
98 */
99global $squirrelmail_plugin_hooks;
100$squirrelmail_plugin_hooks['change_password_dochange']['peardb'] =
101 'cpw_peardb_dochange';
102$squirrelmail_plugin_hooks['change_password_init']['peardb'] =
103 'cpw_peardb_init';
104
105/**
106 * Checks if configuration is correct
107 */
108function cpw_peardb_init() {
1b858d86 109 global $oTemplate, $cpw_peardb_detect, $cpw_peardb_dsn, $cpw_peardb_table;
d032df69 110
d032df69 111 if (! $cpw_peardb_detect) {
1b858d86 112 error_box(_("Plugin is unable to use PHP Pear DB libraries. PHP Pear includes must be available in your PHP include_path setting."));
113 $oTemplate->display('footer.tpl');
d032df69 114 exit();
115 }
116
117 // Test required settings
202bcbcc 118 if ((is_string($cpw_peardb_dsn) && trim($cpw_peardb_dsn)=='')
d032df69 119 || trim($cpw_peardb_table)=='' ) {
1b858d86 120 error_box(_("Required change password backend configuration options are missing."));
121 $oTemplate->display('footer.tpl');
d032df69 122 exit();
123 }
124}
125
126
127/**
128 * Changes password
129 * @param array data The username/curpw/newpw data.
130 * @return array Array of error messages.
131 */
132function cpw_peardb_dochange($data) {
133 global $cpw_peardb_dsn, $cpw_peardb_table, $cpw_peardb_connect_opts, $cpw_peardb_debug,
202bcbcc 134 $cpw_peardb_uid_field, $cpw_peardb_passwd_field, $cpw_peardb_domain_field,
d032df69 135 $cpw_peardb_crypted_passwd, $domain;
136
137 $username = $data['username'];
138 $curpw = $data['curpw'];
139 $newpw = $data['newpw'];
140
141 $msgs = array();
142
143 // split user and domain parts from username, if domain field is set and username looks like email.
144 if ($cpw_peardb_domain_field!='' && preg_match("/(.*)@(.*)/",$username,$match)) {
145 $user=$match[1];
146 $user_domain=$match[2];
147 } else {
148 $user=$username;
149 $user_domain=$domain;
150 }
202bcbcc 151
d032df69 152 // connect to database and make sure that table exists
153 $cpw_db = DB::connect($cpw_peardb_dsn, $cpw_peardb_connect_opts);
154 if (PEAR::isError($cpw_db)) {
155 array_push($msgs,sprintf(_("Connection error: %s"),htmlspecialchars($cpw_db->getMessage())));
156 if ($cpw_peardb_debug)
157 array_push($msgs,htmlspecialchars($cpw_db->getuserinfo()));
158 return $msgs;
159 }
160
161 // get table information
162 $table_info = $cpw_db->tableinfo($cpw_peardb_table);
163 if (PEAR::isError($table_info)) {
164 array_push($msgs,sprintf(_("Invalid table name: %s"),htmlspecialchars($cpw_peardb_table)));
165 $cpw_db->disconnect();
166 return $msgs;
167 }
168
169 if (empty($table_info)) {
170 array_push($msgs,_("User table is empty."));
171 $cpw_db->disconnect();
172 return $msgs;
173 }
174
175 $cpw_peardb_uid_check=false;
176 $cpw_peardb_passwd_check=false;
177 $cpw_peardb_domain_check=(($cpw_peardb_domain_field=='')? true : false);
178 foreach($table_info as $key => $field_data) {
202bcbcc 179 if ($field_data['name']==$cpw_peardb_uid_field)
d032df69 180 $cpw_peardb_uid_check=true;
202bcbcc 181 if ($field_data['name']==$cpw_peardb_passwd_field)
d032df69 182 $cpw_peardb_passwd_check=true;
183 if ($cpw_peardb_domain_field!='' && $field_data['name']==$cpw_peardb_domain_field)
184 $cpw_peardb_domain_check=true;
185 }
186 if (! $cpw_peardb_uid_check) {
187 array_push($msgs,_("Invalid uid field."));
188 }
189 if (! $cpw_peardb_passwd_check) {
190 array_push($msgs,_("Invalid password field"));
191 }
192 if (! $cpw_peardb_domain_check) {
193 array_push($msgs,_("Invalid domain field"));
194 }
195 if (! empty($msgs)) {
196 $cpw_db->disconnect();
197 return $msgs;
198 }
199
200 // find user's entry
201 $query='SELECT'
202 .' '.$cpw_db->quoteIdentifier($cpw_peardb_uid_field)
203 .', '.$cpw_db->quoteIdentifier($cpw_peardb_passwd_field)
204 .(($cpw_peardb_domain_field!='') ? ', '.$cpw_db->quoteIdentifier($cpw_peardb_domain_field):'')
205 .' FROM '.$cpw_db->quoteIdentifier($cpw_peardb_table)
206 .' WHERE '
207 .$cpw_db->quoteIdentifier($cpw_peardb_uid_field).'='.$cpw_db->quoteSmart($user)
202bcbcc 208 .(($cpw_peardb_domain_field!='') ?
d032df69 209 ' AND '.$cpw_db->quoteIdentifier($cpw_peardb_domain_field).'='.$cpw_db->quoteSmart($user_domain):
210 '');
211 $cpw_res=$cpw_db->query($query);
212 if (PEAR::isError($cpw_res)) {
213 array_push($msgs,sprintf(_("Query failed: %s"),htmlspecialchars($cpw_res->getMessage())));
214 $cpw_db->disconnect();
215 return $msgs;
216 }
217
218 // make sure that there is only one user.
219 if ($cpw_res->numRows()==0) {
220 array_push($msgs,_("Unable to find user in user table."));
221 $cpw_db->disconnect();
222 return $msgs;
223 }
224
225 if ($cpw_res->numRows()>1) {
226 array_push($msgs,_("Too many matches found in user table."));
227 $cpw_db->disconnect();
228 return $msgs;
229 }
230
231 // FIXME: process possible errors
232 $cpw_res->fetchInto($userdb,DB_FETCHMODE_ASSOC);
233
234 // validate password
235 $valid_passwd=false;
236 if ($cpw_peardb_crypted_passwd) {
237 // detect password type
238 $pw_type=cpw_peardb_detect_crypto($userdb[$cpw_peardb_passwd_field]);
239 if (! $pw_type) {
240 array_push($msgs,_("Unable to detect password crypto algorithm."));
241 } else {
242 $hashed_pw=cpw_peardb_passwd_hash($curpw,$pw_type,$msgs,$userdb[$cpw_peardb_passwd_field]);
243 if ($hashed_pw==$userdb[$cpw_peardb_passwd_field]) {
244 $valid_passwd=true;
245 }
246 }
247 } elseif ($userdb[$cpw_peardb_passwd_field]==$curpw) {
248 $valid_passwd=true;
249 }
250
251 if (! $valid_passwd) {
252 array_push($msgs,CPW_CURRENT_NOMATCH);
253 $cpw_db->disconnect();
254 return $msgs;
255 }
256
257 // create new password
258 if ($cpw_peardb_crypted_passwd) {
259 $hashed_passwd=cpw_peardb_passwd_hash($newpw,$pw_type,$msgs);
260 } else {
261 $hashed_passwd=$newpw;
262 }
263
264 // make sure that password was created
265 if (! empty($msgs)) {
266 array_push($msgs,_("Unable to encrypt new password."));
267 $cpw_db->disconnect();
268 return $msgs;
269 }
270
271 // create update query
272 $update_query='UPDATE '
273 . $cpw_db->quoteIdentifier($cpw_peardb_table)
274 .' SET '.$cpw_db->quoteIdentifier($cpw_peardb_passwd_field)
275 .'='.$cpw_db->quoteSmart($hashed_passwd)
276 .' WHERE '.$cpw_db->quoteIdentifier($cpw_peardb_uid_field)
277 .'='.$cpw_db->quoteSmart($user)
202bcbcc 278 .(($cpw_peardb_domain_field!='') ?
d032df69 279 ' AND '.$cpw_db->quoteIdentifier($cpw_peardb_domain_field).'='.$cpw_db->quoteSmart($user_domain) :
280 '');
281
282 // update database
283 $cpw_res=$cpw_db->query($update_query);
284
285 // check for update error
286 if (PEAR::isError($cpw_res)) {
287 array_push($msgs,sprintf(_("Unable to set new password: %s"),htmlspecialchars($cpw_res->getMessage())));
288 }
289
290 // close database connection
291 $cpw_db->disconnect();
292
293 return $msgs;
294}
295
296/**
297 * Detects password crypto
298 * reports 'crypt' if fails to detect any other crypt
299 * @param string $password
300 * @return string
301 */
302function cpw_peardb_detect_crypto($password) {
303 $ret = false;
304
305 if (preg_match("/^\{(.+)\}+/",$password,$crypto)) {
306 $ret=strtolower($crypto[1]);
307 }
308
309 if ($ret=='crypt') {
310 // {CRYPT} can be standard des crypt, extended des crypt, md5 crypt or blowfish
311 // depends on first salt symbols (ext_des = '_', md5 = '$1$', blowfish = '$2')
312 // and length of salt (des = 2 chars, ext_des = 9, md5 = 12, blowfish = 16).
313 if (preg_match("/^\{crypt\}\\\$1\\\$+/i",$password)) {
314 $ret='tagged_md5crypt';
315 } elseif (preg_match("/^\{crypt\}\\\$2+/i",$password)) {
316 $ret='tagged_blowfish';
317 } elseif (preg_match("/^\{crypt\}_+/i",$password)) {
318 $ret='tagged_extcrypt';
319 } else {
320 $ret='tagged_crypt';
321 }
322 }
323
324 if (! $ret) {
325 if (preg_match("/^\\\$1\\\$+/i",$password)) {
326 $ret='md5crypt';
327 } elseif (preg_match("/^\\\$2+/i",$password)) {
328 $ret='blowfish';
329 } elseif (preg_match("/^_+/i",$password)) {
330 $ret='extcrypt';
331 } else {
332 $ret='crypt';
333 }
334 }
335 return $ret;
336}
337
338/**
339 * Encode password
340 * @param string $password plain text password
341 * @param string $crypto used crypto
342 * @param array $msgs error messages
343 * @param string $forced_salt old password used to create password hash for verification
344 * @return string hashed password. false, if hashing fails
345 */
346function cpw_peardb_passwd_hash($password,$crypto,&$msgs,$forced_salt='') {
347 global $username;
348
349 $crypto = strtolower($crypto);
350
351 $ret=false;
352 $salt='';
353 // extra symbols used for random string in crypt salt
354 // squirrelmail GenerateRandomString() adds alphanumerics with third argument = 7.
355 $extra_salt_chars='./';
356
357 switch($crypto) {
358 case 'plain-md5':
359 $ret='{PLAIN-MD5}' . md5($password);
360 break;
361 case 'digest-md5':
362 // split username into user and domain parts
363 if (preg_match("/(.*)@(.*)/",$username,$match)) {
364 $ret='{DIGEST-MD5}' . md5($match[1].':'.$match[2].':'.$password);
365 } else {
366 array_push($msgs,_("Unable to use digest-md5 crypto."));
367 }
368 break;
369 case 'tagged_crypt':
370 case 'crypt':
371 if (! defined('CRYPT_STD_DES') || CRYPT_STD_DES==0) {
372 array_push($msgs,sprintf(_("Unsupported crypto: %s"),'crypt'));
373 break;
374 }
375 if ($forced_salt=='') {
376 $salt=GenerateRandomString(2,$extra_salt_chars,7);
377 } else {
378 $salt=$forced_salt;
379 }
380 $ret = ($crypto=='tagged_crypt' ? '{crypt}' : '');
381 $ret.= crypt($password,$salt);
382 break;
383 case 'tagged_md5crypt':
384 case 'md5crypt':
385 if (! defined('CRYPT_MD5') || CRYPT_MD5==0) {
386 array_push($msgs,sprintf(_("Unsupported crypto: %s"),'md5crypt'));
387 break;
388 }
389 if ($forced_salt=='') {
390 $salt='$1$' .GenerateRandomString(9,$extra_salt_chars,7);
391 } else {
392 $salt=$forced_salt;
393 }
394 $ret = ($crypto=='tagged_md5crypt' ? '{crypt}' : '');
395 $ret.= crypt($password,$salt);
396 break;
397 case 'tagged_extcrypt':
398 case 'extcrypt':
399 if (! defined('CRYPT_EXT_DES') || CRYPT_EXT_DES==0) {
400 array_push($msgs,sprintf(_("Unsupported crypto: %s"),'extcrypt'));
401 break;
402 }
403 if ($forced_salt=='') {
404 $salt='_' . GenerateRandomString(8,$extra_salt_chars,7);
405 } else {
406 $salt=$forced_salt;
407 }
408 $ret = ($crypto=='tagged_extcrypt' ? '{crypt}' : '');
409 $ret.= crypt($password,$salt);
410 break;
411 case 'tagged_blowfish':
412 case 'blowfish':
413 if (! defined('CRYPT_BLOWFISH') || CRYPT_BLOWFISH==0) {
414 array_push($msgs,sprintf(_("Unsupported crypto: %s"),'blowfish'));
415 break;
416 }
417 if ($forced_salt=='') {
418 $salt='$2a$12$' . GenerateRandomString(13,$extra_salt_chars,7);
419 } else {
420 $salt=$forced_salt;
421 }
422 $ret = ($crypto=='tagged_blowfish' ? '{crypt}' : '');
423 $ret.= crypt($password,$salt);
424 break;
425 case 'plain':
426 case 'plaintext':
427 $ret = $password;
428 break;
429 default:
430 array_push($msgs,sprintf(_("Unsupported crypto: %s"),htmlspecialchars($crypto)));
431 }
432 return $ret;
433}