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