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