[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

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

   1  <?php
   2  /**
   3   * DokuWiki Plugin authpdo (Auth Component)
   4   *
   5   * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
   6   * @author  Andreas Gohr <andi@splitbrain.org>
   7   */
   8  
   9  // must be run within Dokuwiki
  10  if(!defined('DOKU_INC')) die();
  11  
  12  /**
  13   * Class auth_plugin_authpdo
  14   */
  15  class auth_plugin_authpdo extends DokuWiki_Auth_Plugin {
  16  
  17      /** @var PDO */
  18      protected $pdo;
  19  
  20      /** @var null|array The list of all groups */
  21      protected $groupcache = null;
  22  
  23      /**
  24       * Constructor.
  25       */
  26      public function __construct() {
  27          parent::__construct(); // for compatibility
  28  
  29          if(!class_exists('PDO')) {
  30              $this->_debug('PDO extension for PHP not found.', -1, __LINE__);
  31              $this->success = false;
  32              return;
  33          }
  34  
  35          if(!$this->getConf('dsn')) {
  36              $this->_debug('No DSN specified', -1, __LINE__);
  37              $this->success = false;
  38              return;
  39          }
  40  
  41          try {
  42              $this->pdo = new PDO(
  43                  $this->getConf('dsn'),
  44                  $this->getConf('user'),
  45                  conf_decodeString($this->getConf('pass')),
  46                  array(
  47                      PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // always fetch as array
  48                      PDO::ATTR_EMULATE_PREPARES => true, // emulating prepares allows us to reuse param names
  49                      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // we want exceptions, not error codes
  50                  )
  51              );
  52          } catch(PDOException $e) {
  53              $this->_debug($e);
  54              msg($this->getLang('connectfail'), -1);
  55              $this->success = false;
  56              return;
  57          }
  58  
  59          // can Users be created?
  60          $this->cando['addUser'] = $this->_chkcnf(
  61              array(
  62                  'select-user',
  63                  'select-user-groups',
  64                  'select-groups',
  65                  'insert-user',
  66                  'insert-group',
  67                  'join-group'
  68              )
  69          );
  70  
  71          // can Users be deleted?
  72          $this->cando['delUser'] = $this->_chkcnf(
  73              array(
  74                  'select-user',
  75                  'select-user-groups',
  76                  'select-groups',
  77                  'leave-group',
  78                  'delete-user'
  79              )
  80          );
  81  
  82          // can login names be changed?
  83          $this->cando['modLogin'] = $this->_chkcnf(
  84              array(
  85                  'select-user',
  86                  'select-user-groups',
  87                  'update-user-login'
  88              )
  89          );
  90  
  91          // can passwords be changed?
  92          $this->cando['modPass'] = $this->_chkcnf(
  93              array(
  94                  'select-user',
  95                  'select-user-groups',
  96                  'update-user-pass'
  97              )
  98          );
  99  
 100          // can real names be changed?
 101          $this->cando['modName'] = $this->_chkcnf(
 102              array(
 103                  'select-user',
 104                  'select-user-groups',
 105                  'update-user-info:name'
 106              )
 107          );
 108  
 109          // can real email be changed?
 110          $this->cando['modMail'] = $this->_chkcnf(
 111              array(
 112                  'select-user',
 113                  'select-user-groups',
 114                  'update-user-info:mail'
 115              )
 116          );
 117  
 118          // can groups be changed?
 119          $this->cando['modGroups'] = $this->_chkcnf(
 120              array(
 121                  'select-user',
 122                  'select-user-groups',
 123                  'select-groups',
 124                  'leave-group',
 125                  'join-group',
 126                  'insert-group'
 127              )
 128          );
 129  
 130          // can a filtered list of users be retrieved?
 131          $this->cando['getUsers'] = $this->_chkcnf(
 132              array(
 133                  'list-users'
 134              )
 135          );
 136  
 137          // can the number of users be retrieved?
 138          $this->cando['getUserCount'] = $this->_chkcnf(
 139              array(
 140                  'count-users'
 141              )
 142          );
 143  
 144          // can a list of available groups be retrieved?
 145          $this->cando['getGroups'] = $this->_chkcnf(
 146              array(
 147                  'select-groups'
 148              )
 149          );
 150  
 151          $this->success = true;
 152      }
 153  
 154      /**
 155       * Check user+password
 156       *
 157       * @param   string $user the user name
 158       * @param   string $pass the clear text password
 159       * @return  bool
 160       */
 161      public function checkPass($user, $pass) {
 162  
 163          $userdata = $this->_selectUser($user);
 164          if($userdata == false) return false;
 165  
 166          // password checking done in SQL?
 167          if($this->_chkcnf(array('check-pass'))) {
 168              $userdata['clear'] = $pass;
 169              $userdata['hash'] = auth_cryptPassword($pass);
 170              $result = $this->_query($this->getConf('check-pass'), $userdata);
 171              if($result === false) return false;
 172              return (count($result) == 1);
 173          }
 174  
 175          // we do password checking on our own
 176          if(isset($userdata['hash'])) {
 177              // hashed password
 178              $passhash = new PassHash();
 179              return $passhash->verify_hash($pass, $userdata['hash']);
 180          } else {
 181              // clear text password in the database O_o
 182              return ($pass === $userdata['clear']);
 183          }
 184      }
 185  
 186      /**
 187       * Return user info
 188       *
 189       * Returns info about the given user needs to contain
 190       * at least these fields:
 191       *
 192       * name string  full name of the user
 193       * mail string  email addres of the user
 194       * grps array   list of groups the user is in
 195       *
 196       * @param   string $user the user name
 197       * @param   bool $requireGroups whether or not the returned data must include groups
 198       * @return array|bool containing user data or false
 199       */
 200      public function getUserData($user, $requireGroups = true) {
 201          $data = $this->_selectUser($user);
 202          if($data == false) return false;
 203  
 204          if(isset($data['hash'])) unset($data['hash']);
 205          if(isset($data['clean'])) unset($data['clean']);
 206  
 207          if($requireGroups) {
 208              $data['grps'] = $this->_selectUserGroups($data);
 209              if($data['grps'] === false) return false;
 210          }
 211  
 212          return $data;
 213      }
 214  
 215      /**
 216       * Create a new User [implement only where required/possible]
 217       *
 218       * Returns false if the user already exists, null when an error
 219       * occurred and true if everything went well.
 220       *
 221       * The new user HAS TO be added to the default group by this
 222       * function!
 223       *
 224       * Set addUser capability when implemented
 225       *
 226       * @param  string $user
 227       * @param  string $clear
 228       * @param  string $name
 229       * @param  string $mail
 230       * @param  null|array $grps
 231       * @return bool|null
 232       */
 233      public function createUser($user, $clear, $name, $mail, $grps = null) {
 234          global $conf;
 235  
 236          if(($info = $this->getUserData($user, false)) !== false) {
 237              msg($this->getLang('userexists'), -1);
 238              return false; // user already exists
 239          }
 240  
 241          // prepare data
 242          if($grps == null) $grps = array();
 243          array_unshift($grps, $conf['defaultgroup']);
 244          $grps = array_unique($grps);
 245          $hash = auth_cryptPassword($clear);
 246          $userdata = compact('user', 'clear', 'hash', 'name', 'mail');
 247  
 248          // action protected by transaction
 249          $this->pdo->beginTransaction();
 250          {
 251              // insert the user
 252              $ok = $this->_query($this->getConf('insert-user'), $userdata);
 253              if($ok === false) goto FAIL;
 254              $userdata = $this->getUserData($user, false);
 255              if($userdata === false) goto FAIL;
 256  
 257              // create all groups that do not exist, the refetch the groups
 258              $allgroups = $this->_selectGroups();
 259              foreach($grps as $group) {
 260                  if(!isset($allgroups[$group])) {
 261                      $ok = $this->addGroup($group);
 262                      if($ok === false) goto FAIL;
 263                  }
 264              }
 265              $allgroups = $this->_selectGroups();
 266  
 267              // add user to the groups
 268              foreach($grps as $group) {
 269                  $ok = $this->_joinGroup($userdata, $allgroups[$group]);
 270                  if($ok === false) goto FAIL;
 271              }
 272          }
 273          $this->pdo->commit();
 274          return true;
 275  
 276          // something went wrong, rollback
 277          FAIL:
 278          $this->pdo->rollBack();
 279          $this->_debug('Transaction rolled back', 0, __LINE__);
 280          msg($this->getLang('writefail'), -1);
 281          return null; // return error
 282      }
 283  
 284      /**
 285       * Modify user data
 286       *
 287       * @param   string $user nick of the user to be changed
 288       * @param   array $changes array of field/value pairs to be changed (password will be clear text)
 289       * @return  bool
 290       */
 291      public function modifyUser($user, $changes) {
 292          // secure everything in transaction
 293          $this->pdo->beginTransaction();
 294          {
 295              $olddata = $this->getUserData($user);
 296              $oldgroups = $olddata['grps'];
 297              unset($olddata['grps']);
 298  
 299              // changing the user name?
 300              if(isset($changes['user'])) {
 301                  if($this->getUserData($changes['user'], false)) goto FAIL;
 302                  $params = $olddata;
 303                  $params['newlogin'] = $changes['user'];
 304  
 305                  $ok = $this->_query($this->getConf('update-user-login'), $params);
 306                  if($ok === false) goto FAIL;
 307              }
 308  
 309              // changing the password?
 310              if(isset($changes['pass'])) {
 311                  $params = $olddata;
 312                  $params['clear'] = $changes['pass'];
 313                  $params['hash'] = auth_cryptPassword($changes['pass']);
 314  
 315                  $ok = $this->_query($this->getConf('update-user-pass'), $params);
 316                  if($ok === false) goto FAIL;
 317              }
 318  
 319              // changing info?
 320              if(isset($changes['mail']) || isset($changes['name'])) {
 321                  $params = $olddata;
 322                  if(isset($changes['mail'])) $params['mail'] = $changes['mail'];
 323                  if(isset($changes['name'])) $params['name'] = $changes['name'];
 324  
 325                  $ok = $this->_query($this->getConf('update-user-info'), $params);
 326                  if($ok === false) goto FAIL;
 327              }
 328  
 329              // changing groups?
 330              if(isset($changes['grps'])) {
 331                  $allgroups = $this->_selectGroups();
 332  
 333                  // remove membership for previous groups
 334                  foreach($oldgroups as $group) {
 335                      if(!in_array($group, $changes['grps']) && isset($allgroups[$group])) {
 336                          $ok = $this->_leaveGroup($olddata, $allgroups[$group]);
 337                          if($ok === false) goto FAIL;
 338                      }
 339                  }
 340  
 341                  // create all new groups that are missing
 342                  $added = 0;
 343                  foreach($changes['grps'] as $group) {
 344                      if(!isset($allgroups[$group])) {
 345                          $ok = $this->addGroup($group);
 346                          if($ok === false) goto FAIL;
 347                          $added++;
 348                      }
 349                  }
 350                  // reload group info
 351                  if($added > 0) $allgroups = $this->_selectGroups();
 352  
 353                  // add membership for new groups
 354                  foreach($changes['grps'] as $group) {
 355                      if(!in_array($group, $oldgroups)) {
 356                          $ok = $this->_joinGroup($olddata, $allgroups[$group]);
 357                          if($ok === false) goto FAIL;
 358                      }
 359                  }
 360              }
 361  
 362          }
 363          $this->pdo->commit();
 364          return true;
 365  
 366          // something went wrong, rollback
 367          FAIL:
 368          $this->pdo->rollBack();
 369          $this->_debug('Transaction rolled back', 0, __LINE__);
 370          msg($this->getLang('writefail'), -1);
 371          return false; // return error
 372      }
 373  
 374      /**
 375       * Delete one or more users
 376       *
 377       * Set delUser capability when implemented
 378       *
 379       * @param   array $users
 380       * @return  int    number of users deleted
 381       */
 382      public function deleteUsers($users) {
 383          $count = 0;
 384          foreach($users as $user) {
 385              if($this->_deleteUser($user)) $count++;
 386          }
 387          return $count;
 388      }
 389  
 390      /**
 391       * Bulk retrieval of user data [implement only where required/possible]
 392       *
 393       * Set getUsers capability when implemented
 394       *
 395       * @param   int $start index of first user to be returned
 396       * @param   int $limit max number of users to be returned
 397       * @param   array $filter array of field/pattern pairs, null for no filter
 398       * @return  array list of userinfo (refer getUserData for internal userinfo details)
 399       */
 400      public function retrieveUsers($start = 0, $limit = -1, $filter = null) {
 401          if($limit < 0) $limit = 10000; // we don't support no limit
 402          if(is_null($filter)) $filter = array();
 403  
 404          if(isset($filter['grps'])) $filter['group'] = $filter['grps'];
 405          foreach(array('user', 'name', 'mail', 'group') as $key) {
 406              if(!isset($filter[$key])) {
 407                  $filter[$key] = '%';
 408              } else {
 409                  $filter[$key] = '%' . $filter[$key] . '%';
 410              }
 411          }
 412          $filter['start'] = (int) $start;
 413          $filter['end'] = (int) $start + $limit;
 414          $filter['limit'] = (int) $limit;
 415  
 416          $result = $this->_query($this->getConf('list-users'), $filter);
 417          if(!$result) return array();
 418          $users = array();
 419          foreach($result as $row) {
 420              if(!isset($row['user'])) {
 421                  $this->_debug("Statement did not return 'user' attribute", -1, __LINE__);
 422                  return array();
 423              }
 424              $users[] = $this->getUserData($row['user']);
 425          }
 426          return $users;
 427      }
 428  
 429      /**
 430       * Return a count of the number of user which meet $filter criteria
 431       *
 432       * @param  array $filter array of field/pattern pairs, empty array for no filter
 433       * @return int
 434       */
 435      public function getUserCount($filter = array()) {
 436          if(is_null($filter)) $filter = array();
 437  
 438          if(isset($filter['grps'])) $filter['group'] = $filter['grps'];
 439          foreach(array('user', 'name', 'mail', 'group') as $key) {
 440              if(!isset($filter[$key])) {
 441                  $filter[$key] = '%';
 442              } else {
 443                  $filter[$key] = '%' . $filter[$key] . '%';
 444              }
 445          }
 446  
 447          $result = $this->_query($this->getConf('count-users'), $filter);
 448          if(!$result || !isset($result[0]['count'])) {
 449              $this->_debug("Statement did not return 'count' attribute", -1, __LINE__);
 450          }
 451          return (int) $result[0]['count'];
 452      }
 453  
 454      /**
 455       * Create a new group with the given name
 456       *
 457       * @param string $group
 458       * @return bool
 459       */
 460      public function addGroup($group) {
 461          $sql = $this->getConf('insert-group');
 462  
 463          $result = $this->_query($sql, array(':group' => $group));
 464          $this->_clearGroupCache();
 465          if($result === false) return false;
 466          return true;
 467      }
 468  
 469      /**
 470       * Retrieve groups
 471       *
 472       * Set getGroups capability when implemented
 473       *
 474       * @param   int $start
 475       * @param   int $limit
 476       * @return  array
 477       */
 478      public function retrieveGroups($start = 0, $limit = 0) {
 479          $groups = array_keys($this->_selectGroups());
 480          if($groups === false) return array();
 481  
 482          if(!$limit) {
 483              return array_splice($groups, $start);
 484          } else {
 485              return array_splice($groups, $start, $limit);
 486          }
 487      }
 488  
 489      /**
 490       * Select data of a specified user
 491       *
 492       * @param string $user the user name
 493       * @return bool|array user data, false on error
 494       */
 495      protected function _selectUser($user) {
 496          $sql = $this->getConf('select-user');
 497  
 498          $result = $this->_query($sql, array(':user' => $user));
 499          if(!$result) return false;
 500  
 501          if(count($result) > 1) {
 502              $this->_debug('Found more than one matching user', -1, __LINE__);
 503              return false;
 504          }
 505  
 506          $data = array_shift($result);
 507          $dataok = true;
 508  
 509          if(!isset($data['user'])) {
 510              $this->_debug("Statement did not return 'user' attribute", -1, __LINE__);
 511              $dataok = false;
 512          }
 513          if(!isset($data['hash']) && !isset($data['clear']) && !$this->_chkcnf(array('check-pass'))) {
 514              $this->_debug("Statement did not return 'clear' or 'hash' attribute", -1, __LINE__);
 515              $dataok = false;
 516          }
 517          if(!isset($data['name'])) {
 518              $this->_debug("Statement did not return 'name' attribute", -1, __LINE__);
 519              $dataok = false;
 520          }
 521          if(!isset($data['mail'])) {
 522              $this->_debug("Statement did not return 'mail' attribute", -1, __LINE__);
 523              $dataok = false;
 524          }
 525  
 526          if(!$dataok) return false;
 527          return $data;
 528      }
 529  
 530      /**
 531       * Delete a user after removing all their group memberships
 532       *
 533       * @param string $user
 534       * @return bool true when the user was deleted
 535       */
 536      protected function _deleteUser($user) {
 537          $this->pdo->beginTransaction();
 538          {
 539              $userdata = $this->getUserData($user);
 540              if($userdata === false) goto FAIL;
 541              $allgroups = $this->_selectGroups();
 542  
 543              // remove group memberships (ignore errors)
 544              foreach($userdata['grps'] as $group) {
 545                  if(isset($allgroups[$group])) {
 546                      $this->_leaveGroup($userdata, $allgroups[$group]);
 547                  }
 548              }
 549  
 550              $ok = $this->_query($this->getConf('delete-user'), $userdata);
 551              if($ok === false) goto FAIL;
 552          }
 553          $this->pdo->commit();
 554          return true;
 555  
 556          FAIL:
 557          $this->pdo->rollBack();
 558          return false;
 559      }
 560  
 561      /**
 562       * Select all groups of a user
 563       *
 564       * @param array $userdata The userdata as returned by _selectUser()
 565       * @return array|bool list of group names, false on error
 566       */
 567      protected function _selectUserGroups($userdata) {
 568          global $conf;
 569          $sql = $this->getConf('select-user-groups');
 570          $result = $this->_query($sql, $userdata);
 571          if($result === false) return false;
 572  
 573          $groups = array($conf['defaultgroup']); // always add default config
 574          foreach($result as $row) {
 575              if(!isset($row['group'])) {
 576                  $this->_debug("No 'group' field returned in select-user-groups statement");
 577                  return false;
 578              }
 579              $groups[] = $row['group'];
 580          }
 581  
 582          $groups = array_unique($groups);
 583          sort($groups);
 584          return $groups;
 585      }
 586  
 587      /**
 588       * Select all available groups
 589       *
 590       * @return array|bool list of all available groups and their properties
 591       */
 592      protected function _selectGroups() {
 593          if($this->groupcache) return $this->groupcache;
 594  
 595          $sql = $this->getConf('select-groups');
 596          $result = $this->_query($sql);
 597          if($result === false) return false;
 598  
 599          $groups = array();
 600          foreach($result as $row) {
 601              if(!isset($row['group'])) {
 602                  $this->_debug("No 'group' field returned from select-groups statement", -1, __LINE__);
 603                  return false;
 604              }
 605  
 606              // relayout result with group name as key
 607              $group = $row['group'];
 608              $groups[$group] = $row;
 609          }
 610  
 611          ksort($groups);
 612          return $groups;
 613      }
 614  
 615      /**
 616       * Remove all entries from the group cache
 617       */
 618      protected function _clearGroupCache() {
 619          $this->groupcache = null;
 620      }
 621  
 622      /**
 623       * Adds the user to the group
 624       *
 625       * @param array $userdata all the user data
 626       * @param array $groupdata all the group data
 627       * @return bool
 628       */
 629      protected function _joinGroup($userdata, $groupdata) {
 630          $data = array_merge($userdata, $groupdata);
 631          $sql = $this->getConf('join-group');
 632          $result = $this->_query($sql, $data);
 633          if($result === false) return false;
 634          return true;
 635      }
 636  
 637      /**
 638       * Removes the user from the group
 639       *
 640       * @param array $userdata all the user data
 641       * @param array $groupdata all the group data
 642       * @return bool
 643       */
 644      protected function _leaveGroup($userdata, $groupdata) {
 645          $data = array_merge($userdata, $groupdata);
 646          $sql = $this->getConf('leave-group');
 647          $result = $this->_query($sql, $data);
 648          if($result === false) return false;
 649          return true;
 650      }
 651  
 652      /**
 653       * Executes a query
 654       *
 655       * @param string $sql The SQL statement to execute
 656       * @param array $arguments Named parameters to be used in the statement
 657       * @return array|int|bool The result as associative array for SELECTs, affected rows for others, false on error
 658       */
 659      protected function _query($sql, $arguments = array()) {
 660          $sql = trim($sql);
 661          if(empty($sql)) {
 662              $this->_debug('No SQL query given', -1, __LINE__);
 663              return false;
 664          }
 665  
 666          // execute
 667          $params = array();
 668          $sth = $this->pdo->prepare($sql);
 669          try {
 670              // prepare parameters - we only use those that exist in the SQL
 671              foreach($arguments as $key => $value) {
 672                  if(is_array($value)) continue;
 673                  if(is_object($value)) continue;
 674                  if($key[0] != ':') $key = ":$key"; // prefix with colon if needed
 675                  if(strpos($sql, $key) === false) continue; // skip if parameter is missing
 676  
 677                  if(is_int($value)) {
 678                      $sth->bindValue($key, $value, PDO::PARAM_INT);
 679                  } else {
 680                      $sth->bindValue($key, $value);
 681                  }
 682                  $params[$key] = $value; //remember for debugging
 683              }
 684  
 685              $sth->execute();
 686              if(strtolower(substr($sql, 0, 6)) == 'select') {
 687                  $result = $sth->fetchAll();
 688              } else {
 689                  $result = $sth->rowCount();
 690              }
 691          } catch(Exception $e) {
 692              // report the caller's line
 693              $trace = debug_backtrace();
 694              $line = $trace[0]['line'];
 695              $dsql = $this->_debugSQL($sql, $params, !defined('DOKU_UNITTEST'));
 696              $this->_debug($e, -1, $line);
 697              $this->_debug("SQL: <pre>$dsql</pre>", -1, $line);
 698              $result = false;
 699          }
 700          $sth->closeCursor();
 701          $sth = null;
 702  
 703          return $result;
 704      }
 705  
 706      /**
 707       * Wrapper around msg() but outputs only when debug is enabled
 708       *
 709       * @param string|Exception $message
 710       * @param int $err
 711       * @param int $line
 712       */
 713      protected function _debug($message, $err = 0, $line = 0) {
 714          if(!$this->getConf('debug')) return;
 715          if(is_a($message, 'Exception')) {
 716              $err = -1;
 717              $msg = $message->getMessage();
 718              if(!$line) $line = $message->getLine();
 719          } else {
 720              $msg = $message;
 721          }
 722  
 723          if(defined('DOKU_UNITTEST')) {
 724              printf("\n%s, %s:%d\n", $msg, __FILE__, $line);
 725          } else {
 726              msg('authpdo: ' . $msg, $err, $line, __FILE__);
 727          }
 728      }
 729  
 730      /**
 731       * Check if the given config strings are set
 732       *
 733       * @author  Matthias Grimm <matthiasgrimm@users.sourceforge.net>
 734       *
 735       * @param   string[] $keys
 736       * @return  bool
 737       */
 738      protected function _chkcnf($keys) {
 739          foreach($keys as $key) {
 740              $params = explode(':', $key);
 741              $key = array_shift($params);
 742              $sql = trim($this->getConf($key));
 743  
 744              // check if sql is set
 745              if(!$sql) return false;
 746              // check if needed params are there
 747              foreach($params as $param) {
 748                  if(strpos($sql, ":$param") === false) return false;
 749              }
 750          }
 751  
 752          return true;
 753      }
 754  
 755      /**
 756       * create an approximation of the SQL string with parameters replaced
 757       *
 758       * @param string $sql
 759       * @param array $params
 760       * @param bool $htmlescape Should the result be escaped for output in HTML?
 761       * @return string
 762       */
 763      protected function _debugSQL($sql, $params, $htmlescape = true) {
 764          foreach($params as $key => $val) {
 765              if(is_int($val)) {
 766                  $val = $this->pdo->quote($val, PDO::PARAM_INT);
 767              } elseif(is_bool($val)) {
 768                  $val = $this->pdo->quote($val, PDO::PARAM_BOOL);
 769              } elseif(is_null($val)) {
 770                  $val = 'NULL';
 771              } else {
 772                  $val = $this->pdo->quote($val);
 773              }
 774              $sql = str_replace($key, $val, $sql);
 775          }
 776          if($htmlescape) $sql = hsc($sql);
 777          return $sql;
 778      }
 779  }
 780  
 781  // vim:ts=4:sw=4:et: