[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/lib/plugins/authmysql/ -> auth.php (source)

   1  <?php
   2  // must be run within Dokuwiki
   3  if(!defined('DOKU_INC')) die();
   4  
   5  /**
   6   * MySQL authentication backend
   7   *
   8   * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
   9   * @author     Andreas Gohr <andi@splitbrain.org>
  10   * @author     Chris Smith <chris@jalakai.co.uk>
  11   * @author     Matthias Grimm <matthias.grimmm@sourceforge.net>
  12   * @author     Jan Schumann <js@schumann-it.com>
  13   */
  14  class auth_plugin_authmysql extends DokuWiki_Auth_Plugin {
  15      /** @var resource holds the database connection */
  16      protected $dbcon = 0;
  17      /** @var int database version*/
  18      protected $dbver = 0;
  19      /** @var int database revision */
  20      protected $dbrev = 0;
  21      /** @var int database subrevision */
  22      protected $dbsub = 0;
  23  
  24      /** @var array cache to avoid re-reading user info data */
  25      protected $cacheUserInfo = array();
  26  
  27      /**
  28       * Constructor
  29       *
  30       * checks if the mysql interface is available, otherwise it will
  31       * set the variable $success of the basis class to false
  32       *
  33       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
  34       */
  35      public function __construct() {
  36          parent::__construct();
  37  
  38          if(!function_exists('mysql_connect')) {
  39              $this->_debug("MySQL err: PHP MySQL extension not found.", -1, __LINE__, __FILE__);
  40              $this->success = false;
  41              return;
  42          }
  43  
  44          // set capabilities based upon config strings set
  45          if(!$this->getConf('server') || !$this->getConf('user') || !$this->getConf('database')) {
  46              $this->_debug("MySQL err: insufficient configuration.", -1, __LINE__, __FILE__);
  47  
  48              $this->success = false;
  49              return;
  50          }
  51  
  52          $this->cando['addUser']   = $this->_chkcnf(
  53              array(
  54                   'getUserInfo',
  55                   'getGroups',
  56                   'addUser',
  57                   'getUserID',
  58                   'getGroupID',
  59                   'addGroup',
  60                   'addUserGroup'
  61              ), true
  62          );
  63          $this->cando['delUser']   = $this->_chkcnf(
  64              array(
  65                   'getUserID',
  66                   'delUser',
  67                   'delUserRefs'
  68              ), true
  69          );
  70          $this->cando['modLogin']  = $this->_chkcnf(
  71              array(
  72                   'getUserID',
  73                   'updateUser',
  74                   'UpdateTarget'
  75              ), true
  76          );
  77          $this->cando['modPass']   = $this->cando['modLogin'];
  78          $this->cando['modName']   = $this->cando['modLogin'];
  79          $this->cando['modMail']   = $this->cando['modLogin'];
  80          $this->cando['modGroups'] = $this->_chkcnf(
  81              array(
  82                   'getUserID',
  83                   'getGroups',
  84                   'getGroupID',
  85                   'addGroup',
  86                   'addUserGroup',
  87                   'delGroup',
  88                   'getGroupID',
  89                   'delUserGroup'
  90              ), true
  91          );
  92          /* getGroups is not yet supported
  93             $this->cando['getGroups']    = $this->_chkcnf(array('getGroups',
  94             'getGroupID'),false); */
  95          $this->cando['getUsers']     = $this->_chkcnf(
  96              array(
  97                   'getUsers',
  98                   'getUserInfo',
  99                   'getGroups'
 100              ), false
 101          );
 102          $this->cando['getUserCount'] = $this->_chkcnf(array('getUsers'), false);
 103  
 104          if($this->getConf('debug') >= 2) {
 105              $candoDebug = '';
 106              foreach($this->cando as $cd => $value) {
 107                  if($value) { $value = 'yes'; } else { $value = 'no'; }
 108                  $candoDebug .= $cd . ": " . $value . " | ";
 109              }
 110              $this->_debug("authmysql cando: " . $candoDebug, 0, __LINE__, __FILE__);
 111          }
 112      }
 113  
 114      /**
 115       * Check if the given config strings are set
 116       *
 117       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 118       *
 119       * @param   string[] $keys
 120       * @param   bool  $wop is this a check for a write operation?
 121       * @return  bool
 122       */
 123      protected function _chkcnf($keys, $wop = false) {
 124          foreach($keys as $key) {
 125              if(!$this->getConf($key)) return false;
 126          }
 127  
 128          /* write operation and lock array filled with tables names? */
 129          if($wop && (!is_array($this->getConf('TablesToLock')) ||
 130              !count($this->getConf('TablesToLock')))
 131          ) {
 132              return false;
 133          }
 134  
 135          return true;
 136      }
 137  
 138      /**
 139       * Checks if the given user exists and the given plaintext password
 140       * is correct. Furtheron it might be checked wether the user is
 141       * member of the right group
 142       *
 143       * Depending on which SQL string is defined in the config, password
 144       * checking is done here (getpass) or by the database (passcheck)
 145       *
 146       * @param  string $user user who would like access
 147       * @param  string $pass user's clear text password to check
 148       * @return bool
 149       *
 150       * @author  Andreas Gohr <andi@splitbrain.org>
 151       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 152       */
 153      public function checkPass($user, $pass) {
 154          global $conf;
 155          $rc = false;
 156  
 157          if($this->_openDB()) {
 158              $sql    = str_replace('%{user}', $this->_escape($user), $this->getConf('checkPass'));
 159              $sql    = str_replace('%{pass}', $this->_escape($pass), $sql);
 160              $sql    = str_replace('%{dgroup}', $this->_escape($conf['defaultgroup']), $sql);
 161              $result = $this->_queryDB($sql);
 162  
 163              if($result !== false && count($result) == 1) {
 164                  if($this->getConf('forwardClearPass') == 1) {
 165                      $rc = true;
 166                  } else {
 167                      $rc = auth_verifyPassword($pass, $result[0]['pass']);
 168                  }
 169              }
 170              $this->_closeDB();
 171          }
 172          return $rc;
 173      }
 174  
 175      /**
 176       * Return user info
 177       *
 178       * @author  Andreas Gohr <andi@splitbrain.org>
 179       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 180       *
 181       * @param string $user user login to get data for
 182       * @param bool $requireGroups  when true, group membership information should be included in the returned array;
 183       *                             when false, it maybe included, but is not required by the caller
 184       * @return array|bool
 185       */
 186      public function getUserData($user, $requireGroups=true) {
 187          if($this->_cacheExists($user, $requireGroups)) {
 188              return $this->cacheUserInfo[$user];
 189          }
 190  
 191          if($this->_openDB()) {
 192              $this->_lockTables("READ");
 193              $info = $this->_getUserInfo($user, $requireGroups);
 194              $this->_unlockTables();
 195              $this->_closeDB();
 196          } else {
 197              $info = false;
 198          }
 199          return $info;
 200      }
 201  
 202      /**
 203       * Create a new User. Returns false if the user already exists,
 204       * null when an error occurred and true if everything went well.
 205       *
 206       * The new user will be added to the default group by this
 207       * function if grps are not specified (default behaviour).
 208       *
 209       * @author  Andreas Gohr <andi@splitbrain.org>
 210       * @author  Chris Smith <chris@jalakai.co.uk>
 211       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 212       *
 213       * @param string $user  nick of the user
 214       * @param string $pwd   clear text password
 215       * @param string $name  full name of the user
 216       * @param string $mail  email address
 217       * @param array  $grps  array of groups the user should become member of
 218       * @return bool|null
 219       */
 220      public function createUser($user, $pwd, $name, $mail, $grps = null) {
 221          global $conf;
 222  
 223          if($this->_openDB()) {
 224              if(($info = $this->_getUserInfo($user)) !== false) {
 225                  msg($this->getLang('userexists'), -1);
 226                  return false; // user already exists
 227              }
 228  
 229              // set defaultgroup if no groups were given
 230              if($grps == null) {
 231                  $grps = array($conf['defaultgroup']);
 232              }
 233  
 234              $this->_lockTables("WRITE");
 235              $pwd = $this->getConf('forwardClearPass') ? $pwd : auth_cryptPassword($pwd);
 236              $rc  = $this->_addUser($user, $pwd, $name, $mail, $grps);
 237              $this->_unlockTables();
 238              $this->_closeDB();
 239              if(!$rc) {
 240                  msg($this->getLang('writefail'));
 241                  return null;
 242              }
 243              return true;
 244          } else {
 245              msg($this->getLang('connectfail'), -1);
 246          }
 247          return null; // return error
 248      }
 249  
 250      /**
 251       * Modify user data
 252       *
 253       * An existing user dataset will be modified. Changes are given in an array.
 254       *
 255       * The dataset update will be rejected if the user name should be changed
 256       * to an already existing one.
 257       *
 258       * The password must be provided unencrypted. Pasword encryption is done
 259       * automatically if configured.
 260       *
 261       * If one or more groups can't be updated, an error will be set. In
 262       * this case the dataset might already be changed and we can't rollback
 263       * the changes. Transactions would be really useful here.
 264       *
 265       * modifyUser() may be called without SQL statements defined that are
 266       * needed to change group membership (for example if only the user profile
 267       * should be modified). In this case we assure that we don't touch groups
 268       * even when $changes['grps'] is set by mistake.
 269       *
 270       * @author  Chris Smith <chris@jalakai.co.uk>
 271       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 272       *
 273       * @param   string $user    nick of the user to be changed
 274       * @param   array  $changes array of field/value pairs to be changed (password will be clear text)
 275       * @return  bool   true on success, false on error
 276       */
 277      public function modifyUser($user, $changes) {
 278          $rc = false;
 279  
 280          if(!is_array($changes) || !count($changes)) {
 281              return true; // nothing to change
 282          }
 283  
 284          if($this->_openDB()) {
 285              $this->_lockTables("WRITE");
 286  
 287              $rc = $this->_updateUserInfo($user, $changes);
 288  
 289              if(!$rc) {
 290                  msg($this->getLang('usernotexists'), -1);
 291              } elseif(isset($changes['grps']) && $this->cando['modGroups']) {
 292                  $groups = $this->_getGroups($user);
 293                  $grpadd = array_diff($changes['grps'], $groups);
 294                  $grpdel = array_diff($groups, $changes['grps']);
 295  
 296                  foreach($grpadd as $group) {
 297                      if(($this->_addUserToGroup($user, $group, true)) == false) {
 298                          $rc = false;
 299                      }
 300                  }
 301  
 302                  foreach($grpdel as $group) {
 303                      if(($this->_delUserFromGroup($user, $group)) == false) {
 304                          $rc = false;
 305                      }
 306                  }
 307  
 308                  if(!$rc) msg($this->getLang('writefail'));
 309              }
 310  
 311              $this->_unlockTables();
 312              $this->_closeDB();
 313          } else {
 314              msg($this->getLang('connectfail'), -1);
 315          }
 316          return $rc;
 317      }
 318  
 319      /**
 320       * [public function]
 321       *
 322       * Remove one or more users from the list of registered users
 323       *
 324       * @param   array  $users   array of users to be deleted
 325       * @return  int             the number of users deleted
 326       *
 327       * @author  Christopher Smith <chris@jalakai.co.uk>
 328       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 329       */
 330      function deleteUsers($users) {
 331          $count = 0;
 332  
 333          if($this->_openDB()) {
 334              if(is_array($users) && count($users)) {
 335                  $this->_lockTables("WRITE");
 336                  foreach($users as $user) {
 337                      if($this->_delUser($user)) {
 338                          $count++;
 339                      }
 340                  }
 341                  $this->_unlockTables();
 342              }
 343              $this->_closeDB();
 344          } else {
 345              msg($this->getLang('connectfail'), -1);
 346          }
 347          return $count;
 348      }
 349  
 350      /**
 351       * Counts users which meet certain $filter criteria.
 352       *
 353       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 354       *
 355       * @param  array $filter  filter criteria in item/pattern pairs
 356       * @return int count of found users
 357       */
 358      public function getUserCount($filter = array()) {
 359          $rc = 0;
 360  
 361          if($this->_openDB()) {
 362              $sql = $this->_createSQLFilter($this->getConf('getUsers'), $filter);
 363  
 364              if($this->dbver >= 4) {
 365                  $sql = substr($sql, 6); /* remove 'SELECT' or 'select' */
 366                  $sql = "SELECT SQL_CALC_FOUND_ROWS".$sql." LIMIT 1";
 367                  $this->_queryDB($sql);
 368                  $result = $this->_queryDB("SELECT FOUND_ROWS()");
 369                  $rc     = $result[0]['FOUND_ROWS()'];
 370              } else if(($result = $this->_queryDB($sql)))
 371                  $rc = count($result);
 372  
 373              $this->_closeDB();
 374          }
 375          return $rc;
 376      }
 377  
 378      /**
 379       * Bulk retrieval of user data
 380       *
 381       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 382       *
 383       * @param  int          $first  index of first user to be returned
 384       * @param  int          $limit  max number of users to be returned
 385       * @param  array $filter array of field/pattern pairs
 386       * @return  array userinfo (refer getUserData for internal userinfo details)
 387       */
 388      public function retrieveUsers($first = 0, $limit = 0, $filter = array()) {
 389          $out = array();
 390  
 391          if($this->_openDB()) {
 392              $this->_lockTables("READ");
 393              $sql = $this->_createSQLFilter($this->getConf('getUsers'), $filter);
 394              $sql .= " ".$this->getConf('SortOrder');
 395              if($limit) {
 396                  $sql .= " LIMIT $first, $limit";
 397              } elseif($first) {
 398                  $sql .= " LIMIT $first";
 399              }
 400              $result = $this->_queryDB($sql);
 401  
 402              if(!empty($result)) {
 403                  foreach($result as $user) {
 404                      if(($info = $this->_getUserInfo($user['user']))) {
 405                          $out[$user['user']] = $info;
 406                      }
 407                  }
 408              }
 409  
 410              $this->_unlockTables();
 411              $this->_closeDB();
 412          }
 413          return $out;
 414      }
 415  
 416      /**
 417       * Give user membership of a group
 418       *
 419       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 420       *
 421       * @param   string $user
 422       * @param   string $group
 423       * @return  bool   true on success, false on error
 424       */
 425      protected function joinGroup($user, $group) {
 426          $rc = false;
 427  
 428          if($this->_openDB()) {
 429              $this->_lockTables("WRITE");
 430              $rc = $this->_addUserToGroup($user, $group);
 431              $this->_unlockTables();
 432              $this->_closeDB();
 433          }
 434          return $rc;
 435      }
 436  
 437      /**
 438       * Remove user from a group
 439       *
 440       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 441       *
 442       * @param   string $user  user that leaves a group
 443       * @param   string $group group to leave
 444       * @return  bool
 445       */
 446      protected function leaveGroup($user, $group) {
 447          $rc = false;
 448  
 449          if($this->_openDB()) {
 450              $this->_lockTables("WRITE");
 451              $rc  = $this->_delUserFromGroup($user, $group);
 452              $this->_unlockTables();
 453              $this->_closeDB();
 454          }
 455          return $rc;
 456      }
 457  
 458      /**
 459       * MySQL is case-insensitive
 460       */
 461      public function isCaseSensitive() {
 462          return false;
 463      }
 464  
 465      /**
 466       * Adds a user to a group.
 467       *
 468       * If $force is set to true non existing groups would be created.
 469       *
 470       * The database connection must already be established. Otherwise
 471       * this function does nothing and returns 'false'. It is strongly
 472       * recommended to call this function only after all participating
 473       * tables (group and usergroup) have been locked.
 474       *
 475       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 476       *
 477       * @param   string $user    user to add to a group
 478       * @param   string $group   name of the group
 479       * @param   bool   $force   create missing groups
 480       * @return  bool   true on success, false on error
 481       */
 482      protected function _addUserToGroup($user, $group, $force = false) {
 483          $newgroup = 0;
 484  
 485          if(($this->dbcon) && ($user)) {
 486              $gid = $this->_getGroupID($group);
 487              if(!$gid) {
 488                  if($force) { // create missing groups
 489                      $sql      = str_replace('%{group}', $this->_escape($group), $this->getConf('addGroup'));
 490                      $gid      = $this->_modifyDB($sql);
 491                      $newgroup = 1; // group newly created
 492                  }
 493                  if(!$gid) return false; // group didn't exist and can't be created
 494              }
 495  
 496              $sql = $this->getConf('addUserGroup');
 497              if(strpos($sql, '%{uid}') !== false) {
 498                  $uid = $this->_getUserID($user);
 499                  $sql = str_replace('%{uid}', $this->_escape($uid), $sql);
 500              }
 501              $sql = str_replace('%{user}', $this->_escape($user), $sql);
 502              $sql = str_replace('%{gid}', $this->_escape($gid), $sql);
 503              $sql = str_replace('%{group}', $this->_escape($group), $sql);
 504              if($this->_modifyDB($sql) !== false) {
 505                  $this->_flushUserInfoCache($user);
 506                  return true;
 507              }
 508  
 509              if($newgroup) { // remove previously created group on error
 510                  $sql = str_replace('%{gid}', $this->_escape($gid), $this->getConf('delGroup'));
 511                  $sql = str_replace('%{group}', $this->_escape($group), $sql);
 512                  $this->_modifyDB($sql);
 513              }
 514          }
 515          return false;
 516      }
 517  
 518      /**
 519       * Remove user from a group
 520       *
 521       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 522       *
 523       * @param   string $user  user that leaves a group
 524       * @param   string $group group to leave
 525       * @return  bool   true on success, false on error
 526       */
 527      protected function _delUserFromGroup($user, $group) {
 528          $rc = false;
 529  
 530          if(($this->dbcon) && ($user)) {
 531              $sql = $this->getConf('delUserGroup');
 532              if(strpos($sql, '%{uid}') !== false) {
 533                  $uid = $this->_getUserID($user);
 534                  $sql = str_replace('%{uid}', $this->_escape($uid), $sql);
 535              }
 536              $gid = $this->_getGroupID($group);
 537              if($gid) {
 538                  $sql = str_replace('%{user}', $this->_escape($user), $sql);
 539                  $sql = str_replace('%{gid}', $this->_escape($gid), $sql);
 540                  $sql = str_replace('%{group}', $this->_escape($group), $sql);
 541                  $rc  = $this->_modifyDB($sql) == 0 ? true : false;
 542  
 543                  if ($rc) {
 544                      $this->_flushUserInfoCache($user);
 545                  }
 546              }
 547          }
 548          return $rc;
 549      }
 550  
 551      /**
 552       * Retrieves a list of groups the user is a member off.
 553       *
 554       * The database connection must already be established
 555       * for this function to work. Otherwise it will return
 556       * false.
 557       *
 558       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 559       *
 560       * @param  string $user user whose groups should be listed
 561       * @return bool|array false on error, all groups on success
 562       */
 563      protected function _getGroups($user) {
 564          $groups = array();
 565  
 566          if($this->dbcon) {
 567              $sql    = str_replace('%{user}', $this->_escape($user), $this->getConf('getGroups'));
 568              $result = $this->_queryDB($sql);
 569  
 570              if($result !== false && count($result)) {
 571                  foreach($result as $row) {
 572                      $groups[] = $row['group'];
 573                  }
 574              }
 575              return $groups;
 576          }
 577          return false;
 578      }
 579  
 580      /**
 581       * Retrieves the user id of a given user name
 582       *
 583       * The database connection must already be established
 584       * for this function to work. Otherwise it will return
 585       * false.
 586       *
 587       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 588       *
 589       * @param  string $user user whose id is desired
 590       * @return mixed  user id
 591       */
 592      protected function _getUserID($user) {
 593          if($this->dbcon) {
 594              $sql    = str_replace('%{user}', $this->_escape($user), $this->getConf('getUserID'));
 595              $result = $this->_queryDB($sql);
 596              return $result === false ? false : $result[0]['id'];
 597          }
 598          return false;
 599      }
 600  
 601      /**
 602       * Adds a new User to the database.
 603       *
 604       * The database connection must already be established
 605       * for this function to work. Otherwise it will return
 606       * false.
 607       *
 608       * @author  Andreas Gohr <andi@splitbrain.org>
 609       * @author  Chris Smith <chris@jalakai.co.uk>
 610       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 611       *
 612       * @param  string $user  login of the user
 613       * @param  string $pwd   encrypted password
 614       * @param  string $name  full name of the user
 615       * @param  string $mail  email address
 616       * @param  array  $grps  array of groups the user should become member of
 617       * @return bool
 618       */
 619      protected function _addUser($user, $pwd, $name, $mail, $grps) {
 620          if($this->dbcon && is_array($grps)) {
 621              $sql = str_replace('%{user}', $this->_escape($user), $this->getConf('addUser'));
 622              $sql = str_replace('%{pass}', $this->_escape($pwd), $sql);
 623              $sql = str_replace('%{name}', $this->_escape($name), $sql);
 624              $sql = str_replace('%{email}', $this->_escape($mail), $sql);
 625              $uid = $this->_modifyDB($sql);
 626              $gid = false;
 627              $group = '';
 628  
 629              if($uid) {
 630                  foreach($grps as $group) {
 631                      $gid = $this->_addUserToGroup($user, $group, true);
 632                      if($gid === false) break;
 633                  }
 634  
 635                  if($gid !== false){
 636                      $this->_flushUserInfoCache($user);
 637                      return true;
 638                  } else {
 639                      /* remove the new user and all group relations if a group can't
 640                       * be assigned. Newly created groups will remain in the database
 641                       * and won't be removed. This might create orphaned groups but
 642                       * is not a big issue so we ignore this problem here.
 643                       */
 644                      $this->_delUser($user);
 645                      $this->_debug("MySQL err: Adding user '$user' to group '$group' failed.", -1, __LINE__, __FILE__);
 646                  }
 647              }
 648          }
 649          return false;
 650      }
 651  
 652      /**
 653       * Deletes a given user and all his group references.
 654       *
 655       * The database connection must already be established
 656       * for this function to work. Otherwise it will return
 657       * false.
 658       *
 659       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 660       *
 661       * @param  string $user username of the user to be deleted
 662       * @return bool
 663       */
 664      protected function _delUser($user) {
 665          if($this->dbcon) {
 666              $uid = $this->_getUserID($user);
 667              if($uid) {
 668                  $sql = str_replace('%{uid}', $this->_escape($uid), $this->getConf('delUserRefs'));
 669                  $this->_modifyDB($sql);
 670                  $sql = str_replace('%{uid}', $this->_escape($uid), $this->getConf('delUser'));
 671                  $sql = str_replace('%{user}', $this->_escape($user), $sql);
 672                  $this->_modifyDB($sql);
 673                  $this->_flushUserInfoCache($user);
 674                  return true;
 675              }
 676          }
 677          return false;
 678      }
 679  
 680      /**
 681       * Flush cached user information
 682       *
 683       * @author Christopher Smith <chris@jalakai.co.uk>
 684       *
 685       * @param  string  $user username of the user whose data is to be removed from the cache
 686       *                       if null, empty the whole cache
 687       */
 688      protected function _flushUserInfoCache($user=null) {
 689          if (is_null($user)) {
 690              $this->cacheUserInfo = array();
 691          } else {
 692              unset($this->cacheUserInfo[$user]);
 693          }
 694      }
 695  
 696      /**
 697       * Quick lookup to see if a user's information has been cached
 698       *
 699       * This test does not need a database connection or read lock
 700       *
 701       * @author Christopher Smith <chris@jalakai.co.uk>
 702       *
 703       * @param  string  $user  username to be looked up in the cache
 704       * @param  bool    $requireGroups  true, if cached info should include group memberships
 705       *
 706       * @return bool    existence of required user information in the cache
 707       */
 708      protected function _cacheExists($user, $requireGroups=true) {
 709          if (isset($this->cacheUserInfo[$user])) {
 710              if (!is_array($this->cacheUserInfo[$user])) {
 711                  return true;          // user doesn't exist
 712              }
 713  
 714              if (!$requireGroups || isset($this->cacheUserInfo[$user]['grps'])) {
 715                  return true;
 716              }
 717          }
 718  
 719          return false;
 720      }
 721  
 722      /**
 723       * Get a user's information
 724       *
 725       * The database connection must already be established for this function to work.
 726       *
 727       * @author Christopher Smith <chris@jalakai.co.uk>
 728       *
 729       * @param  string  $user  username of the user whose information is being reterieved
 730       * @param  bool    $requireGroups  true if group memberships should be included
 731       * @param  bool    $useCache       true if ok to return cached data & to cache returned data
 732       *
 733       * @return mixed   false|array     false if the user doesn't exist
 734       *                                 array containing user information if user does exist
 735       */
 736      protected function _getUserInfo($user, $requireGroups=true, $useCache=true) {
 737          $info = null;
 738  
 739          if ($useCache && isset($this->cacheUserInfo[$user])) {
 740              $info = $this->cacheUserInfo[$user];
 741          }
 742  
 743          if (is_null($info)) {
 744              $info = $this->_retrieveUserInfo($user);
 745          }
 746  
 747          if (($requireGroups == true) && $info && !isset($info['grps'])) {
 748              $info['grps'] = $this->_getGroups($user);
 749          }
 750  
 751          if ($useCache) {
 752              $this->cacheUserInfo[$user] = $info;
 753          }
 754  
 755          return $info;
 756      }
 757  
 758      /**
 759       * retrieveUserInfo
 760       *
 761       * Gets the data for a specific user. The database connection
 762       * must already be established for this function to work.
 763       * Otherwise it will return 'false'.
 764       *
 765       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 766       *
 767       * @param  string $user  user's nick to get data for
 768       * @return false|array false on error, user info on success
 769       */
 770      protected function _retrieveUserInfo($user) {
 771          $sql    = str_replace('%{user}', $this->_escape($user), $this->getConf('getUserInfo'));
 772          $result = $this->_queryDB($sql);
 773          if($result !== false && count($result)) {
 774              $info         = $result[0];
 775              return $info;
 776          }
 777          return false;
 778      }
 779  
 780      /**
 781       * Updates the user info in the database
 782       *
 783       * Update a user data structure in the database according changes
 784       * given in an array. The user name can only be changes if it didn't
 785       * exists already. If the new user name exists the update procedure
 786       * will be aborted. The database keeps unchanged.
 787       *
 788       * The database connection has already to be established for this
 789       * function to work. Otherwise it will return 'false'.
 790       *
 791       * The password will be encrypted if necessary.
 792       *
 793       * @param  string $user    user's nick being updated
 794       * @param  array $changes  array of items to change as pairs of item and value
 795       * @return bool true on success or false on error
 796       *
 797       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 798       */
 799      protected function _updateUserInfo($user, $changes) {
 800          $sql = $this->getConf('updateUser')." ";
 801          $cnt = 0;
 802          $err = 0;
 803  
 804          if($this->dbcon) {
 805              $uid = $this->_getUserID($user);
 806              if ($uid === false) {
 807                  return false;
 808              }
 809  
 810              foreach($changes as $item => $value) {
 811                  if($item == 'user') {
 812                      if(($this->_getUserID($changes['user']))) {
 813                          $err = 1; /* new username already exists */
 814                          break; /* abort update */
 815                      }
 816                      if($cnt++ > 0) $sql .= ", ";
 817                      $sql .= str_replace('%{user}', $value, $this->getConf('UpdateLogin'));
 818                  } else if($item == 'name') {
 819                      if($cnt++ > 0) $sql .= ", ";
 820                      $sql .= str_replace('%{name}', $value, $this->getConf('UpdateName'));
 821                  } else if($item == 'pass') {
 822                      if(!$this->getConf('forwardClearPass'))
 823                          $value = auth_cryptPassword($value);
 824                      if($cnt++ > 0) $sql .= ", ";
 825                      $sql .= str_replace('%{pass}', $value, $this->getConf('UpdatePass'));
 826                  } else if($item == 'mail') {
 827                      if($cnt++ > 0) $sql .= ", ";
 828                      $sql .= str_replace('%{email}', $value, $this->getConf('UpdateEmail'));
 829                  }
 830              }
 831  
 832              if($err == 0) {
 833                  if($cnt > 0) {
 834                      $sql .= " ".str_replace('%{uid}', $uid, $this->getConf('UpdateTarget'));
 835                      if(get_class($this) == 'auth_mysql') $sql .= " LIMIT 1"; //some PgSQL inheritance comp.
 836                      $this->_modifyDB($sql);
 837                      $this->_flushUserInfoCache($user);
 838                  }
 839                  return true;
 840              }
 841          }
 842          return false;
 843      }
 844  
 845      /**
 846       * Retrieves the group id of a given group name
 847       *
 848       * The database connection must already be established
 849       * for this function to work. Otherwise it will return
 850       * false.
 851       *
 852       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 853       *
 854       * @param  string $group   group name which id is desired
 855       * @return false|string group id
 856       */
 857      protected function _getGroupID($group) {
 858          if($this->dbcon) {
 859              $sql    = str_replace('%{group}', $this->_escape($group), $this->getConf('getGroupID'));
 860              $result = $this->_queryDB($sql);
 861              return $result === false ? false : $result[0]['id'];
 862          }
 863          return false;
 864      }
 865  
 866      /**
 867       * Opens a connection to a database and saves the handle for further
 868       * usage in the object. The successful call to this functions is
 869       * essential for most functions in this object.
 870       *
 871       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 872       *
 873       * @return bool
 874       */
 875      protected function _openDB() {
 876          if(!$this->dbcon) {
 877              $con = @mysql_connect($this->getConf('server'), $this->getConf('user'), conf_decodeString($this->getConf('password')));
 878              if($con) {
 879                  if((mysql_select_db($this->getConf('database'), $con))) {
 880                      if((preg_match('/^(\d+)\.(\d+)\.(\d+).*/', mysql_get_server_info($con), $result)) == 1) {
 881                          $this->dbver = $result[1];
 882                          $this->dbrev = $result[2];
 883                          $this->dbsub = $result[3];
 884                      }
 885                      $this->dbcon = $con;
 886                      if($this->getConf('charset')) {
 887                          mysql_query('SET CHARACTER SET "'.$this->getConf('charset').'"', $con);
 888                      }
 889                      return true; // connection and database successfully opened
 890                  } else {
 891                      mysql_close($con);
 892                      $this->_debug("MySQL err: No access to database {$this->getConf('database')}.", -1, __LINE__, __FILE__);
 893                  }
 894              } else {
 895                  $this->_debug(
 896                      "MySQL err: Connection to {$this->getConf('user')}@{$this->getConf('server')} not possible.",
 897                      -1, __LINE__, __FILE__
 898                  );
 899              }
 900  
 901              return false; // connection failed
 902          }
 903          return true; // connection already open
 904      }
 905  
 906      /**
 907       * Closes a database connection.
 908       *
 909       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 910       */
 911      protected function _closeDB() {
 912          if($this->dbcon) {
 913              mysql_close($this->dbcon);
 914              $this->dbcon = 0;
 915          }
 916      }
 917  
 918      /**
 919       * Sends a SQL query to the database and transforms the result into
 920       * an associative array.
 921       *
 922       * This function is only able to handle queries that returns a
 923       * table such as SELECT.
 924       *
 925       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 926       *
 927       * @param string $query  SQL string that contains the query
 928       * @return array|false with the result table
 929       */
 930      protected function _queryDB($query) {
 931          if($this->getConf('debug') >= 2) {
 932              msg('MySQL query: '.hsc($query), 0, __LINE__, __FILE__);
 933          }
 934  
 935          $resultarray = array();
 936          if($this->dbcon) {
 937              $result = @mysql_query($query, $this->dbcon);
 938              if($result) {
 939                  while(($t = mysql_fetch_assoc($result)) !== false)
 940                      $resultarray[] = $t;
 941                  mysql_free_result($result);
 942                  return $resultarray;
 943              }
 944              $this->_debug('MySQL err: '.mysql_error($this->dbcon), -1, __LINE__, __FILE__);
 945          }
 946          return false;
 947      }
 948  
 949      /**
 950       * Sends a SQL query to the database
 951       *
 952       * This function is only able to handle queries that returns
 953       * either nothing or an id value such as INPUT, DELETE, UPDATE, etc.
 954       *
 955       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 956       *
 957       * @param string $query  SQL string that contains the query
 958       * @return int|bool insert id or 0, false on error
 959       */
 960      protected function _modifyDB($query) {
 961          if($this->getConf('debug') >= 2) {
 962              msg('MySQL query: '.hsc($query), 0, __LINE__, __FILE__);
 963          }
 964  
 965          if($this->dbcon) {
 966              $result = @mysql_query($query, $this->dbcon);
 967              if($result) {
 968                  $rc = mysql_insert_id($this->dbcon); //give back ID on insert
 969                  if($rc !== false) return $rc;
 970              }
 971              $this->_debug('MySQL err: '.mysql_error($this->dbcon), -1, __LINE__, __FILE__);
 972          }
 973          return false;
 974      }
 975  
 976      /**
 977       * Locked a list of tables for exclusive access so that modifications
 978       * to the database can't be disturbed by other threads. The list
 979       * could be set with $conf['plugin']['authmysql']['TablesToLock'] = array()
 980       *
 981       * If aliases for tables are used in SQL statements, also this aliases
 982       * must be locked. For eg. you use a table 'user' and the alias 'u' in
 983       * some sql queries, the array must looks like this (order is important):
 984       *   array("user", "user AS u");
 985       *
 986       * MySQL V3 is not able to handle transactions with COMMIT/ROLLBACK
 987       * so that this functionality is simulated by this function. Nevertheless
 988       * it is not as powerful as transactions, it is a good compromise in safty.
 989       *
 990       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 991       *
 992       * @param string $mode  could be 'READ' or 'WRITE'
 993       * @return bool
 994       */
 995      protected function _lockTables($mode) {
 996          if($this->dbcon) {
 997              $ttl = $this->getConf('TablesToLock');
 998              if(is_array($ttl) && !empty($ttl)) {
 999                  if($mode == "READ" || $mode == "WRITE") {
1000                      $sql = "LOCK TABLES ";
1001                      $cnt = 0;
1002                      foreach($ttl as $table) {
1003                          if($cnt++ != 0) $sql .= ", ";
1004                          $sql .= "$table $mode";
1005                      }
1006                      $this->_modifyDB($sql);
1007                      return true;
1008                  }
1009              }
1010          }
1011          return false;
1012      }
1013  
1014      /**
1015       * Unlock locked tables. All existing locks of this thread will be
1016       * abrogated.
1017       *
1018       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
1019       *
1020       * @return bool
1021       */
1022      protected function _unlockTables() {
1023          if($this->dbcon) {
1024              $this->_modifyDB("UNLOCK TABLES");
1025              return true;
1026          }
1027          return false;
1028      }
1029  
1030      /**
1031       * Transforms the filter settings in an filter string for a SQL database
1032       * The database connection must already be established, otherwise the
1033       * original SQL string without filter criteria will be returned.
1034       *
1035       * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
1036       *
1037       * @param  string $sql     SQL string to which the $filter criteria should be added
1038       * @param  array $filter  array of filter criteria as pairs of item and pattern
1039       * @return string SQL string with attached $filter criteria on success, original SQL string on error
1040       */
1041      protected function _createSQLFilter($sql, $filter) {
1042          $SQLfilter = "";
1043          $cnt       = 0;
1044  
1045          if($this->dbcon) {
1046              foreach($filter as $item => $pattern) {
1047                  $tmp = '%'.$this->_escape($pattern).'%';
1048                  if($item == 'user') {
1049                      if($cnt++ > 0) $SQLfilter .= " AND ";
1050                      $SQLfilter .= str_replace('%{user}', $tmp, $this->getConf('FilterLogin'));
1051                  } else if($item == 'name') {
1052                      if($cnt++ > 0) $SQLfilter .= " AND ";
1053                      $SQLfilter .= str_replace('%{name}', $tmp, $this->getConf('FilterName'));
1054                  } else if($item == 'mail') {
1055                      if($cnt++ > 0) $SQLfilter .= " AND ";
1056                      $SQLfilter .= str_replace('%{email}', $tmp, $this->getConf('FilterEmail'));
1057                  } else if($item == 'grps') {
1058                      if($cnt++ > 0) $SQLfilter .= " AND ";
1059                      $SQLfilter .= str_replace('%{group}', $tmp, $this->getConf('FilterGroup'));
1060                  }
1061              }
1062  
1063              // we have to check SQLfilter here and must not use $cnt because if
1064              // any of cnf['Filter????'] is not defined, a malformed SQL string
1065              // would be generated.
1066  
1067              if(strlen($SQLfilter)) {
1068                  $glue = strpos(strtolower($sql), "where") ? " AND " : " WHERE ";
1069                  $sql  = $sql.$glue.$SQLfilter;
1070              }
1071          }
1072  
1073          return $sql;
1074      }
1075  
1076      /**
1077       * Escape a string for insertion into the database
1078       *
1079       * @author Andreas Gohr <andi@splitbrain.org>
1080       *
1081       * @param  string  $string The string to escape
1082       * @param  boolean $like   Escape wildcard chars as well?
1083       * @return string
1084       */
1085      protected function _escape($string, $like = false) {
1086          if($this->dbcon) {
1087              $string = mysql_real_escape_string($string, $this->dbcon);
1088          } else {
1089              $string = addslashes($string);
1090          }
1091          if($like) {
1092              $string = addcslashes($string, '%_');
1093          }
1094          return $string;
1095      }
1096  
1097      /**
1098       * Wrapper around msg() but outputs only when debug is enabled
1099       *
1100       * @param string $message
1101       * @param int    $err
1102       * @param int    $line
1103       * @param string $file
1104       * @return void
1105       */
1106      protected function _debug($message, $err, $line, $file) {
1107          if(!$this->getConf('debug')) return;
1108          msg($message, $err, $line, $file);
1109      }
1110  }