]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiUserNew.php
Partial revert to get saving UserPreferences work again
[SourceForge/phpwiki.git] / lib / WikiUserNew.php
1 <?php //-*-php-*-
2 //rcs_id('$Id$');
3 /* Copyright (C) 2004,2005,2006,2007,2009,2010 $ThePhpWikiProgrammingTeam
4 * Copyright (C) 2009-2010 Marc-Etienne Vargenau, Alcatel-Lucent
5 * Copyright (C) 2009-2010 Roger Guignard, Alcatel-Lucent
6 *
7 * This file is part of PhpWiki.
8 *
9 * PhpWiki is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * PhpWiki is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with PhpWiki; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22 */
23 /**
24  * This is a complete OOP rewrite of the old WikiUser code with various
25  * configurable external authentication methods.
26  *
27  * There's only one entry point, the function WikiUser which returns 
28  * a WikiUser object, which contains the name, authlevel and user's preferences.
29  * This object might get upgraded during the login step and later also.
30  * There exist three preferences storage methods: cookie, homepage and db,
31  * and multiple password checking methods.
32  * See index.php for $USER_AUTH_ORDER[] and USER_AUTH_POLICY if 
33  * ALLOW_USER_PASSWORDS is defined.
34  *
35  * Each user object must define the two preferences methods 
36  *  getPreferences(), setPreferences(), 
37  * and the following 1-4 auth methods
38  *  checkPass()  must be defined by all classes,
39  *  userExists() only if USER_AUTH_POLICY'=='strict' 
40  *  mayChangePass()  only if the password is storable.
41  *  storePass()  only if the password is storable.
42  *
43  * WikiUser() given no name, returns an _AnonUser (anonymous user)
44  * object, who may or may not have a cookie. 
45  * However, if the there's a cookie with the userid or a session, 
46  * the user is upgraded to the matching user object.
47  * Given a user name, returns a _BogoUser object, who may or may not 
48  * have a cookie and/or PersonalPage, one of the various _PassUser objects 
49  * or an _AdminUser object.
50  * BTW: A BogoUser is a userid (loginname) as valid WikiWord, who might 
51  * have stored a password or not. If so, his account is secure, if not
52  * anybody can use it, because the username is visible e.g. in RecentChanges.
53  *
54  * Takes care of passwords, all preference loading/storing in the
55  * user's page and any cookies. lib/main.php will query the user object to
56  * verify the password as appropriate.
57  *
58  * @author: Reini Urban (the tricky parts), 
59  *          Carsten Klapp (started rolling the ball)
60  *
61  * Random architectural notes, sorted by date:
62  * 2004-01-25 rurban
63  * Test it by defining ENABLE_USER_NEW in config/config.ini
64  * 1) Now a ForbiddenUser is returned instead of false.
65  * 2) Previously ALLOW_ANON_USER = false meant that anon users cannot edit, 
66  *    but may browse. Now with ALLOW_ANON_USER = false he may not browse, 
67  *    which is needed to disable browse PagePermissions.
68  *    I added now ALLOW_ANON_EDIT = true to makes things clear. 
69  *    (which replaces REQUIRE_SIGNIN_BEFORE_EDIT)
70  * 2004-02-27 rurban:
71  * 3) Removed pear prepare. Performance hog, and using integers as 
72  *    handler doesn't help. Do simple sprintf as with adodb. And a prepare
73  *    in the object init is no advantage, because in the init loop a lot of 
74  *    objects are tried, but not used.
75  * 4) Already gotten prefs are passed to the next object to avoid 
76  *    duplicate getPreferences() calls.
77  * 2004-03-18 rurban
78  * 5) Major php-5 problem: $this re-assignment is disallowed by the parser
79  *    So we cannot just discrimate with 
80  *      if (!check_php_version(5))
81  *          $this = $user;
82  *    A /php5-patch.php is provided, which patches the src automatically 
83  *    for php4 and php5. Default is php4.
84  *    Update: not needed anymore. we use eval to fool the load-time syntax checker.
85  * 2004-03-24 rurban
86  * 6) enforced new cookie policy: prefs don't get stored in cookies
87  *    anymore, only in homepage and/or database, but always in the 
88  *    current session. old pref cookies will get deleted.
89  * 2004-04-04 rurban
90  * 7) Certain themes should be able to extend the predefined list 
91  *    of preferences. Display/editing is done in the theme specific userprefs.tmpl,
92  *    but storage must be extended to the Get/SetPreferences methods.
93  *    <theme>/themeinfo.php must provide CustomUserPreferences:
94  *      A list of name => _UserPreference class pairs.
95  * 2010-06-07 rurban
96  *    Fixed a nasty recursion bug (i.e. php crash), when user = new class 
97  *    which returned false, did not return false on php-4.4.7. Check for 
98  *    a object member now.
99  */
100
101 define('WIKIAUTH_FORBIDDEN', -1); // Completely not allowed.
102 define('WIKIAUTH_ANON', 0);       // Not signed in.
103 define('WIKIAUTH_BOGO', 1);       // Any valid WikiWord is enough.
104 define('WIKIAUTH_USER', 2);       // Bogo user with a password.
105 define('WIKIAUTH_ADMIN', 10);     // UserName == ADMIN_USER.
106 define('WIKIAUTH_UNOBTAINABLE', 100);  // Permissions that no user can achieve
107
108 //if (!defined('COOKIE_EXPIRATION_DAYS')) define('COOKIE_EXPIRATION_DAYS', 365);
109 //if (!defined('COOKIE_DOMAIN'))          define('COOKIE_DOMAIN', '/');
110 if (!defined('EDITWIDTH_MIN_COLS'))     define('EDITWIDTH_MIN_COLS',     30);
111 if (!defined('EDITWIDTH_MAX_COLS'))     define('EDITWIDTH_MAX_COLS',    150);
112 if (!defined('EDITWIDTH_DEFAULT_COLS')) define('EDITWIDTH_DEFAULT_COLS', 80);
113
114 if (!defined('EDITHEIGHT_MIN_ROWS'))     define('EDITHEIGHT_MIN_ROWS',      5);
115 if (!defined('EDITHEIGHT_MAX_ROWS'))     define('EDITHEIGHT_MAX_ROWS',     80);
116 if (!defined('EDITHEIGHT_DEFAULT_ROWS')) define('EDITHEIGHT_DEFAULT_ROWS', 22);
117
118 define('TIMEOFFSET_MIN_HOURS', -26);
119 define('TIMEOFFSET_MAX_HOURS',  26);
120 if (!defined('TIMEOFFSET_DEFAULT_HOURS')) define('TIMEOFFSET_DEFAULT_HOURS', 0);
121
122 /* EMAIL VERIFICATION
123  * On certain nets or hosts the email domain cannot be determined automatically from the DNS.
124  * Provide some overrides here.
125  *    ( username @ ) domain => mail-domain
126  */
127 $EMailHosts = array('avl.com' => 'mail.avl.com');
128
129 /**
130  * There are be the following constants in config/config.ini to 
131  * establish login parameters:
132  *
133  * ALLOW_ANON_USER         default true
134  * ALLOW_ANON_EDIT         default true
135  * ALLOW_BOGO_LOGIN        default true
136  * ALLOW_USER_PASSWORDS    default true
137  * PASSWORD_LENGTH_MINIMUM default 0
138  *
139  * To require user passwords for editing:
140  * ALLOW_ANON_USER  = true
141  * ALLOW_ANON_EDIT  = false   (before named REQUIRE_SIGNIN_BEFORE_EDIT)
142  * ALLOW_BOGO_LOGIN = false
143  * ALLOW_USER_PASSWORDS = true
144  *
145  * To establish a COMPLETELY private wiki, such as an internal
146  * corporate one:
147  * ALLOW_ANON_USER = false
148  * (and probably require user passwords as described above). In this
149  * case the user will be prompted to login immediately upon accessing
150  * any page.
151  *
152  * There are other possible combinations, but the typical wiki (such
153  * as http://PhpWiki.sf.net/phpwiki) would usually just leave all four 
154  * enabled.
155  *
156  */
157
158 // The last object in the row is the bad guy...
159 if (!is_array($USER_AUTH_ORDER))
160     $USER_AUTH_ORDER = array("Forbidden");
161 else
162     $USER_AUTH_ORDER[] = "Forbidden";
163
164 // Local convenience functions.
165 function _isAnonUserAllowed() {
166     return (defined('ALLOW_ANON_USER') && ALLOW_ANON_USER);
167 }
168 function _isBogoUserAllowed() {
169     return (defined('ALLOW_BOGO_LOGIN') && ALLOW_BOGO_LOGIN);
170 }
171 function _isUserPasswordsAllowed() {
172     return (defined('ALLOW_USER_PASSWORDS') && ALLOW_USER_PASSWORDS);
173 }
174
175 // Possibly upgrade userobject functions.
176 function _determineAdminUserOrOtherUser($UserName) {
177     // Sanity check. User name is a condition of the definition of the
178     // _AdminUser, _BogoUser and _passuser.
179     if (!$UserName)
180         return $GLOBALS['ForbiddenUser'];
181
182     //FIXME: check admin membership later at checkPass. Now we cannot raise the level.
183     //$group = &WikiGroup::getGroup($GLOBALS['request']);
184     if ($UserName == ADMIN_USER)
185         return new _AdminUser($UserName);
186     /* elseif ($group->isMember(GROUP_ADMIN)) { // unneeded code
187         return _determineBogoUserOrPassUser($UserName);
188     }
189     */
190     else
191         return _determineBogoUserOrPassUser($UserName);
192 }
193
194 function _determineBogoUserOrPassUser($UserName) {
195     global $ForbiddenUser;
196
197     // Sanity check. User name is a condition of the definition of
198     // _BogoUser and _PassUser.
199     if (!$UserName)
200         return $ForbiddenUser;
201
202     // Check for password and possibly upgrade user object.
203     // $_BogoUser = new _BogoUser($UserName);
204     if (_isBogoUserAllowed() and isWikiWord($UserName)) {
205         include_once("lib/WikiUser/BogoLogin.php");
206         $_BogoUser = new _BogoLoginPassUser($UserName);
207         if ($_BogoUser->userExists() or $GLOBALS['request']->getArg('auth'))
208             return $_BogoUser;
209     }
210     if (_isUserPasswordsAllowed()) {
211         // PassUsers override BogoUsers if a password is stored
212         if (isset($_BogoUser) and isset($_BogoUser->_prefs) 
213             and $_BogoUser->_prefs->get('passwd'))
214             return new _PassUser($UserName, $_BogoUser->_prefs);
215         else { 
216             $_PassUser = new _PassUser($UserName,
217                                        isset($_BogoUser) ? $_BogoUser->_prefs : false);
218             if ($_PassUser->userExists() or $GLOBALS['request']->getArg('auth')) {
219                 if (isset($GLOBALS['request']->_user_class))
220                     $class = $GLOBALS['request']->_user_class;
221                 elseif (strtolower(get_class($_PassUser)) == "_passuser")
222                     $class = $_PassUser->nextClass();
223                 else
224                     $class = get_class($_PassUser);
225                 if ($user = new $class($UserName, $_PassUser->_prefs)) {
226                     return $user;
227                 } else {
228                     return $_PassUser;
229                 }
230             }
231         }
232     }
233     // No Bogo- or PassUser exists, or
234     // passwords are not allowed, and bogo is disallowed too.
235     // (Only the admin can sign in).
236     return $ForbiddenUser;
237 }
238
239 /**
240  * Primary WikiUser function, called by lib/main.php.
241  * 
242  * This determines the user's type and returns an appropriate user
243  * object. lib/main.php then querys the resultant object for password
244  * validity as necessary.
245  *
246  * If an _AnonUser object is returned, the user may only browse pages
247  * (and save prefs in a cookie).
248  *
249  * To disable access but provide prefs the global $ForbiddenUser class 
250  * is returned. (was previously false)
251  * 
252  */
253 function WikiUser ($UserName = '') {
254     global $ForbiddenUser, $HTTP_SESSION_VARS;
255
256     //Maybe: Check sessionvar for username & save username into
257     //sessionvar (may be more appropriate to do this in lib/main.php).
258     if ($UserName) {
259         $ForbiddenUser = new _ForbiddenUser($UserName);
260         // Found a user name.
261         return _determineAdminUserOrOtherUser($UserName);
262     }
263     elseif (!empty($HTTP_SESSION_VARS['userid'])) {
264         // Found a user name.
265         $ForbiddenUser = new _ForbiddenUser($_SESSION['userid']);
266         return _determineAdminUserOrOtherUser($_SESSION['userid']);
267     }
268     else {
269         // Check for autologin pref in cookie and possibly upgrade
270         // user object to another type.
271         $_AnonUser = new _AnonUser();
272         if ($UserName = $_AnonUser->_userid && $_AnonUser->_prefs->get('autologin')) {
273             // Found a user name.
274             $ForbiddenUser = new _ForbiddenUser($UserName);
275             return _determineAdminUserOrOtherUser($UserName);
276         }
277         else {
278             $ForbiddenUser = new _ForbiddenUser();
279             if (_isAnonUserAllowed())
280                 return $_AnonUser;
281             return $ForbiddenUser; // User must sign in to browse pages.
282         }
283         return $ForbiddenUser;     // User must sign in with a password.
284     }
285     /*
286     trigger_error("DEBUG: Note: End of function reached in WikiUser." . " "
287                   . "Unexpectedly, an appropriate user class could not be determined.");
288     return $ForbiddenUser; // Failsafe.
289     */
290 }
291
292 /**
293  * WikiUser.php use the name 'WikiUser'
294  */
295 function WikiUserClassname() {
296     return '_WikiUser';
297 }
298
299
300 /**
301  * Upgrade olduser by copying properties from user to olduser.
302  * We are not sure yet, for which php's a simple $this = $user works reliably,
303  * (on php4 it works ok, on php5 it's currently disallowed on the parser level)
304  * that's why try it the hard way.
305  */
306 function UpgradeUser ($user, $newuser) {
307     if (isa($user,'_WikiUser') and isa($newuser,'_WikiUser')) {
308         // populate the upgraded class $newuser with the values from the current user object
309         //only _auth_level, _current_method, _current_index,
310         if (!empty($user->_level) and 
311             $user->_level > $newuser->_level)
312             $newuser->_level = $user->_level;
313         if (!empty($user->_current_index) and
314             $user->_current_index > $newuser->_current_index) {
315             $newuser->_current_index = $user->_current_index;
316             $newuser->_current_method = $user->_current_method;
317         }
318         if (!empty($user->_authmethod))
319             $newuser->_authmethod = $user->_authmethod;
320         $GLOBALS['request']->_user_class = get_class($newuser);
321         /*
322         foreach (get_object_vars($user) as $k => $v) {
323             if (!empty($v)) $olduser->$k = $v;  
324         }
325         */
326         $newuser->hasHomePage(); // revive db handle, because these don't survive sessions
327         //$GLOBALS['request']->_user = $olduser;
328         return $newuser;
329     } else {
330         return false;
331     }
332 }
333
334 /**
335  * Probably not needed, since we use the various user objects methods so far.
336  * Anyway, here it is, looping through all available objects.
337  */
338 function UserExists ($UserName) {
339     global $request;
340     if (!($user = $request->getUser()))
341         $user = WikiUser($UserName);
342     if (!$user) 
343         return false;
344     if ($user->userExists($UserName)) {
345         $request->_user = $user;
346         return true;
347     }
348     if (isa($user,'_BogoUser'))
349         $user = new _PassUser($UserName,$user->_prefs);
350     $class = $user->nextClass();
351     if ($user = new $class($UserName, $user->_prefs)) {
352         return $user->userExists($UserName);
353     }
354     $request->_user = $GLOBALS['ForbiddenUser'];
355     return false;
356 }
357
358 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
359
360 /**
361  * Base WikiUser class.
362  */
363 class _WikiUser
364 {
365      var $_userid = '';
366      var $_level = WIKIAUTH_ANON;
367      var $_prefs = false;
368      var $_HomePagehandle = false;
369
370     // constructor
371     function _WikiUser($UserName='', $prefs=false) {
372
373         $this->_userid = $UserName;
374         $this->_HomePagehandle = false;
375         if ($UserName) {
376             $this->hasHomePage();
377         }
378         if (empty($this->_prefs)) {
379             if ($prefs) $this->_prefs = $prefs;
380             else $this->getPreferences();
381         }
382     }
383
384     function UserName() {
385         if (!empty($this->_userid))
386             return $this->_userid;
387     }
388
389     function getPreferences() {
390         trigger_error("DEBUG: Note: undefined _WikiUser class trying to load prefs." . " "
391                       . "New subclasses of _WikiUser must override this function.");
392         return false;
393     }
394
395     function setPreferences($prefs, $id_only) {
396         trigger_error("DEBUG: Note: undefined _WikiUser class trying to save prefs." 
397                       . " "
398                       . "New subclasses of _WikiUser must override this function.");
399         return false;
400     }
401
402     function userExists() {
403         return $this->hasHomePage();
404     }
405
406     function checkPass($submitted_password) {
407         // By definition, an undefined user class cannot sign in.
408         trigger_error("DEBUG: Warning: undefined _WikiUser class trying to sign in." 
409                       . " "
410                       . "New subclasses of _WikiUser must override this function.");
411         return false;
412     }
413
414     // returns page_handle to user's home page or false if none
415     function hasHomePage() {
416         if ($this->_userid) {
417             if (!empty($this->_HomePagehandle) and is_object($this->_HomePagehandle)) {
418                 return $this->_HomePagehandle->exists();
419             }
420             else {
421                 // check db again (maybe someone else created it since
422                 // we logged in.)
423                 global $request;
424                 $this->_HomePagehandle = $request->getPage($this->_userid);
425                 return $this->_HomePagehandle->exists();
426             }
427         }
428         // nope
429         return false;
430     }
431
432     function createHomePage() {
433         global $request;
434         $versiondata = array('author' => ADMIN_USER);
435         $request->_dbi->save(_("Automatically created user homepage to be able to store UserPreferences.").
436                              "\n{{Template/UserPage}}",
437                              1, $versiondata);
438         $request->_dbi->touch();
439         $this->_HomePagehandle = $request->getPage($this->_userid);
440     }
441
442     // innocent helper: case-insensitive position in _auth_methods
443     function array_position ($string, $array) {
444         $string = strtolower($string);
445         for ($found = 0; $found < count($array); $found++) {
446             if (strtolower($array[$found]) == $string)
447                 return $found;
448         }
449         return false;
450     }
451
452     function nextAuthMethodIndex() {
453         if (empty($this->_auth_methods)) 
454             $this->_auth_methods = $GLOBALS['USER_AUTH_ORDER'];
455         if (empty($this->_current_index)) {
456             if (strtolower(get_class($this)) != '_passuser') {
457                 $this->_current_method = substr(get_class($this),1,-8);
458                 $this->_current_index = $this->array_position($this->_current_method,
459                                                               $this->_auth_methods);
460             } else {
461                 $this->_current_index = -1;
462             }
463         }
464         $this->_current_index++;
465         if ($this->_current_index >= count($this->_auth_methods))
466             return false;
467         $this->_current_method = $this->_auth_methods[$this->_current_index];
468         return $this->_current_index;
469     }
470
471     function AuthMethod($index = false) {
472         return $this->_auth_methods[ $index === false 
473                                      ? count($this->_auth_methods)-1 
474                                      : $index];
475     }
476
477     // upgrade the user object
478     function nextClass() {
479         $method = $this->AuthMethod($this->nextAuthMethodIndex());
480         include_once("lib/WikiUser/$method.php");
481         return "_".$method."PassUser";
482     }
483
484     //Fixme: for _HttpAuthPassUser
485     function PrintLoginForm (&$request, $args, $fail_message = false,
486                              $seperate_page = false) {
487         include_once('lib/Template.php');
488         // Call update_locale in case the system's default language is not 'en'.
489         // (We have no user pref for lang at this point yet, no one is logged in.)
490         if ($GLOBALS['LANG'] != DEFAULT_LANGUAGE)
491             update_locale(DEFAULT_LANGUAGE);
492         $userid = $this->_userid;
493         $require_level = 0;
494         extract($args); // fixme
495
496         $require_level = max(0, min(WIKIAUTH_ADMIN, (int)$require_level));
497
498         $pagename = $request->getArg('pagename');
499         $nocache = 1;
500         $login = Template('login',
501                           compact('pagename', 'userid', 'require_level',
502                                   'fail_message', 'pass_required', 'nocache'));
503         // check if the html template was already processed
504         $seperate_page = $seperate_page ? true : !alreadyTemplateProcessed('html');
505         if ($seperate_page) {
506             $page = $request->getPage($pagename);
507             $revision = $page->getCurrentRevision();
508             return GeneratePage($login,_("Sign In"), $revision);
509         } else {
510             return $login->printExpansion();
511         }
512     }
513
514     /** Signed in but not password checked or empty password.
515      */
516     function isSignedIn() {
517         return (isa($this,'_BogoUser') or isa($this,'_PassUser'));
518     }
519
520     /** This is password checked for sure.
521      */
522     function isAuthenticated() {
523         //return isa($this,'_PassUser');
524         //return isa($this,'_BogoUser') || isa($this,'_PassUser');
525         return $this->_level >= WIKIAUTH_BOGO;
526     }
527
528     function isAdmin () {
529         static $group; 
530         if ($this->_level == WIKIAUTH_ADMIN) return true;
531         if (!$this->isSignedIn()) return false;
532         if (!$this->isAuthenticated()) return false;
533
534         if (!$group) $group = &$GLOBALS['request']->getGroup();
535         return ($this->_level > WIKIAUTH_BOGO and $group->isMember(GROUP_ADMIN));
536     }
537
538     /** Name or IP for a signed user. UserName could come from a cookie e.g.
539      */
540     function getId () {
541         return ( $this->UserName()
542                  ? $this->UserName()
543                  : $GLOBALS['request']->get('REMOTE_ADDR') );
544     }
545
546     /** Name for an authenticated user. No IP here.
547      */
548     function getAuthenticatedId() {
549         return ( $this->isAuthenticated()
550                  ? $this->_userid
551                  : ''); //$GLOBALS['request']->get('REMOTE_ADDR') );
552     }
553
554     function hasAuthority ($require_level) {
555         return $this->_level >= $require_level;
556     }
557
558     /* This is quite restrictive and not according the login description online. 
559        Any word char (A-Za-z0-9_), " ", ".", "@" and "-"
560        The backends may loosen or tighten this.
561     */
562     function isValidName ($userid = false) {
563         if (!$userid) $userid = $this->_userid;
564         if (!$userid) return false;
565         if (FUSIONFORGE) {
566             return true;
567         }
568         return preg_match("/^[\-\w\.@ ]+$/U", $userid) and strlen($userid) < 32;
569     }
570
571     /**
572      * Called on an auth_args POST request, such as login, logout or signin.
573      * TODO: Check BogoLogin users with empty password. (self-signed users)
574      */
575     function AuthCheck ($postargs) {
576         // Normalize args, and extract.
577         $keys = array('userid', 'passwd', 'require_level', 'login', 'logout',
578                       'cancel');
579         foreach ($keys as $key)
580             $args[$key] = isset($postargs[$key]) ? $postargs[$key] : false;
581         extract($args);
582         $require_level = max(0, min(WIKIAUTH_ADMIN, (int)$require_level));
583
584         if ($logout) { // Log out
585             if (LOGIN_LOG and is_writeable(LOGIN_LOG)) {
586                 global $request;
587                 $zone_offset = Request_AccessLogEntry::_zone_offset();
588                 $ncsa_time = date("d/M/Y:H:i:s", time());
589                 $entry = sprintf('%s - %s - [%s %s] "%s" %s - "%s" "%s"',
590                                  (string) $request->get('REMOTE_HOST'),
591                                  (string) $request->_user->_userid,
592                                  $ncsa_time, $zone_offset, 
593                                  "logout ".get_class($request->_user),
594                                  "401",
595                                  (string) $request->get('HTTP_REFERER'),
596                                  (string) $request->get('HTTP_USER_AGENT')
597                                  );
598                 if (($fp = fopen(LOGIN_LOG, "a"))) {
599                     flock($fp, LOCK_EX);
600                     fputs($fp, "$entry\n");
601                     fclose($fp);
602                 }
603                 //error_log("$entry\n", 3, LOGIN_LOG);
604             }
605             if (method_exists($GLOBALS['request']->_user, "logout")) { //_HttpAuthPassUser
606                 $GLOBALS['request']->_user->logout();
607             }
608             $user = new _AnonUser();
609             $user->_userid = '';
610             $user->_level = WIKIAUTH_ANON;
611             return $user; 
612         } elseif ($cancel)
613             return false;        // User hit cancel button.
614         elseif (!$login && !$userid)
615             return false;       // Nothing to do?
616
617         if (!$this->isValidName($userid))
618             return _("Invalid username.");;
619
620         $authlevel = $this->checkPass($passwd === false ? '' : $passwd);
621
622         if (LOGIN_LOG and is_writeable(LOGIN_LOG)) {
623             global $request;
624             $zone_offset = Request_AccessLogEntry::_zone_offset();
625             $ncsa_time = date("d/M/Y:H:i:s", time());
626             $manglepasswd = $passwd;
627             for ($i=0; $i<strlen($manglepasswd); $i++) {
628                 $c = substr($manglepasswd,$i,1);
629                 if (ord($c) < 32) $manglepasswd[$i] = "<";
630                 elseif ($c == '*') $manglepasswd[$i] = "*";
631                 elseif ($c == '?') $manglepasswd[$i] = "?";
632                 elseif ($c == '(') $manglepasswd[$i] = "(";
633                 elseif ($c == ')') $manglepasswd[$i] = ")";
634                 elseif ($c == "\\") $manglepasswd[$i] = "\\";
635                 elseif (ord($c) < 127) $manglepasswd[$i] = "x";
636                 elseif (ord($c) >= 127) $manglepasswd[$i] = ">";
637             }
638             if ((DEBUG & _DEBUG_LOGIN) and $authlevel <= 0) $manglepasswd = $passwd;
639             $entry = sprintf('%s - %s - [%s %s] "%s" %s - "%s" "%s"',
640                              $request->get('REMOTE_HOST'),
641                              (string) $request->_user->_userid,
642                              $ncsa_time, $zone_offset, 
643                              "login $userid/$manglepasswd => $authlevel ".get_class($request->_user),
644                              $authlevel > 0 ? "200" : "403",
645                              (string) $request->get('HTTP_REFERER'),
646                              (string) $request->get('HTTP_USER_AGENT')
647                              );
648             if (($fp = fopen(LOGIN_LOG, "a"))) {
649                 flock($fp, LOCK_EX);
650                 fputs($fp, "$entry\n");
651                 fclose($fp);
652             }
653             //error_log("$entry\n", 3, LOGIN_LOG);
654         }
655
656         if ($authlevel <= 0) { // anon or forbidden
657             if ($passwd)
658                 return _("Invalid password.");
659             else
660                 return _("Invalid password or userid.");
661         } elseif ($authlevel < $require_level) { // auth ok, but not enough 
662             if (!empty($this->_current_method) and strtolower(get_class($this)) == '_passuser') 
663             {
664                 // upgrade class
665                 $class = "_" . $this->_current_method . "PassUser";
666                 include_once("lib/WikiUser/".$this->_current_method.".php");
667                 $user = new $class($userid,$this->_prefs);
668                 if (!check_php_version(5))
669                     eval("\$this = \$user;");
670                 // /*PHP5 patch*/$this = $user;
671                 $this->_level = $authlevel;
672                 return $user;
673             }
674             $this->_userid = $userid;
675             $this->_level = $authlevel;
676             return _("Insufficient permissions.");
677         }
678
679         // Successful login.
680         //$user = $GLOBALS['request']->_user;
681         if (!empty($this->_current_method) and 
682             strtolower(get_class($this)) == '_passuser') 
683         {
684             // upgrade class
685             $class = "_" . $this->_current_method . "PassUser";
686             include_once("lib/WikiUser/".$this->_current_method.".php");
687             $user = new $class($userid, $this->_prefs);
688             if (!check_php_version(5))
689                 eval("\$this = \$user;");
690             // /*PHP5 patch*/$this = $user;
691             $user->_level = $authlevel;
692             return $user;
693         }
694         $this->_userid = $userid;
695         $this->_level = $authlevel;
696         return $this;
697     }
698
699 }
700
701 /**
702  * Not authenticated in user, but he may be signed in. Basicly with view access only.
703  * prefs are stored in cookies, but only the userid.
704  */
705 class _AnonUser
706 extends _WikiUser
707 {
708     var $_level = WIKIAUTH_ANON;        // var in php-5.0.0RC1 deprecated
709
710     /** Anon only gets to load and save prefs in a cookie, that's it.
711      */
712     function getPreferences() {
713         global $request;
714
715         if (empty($this->_prefs))
716             $this->_prefs = new UserPreferences;
717         $UserName = $this->UserName();
718
719         // Try to read deprecated 1.3.x style cookies
720         if ($cookie = $request->cookies->get_old(WIKI_NAME)) {
721             if (! $unboxedcookie = $this->_prefs->retrieve($cookie)) {
722                 trigger_error(_("Empty Preferences or format of UserPreferences cookie not recognised.") 
723                               . "\n"
724                               . sprintf("%s='%s'", WIKI_NAME, $cookie)
725                               . "\n"
726                               . _("Default preferences will be used."),
727                               E_USER_NOTICE);
728             }
729             /**
730              * Only set if it matches the UserName who is
731              * signing in or if this really is an Anon login (no
732              * username). (Remember, _BogoUser and higher inherit this
733              * function too!).
734              */
735             if (! $UserName || $UserName == @$unboxedcookie['userid']) {
736                 $updated = $this->_prefs->updatePrefs($unboxedcookie);
737                 //$this->_prefs = new UserPreferences($unboxedcookie);
738                 $UserName = @$unboxedcookie['userid'];
739                 if (is_string($UserName) and (substr($UserName,0,2) != 's:'))
740                     $this->_userid = $UserName;
741                 else 
742                     $UserName = false;    
743             }
744             // v1.3.8 policy: don't set PhpWiki cookies, only plaintext WIKI_ID cookies
745             if (!headers_sent())
746                 $request->deleteCookieVar(WIKI_NAME);
747         }
748         // Try to read deprecated 1.3.4 style cookies
749         if (! $UserName and ($cookie = $request->cookies->get_old("WIKI_PREF2"))) {
750             if (! $unboxedcookie = $this->_prefs->retrieve($cookie)) {
751                 if (! $UserName || $UserName == $unboxedcookie['userid']) {
752                     $updated = $this->_prefs->updatePrefs($unboxedcookie);
753                     //$this->_prefs = new UserPreferences($unboxedcookie);
754                     $UserName = $unboxedcookie['userid'];
755                     if (is_string($UserName) and (substr($UserName,0,2) != 's:'))
756                         $this->_userid = $UserName;
757                     else 
758                         $UserName = false;    
759                 }
760                 if (!headers_sent())
761                     $request->deleteCookieVar("WIKI_PREF2");
762             }
763         }
764         if (! $UserName ) {
765             // Try reading userid from old PhpWiki cookie formats:
766             if ($cookie = $request->cookies->get_old(getCookieName())) {
767                 if (is_string($cookie) and (substr($cookie,0,2) != 's:'))
768                     $UserName = $cookie;
769                 elseif (is_array($cookie) and !empty($cookie['userid']))
770                     $UserName = $cookie['userid'];
771             }
772             if (! $UserName and !headers_sent())
773                 $request->deleteCookieVar(getCookieName());
774             else
775                 $this->_userid = $UserName;
776         }
777
778         // initializeTheme() needs at least an empty object
779         /*
780          if (empty($this->_prefs))
781             $this->_prefs = new UserPreferences;
782         */
783         return $this->_prefs;
784     }
785
786     /** _AnonUser::setPreferences(): Save prefs in a cookie and session and update all global vars
787      *
788      * Allow for multiple wikis in same domain. Encode only the
789      * _prefs array of the UserPreference object. Ideally the
790      * prefs array should just be imploded into a single string or
791      * something so it is completely human readable by the end
792      * user. In that case stricter error checking will be needed
793      * when loading the cookie.
794      */
795     function setPreferences($prefs, $id_only=false) {
796         if (!is_object($prefs)) {
797             if (is_object($this->_prefs)) {
798                 $updated = $this->_prefs->updatePrefs($prefs);
799                 $prefs =& $this->_prefs;
800             } else {
801                 // update the prefs values from scratch. This could leed to unnecessary
802                 // side-effects: duplicate emailVerified, ...
803                 $this->_prefs = new UserPreferences($prefs);
804                 $updated = true;
805             }
806         } else {
807             if (!isset($this->_prefs))
808                 $this->_prefs =& $prefs;
809             else
810                 $updated = $this->_prefs->isChanged($prefs);
811         }
812         if ($updated) {
813             if ($id_only and !headers_sent()) {
814                 global $request;
815                 // new 1.3.8 policy: no array cookies, only plain userid string as in 
816                 // the pre 1.3.x versions.
817                 // prefs should be stored besides the session in the homepagehandle or in a db.
818                 $request->setCookieVar(getCookieName(), $this->_userid,
819                                        COOKIE_EXPIRATION_DAYS, COOKIE_DOMAIN);
820                 //$request->setCookieVar(WIKI_NAME, array('userid' => $prefs->get('userid')),
821                 //                       COOKIE_EXPIRATION_DAYS, COOKIE_DOMAIN);
822             }
823         }
824         if (is_object($prefs)) {
825             $packed = $prefs->store();
826             $unpacked = $prefs->unpack($packed);
827             if (count($unpacked)) {
828                 foreach (array('_method','_select','_update','_insert') as $param) {
829                     if (!empty($this->_prefs->{$param}))
830                         $prefs->{$param} = $this->_prefs->{$param};
831                 }
832                 $this->_prefs = $prefs;
833             }
834         }
835         return $updated;
836     }
837
838     function userExists() {
839         return true;
840     }
841
842     function checkPass($submitted_password) {
843         return false;
844         // this might happen on a old-style signin button.
845
846         // By definition, the _AnonUser does not HAVE a password
847         // (compared to _BogoUser, who has an EMPTY password).
848         trigger_error("DEBUG: Warning: _AnonUser unexpectedly asked to checkPass()." . " "
849                       . "Check isa(\$user, '_PassUser'), or: isa(\$user, '_AdminUser') etc. first." . " "
850                       . "New subclasses of _WikiUser must override this function.");
851         return false;
852     }
853
854 }
855
856 /**
857  * Helper class to finish the PassUser auth loop. 
858  * This is added automatically to USER_AUTH_ORDER.
859  */
860 class _ForbiddenUser
861 extends _AnonUser
862 {
863     var $_level = WIKIAUTH_FORBIDDEN;
864
865     function checkPass($submitted_password) {
866         return WIKIAUTH_FORBIDDEN;
867     }
868
869     function userExists() {
870         if ($this->_HomePagehandle) return true;
871         return false;
872     }
873 }
874
875 /**
876  * Do NOT extend _BogoUser to other classes, for checkPass()
877  * security. (In case of defects in code logic of the new class!)
878  * The intermediate step between anon and passuser.
879  * We also have the _BogoLoginPassUser class with stricter 
880  * password checking, which fits into the auth loop.
881  * Note: This class is not called anymore by WikiUser()
882  */
883 class _BogoUser
884 extends _AnonUser
885 {
886     function userExists() {
887         if (isWikiWord($this->_userid)) {
888             $this->_level = WIKIAUTH_BOGO;
889             return true;
890         } else {
891             $this->_level = WIKIAUTH_ANON;
892             return false;
893         }
894     }
895
896     function checkPass($submitted_password) {
897         // By definition, BogoUser has an empty password.
898         $this->userExists();
899         return $this->_level;
900     }
901 }
902
903 class _PassUser
904 extends _AnonUser
905 /**
906  * Called if ALLOW_USER_PASSWORDS and Anon and Bogo failed.
907  *
908  * The classes for all subsequent auth methods extend from this class. 
909  * This handles the auth method type dispatcher according $USER_AUTH_ORDER, 
910  * the three auth method policies first-only, strict and stacked
911  * and the two methods for prefs: homepage or database, 
912  * if $DBAuthParams['pref_select'] is defined.
913  *
914  * Default is PersonalPage auth and prefs.
915  * 
916  * @author: Reini Urban
917  * @tables: pref
918  */
919 {
920     var $_auth_dbi, $_prefs;
921     var $_current_method, $_current_index;
922
923     // check and prepare the auth and pref methods only once
924     function _PassUser($UserName='', $prefs=false) {
925         //global $DBAuthParams, $DBParams;
926         if ($UserName) {
927             /*if (!$this->isValidName($UserName))
928                 return false;*/
929             $this->_userid = $UserName;
930             if ($this->hasHomePage())
931                 $this->_HomePagehandle = $GLOBALS['request']->getPage($this->_userid);
932         }
933         $this->_authmethod = substr(get_class($this),1,-8);
934         if ($this->_authmethod == 'a') $this->_authmethod = 'admin';
935
936         // Check the configured Prefs methods
937         $dbi = $this->getAuthDbh();
938         $dbh = $GLOBALS['request']->getDbh();
939         if ( $dbi 
940              and !$dbh->readonly 
941              and !isset($this->_prefs->_select) 
942              and $dbh->getAuthParam('pref_select')) 
943         {
944             if (!$this->_prefs) {
945                 $this->_prefs = new UserPreferences();
946                 $need_pref = true;
947             }
948             $this->_prefs->_method = $dbh->getParam('dbtype');
949             $this->_prefs->_select = $this->prepare($dbh->getAuthParam('pref_select'), "userid");
950             // read-only prefs?
951             if ( !isset($this->_prefs->_update) and $dbh->getAuthParam('pref_update')) {
952                 $this->_prefs->_update = $this->prepare($dbh->getAuthParam('pref_update'), 
953                                                         array("userid", "pref_blob"));
954             }
955         } else {
956             if (!$this->_prefs) {
957                 $this->_prefs = new UserPreferences();
958                 $need_pref = true;
959             }
960             $this->_prefs->_method = 'HomePage';
961         }
962         
963         if (! $this->_prefs or isset($need_pref) ) {
964             if ($prefs) $this->_prefs = $prefs;
965             else $this->getPreferences();
966         }
967         
968         // Upgrade to the next parent _PassUser class. Avoid recursion.
969         if ( strtolower(get_class($this)) === '_passuser' ) {
970             //auth policy: Check the order of the configured auth methods
971             // 1. first-only: Upgrade the class here in the constructor
972             // 2. old:       ignore USER_AUTH_ORDER and try to use all available methods as 
973             ///              in the previous PhpWiki releases (slow)
974             // 3. strict:    upgrade the class after checking the user existance in userExists()
975             // 4. stacked:   upgrade the class after the password verification in checkPass()
976             // Methods: PersonalPage, HttpAuth, DB, Ldap, Imap, File
977             //if (!defined('USER_AUTH_POLICY')) define('USER_AUTH_POLICY','old');
978             if (defined('USER_AUTH_POLICY')) {
979                 // policy 1: only pre-define one method for all users
980                 if (USER_AUTH_POLICY === 'first-only') {
981                     $class = $this->nextClass();
982                     return new $class($UserName,$this->_prefs);
983                 }
984                 // Use the default behaviour from the previous versions:
985                 elseif (USER_AUTH_POLICY === 'old') {
986                     // Default: try to be smart
987                     // On php5 we can directly return and upgrade the Object,
988                     // before we have to upgrade it manually.
989                     if (!empty($GLOBALS['PHP_AUTH_USER']) or !empty($_SERVER['REMOTE_USER'])) {
990                         include_once("lib/WikiUser/HttpAuth.php");
991                         if (check_php_version(5))
992                             return new _HttpAuthPassUser($UserName,$this->_prefs);
993                         else {
994                             $user = new _HttpAuthPassUser($UserName,$this->_prefs);
995                             eval("\$this = \$user;");
996                             // /*PHP5 patch*/$this = $user;
997                             return $user;
998                         }
999                     } elseif (in_array('Db', $dbh->getAuthParam('USER_AUTH_ORDER')) and
1000                               $dbh->getAuthParam('auth_check') and
1001                               ($dbh->getAuthParam('auth_dsn') or $dbh->getParam('dsn'))) {
1002                         if (check_php_version(5))
1003                             return new _DbPassUser($UserName,$this->_prefs);
1004                         else {
1005                             $user = new _DbPassUser($UserName,$this->_prefs);
1006                             eval("\$this = \$user;");
1007                             // /*PHP5 patch*/$this = $user;
1008                             return $user;
1009                         }
1010                     } elseif (in_array('LDAP', $dbh->getAuthParam('USER_AUTH_ORDER')) and
1011                               defined('LDAP_AUTH_HOST') and defined('LDAP_BASE_DN') and 
1012                               function_exists('ldap_connect')) {
1013                         include_once("lib/WikiUser/LDAP.php");
1014                         if (check_php_version(5))
1015                             return new _LDAPPassUser($UserName,$this->_prefs);
1016                         else {
1017                             $user = new _LDAPPassUser($UserName,$this->_prefs);
1018                             eval("\$this = \$user;");
1019                             // /*PHP5 patch*/$this = $user;
1020                             return $user;
1021                         }
1022                     } elseif (in_array('IMAP', $dbh->getAuthParam('USER_AUTH_ORDER')) and
1023                               defined('IMAP_AUTH_HOST') and function_exists('imap_open')) {
1024                         include_once("lib/WikiUser/IMAP.php");
1025                         if (check_php_version(5))
1026                             return new _IMAPPassUser($UserName,$this->_prefs);
1027                         else {
1028                             $user = new _IMAPPassUser($UserName,$this->_prefs);
1029                             eval("\$this = \$user;");
1030                             // /*PHP5 patch*/$this = $user;
1031                             return $user;
1032                         }
1033                     } elseif (in_array('File', $dbh->getAuthParam('USER_AUTH_ORDER')) and
1034                               defined('AUTH_USER_FILE') and file_exists(AUTH_USER_FILE)) {
1035                         include_once("lib/WikiUser/File.php");
1036                         if (check_php_version(5))
1037                             return new _FilePassUser($UserName, $this->_prefs);
1038                         else {
1039                             $user = new _FilePassUser($UserName, $this->_prefs);
1040                             eval("\$this = \$user;");
1041                             // /*PHP5 patch*/$this = $user;
1042                             return $user;
1043                         }
1044                     } else {
1045                         include_once("lib/WikiUser/PersonalPage.php");
1046                         if (check_php_version(5))
1047                             return new _PersonalPagePassUser($UserName,$this->_prefs);
1048                         else {
1049                             $user = new _PersonalPagePassUser($UserName,$this->_prefs);
1050                             eval("\$this = \$user;");
1051                             // /*PHP5 patch*/$this = $user;
1052                             return $user;
1053                         }
1054                     }
1055                 }
1056                 else 
1057                     // else use the page methods defined in _PassUser.
1058                     return $this;
1059             }
1060         }
1061     }
1062
1063     function getAuthDbh () {
1064         global $request; //, $DBParams, $DBAuthParams;
1065
1066         $dbh = $request->getDbh();
1067         // session restauration doesn't re-connect to the database automatically, 
1068         // so dirty it here, to force a reconnect.
1069         if (isset($this->_auth_dbi)) {
1070             if (($dbh->getParam('dbtype') == 'SQL') and empty($this->_auth_dbi->connection))
1071                 unset($this->_auth_dbi);
1072             if (($dbh->getParam('dbtype') == 'ADODB') and empty($this->_auth_dbi->_connectionID))
1073                 unset($this->_auth_dbi);
1074         }
1075         if (empty($this->_auth_dbi)) {
1076             if ($dbh->getParam('dbtype') != 'SQL' 
1077                 and $dbh->getParam('dbtype') != 'ADODB'
1078                 and $dbh->getParam('dbtype') != 'PDO')
1079                 return false;
1080             if (empty($GLOBALS['DBAuthParams']))
1081                 return false;
1082             if (!$dbh->getAuthParam('auth_dsn')) {
1083                 $dbh = $request->getDbh(); // use phpwiki database 
1084             } elseif ($dbh->getAuthParam('auth_dsn') == $dbh->getParam('dsn')) {
1085                 $dbh = $request->getDbh(); // same phpwiki database 
1086             } else { // use another external database handle. needs PHP >= 4.1
1087                 $local_params = array_merge($GLOBALS['DBParams'],$GLOBALS['DBAuthParams']);
1088                 $local_params['dsn'] = $local_params['auth_dsn'];
1089                 $dbh = WikiDB::open($local_params);
1090             }       
1091             $this->_auth_dbi =& $dbh->_backend->_dbh;    
1092         }
1093         return $this->_auth_dbi;
1094     }
1095
1096     function _normalize_stmt_var($var, $oldstyle = false) {
1097         static $valid_variables = array('userid','password','pref_blob','groupname');
1098         // old-style: "'$userid'"
1099         // new-style: '"\$userid"' or just "userid"
1100         $new = str_replace(array("'",'"','\$','$'),'',$var);
1101         if (!in_array($new, $valid_variables)) {
1102             trigger_error("Unknown DBAuthParam statement variable: ". $new, E_USER_ERROR);
1103             return false;
1104         }
1105         return !$oldstyle ? "'$".$new."'" : '\$'.$new;
1106     }
1107
1108     // TODO: use it again for the auth and member tables
1109     // sprintfstyle vs prepare style: %s or ?
1110     //   multiple vars should be executed via prepare(?,?)+execute, 
1111     //   single vars with execute(sprintf(quote(var)))
1112     // help with position independency
1113     function prepare ($stmt, $variables, $oldstyle = false, $sprintfstyle = true) {
1114         global $request;
1115         $dbi = $request->getDbh();
1116         $this->getAuthDbh();
1117         // "'\$userid"' => %s
1118         // variables can be old-style: '"\$userid"' or new-style: "'$userid'" or just "userid"
1119         // old-style strings don't survive pear/Config/IniConfig treatment, that's why we changed it.
1120         $new = array();
1121         if (is_array($variables)) {
1122             //$sprintfstyle = false;
1123             for ($i=0; $i < count($variables); $i++) { 
1124                 $var = $this->_normalize_stmt_var($variables[$i], $oldstyle);
1125                 if (!$var)
1126                     trigger_error(sprintf("DbAuthParams: Undefined or empty statement variable %s in %s",
1127                                           $variables[$i], $stmt), E_USER_WARNING);
1128                 $variables[$i] = $var;
1129                 if (!$var) $new[] = '';
1130                 else {
1131                     $s = "%" . ($i+1) . "s";    
1132                     $new[] = $sprintfstyle ? $s : "?";
1133                 }
1134             }
1135         } else {
1136             $var = $this->_normalize_stmt_var($variables, $oldstyle);
1137             if (!$var)
1138                 trigger_error(sprintf("DbAuthParams: Undefined or empty statement variable %s in %s",
1139                                       $variables, $stmt), E_USER_WARNING);
1140             $variables = $var;
1141             if (!$var) $new = ''; 
1142             else $new = $sprintfstyle ? '%s' : "?"; 
1143         }
1144         $prefix = $dbi->getParam('prefix');
1145         // probably prefix table names if in same database
1146         if ($prefix and isset($this->_auth_dbi) and isset($dbi->_backend->_dbh) and 
1147             ($dbi->getAuthParam('auth_dsn') and $dbi->getParam('dsn') == $dbi->getAuthParam('auth_dsn')))
1148         {
1149             if (!stristr($stmt, $prefix)) {
1150                 $oldstmt = $stmt;
1151                 $stmt = str_replace(array(" user "," pref "," member "),
1152                                     array(" ".$prefix."user ",
1153                                           " ".$prefix."pref ",
1154                                           " ".$prefix."member "), $stmt);
1155                 //Do it automatically for the lazy admin? Esp. on sf.net it's nice to have
1156                 trigger_error("Need to prefix the DBAUTH tablename in config/config.ini:\n  $oldstmt \n=> $stmt",
1157                               E_USER_WARNING);
1158             }
1159         }
1160         // Preparate the SELECT statement, for ADODB and PearDB (MDB not).
1161         // Simple sprintf-style.
1162         $new_stmt = str_replace($variables, $new, $stmt);
1163         if ($new_stmt == $stmt) {
1164             if ($oldstyle) {
1165                 trigger_error(sprintf("DbAuthParams: Invalid statement in %s",
1166                                   $stmt), E_USER_WARNING);
1167             } else {
1168                 trigger_error(sprintf("DbAuthParams: Old statement quoting style in %s",
1169                                   $stmt), E_USER_WARNING);
1170                 $new_stmt = $this->prepare($stmt, $variables, 'oldstyle');
1171             }
1172         }
1173         return $new_stmt;
1174     }
1175
1176     function getPreferences() {
1177         if (!empty($this->_prefs->_method)) {
1178             if ($this->_prefs->_method == 'ADODB') {
1179                 // FIXME: strange why this should be needed...
1180                 include_once("lib/WikiUser/Db.php");
1181                 include_once("lib/WikiUser/AdoDb.php");
1182                 return _AdoDbPassUser::getPreferences();
1183             } elseif ($this->_prefs->_method == 'SQL') {
1184                 include_once("lib/WikiUser/Db.php");
1185                 include_once("lib/WikiUser/PearDb.php");
1186                 return _PearDbPassUser::getPreferences();
1187             } elseif ($this->_prefs->_method == 'PDO') {
1188                 include_once("lib/WikiUser/Db.php");
1189                 include_once("lib/WikiUser/PdoDb.php");
1190                 return _PdoDbPassUser::getPreferences();
1191             }
1192         }
1193
1194         // We don't necessarily have to read the cookie first. Since
1195         // the user has a password, the prefs stored in the homepage
1196         // cannot be arbitrarily altered by other Bogo users.
1197         _AnonUser::getPreferences();
1198         // User may have deleted cookie, retrieve from his
1199         // PersonalPage if there is one.
1200         if (!empty($this->_HomePagehandle)) {
1201             if ($restored_from_page = $this->_prefs->retrieve
1202                 ($this->_HomePagehandle->get('pref'))) {
1203                 $updated = $this->_prefs->updatePrefs($restored_from_page,'init');
1204                 //$this->_prefs = new UserPreferences($restored_from_page);
1205                 return $this->_prefs;
1206             }
1207         }
1208         return $this->_prefs;
1209     }
1210
1211     function setPreferences($prefs, $id_only=false) {
1212         if (!empty($this->_prefs->_method)) {
1213             if ($this->_prefs->_method == 'ADODB') {
1214                 // FIXME: strange why this should be needed...
1215                 include_once("lib/WikiUser/Db.php");
1216                 include_once("lib/WikiUser/AdoDb.php");
1217                 return _AdoDbPassUser::setPreferences($prefs, $id_only);
1218             }
1219             elseif ($this->_prefs->_method == 'SQL') {
1220                 include_once("lib/WikiUser/Db.php");
1221                 include_once("lib/WikiUser/PearDb.php");
1222                 return _PearDbPassUser::setPreferences($prefs, $id_only);
1223             }
1224             elseif ($this->_prefs->_method == 'PDO') {
1225                 include_once("lib/WikiUser/Db.php");
1226                 include_once("lib/WikiUser/PdoDb.php");
1227                 return _PdoDbPassUser::setPreferences($prefs, $id_only);
1228             }
1229         }
1230         if ($updated = _AnonUser::setPreferences($prefs, $id_only)) {
1231             // Encode only the _prefs array of the UserPreference object
1232             // If no DB method exists to store the prefs we must store it in the page, not in the cookies.
1233             if (empty($this->_HomePagehandle)) {
1234                 $this->_HomePagehandle = $GLOBALS['request']->getPage($this->_userid);
1235             }
1236             if (! $this->_HomePagehandle->exists() ) {
1237                 $this->createHomePage();
1238             }
1239             if (!empty($this->_HomePagehandle) and !$id_only) {
1240                 $this->_HomePagehandle->set('pref', $this->_prefs->store());
1241             }
1242         }
1243         return $updated;
1244     }
1245
1246     function mayChangePass() {
1247         return true;
1248     }
1249
1250     //The default method is getting the password from prefs. 
1251     // child methods obtain $stored_password from external auth.
1252     function userExists() {
1253         //if ($this->_HomePagehandle) return true;
1254         if (strtolower(get_class($this)) == "_passuser") {
1255             $class = $this->nextClass();
1256             $user = new $class($this->_userid, $this->_prefs);
1257         } else {
1258             $user = $this;
1259         }
1260         /* new user => false does not return false, but the _userid is empty then */
1261         while ($user and $user->_userid) {
1262             if (!check_php_version(5))
1263                 eval("\$this = \$user;");
1264             $user = UpgradeUser($this, $user);
1265             if ($user->userExists()) {
1266                 $user = UpgradeUser($this, $user);
1267                 return true;
1268             }
1269             // prevent endless loop. does this work on all PHP's?
1270             // it just has to set the classname, what it correctly does.
1271             $class = $user->nextClass();
1272             if ($class == "_ForbiddenPassUser")
1273                 return false;
1274         }
1275         return false;
1276     }
1277
1278     //The default method is getting the password from prefs. 
1279     // child methods obtain $stored_password from external auth.
1280     function checkPass($submitted_password) {
1281         $stored_password = $this->_prefs->get('passwd');
1282         if ($this->_checkPass($submitted_password, $stored_password)) {
1283             $this->_level = WIKIAUTH_USER;
1284             return $this->_level;
1285         } else {
1286             if ((USER_AUTH_POLICY === 'strict') and $this->userExists()) {
1287                 $this->_level = WIKIAUTH_FORBIDDEN;
1288                 return $this->_level;
1289             }
1290             return $this->_tryNextPass($submitted_password);
1291         }
1292     }
1293
1294
1295     function _checkPassLength($submitted_password) {
1296         if (strlen($submitted_password) < PASSWORD_LENGTH_MINIMUM) {
1297             trigger_error(_("The length of the password is shorter than the system policy allows."));
1298             return false;
1299         }
1300         return true;
1301     }
1302
1303     /**
1304      * The basic password checker for all PassUser objects.
1305      * Uses global ENCRYPTED_PASSWD and PASSWORD_LENGTH_MINIMUM.
1306      * Empty passwords are always false!
1307      * PASSWORD_LENGTH_MINIMUM is enforced here and in the preference set method.
1308      * @see UserPreferences::set
1309      *
1310      * DBPassUser password's have their own crypt definition.
1311      * That's why DBPassUser::checkPass() doesn't call this method, if 
1312      * the db password method is 'plain', which means that the DB SQL 
1313      * statement just returns 1 or 0. To use CRYPT() or PASSWORD() and 
1314      * don't store plain passwords in the DB.
1315      * 
1316      * TODO: remove crypt() function check from config.php:396 ??
1317      */
1318     function _checkPass($submitted_password, $stored_password) {
1319         if (!empty($submitted_password)) {
1320             // This works only on plaintext passwords.
1321             if (!ENCRYPTED_PASSWD and (strlen($stored_password) < PASSWORD_LENGTH_MINIMUM)) {
1322                 // With the EditMetaData plugin
1323                 trigger_error(_("The length of the stored password is shorter than the system policy allows. Sorry, you cannot login.\n You have to ask the System Administrator to reset your password."));
1324                 return false;
1325             }
1326             if (!$this->_checkPassLength($submitted_password)) {
1327                 return false;
1328             }
1329             if (ENCRYPTED_PASSWD) {
1330                 // Verify against encrypted password.
1331                 if (function_exists('crypt')) {
1332                     if (crypt($submitted_password, $stored_password) == $stored_password )
1333                         return true; // matches encrypted password
1334                     else
1335                         return false;
1336                 }
1337                 else {
1338                     trigger_error(_("The crypt function is not available in this version of PHP.") . " "
1339                                   . _("Please set ENCRYPTED_PASSWD to false in config/config.ini and probably change ADMIN_PASSWD."),
1340                                   E_USER_WARNING);
1341                     return false;
1342                 }
1343             }
1344             else {
1345                 // Verify against cleartext password.
1346                 if ($submitted_password == $stored_password)
1347                     return true;
1348                 else {
1349                     // Check whether we forgot to enable ENCRYPTED_PASSWD
1350                     if (function_exists('crypt')) {
1351                         if (crypt($submitted_password, $stored_password) == $stored_password) {
1352                             trigger_error(_("Please set ENCRYPTED_PASSWD to true in config/config.ini."),
1353                                           E_USER_WARNING);
1354                             return true;
1355                         }
1356                     }
1357                 }
1358             }
1359         }
1360         return false;
1361     }
1362
1363     /** The default method is storing the password in prefs. 
1364      *  Child methods (DB, File) may store in external auth also, but this 
1365      *  must be explicitly enabled.
1366      *  This may be called by plugin/UserPreferences or by ->SetPreferences()
1367      */
1368     function changePass($submitted_password) {
1369         $stored_password = $this->_prefs->get('passwd');
1370         // check if authenticated
1371         if (!$this->isAuthenticated()) return false;
1372         if (ENCRYPTED_PASSWD) {
1373             $submitted_password = crypt($submitted_password);
1374         }
1375         // check other restrictions, with side-effects only.
1376         $result = $this->_checkPass($submitted_password, $stored_password);
1377         if ($stored_password != $submitted_password) {
1378             $this->_prefs->set('passwd', $submitted_password);
1379             //update the storage (session, homepage, ...)
1380             $this->SetPreferences($this->_prefs);
1381             return true;
1382         }
1383         //Todo: return an error msg to the caller what failed? 
1384         // same password or no privilege
1385         return ENCRYPTED_PASSWD ? true : false;
1386     }
1387
1388     function _tryNextPass($submitted_password) {
1389         if (DEBUG & _DEBUG_LOGIN) {
1390             $class = strtolower(get_class($this));
1391             if (substr($class,-10) == "dbpassuser") $class = "_dbpassuser";
1392             $GLOBALS['USER_AUTH_ERROR'][$class] = 'wrongpass';
1393         }
1394         if (USER_AUTH_POLICY === 'strict') {
1395             $class = $this->nextClass();
1396             if ($user = new $class($this->_userid,$this->_prefs)) {
1397                 if ($user->userExists()) {
1398                     return $user->checkPass($submitted_password);
1399                 }
1400             }
1401         }
1402         if (USER_AUTH_POLICY === 'stacked' or USER_AUTH_POLICY === 'old') {
1403             $class = $this->nextClass();
1404             if ($user = new $class($this->_userid,$this->_prefs))
1405                 return $user->checkPass($submitted_password);
1406         }
1407         return $this->_level;
1408     }
1409
1410     function _tryNextUser() {
1411         if (DEBUG & _DEBUG_LOGIN) {
1412             $class = strtolower(get_class($this));
1413             if (substr($class,-10) == "dbpassuser") $class = "_dbpassuser";
1414             $GLOBALS['USER_AUTH_ERROR'][$class] = 'nosuchuser';
1415         }
1416         if (USER_AUTH_POLICY === 'strict'
1417             or USER_AUTH_POLICY === 'stacked') {
1418             $class = $this->nextClass();
1419             while ($user = new $class($this->_userid, $this->_prefs)) {
1420                 if (!check_php_version(5))
1421                     eval("\$this = \$user;");
1422                 $user = UpgradeUser($this, $user);
1423                 if ($user->userExists()) {
1424                     $user = UpgradeUser($this, $user);
1425                     return true;
1426                 }
1427                 if ($class == "_ForbiddenPassUser") return false;
1428                 $class = $this->nextClass();
1429             }
1430         }
1431         return false;
1432     }
1433
1434 }
1435
1436 /**
1437  * Insert more auth classes here...
1438  * For example a customized db class for another db connection 
1439  * or a socket-based auth server.
1440  *
1441  */
1442
1443
1444 /**
1445  * For security, this class should not be extended. Instead, extend
1446  * from _PassUser (think of this as unix "root").
1447  *
1448  * FIXME: This should be a singleton class. Only ADMIN_USER may be of class AdminUser!
1449  * Other members of the Administrators group must raise their level otherwise somehow.
1450  * Currently every member is a AdminUser, which will not work for the various 
1451  * storage methods.
1452  */
1453 class _AdminUser
1454 extends _PassUser
1455 {
1456     function mayChangePass() {
1457         return false;
1458     }
1459     function checkPass($submitted_password) {
1460         if ($this->_userid == ADMIN_USER)
1461             $stored_password = ADMIN_PASSWD;
1462         else {
1463             // Should not happen! Only ADMIN_USER should use this class.
1464             // return $this->_tryNextPass($submitted_password); // ???
1465             // TODO: safety check if really member of the ADMIN group?
1466             $stored_password = $this->_pref->get('passwd');
1467         }
1468         if ($this->_checkPass($submitted_password, $stored_password)) {
1469             $this->_level = WIKIAUTH_ADMIN;
1470             if (!empty($GLOBALS['HTTP_SERVER_VARS']['PHP_AUTH_USER']) and class_exists("_HttpAuthPassUser")) {
1471                 // fake http auth
1472                 _HttpAuthPassUser::_fake_auth($this->_userid, $submitted_password);
1473             }
1474             return $this->_level;
1475         } else {
1476             return $this->_tryNextPass($submitted_password);
1477             //$this->_level = WIKIAUTH_ANON;
1478             //return $this->_level;
1479         }
1480     }
1481
1482     function storePass($submitted_password) {
1483         if ($this->_userid == ADMIN_USER)
1484             return false;
1485         else {
1486             // should not happen! only ADMIN_USER should use this class.
1487             return parent::storePass($submitted_password);
1488         }
1489     }
1490 }
1491
1492 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
1493 /**
1494  * Various data classes for the preference types, 
1495  * to support get, set, sanify (range checking, ...)
1496  * update() will do the neccessary side-effects if a 
1497  * setting gets changed (theme, language, ...)
1498 */
1499
1500 class _UserPreference
1501 {
1502     var $default_value;
1503
1504     function _UserPreference ($default_value) {
1505         $this->default_value = $default_value;
1506     }
1507
1508     function sanify ($value) {
1509         return (string)$value;
1510     }
1511
1512     function get ($name) {
1513         if (isset($this->{$name}))
1514             return $this->{$name};
1515         else 
1516             return $this->default_value;
1517     }
1518
1519     function getraw ($name) {
1520         if (!empty($this->{$name}))
1521             return $this->{$name};
1522     }
1523
1524     // stores the value as $this->$name, and not as $this->value (clever?)
1525     function set ($name, $value) {
1526         $return = 0;
1527         $value = $this->sanify($value);
1528         if ($this->get($name) != $value) {
1529             $this->update($value);
1530             $return = 1;
1531         }
1532         if ($value != $this->default_value) {
1533             $this->{$name} = $value;
1534         } else {
1535             unset($this->{$name});
1536         }
1537         return $return;
1538     }
1539
1540     // default: no side-effects 
1541     function update ($value) {
1542         ;
1543     }
1544 }
1545
1546 class _UserPreference_numeric
1547 extends _UserPreference
1548 {
1549     function _UserPreference_numeric ($default, $minval = false,
1550                                       $maxval = false) {
1551         $this->_UserPreference((double)$default);
1552         $this->_minval = (double)$minval;
1553         $this->_maxval = (double)$maxval;
1554     }
1555
1556     function sanify ($value) {
1557         $value = (double)$value;
1558         if ($this->_minval !== false && $value < $this->_minval)
1559             $value = $this->_minval;
1560         if ($this->_maxval !== false && $value > $this->_maxval)
1561             $value = $this->_maxval;
1562         return $value;
1563     }
1564 }
1565
1566 class _UserPreference_int
1567 extends _UserPreference_numeric
1568 {
1569     function _UserPreference_int ($default, $minval = false, $maxval = false) {
1570         $this->_UserPreference_numeric((int)$default, (int)$minval, (int)$maxval);
1571     }
1572
1573     function sanify ($value) {
1574         return (int)parent::sanify((int)$value);
1575     }
1576 }
1577
1578 class _UserPreference_bool
1579 extends _UserPreference
1580 {
1581     function _UserPreference_bool ($default = false) {
1582         $this->_UserPreference((bool)$default);
1583     }
1584
1585     function sanify ($value) {
1586         if (is_array($value)) {
1587             /* This allows for constructs like:
1588              *
1589              *   <input type="hidden" name="pref[boolPref][]" value="0" />
1590              *   <input type="checkbox" name="pref[boolPref][]" value="1" />
1591              *
1592              * (If the checkbox is not checked, only the hidden input
1593              * gets sent. If the checkbox is sent, both inputs get
1594              * sent.)
1595              */
1596             foreach ($value as $val) {
1597                 if ($val)
1598                     return true;
1599             }
1600             return false;
1601         }
1602         return (bool) $value;
1603     }
1604 }
1605
1606 class _UserPreference_language
1607 extends _UserPreference
1608 {
1609     function _UserPreference_language ($default = DEFAULT_LANGUAGE) {
1610         $this->_UserPreference($default);
1611     }
1612
1613     // FIXME: check for valid locale
1614     function sanify ($value) {
1615         // Revert to DEFAULT_LANGUAGE if user does not specify
1616         // language in UserPreferences or chooses <system language>.
1617         if ($value == '' or empty($value))
1618             $value = DEFAULT_LANGUAGE;
1619
1620         return (string) $value;
1621     }
1622     
1623     function update ($newvalue) {
1624         if (! $this->_init ) {
1625             // invalidate etag to force fresh output
1626             $GLOBALS['request']->setValidators(array('%mtime' => false));
1627             update_locale($newvalue ? $newvalue : $GLOBALS['LANG']);
1628         }
1629     }
1630 }
1631
1632 class _UserPreference_theme
1633 extends _UserPreference
1634 {
1635     function _UserPreference_theme ($default = THEME) {
1636         $this->_UserPreference($default);
1637     }
1638
1639     function sanify ($value) {
1640         if (!empty($value) and FindFile($this->_themefile($value)))
1641             return $value;
1642         return $this->default_value;
1643     }
1644
1645     function update ($newvalue) {
1646         global $WikiTheme;
1647         // invalidate etag to force fresh output
1648         if (! $this->_init )
1649             $GLOBALS['request']->setValidators(array('%mtime' => false));
1650         if ($newvalue)
1651             include_once($this->_themefile($newvalue));
1652         if (empty($WikiTheme))
1653             include_once($this->_themefile(THEME));
1654     }
1655
1656     function _themefile ($theme) {
1657         return "themes/$theme/themeinfo.php";
1658     }
1659 }
1660
1661 class _UserPreference_notify
1662 extends _UserPreference
1663 {
1664     function sanify ($value) {
1665         if (!empty($value))
1666             return $value;
1667         else
1668             return $this->default_value;
1669     }
1670
1671     /** update to global user prefs: side-effect on set notify changes
1672      * use a global_data notify hash:
1673      * notify = array('pagematch' => array(userid => ('email' => mail, 
1674      *                                                'verified' => 0|1),
1675      *                                     ...),
1676      *                ...);
1677      */
1678     function update ($value) {
1679         if (!empty($this->_init)) return;
1680         $dbh = $GLOBALS['request']->getDbh();
1681         $notify = $dbh->get('notify');
1682         if (empty($notify))
1683             $data = array();
1684         else 
1685             $data =& $notify;
1686         // expand to existing pages only or store matches?
1687         // for now we store (glob-style) matches which is easier for the user
1688         $pages = $this->_page_split($value);
1689         // Limitation: only current user.
1690         $user = $GLOBALS['request']->getUser();
1691         if (!$user or !method_exists($user,'UserName')) return;
1692         // This fails with php5 and a WIKI_ID cookie:
1693         $userid = $user->UserName();
1694         $email  = $user->_prefs->get('email');
1695         $verified = $user->_prefs->_prefs['email']->getraw('emailVerified');
1696         // check existing notify hash and possibly delete pages for email
1697         if (!empty($data)) {
1698             foreach ($data as $page => $users) {
1699                 if (isset($data[$page][$userid]) and !in_array($page, $pages)) {
1700                     unset($data[$page][$userid]);
1701                 }
1702                 if (count($data[$page]) == 0)
1703                     unset($data[$page]);
1704             }
1705         }
1706         // add the new pages
1707         if (!empty($pages)) {
1708             foreach ($pages as $page) {
1709                 if (!isset($data[$page]))
1710                     $data[$page] = array();
1711                 if (!isset($data[$page][$userid])) {
1712                     // should we really store the verification notice here or 
1713                     // check it dynamically at every page->save?
1714                     if ($verified) {
1715                         $data[$page][$userid] = array('email' => $email,
1716                                                       'verified' => $verified);
1717                     } else {
1718                         $data[$page][$userid] = array('email' => $email);
1719                     }
1720                 }
1721             }
1722         }
1723         // store users changes
1724         $dbh->set('notify',$data);
1725     }
1726
1727     /** split the user-given comma or whitespace delimited pagenames
1728      *  to array
1729      */
1730     function _page_split($value) {
1731         return preg_split('/[\s,]+/',$value,-1,PREG_SPLIT_NO_EMPTY);
1732     }
1733 }
1734
1735 class _UserPreference_email
1736 extends _UserPreference
1737 {
1738     function get($name) {
1739         // get e-mail address from FusionForge
1740         if (FUSIONFORGE && session_loggedin()) {
1741             $user = session_get_user();
1742             return $user->getEmail();
1743         } else {
1744             parent::get($name);
1745         }
1746     }
1747
1748     function sanify($value) {
1749         // e-mail address is already checked by FusionForge
1750         if (FUSIONFORGE) return $value;
1751         // check for valid email address
1752         if ($this->get('email') == $value and $this->getraw('emailVerified'))
1753             return $value;
1754         // hack!
1755         if ($value == 1 or $value === true)
1756             return $value;
1757         list($ok,$msg) = ValidateMail($value,'noconnect');
1758         if ($ok) {
1759             return $value;
1760         } else {
1761             trigger_error("E-Mail Validation Error: ".$msg, E_USER_WARNING);
1762             return $this->default_value;
1763         }
1764     }
1765     
1766     /** Side-effect on email changes:
1767      * Send a verification mail or for now just a notification email.
1768      * For true verification (value = 2), we'd need a mailserver hook.
1769      */
1770     function update($value) {
1771         // e-mail address is already checked by FusionForge
1772         if (FUSIONFORGE) return $value;
1773         if (!empty($this->_init)) return;
1774         $verified = $this->getraw('emailVerified');
1775         // hack!
1776         if (($value == 1 or $value === true) and $verified)
1777             return;
1778         if (!empty($value) and !$verified) {
1779             list($ok,$msg) = ValidateMail($value);
1780             if ($ok and mail($value,"[".WIKI_NAME ."] "._("Email Verification"),
1781                      sprintf(_("Welcome to %s!\nYour email account is verified and\nwill be used to send page change notifications.\nSee %s"),
1782                              WIKI_NAME, WikiURL($GLOBALS['request']->getArg('pagename'),'',true)))) {
1783                 $this->set('emailVerified',1);
1784             } else {
1785                 trigger_error($msg, E_USER_WARNING);
1786             }
1787         }
1788     }
1789 }
1790
1791 /** Check for valid email address
1792     fixed version from http://www.zend.com/zend/spotlight/ev12apr.php
1793     Note: too strict, Bug #1053681
1794  */
1795 function ValidateMail($email, $noconnect=false) {
1796     global $EMailHosts;
1797     $HTTP_HOST = $GLOBALS['request']->get('HTTP_HOST');
1798
1799     // if this check is too strict (like invalid mail addresses in a local network only)
1800     // uncomment the following line:
1801     //return array(true,"not validated");
1802     // see http://sourceforge.net/tracker/index.php?func=detail&aid=1053681&group_id=6121&atid=106121
1803
1804     $result = array();
1805
1806     // This is Paul Warren's (pdw@ex-parrot.com) monster regex for RFC822
1807     // addresses, from the Perl module Mail::RFC822::Address, reduced to
1808     // accept single RFC822 addresses without comments only. (The original
1809     // accepts groups and properly commented addresses also.)
1810     $lwsp = "(?:(?:\\r\\n)?[ \\t])";
1811
1812     $specials = '()<>@,;:\\\\".\\[\\]';
1813     $controls = '\\000-\\031';
1814
1815     $dtext = "[^\\[\\]\\r\\\\]";
1816     $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$lwsp*";
1817
1818     $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$lwsp)*\"$lwsp*";
1819
1820     $atom = "[^$specials $controls]+(?:$lwsp+|\\Z|(?=[\\[\"$specials]))";
1821     $word = "(?:$atom|$quoted_string)";
1822     $localpart = "$word(?:\\.$lwsp*$word)*";
1823
1824     $sub_domain = "(?:$atom|$domain_literal)";
1825     $domain = "$sub_domain(?:\\.$lwsp*$sub_domain)*";
1826
1827     $addr_spec = "$localpart\@$lwsp*$domain";
1828
1829     $phrase = "$word*";
1830     $route = "(?:\@$domain(?:,\@$lwsp*$domain)*:$lwsp*)";
1831     $route_addr = "\\<$lwsp*$route?$addr_spec\\>$lwsp*";
1832     $mailbox = "(?:$addr_spec|$phrase$route_addr)";
1833
1834     $rfc822re = "/$lwsp*$mailbox/";
1835     unset($domain, $route_addr, $route, $phrase, $addr_spec, $sub_domain, $localpart, 
1836           $atom, $word, $quoted_string);
1837     unset($dtext, $controls, $specials, $lwsp, $domain_literal);
1838
1839     if (!preg_match($rfc822re, $email)) {
1840         $result[0] = false;
1841         $result[1] = sprintf(_("E-Mail address '%s' is not properly formatted"), $email);
1842         return $result;
1843     }
1844     if ($noconnect)
1845       return array(true, sprintf(_("E-Mail address '%s' is properly formatted"), $email));
1846
1847     list ( $Username, $Domain ) = explode("@", $email);
1848     //Todo: getmxrr workaround on windows or manual input field to verify it manually
1849     if (!isWindows() and getmxrr($Domain, $MXHost)) { // avoid warning on Windows. 
1850         $ConnectAddress = $MXHost[0];
1851     } else {
1852         $ConnectAddress = $Domain;
1853         if (isset($EMailHosts[ $Domain ])) {
1854             $ConnectAddress = $EMailHosts[ $Domain ];
1855         }
1856     }
1857     $Connect = @fsockopen ( $ConnectAddress, 25 );
1858     if ($Connect) {
1859         if (ereg("^220", $Out = fgets($Connect, 1024))) {
1860             fputs ($Connect, "HELO $HTTP_HOST\r\n");
1861             $Out = fgets ( $Connect, 1024 );
1862             fputs ($Connect, "MAIL FROM: <".$email.">\r\n");
1863             $From = fgets ( $Connect, 1024 );
1864             fputs ($Connect, "RCPT TO: <".$email.">\r\n");
1865             $To = fgets ($Connect, 1024);
1866             fputs ($Connect, "QUIT\r\n");
1867             fclose($Connect);
1868             if (!ereg ("^250", $From)) {
1869                 $result[0]=false;
1870                 $result[1]="Server rejected address: ". $From;
1871                 return $result;
1872             }
1873             if (!ereg ( "^250", $To )) {
1874                 $result[0]=false;
1875                 $result[1]="Server rejected address: ". $To;
1876                 return $result;
1877             }
1878         } else {
1879             $result[0] = false;
1880             $result[1] = "No response from server";
1881             return $result;
1882           }
1883     }  else {
1884         $result[0]=false;
1885         $result[1]="Can not connect E-Mail server.";
1886         return $result;
1887     }
1888     $result[0]=true;
1889     $result[1]="E-Mail address '$email' appears to be valid.";
1890     return $result;
1891 } // end of function 
1892
1893 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
1894
1895 /**
1896  * UserPreferences
1897  * 
1898  * This object holds the $request->_prefs subobjects.
1899  * A simple packed array of non-default values get's stored as cookie,
1900  * homepage, or database, which are converted to the array of 
1901  * ->_prefs objects.
1902  * We don't store the objects, because otherwise we will
1903  * not be able to upgrade any subobject. And it's a waste of space also.
1904  *
1905  */
1906 class UserPreferences
1907 {
1908     var $notifyPagesAll;
1909         
1910     function UserPreferences($saved_prefs = false) {
1911         // userid stored too, to ensure the prefs are being loaded for
1912         // the correct (currently signing in) userid if stored in a
1913         // cookie.
1914         // Update: for db prefs we disallow passwd. 
1915         // userid is needed for pref reflexion. current pref must know its username, 
1916         // if some app needs prefs from different users, different from current user.
1917         $this->_prefs
1918             = array(
1919                     'userid'        => new _UserPreference(''),
1920                     'passwd'        => new _UserPreference(''),
1921                     'autologin'     => new _UserPreference_bool(),
1922                     //'emailVerified' => new _UserPreference_emailVerified(), 
1923                     //fixed: store emailVerified as email parameter, 1.3.8
1924                     'email'         => new _UserPreference_email(''),
1925                     'notifyPages'   => new _UserPreference_notify(''), // 1.3.8
1926                     'theme'         => new _UserPreference_theme(THEME),
1927                     'lang'          => new _UserPreference_language(DEFAULT_LANGUAGE),
1928                     'editWidth'     => new _UserPreference_int(EDITWIDTH_DEFAULT_COLS,
1929                                                                EDITWIDTH_MIN_COLS,
1930                                                                EDITWIDTH_MAX_COLS),
1931                     'noLinkIcons'   => new _UserPreference_bool(),    // 1.3.8 
1932                     'editHeight'    => new _UserPreference_int(EDITHEIGHT_DEFAULT_ROWS,
1933                                                                EDITHEIGHT_MIN_ROWS,
1934                                                                EDITHEIGHT_MAX_ROWS),
1935                     'timeOffset'    => new _UserPreference_numeric(TIMEOFFSET_DEFAULT_HOURS,
1936                                                                    TIMEOFFSET_MIN_HOURS,
1937                                                                    TIMEOFFSET_MAX_HOURS),
1938                     'ownModifications' => new _UserPreference_bool(),
1939                     'majorModificationsOnly' => new _UserPreference_bool(),
1940                     'relativeDates' => new _UserPreference_bool(),
1941                     'googleLink'    => new _UserPreference_bool(), // 1.3.10
1942                     'doubleClickEdit' => new _UserPreference_bool(), // 1.3.11
1943                     );
1944
1945         // This should be probably be done with $customUserPreferenceColumns
1946         // For now, we use FUSIONFORGE define
1947         if (FUSIONFORGE) {
1948             $fusionforgeprefs = array(
1949                     'pageTrail'     => new _UserPreference_bool(),
1950                     'diffMenuItem' => new _UserPreference_bool(),
1951                     'pageInfoMenuItem' => new _UserPreference_bool(),
1952                     'pdfMenuItem' => new _UserPreference_bool(),
1953                     'lockMenuItem' => new _UserPreference_bool(),
1954                     'chownMenuItem' => new _UserPreference_bool(),
1955                     'setaclMenuItem' => new _UserPreference_bool(),
1956                     'removeMenuItem' => new _UserPreference_bool(),
1957                     'renameMenuItem' => new _UserPreference_bool(),
1958                     'revertMenuItem' => new _UserPreference_bool(),
1959                     'backLinksMenuItem' => new _UserPreference_bool(),
1960                     'watchPageMenuItem' => new _UserPreference_bool(),
1961                     'recentChangesMenuItem' => new _UserPreference_bool(),
1962                     'randomPageMenuItem' => new _UserPreference_bool(),
1963                     'likePagesMenuItem' => new _UserPreference_bool(),
1964                     'specialPagesMenuItem' => new _UserPreference_bool(),
1965                     );
1966             $this->_prefs = array_merge($this->_prefs, $fusionforgeprefs);
1967         }
1968
1969         // add custom theme-specific pref types:
1970         // FIXME: on theme changes the wiki_user session pref object will fail. 
1971         // We will silently ignore this.
1972         if (!empty($customUserPreferenceColumns))
1973             $this->_prefs = array_merge($this->_prefs, $customUserPreferenceColumns);
1974 /*
1975         if (isset($this->_method) and $this->_method == 'SQL') {
1976             //unset($this->_prefs['userid']);
1977             unset($this->_prefs['passwd']);
1978         }
1979 */
1980         if (is_array($saved_prefs)) {
1981             foreach ($saved_prefs as $name => $value)
1982                 $this->set($name, $value);
1983         }
1984     }
1985
1986     function __clone() {
1987         foreach ($this as $key => $val) {
1988             if (is_object($val) || (is_array($val))) {
1989                 $this->{$key} = unserialize(serialize($val));
1990             }
1991         }
1992     }
1993
1994     function _getPref($name) {
1995         if ($name == 'emailVerified')
1996             $name = 'email';
1997         if (!isset($this->_prefs[$name])) {
1998             if ($name == 'passwd2') return false;
1999             if ($name == 'passwd') return false;
2000             trigger_error("$name: unknown preference", E_USER_NOTICE);
2001             return false;
2002         }
2003         return $this->_prefs[$name];
2004     }
2005     
2006     // get the value or default_value of the subobject
2007     function get($name) {
2008         if ($_pref = $this->_getPref($name))
2009             if ($name == 'emailVerified')
2010                 return $_pref->getraw($name);
2011             else
2012                 return $_pref->get($name);
2013         else
2014             return false;  
2015     }
2016
2017     // check and set the new value in the subobject
2018     function set($name, $value) {
2019         $pref = $this->_getPref($name);
2020         if ($pref === false)
2021             return false;
2022
2023         /* do it here or outside? */
2024         if ($name == 'passwd' and 
2025             defined('PASSWORD_LENGTH_MINIMUM') and 
2026             strlen($value) <= PASSWORD_LENGTH_MINIMUM ) {
2027             //TODO: How to notify the user?
2028             return false;
2029         }
2030         /*
2031         if ($name == 'theme' and $value == '')
2032            return true;
2033         */
2034         // Fix Fatal error for undefined value. Thanks to Jim Ford and Joel Schaubert
2035         if ((!$value and $pref->default_value)
2036             or ($value and !isset($pref->{$name})) // bug #1355533
2037             or ($value and ($pref->{$name} != $pref->default_value)))
2038         {
2039             if ($name == 'emailVerified') $newvalue = $value;
2040             else $newvalue = $pref->sanify($value);
2041             $pref->set($name, $newvalue);
2042         }
2043         $this->_prefs[$name] =& $pref;
2044         return true;
2045     }
2046     /**
2047      * use init to avoid update on set
2048      */
2049     function updatePrefs($prefs, $init = false) {
2050         $count = 0;
2051         if ($init) $this->_init = $init;
2052         if (is_object($prefs)) {
2053             $type = 'emailVerified'; $obj =& $this->_prefs['email'];
2054             $obj->_init = $init;
2055             if ($obj->get($type) !== $prefs->get($type)) {
2056                 if ($obj->set($type, $prefs->get($type)))
2057                     $count++;
2058             }
2059             foreach (array_keys($this->_prefs) as $type) {
2060                 $obj =& $this->_prefs[$type];
2061                 $obj->_init = $init;
2062                 if ($prefs->get($type) !== $obj->get($type)) {
2063                     // special systemdefault prefs: (probably not needed)
2064                     if ($type == 'theme' and $prefs->get($type) == '' and 
2065                         $obj->get($type) == THEME) continue;
2066                     if ($type == 'lang' and $prefs->get($type) == '' and 
2067                         $obj->get($type) == DEFAULT_LANGUAGE) continue;
2068                     if ($this->_prefs[$type]->set($type, $prefs->get($type)))
2069                         $count++;
2070                 }
2071             }
2072         } elseif (is_array($prefs)) {
2073             //unset($this->_prefs['userid']);
2074             /*
2075             if (isset($this->_method) and 
2076                  ($this->_method == 'SQL' or $this->_method == 'ADODB')) {
2077                 unset($this->_prefs['passwd']);
2078             }
2079             */
2080             // emailVerified at first, the rest later
2081             $type = 'emailVerified'; $obj =& $this->_prefs['email'];
2082             $obj->_init = $init;
2083             if (isset($prefs[$type]) and $obj->get($type) !== $prefs[$type]) {
2084                 if ($obj->set($type,$prefs[$type]))
2085                     $count++;
2086             }
2087             foreach (array_keys($this->_prefs) as $type) {
2088                 $obj =& $this->_prefs[$type];
2089                 $obj->_init = $init;
2090                 if (!isset($prefs[$type]) and isa($obj,"_UserPreference_bool")) 
2091                     $prefs[$type] = false;
2092                 if (isset($prefs[$type]) and isa($obj,"_UserPreference_int"))
2093                     $prefs[$type] = (int) $prefs[$type];
2094                 if (isset($prefs[$type]) and $obj->get($type) != $prefs[$type]) {
2095                     // special systemdefault prefs:
2096                     if ($type == 'theme' and $prefs[$type] == '' and 
2097                         $obj->get($type) == THEME) continue;
2098                     if ($type == 'lang' and $prefs[$type] == '' and 
2099                         $obj->get($type) == DEFAULT_LANGUAGE) continue;
2100                     if ($obj->set($type,$prefs[$type]))
2101                         $count++;
2102                 }
2103             }
2104         }
2105         return $count;
2106     }
2107
2108     // For now convert just array of objects => array of values
2109     // Todo: the specialized subobjects must override this.
2110     function store() {
2111         $prefs = array();
2112         foreach ($this->_prefs as $name => $object) {
2113             if ($value = $object->getraw($name))
2114                 $prefs[$name] = $value;
2115             if ($name == 'email' and ($value = $object->getraw('emailVerified')))
2116                 $prefs['emailVerified'] = $value;
2117             if ($name == 'passwd' and $value and ENCRYPTED_PASSWD) {
2118                 if (strlen($value) != strlen(crypt('test')))
2119                     $prefs['passwd'] = crypt($value);
2120                 else // already crypted
2121                     $prefs['passwd'] = $value;
2122             }
2123         }
2124
2125         if (FUSIONFORGE) {
2126             // Merge current notifyPages with notifyPagesAll
2127             // notifyPages are pages to notify in the current project
2128             // while $notifyPagesAll is used to store all the monitored pages.
2129             if (isset($prefs['notifyPages'])) {
2130                 $this->notifyPagesAll[PAGE_PREFIX] = $prefs['notifyPages'];
2131                 $prefs['notifyPages'] = @serialize($this->notifyPagesAll);
2132             }
2133         }
2134
2135         return $this->pack($prefs);
2136     }
2137
2138     // packed string or array of values => array of values
2139     // Todo: the specialized subobjects must override this.
2140     function retrieve($packed) {
2141         if (is_string($packed) and (substr($packed, 0, 2) == "a:"))
2142             $packed = unserialize($packed);
2143         if (!is_array($packed)) return false;
2144         $prefs = array();
2145         foreach ($packed as $name => $packed_pref) {
2146             if (is_string($packed_pref)
2147                 and isSerialized($packed_pref)
2148                 and substr($packed_pref, 0, 2) == "O:")
2149             {
2150                 //legacy: check if it's an old array of objects
2151                 // Looks like a serialized object. 
2152                 // This might fail if the object definition does not exist anymore.
2153                 // object with ->$name and ->default_value vars.
2154                 $pref =  @unserialize($packed_pref);
2155                 if (is_object($pref))
2156                     $prefs[$name] = $pref->get($name);
2157             // fix old-style prefs
2158             } elseif (is_numeric($name) and is_array($packed_pref)) {
2159                 if (count($packed_pref) == 1) {
2160                     list($name,$value) = each($packed_pref);
2161                     $prefs[$name] = $value;
2162                 }
2163             } else {
2164                 if (isSerialized($packed_pref))
2165                     $prefs[$name] = @unserialize($packed_pref);
2166                 if (empty($prefs[$name]) and isSerialized(base64_decode($packed_pref)))
2167                     $prefs[$name] = @unserialize(base64_decode($packed_pref));
2168                 // patched by frederik@pandora.be
2169                 if (empty($prefs[$name]))
2170                     $prefs[$name] = $packed_pref;
2171             }
2172         }
2173         
2174         if (FUSIONFORGE) {
2175             // Restore notifyPages from notifyPagesAll
2176             // notifyPages are pages to notify in the current project
2177             // while $notifyPagesAll is used to store all the monitored pages.
2178             if (isset($prefs['notifyPages'])) {
2179                 $this->notifyPagesAll = $prefs['notifyPages'];
2180                 if (isset($this->notifyPagesAll[PAGE_PREFIX])) {
2181                     $prefs['notifyPages'] = $this->notifyPagesAll[PAGE_PREFIX];
2182                 } else {
2183                     $prefs['notifyPages'] = '';
2184                 }
2185             }
2186         }
2187
2188         return $prefs;
2189     }
2190
2191     /**
2192      * Check if the given prefs object is different from the current prefs object
2193      */
2194     function isChanged($other) {
2195         foreach ($this->_prefs as $type => $obj) {
2196             if ($obj->get($type) !== $other->get($type))
2197                 return true;
2198         }
2199         return false;
2200     }
2201
2202     function defaultPreferences() {
2203         $prefs = array();
2204         foreach ($this->_prefs as $key => $obj) {
2205             $prefs[$key] = $obj->default_value;
2206         }
2207         return $prefs;
2208     }
2209     
2210     // array of objects
2211     function getAll() {
2212         return $this->_prefs;
2213     }
2214
2215     function pack($nonpacked) {
2216         return serialize($nonpacked);
2217     }
2218
2219     function unpack($packed) {
2220         if (!$packed)
2221             return false;
2222         //$packed = base64_decode($packed);
2223         if (substr($packed, 0, 2) == "O:") {
2224             // Looks like a serialized object
2225             return unserialize($packed);
2226         }
2227         if (substr($packed, 0, 2) == "a:") {
2228             return unserialize($packed);
2229         }
2230         //trigger_error("DEBUG: Can't unpack bad UserPreferences",
2231         //E_USER_WARNING);
2232         return false;
2233     }
2234
2235     function hash () {
2236         return wikihash($this->_prefs);
2237     }
2238 }
2239
2240 /** TODO: new pref storage classes
2241  *  These are currently user specific and should be rewritten to be pref specific.
2242  *  i.e. $this == $user->_prefs
2243  */
2244 /*
2245 class CookieUserPreferences
2246 extends UserPreferences
2247 {
2248     function CookieUserPreferences ($saved_prefs = false) {
2249         //_AnonUser::_AnonUser('',$saved_prefs);
2250         UserPreferences::UserPreferences($saved_prefs);
2251     }
2252 }
2253
2254 class PageUserPreferences
2255 extends UserPreferences
2256 {
2257     function PageUserPreferences ($saved_prefs = false) {
2258         UserPreferences::UserPreferences($saved_prefs);
2259     }
2260 }
2261
2262 class PearDbUserPreferences
2263 extends UserPreferences
2264 {
2265     function PearDbUserPreferences ($saved_prefs = false) {
2266         UserPreferences::UserPreferences($saved_prefs);
2267     }
2268 }
2269
2270 class AdoDbUserPreferences
2271 extends UserPreferences
2272 {
2273     function AdoDbUserPreferences ($saved_prefs = false) {
2274         UserPreferences::UserPreferences($saved_prefs);
2275     }
2276     function getPreferences() {
2277         // override the generic slow method here for efficiency
2278         _AnonUser::getPreferences();
2279         $this->getAuthDbh();
2280         if (isset($this->_select)) {
2281             $dbh = & $this->_auth_dbi;
2282             $rs = $dbh->Execute(sprintf($this->_select,$dbh->qstr($this->_userid)));
2283             if ($rs->EOF) {
2284                 $rs->Close();
2285             } else {
2286                 $prefs_blob = $rs->fields['pref_blob'];
2287                 $rs->Close();
2288                 if ($restored_from_db = $this->_prefs->retrieve($prefs_blob)) {
2289                     $updated = $this->_prefs->updatePrefs($restored_from_db);
2290                     //$this->_prefs = new UserPreferences($restored_from_db);
2291                     return $this->_prefs;
2292                 }
2293             }
2294         }
2295         if (empty($this->_prefs->_prefs) and $this->_HomePagehandle) {
2296             if ($restored_from_page = $this->_prefs->retrieve
2297                 ($this->_HomePagehandle->get('pref'))) {
2298                 $updated = $this->_prefs->updatePrefs($restored_from_page);
2299                 //$this->_prefs = new UserPreferences($restored_from_page);
2300                 return $this->_prefs;
2301             }
2302         }
2303         return $this->_prefs;
2304     }
2305 }
2306 */
2307
2308 // Local Variables:
2309 // mode: php
2310 // tab-width: 8
2311 // c-basic-offset: 4
2312 // c-hanging-comment-ender-p: nil
2313 // indent-tabs-mode: nil
2314 // End:
2315 ?>