Overview

Packages

  • DataTree

Classes

  • Horde_DataTree
  • Horde_DataTree_Null
  • Horde_DataTree_Sql
  • Horde_DataTreeObject
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * The Horde_DataTree_Sql class provides an SQL implementation of the Horde
   4:  * DataTree system.
   5:  *
   6:  * Required parameters:<pre>
   7:  *   'phptype'      The database type (ie. 'pgsql', 'mysql', etc.).
   8:  *   'charset'      The charset used by the database.</pre>
   9:  *
  10:  * Optional parameters:<pre>
  11:  *   'table'        The name of the data table in 'database'.
  12:  *                  DEFAULT: 'horde_datatree'</pre>
  13:  *
  14:  * Required by some database implementations:<pre>
  15:  *   'database'     The name of the database.
  16:  *   'username'     The username with which to connect to the database.
  17:  *   'password'     The password associated with 'username'.
  18:  *   'hostspec'     The hostname of the database server.
  19:  *   'protocol'     The communication protocol ('tcp', 'unix', etc.).
  20:  *   'options'      Additional options to pass to the database.
  21:  *   'port'         The port on which to connect to the database.
  22:  *   'tty'          The TTY on which to connect to the database.</pre>
  23:  *
  24:  * Optional values when using separate reading and writing servers, for example
  25:  * in replication settings:<pre>
  26:  *   'splitread'   Boolean, whether to implement the separation or not.
  27:  *   'read'        Array containing the parameters which are different for
  28:  *                 the read database connection, currently supported
  29:  *                 only 'hostspec' and 'port' parameters.</pre>
  30:  *
  31:  * The table structure for the Horde_DataTree system is in
  32:  * scripts/sql/horde_datatree.sql.
  33:  *
  34:  * Copyright 1999-2012 Horde LLC (http://www.horde.org/)
  35:  *
  36:  * See the enclosed file COPYING for license information (LGPL). If
  37:  * you did not receive this file, see
  38:  * http://www.horde.org/licenses/lgpl21.
  39:  *
  40:  * @author  Stephane Huther <shuther1@free.fr>
  41:  * @author  Chuck Hagenbuch <chuck@horde.org>
  42:  * @author  Jan Schneider <jan@horde.org>
  43:  * @package DataTree
  44:  */
  45: class Horde_DataTree_Sql extends Horde_DataTree {
  46: 
  47:     /**
  48:      * Handle for the current database connection, used for reading.
  49:      *
  50:      * @var DB
  51:      */
  52:     var $_db;
  53: 
  54:     /**
  55:      * Handle for the current database connection, used for writing. Defaults
  56:      * to the same handle as $_db if a separate write database is not required.
  57:      *
  58:      * @var DB
  59:      */
  60:     var $_write_db;
  61: 
  62:     /**
  63:      * The number of copies of the horde_datatree_attributes table
  64:      * that we need to join on in the current query.
  65:      *
  66:      * @var integer
  67:      */
  68:     var $_tableCount = 1;
  69: 
  70:     /**
  71:      * Returns a list of all groups (root nodes) of the data tree.
  72:      *
  73:      * @return array  The the group IDs
  74:      */
  75:     function getGroups()
  76:     {
  77:         $query = 'SELECT DISTINCT group_uid FROM ' .  $this->_params['table'];
  78: 
  79:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::getGroups(): ' . $query, 'DEBUG');
  80: 
  81:         return $this->_db->getCol($query);
  82:     }
  83: 
  84:     /**
  85:      * Loads (a subset of) the datatree into the $_data array.
  86:      *
  87:      * @access private
  88:      *
  89:      * @param string  $root         Which portion of the tree to load.
  90:      *                              Defaults to all of it.
  91:      * @param boolean $loadTree     Load a tree starting at $root, or just the
  92:      *                              requested level and direct parents?
  93:      *                              Defaults to single level.
  94:      * @param boolean $reload       Re-load already loaded values?
  95:      * @param string  $sortby_name  Attribute name to use for sorting.
  96:      * @param string  $sortby_key   Attribute key to use for sorting.
  97:      * @param integer $direction    Sort direction:
  98:      *                              0 - ascending
  99:      *                              1 - descending
 100:      *
 101:      * @return mixed  True on success or a PEAR_Error on failure.
 102:      */
 103:     function _load($root = DATATREE_ROOT, $loadTree = false, $reload = false,
 104:                    $sortby_name = null, $sortby_key = null, $direction = 0)
 105:     {
 106:         /* Do NOT use Horde_DataTree::exists() here; that would cause an infinite
 107:          * loop. */
 108:         if (!$reload &&
 109:             (in_array($root, $this->_nameMap) ||
 110:              (count($this->_data) && $root == DATATREE_ROOT)) ||
 111:             (!is_null($this->_sortHash) &&
 112:              isset($this->_data[$root]['sorter'][$this->_sortHash]))) {
 113:             return true;
 114:         }
 115: 
 116:         $query = $this->_buildLoadQuery($root,
 117:                                         $loadTree,
 118:                                         DATATREE_BUILD_SELECT,
 119:                                         $sortby_name,
 120:                                         $sortby_key,
 121:                                         $direction);
 122:         if (empty($query)) {
 123:             return true;
 124:         }
 125: 
 126:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::_load(): ' . $query, 'DEBUG');
 127:         $data = $this->_db->getAll($query);
 128:         if (is_a($data, 'PEAR_Error')) {
 129:             return $data;
 130:         }
 131:         return $this->set($data, $this->_params['charset']);
 132:     }
 133: 
 134:     /**
 135:      * Counts (a subset of) the datatree which would be loaded into the $_data
 136:      * array if _load() is called with the same value of $root.
 137:      *
 138:      * @access private
 139:      *
 140:      * @param string $root  Which portion of the tree to load. Defaults to all
 141:      *                      of it.
 142:      *
 143:      * @return integer  Number of objects
 144:      */
 145:     function _count($root = DATATREE_ROOT)
 146:     {
 147:         $query = $this->_buildLoadQuery($root, true, DATATREE_BUILD_COUNT);
 148:         if (empty($query)) {
 149:             return 0;
 150:         }
 151:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::_count(): ' . $query, 'DEBUG');
 152:         return (int)$this->_db->getOne($query);
 153:     }
 154: 
 155:     /**
 156:      * Loads (a subset of) the datatree into the $_data array.
 157:      *
 158:      * @access private
 159:      *
 160:      * @param string  $root         Which portion of the tree to load.
 161:      *                              Defaults to all of it.
 162:      * @param boolean $loadTree     Load a tree starting at $root, or just the
 163:      *                              requested level and direct parents?
 164:      *                              Defaults to single level.
 165:      * @param integer $operation    Type of query to build
 166:      * @param string  $sortby_name  Attribute name to use for sorting.
 167:      * @param string  $sortby_key   Attribute key to use for sorting.
 168:      * @param integer $direction    Sort direction:
 169:      *                              0 - ascending
 170:      *                              1 - descending
 171:      *
 172:      * @return mixed  True on success or a PEAR_Error on failure.
 173:      */
 174:     function _buildLoadQuery($root = DATATREE_ROOT, $loadTree = false,
 175:                              $operation = DATATREE_BUILD_SELECT,
 176:                              $sortby_name = null, $sortby_key = null,
 177:                              $direction = 0)
 178:     {
 179:         $sorted = false;
 180:         $where = sprintf('c.group_uid = %s ', $this->_db->quote($this->_params['group']));
 181: 
 182:         if (!empty($root) && $root != DATATREE_ROOT) {
 183:             $parent_where = $this->_buildParentIds($root, $loadTree, 'c.');
 184:             if (empty($parent_where)) {
 185:                 return '';
 186:             } elseif (!is_a($parent_where, 'PEAR_Error')) {
 187:                 $where = sprintf('%s AND (%s)', $where, $parent_where);
 188:             }
 189:         }
 190:         if (!is_null($sortby_name)) {
 191:             $where = sprintf('%s AND a.attribute_name = %s ', $where, $this->_db->quote($sortby_name));
 192:             $sorted = true;
 193:         }
 194:         if (!is_null($sortby_key)) {
 195:             $where = sprintf('%s AND a.attribute_key = %s ', $where, $this->_db->quote($sortby_key));
 196:             $sorted = true;
 197:         }
 198: 
 199:         switch ($operation) {
 200:         case DATATREE_BUILD_COUNT:
 201:             $what = 'COUNT(*)';
 202:             break;
 203: 
 204:         default:
 205:             $what = 'c.datatree_id, c.datatree_name, c.datatree_parents, c.datatree_order';
 206:             break;
 207:         }
 208: 
 209:         if ($sorted) {
 210:             $query = sprintf('SELECT %s FROM %s c LEFT JOIN %s a ON (c.datatree_id = a.datatree_id OR c.datatree_name=%s) '.
 211:                              'WHERE %s GROUP BY c.datatree_id, c.datatree_name, c.datatree_parents, c.datatree_order ORDER BY a.attribute_value %s',
 212:                              $what,
 213:                              $this->_params['table'],
 214:                              $this->_params['table_attributes'],
 215:                              $this->_db->quote($root),
 216:                              $where,
 217:                              ($direction == 1) ? 'DESC' : 'ASC');
 218:         } else {
 219:             $query = sprintf('SELECT %s FROM %s c WHERE %s',
 220:                              $what,
 221:                              $this->_params['table'],
 222:                              $where);
 223:         }
 224: 
 225:         return $query;
 226:     }
 227: 
 228:     /**
 229:      * Builds parent ID string for selecting trees.
 230:      *
 231:      * @access private
 232:      *
 233:      * @param string  $root      Which portion of the tree to load.
 234:      * @param boolean $loadTree  Load a tree starting at $root, or just the
 235:      *                           requested level and direct parents?
 236:      *                           Defaults to single level.
 237:      * @param string  $join_name Table join name
 238:      *
 239:      * @return string  Id list.
 240:      */
 241:     function _buildParentIds($root, $loadTree = false, $join_name = '')
 242:     {
 243:         if (strpos($root, ':') !== false) {
 244:             $parts = explode(':', $root);
 245:             $root = array_pop($parts);
 246:         }
 247:         $root = (string)$root;
 248: 
 249:         $query = 'SELECT datatree_id, datatree_parents' .
 250:             ' FROM ' . $this->_params['table'] .
 251:             ' WHERE datatree_name = ? AND group_uid = ?' .
 252:             ' ORDER BY datatree_id';
 253:         $values = array($root,
 254:                         $this->_params['group']);
 255: 
 256:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::_buildParentIds(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 257:         $root = $this->_db->getAssoc($query, false, $values);
 258:         if (is_a($root, 'PEAR_Error') || !count($root)) {
 259:             return '';
 260:         }
 261: 
 262:         $where = '';
 263:         $first_time = true;
 264:         foreach ($root as $object_id => $object_parents) {
 265:             $pstring = $object_parents . ':' . $object_id . '%';
 266:             $pquery = '';
 267:             if (!empty($object_parents)) {
 268:                 $ids = substr($object_parents, 1);
 269:                 $pquery = ' OR ' . $join_name . 'datatree_id IN (' . str_replace(':', ', ', $ids) . ')';
 270:             }
 271:             if ($loadTree) {
 272:                 $pquery .= ' OR ' . $join_name . 'datatree_parents = ' . $this->_db->quote(substr($pstring, 0, -1));
 273:             }
 274: 
 275:             if (!$first_time) {
 276:                 $where .= ' OR ';
 277:             }
 278:             $where .= sprintf($join_name . 'datatree_parents LIKE %s OR ' . $join_name . 'datatree_id = %s%s',
 279:                               $this->_db->quote($pstring),
 280:                               $object_id,
 281:                               $pquery);
 282: 
 283:             $first_time = false;
 284:         }
 285: 
 286:         return $where;
 287:     }
 288: 
 289:     /**
 290:      * Loads a set of objects identified by their unique IDs, and their
 291:      * parents, into the $_data array.
 292:      *
 293:      * @access private
 294:      *
 295:      * @param mixed $cids  The unique ID of the object to load, or an array of
 296:      *                     object ids.
 297:      *
 298:      * @return mixed  True on success or a PEAR_Error on failure.
 299:      */
 300:     function _loadById($cids)
 301:     {
 302:         /* Make sure we have an array. */
 303:         if (!is_array($cids)) {
 304:             $cids = array((int)$cids);
 305:         } else {
 306:             array_walk($cids, 'intval');
 307:         }
 308: 
 309:         /* Bail out now if there's nothing to load. */
 310:         if (!count($cids)) {
 311:             return true;
 312:         }
 313: 
 314:         /* Don't load any that are already loaded. Also, make sure that
 315:          * everything in the $ids array that we are building is an integer. */
 316:         $ids = array();
 317:         foreach ($cids as $cid) {
 318:             /* Do NOT use Horde_DataTree::exists() here; that would cause an
 319:              * infinite loop. */
 320:             if (!isset($this->_data[$cid])) {
 321:                 $ids[] = (int)$cid;
 322:             }
 323:         }
 324: 
 325:         /* If there are none left to load, return. */
 326:         if (!count($ids)) {
 327:             return true;
 328:         }
 329: 
 330:         $in = array_search(DATATREE_ROOT, $ids) === false ? sprintf('datatree_id IN (%s) AND ', implode(', ', $ids)) : '';
 331:         $query = sprintf('SELECT datatree_id, datatree_parents FROM %s' .
 332:                          ' WHERE %sgroup_uid = %s' .
 333:                          ' ORDER BY datatree_id',
 334:                          $this->_params['table'],
 335:                          $in,
 336:                          $this->_db->quote($this->_params['group']));
 337:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::_loadById(): ' . $query, 'DEBUG');
 338:         $parents = $this->_db->getAssoc($query);
 339:         if (is_a($parents, 'PEAR_Error')) {
 340:             return $parents;
 341:         }
 342:         if (empty($parents)) {
 343:             return PEAR::raiseError('Object not found.', null, null, null, 'Horde_DataTree ids ' . implode(', ', $ids) . ' not found.');
 344:         }
 345: 
 346:         $ids = array();
 347:         foreach ($parents as $cid => $parent) {
 348:             $ids[(int)$cid] = (int)$cid;
 349: 
 350:             $pids = explode(':', substr($parent, 1));
 351:             foreach ($pids as $pid) {
 352:                 $pid = (int)$pid;
 353:                 if (!isset($this->_data[$pid])) {
 354:                     $ids[$pid] = $pid;
 355:                 }
 356:             }
 357:         }
 358: 
 359:         /* If $ids is empty, we have nothing to load. */
 360:         if (!count($ids)) {
 361:             return true;
 362:         }
 363: 
 364:         $query = 'SELECT datatree_id, datatree_name, datatree_parents, datatree_order' .
 365:                  ' FROM ' . $this->_params['table'] .
 366:                  ' WHERE datatree_id IN (?' . str_repeat(', ?', count($ids) - 1) . ')' .
 367:                  ' AND group_uid = ? ORDER BY datatree_id';
 368:         $values = array_merge($ids, array($this->_params['group']));
 369: 
 370:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::_loadById(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 371:         $data = $this->_db->getAll($query, $values);
 372:         if (is_a($data, 'PEAR_Error')) {
 373:             return $data;
 374:         }
 375: 
 376:         return $this->set($data, $this->_params['charset']);
 377:     }
 378: 
 379:     /**
 380:      * Check for existance of an object in a backend-specific manner.
 381:      *
 382:      * @param string $object_name Object name to check for.
 383:      *
 384:      * @return boolean True if the object exists, false otherwise.
 385:      */
 386:     function _exists($object_name)
 387:     {
 388:         $query = 'SELECT datatree_id FROM ' . $this->_params['table'] .
 389:             ' WHERE group_uid = ? AND datatree_name = ? AND datatree_parents = ?';
 390: 
 391:         $object_name = Horde_String::convertCharset($object_name, 'UTF-8', $this->_params['charset']);
 392:         $object_names = explode(':', $object_name);
 393:         $object_parents = '';
 394:         foreach ($object_names as $name) {
 395:             $values = array($this->_params['group'], $name, $object_parents);
 396:             Horde::logMessage('SQL Query by Horde_DataTree_Sql::_exists(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 397: 
 398:             $result = $this->_db->getOne($query, $values);
 399:             if (is_a($result, 'PEAR_Error') || !$result) {
 400:                 return false;
 401:             }
 402: 
 403:             $object_parents .= ':' . $result;
 404:         }
 405: 
 406:         return true;
 407:     }
 408: 
 409:     /**
 410:      * Look up a datatree id by name.
 411:      *
 412:      * @param string $name
 413:      *
 414:      * @return integer Horde_DataTree id
 415:      */
 416:     function _getId($name)
 417:     {
 418:         $query = 'SELECT datatree_id FROM ' . $this->_params['table']
 419:             . ' WHERE group_uid = ? AND datatree_name = ?'
 420:             . ' AND datatree_parents = ?';
 421: 
 422:         $ids = array();
 423:         $name = Horde_String::convertCharset($name, 'UTF-8', $this->_params['charset']);
 424:         $parts = explode(':', $name);
 425:         foreach ($parts as $part) {
 426:             $result = $this->_db->getOne($query, array($this->_params['group'], $part, count($ids) ? ':' . implode(':', $ids) : ''));
 427:             if (is_a($result, 'PEAR_Error') || !$result) {
 428:                 return null;
 429:             } else {
 430:                 $ids[] = $result;
 431:             }
 432:         }
 433: 
 434:         return (int)array_pop($ids);
 435:     }
 436: 
 437:     /**
 438:      * Look up a datatree name by id.
 439:      *
 440:      * @param integer $id
 441:      *
 442:      * @return string Horde_DataTree name
 443:      */
 444:     function _getName($id)
 445:     {
 446:         $query = 'SELECT datatree_name FROM ' . $this->_params['table'] .
 447:             ' WHERE group_uid = ? AND datatree_id = ?';
 448:         $values = array($this->_params['group'], (int)$id);
 449:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::_getName(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 450: 
 451:         $name = $this->_db->getOne($query, $values);
 452:         if (is_a($name, 'PEAR_Error')) {
 453:             return null;
 454:         } else {
 455:             $name = Horde_String::convertCharset($name, $this->_params['charset'], 'UTF-8');
 456:             // Get the parent names, if any.
 457:             $parent = $this->getParentById($id);
 458:             if ($parent && !is_a($parent, 'PEAR_Error') &&
 459:                 $parent != DATATREE_ROOT) {
 460:                 return $this->getName($parent) . ':' . $name;
 461:             } else {
 462:                 return $name;
 463:             }
 464:         }
 465:     }
 466: 
 467:     /**
 468:      * Returns a tree sorted by the specified attribute name and/or key.
 469:      *
 470:      * @param string $root         Which portion of the tree to sort.
 471:      *                             Defaults to all of it.
 472:      * @param boolean $loadTree    Sort the tree starting at $root, or just the
 473:      *                             requested level and direct parents?
 474:      *                             Defaults to single level.
 475:      * @param string $sortby_name  Attribute name to use for sorting.
 476:      * @param string $sortby_key   Attribute key to use for sorting.
 477:      * @param integer $direction   Sort direction:
 478:      *                             0 - ascending
 479:      *                             1 - descending
 480:      *
 481:      * @return array TODO
 482:      */
 483:     function getSortedTree($root, $loadTree = false, $sortby_name = null,
 484:                            $sortby_key = null, $direction = 0)
 485:     {
 486:         $query = $this->_buildLoadQuery($root,
 487:                                         $loadTree,
 488:                                         DATATREE_BUILD_SELECT,
 489:                                         $sortby_name,
 490:                                         $sortby_key,
 491:                                         $direction);
 492: 
 493:         if (empty($query)) {
 494:             return array();
 495:         }
 496:         return $this->_db->getAll($query);
 497:     }
 498: 
 499:     /**
 500:      * Adds an object.
 501:      *
 502:      * @param mixed $object        The object to add (string or
 503:      *                             Horde_DataTreeObject).
 504:      * @param boolean $id_as_name  Whether the object ID is to be used as
 505:      *                             object name.  Used in situations where
 506:      *                             there is no available unique input for
 507:      *                             object name.
 508:      */
 509:     function add($object, $id_as_name = false)
 510:     {
 511:         $attributes = false;
 512:         if (is_a($object, 'Horde_DataTreeObject')) {
 513:             $fullname = $object->getName();
 514:             $order = $object->order;
 515: 
 516:             /* We handle data differently if we can map it to the
 517:              * horde_datatree_attributes table. */
 518:             if (method_exists($object, '_toAttributes')) {
 519:                 $data = '';
 520:                 $ser = null;
 521: 
 522:                 /* Set a flag for later so that we know to insert the
 523:                  * attribute rows. */
 524:                 $attributes = true;
 525:             } else {
 526:                 $ser = Horde_Serialize::UTF7_BASIC;
 527:                 $data = Horde_Serialize::serialize($object->getData(), $ser, 'UTF-8');
 528:             }
 529:         } else {
 530:             $fullname = $object;
 531:             $order = null;
 532:             $data = '';
 533:             $ser = null;
 534:         }
 535: 
 536:         /* Get the next unique ID. */
 537:         $id = $this->_write_db->nextId($this->_params['table']);
 538:         if (is_a($id, 'PEAR_Error')) {
 539:             Horde::logMessage($id, 'ERR');
 540:             return $id;
 541:         }
 542: 
 543:         if (strpos($fullname, ':') !== false) {
 544:             $parts = explode(':', $fullname);
 545:             $parents = '';
 546:             $pstring = '';
 547:             if ($id_as_name) {
 548:                 /* Requested use of ID as name, so discard current name. */
 549:                 array_pop($parts);
 550:                 /* Set name to ID. */
 551:                 $name = $id;
 552:                 /* Modify fullname to reflect new name. */
 553:                 $fullname = implode(':', $parts) . ':' . $id;
 554:                 if (is_a($object, 'Horde_DataTreeObject')) {
 555:                     $object->setName($fullname);
 556:                 } else {
 557:                     $object = $fullname;
 558:                 }
 559:             } else {
 560:                 $name = array_pop($parts);
 561:             }
 562:             foreach ($parts as $par) {
 563:                 $pstring .= (empty($pstring) ? '' : ':') . $par;
 564:                 $pid = $this->getId($pstring);
 565:                 if (is_a($pid, 'PEAR_Error')) {
 566:                     /* Auto-create parents. */
 567:                     $pid = $this->add($pstring);
 568:                     if (is_a($pid, 'PEAR_Error')) {
 569:                         return $pid;
 570:                     }
 571:                 }
 572:                 $parents .= ':' . $pid;
 573:             }
 574:         } else {
 575:             if ($id_as_name) {
 576:                 /* Requested use of ID as name, set fullname and name to ID. */
 577:                 $fullname = $id;
 578:                 $name = $id;
 579:                 if (is_a($object, 'Horde_DataTreeObject')) {
 580:                     $object->setName($fullname);
 581:                 } else {
 582:                     $object = $fullname;
 583:                 }
 584:             } else {
 585:                 $name = $fullname;
 586:             }
 587:             $parents = '';
 588:             $pid = DATATREE_ROOT;
 589:         }
 590: 
 591:         if (parent::exists($fullname)) {
 592:             return PEAR::raiseError(sprintf('"%s" already exists', $fullname));
 593:         }
 594: 
 595:         $query = 'INSERT INTO ' . $this->_params['table'] .
 596:                  ' (datatree_id, group_uid, datatree_name, datatree_order,' .
 597:                  ' datatree_data, user_uid, datatree_serialized,' .
 598:                  ' datatree_parents)' .
 599:                  ' VALUES (?, ?, ?, ?, ?, ?, ?, ?)';
 600:         $values = array((int)$id,
 601:                         $this->_params['group'],
 602:                         Horde_String::convertCharset($name, 'UTF-8', $this->_params['charset']),
 603:                         is_null($order) ? NULL : (int)$order,
 604:                         $data,
 605:                         (string)$GLOBALS['registry']->getAuth(),
 606:                         (int)$ser,
 607:                         $parents);
 608: 
 609:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::add(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 610:         $result = $this->_write_db->query($query, $values);
 611:         if (is_a($result, 'PEAR_Error')) {
 612:             Horde::logMessage($result, 'ERR');
 613:             return $result;
 614:         }
 615: 
 616:         $reorder = $this->reorder($parents, $order, $id);
 617:         if (is_a($reorder, 'PEAR_Error')) {
 618:             Horde::logMessage($reorder, 'ERR');
 619:             return $reorder;
 620:         }
 621: 
 622:         $result = parent::_add($fullname, $id, $pid, $order);
 623:         if (is_a($result, 'PEAR_Error')) {
 624:             return $result;
 625:         }
 626: 
 627:         /* If we succesfully inserted the object and it supports
 628:          * being mapped to the attributes table, do that now: */
 629:         if (!empty($attributes)) {
 630:             $result = $this->updateData($object);
 631:             if (is_a($result, 'PEAR_Error')) {
 632:                 return $result;
 633:             }
 634:         }
 635: 
 636:         return $id;
 637:     }
 638: 
 639:     /**
 640:      * Changes the order of the children of an object.
 641:      *
 642:      * @param string $parent  The full id path of the parent object.
 643:      * @param mixed $order    If an array it specifies the new positions for
 644:      *                        all child objects.
 645:      *                        If an integer and $cid is specified, the position
 646:      *                        where the child specified by $cid is inserted. If
 647:      *                        $cid is not specified, the position gets deleted,
 648:      *                        causing the following positions to shift up.
 649:      * @param integer $cid    See $order.
 650:      */
 651:     function reorder($parent, $order = null, $cid = null)
 652:     {
 653:         if (!$parent || is_a($parent, 'PEAR_Error')) {
 654:             // Abort immediately if the parent string is empty; we
 655:             // cannot safely reorder all top-level elements.
 656:             return;
 657:         }
 658: 
 659:         $pquery = '';
 660:         if (!is_array($order) && !is_null($order)) {
 661:             /* Single update (add/del). */
 662:             if (is_null($cid)) {
 663:                 /* No object id given so shuffle down. */
 664:                 $direction = '-';
 665:             } else {
 666:                 /* We have an object id so shuffle up. */
 667:                 $direction = '+';
 668: 
 669:                 /* Leaving the newly inserted object alone. */
 670:                 $pquery = sprintf(' AND datatree_id != %s', (int)$cid);
 671:             }
 672:             $query = sprintf('UPDATE %s SET datatree_order = datatree_order %s 1 WHERE group_uid = %s AND datatree_parents = %s AND datatree_order >= %s',
 673:                              $this->_params['table'],
 674:                              $direction,
 675:                              $this->_write_db->quote($this->_params['group']),
 676:                              $this->_write_db->quote($parent),
 677:                              is_null($order) ? 'NULL' : (int)$order) . $pquery;
 678: 
 679:             Horde::logMessage('SQL Query by Horde_DataTree_Sql::reorder(): ' . $query, 'DEBUG');
 680:             $result = $this->_write_db->query($query);
 681:         } elseif (is_array($order)) {
 682:             /* Multi update. */
 683:             $query = 'SELECT COUNT(datatree_id)' .
 684:                      ' FROM ' . $this->_params['table'] .
 685:                      ' WHERE group_uid = ? AND datatree_parents = ?' .
 686:                      ' GROUP BY datatree_parents';
 687:             $values = array($this->_params['group'],
 688:                             $parent);
 689: 
 690:             Horde::logMessage('SQL Query by Horde_DataTree_Sql::reorder(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 691: 
 692:             $result = $this->_db->getOne($query, $values);
 693:             if (is_a($result, 'PEAR_Error')) {
 694:                 return $result;
 695:             } elseif (count($order) != $result) {
 696:                 return PEAR::raiseError('Cannot reorder, number of entries supplied for reorder does not match number stored.');
 697:             }
 698: 
 699:             $o_key = 0;
 700:             foreach ($order as $o_cid) {
 701:                 $query = 'UPDATE ' . $this->_params['table'] .
 702:                          ' SET datatree_order = ? WHERE datatree_id = ?';
 703:                 $values = array($o_key, is_null($o_cid) ? NULL : (int)$o_cid);
 704: 
 705:                 Horde::logMessage('SQL Query by Horde_DataTree_Sql::reorder(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 706:                 $result = $this->_write_db->query($query, $values);
 707:                 if (is_a($result, 'PEAR_Error')) {
 708:                     return $result;
 709:                 }
 710: 
 711:                 $o_key++;
 712:             }
 713: 
 714:             $pid = $this->getId($parent);
 715: 
 716:             /* Re-order our cache. */
 717:             return $this->_reorder($pid, $order);
 718:         }
 719:     }
 720: 
 721:     /**
 722:      * Explicitly set the order for a datatree object.
 723:      *
 724:      * @param integer $id     The datatree object id to change.
 725:      * @param integer $order  The new order.
 726:      */
 727:     function setOrder($id, $order)
 728:     {
 729:         $query = 'UPDATE ' . $this->_params['table'] .
 730:                  ' SET datatree_order = ? WHERE datatree_id = ?';
 731:         $values = array(is_null($order) ? NULL : (int)$order,
 732:                         (int)$id);
 733: 
 734:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::setOrder(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 735:         return $this->_write_db->query($query, $values);
 736:     }
 737: 
 738:     /**
 739:      * Removes an object.
 740:      *
 741:      * @param mixed   $object  The object to remove.
 742:      * @param boolean $force   Force removal of every child object?
 743:      */
 744:     function remove($object, $force = false)
 745:     {
 746:         $id = $this->getId($object);
 747:         if (is_a($id, 'PEAR_Error')) {
 748:             return $id;
 749:         }
 750:         $order = $this->getOrder($object);
 751: 
 752:         $query = 'SELECT datatree_id FROM ' . $this->_params['table'] .
 753:                  ' WHERE group_uid = ? AND datatree_parents LIKE ?' .
 754:                  ' ORDER BY datatree_id';
 755:         $values = array($this->_params['group'],
 756:                         '%:' . (int)$id . '');
 757: 
 758:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::remove(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 759:         $children = $this->_db->getAll($query, $values, DB_FETCHMODE_ASSOC);
 760: 
 761:         if (count($children)) {
 762:             if ($force) {
 763:                 foreach ($children as $child) {
 764:                     $cat = $this->getName($child['datatree_id']);
 765:                     $result = $this->remove($cat, true);
 766:                     if (is_a($result, 'PEAR_Error')) {
 767:                         return $result;
 768:                     }
 769:                 }
 770:             } else {
 771:                 return PEAR::raiseError(sprintf('Cannot remove, %d children exist.', count($children)));
 772:             }
 773:         }
 774: 
 775:         /* Remove attributes for this object. */
 776:         $query = 'DELETE FROM ' . $this->_params['table_attributes'] .
 777:                  ' WHERE datatree_id = ?';
 778:         $values = array((int)$id);
 779: 
 780:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::remove(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 781:         $result = $this->_write_db->query($query, $values);
 782:         if (is_a($result, 'PEAR_Error')) {
 783:             return $result;
 784:         }
 785: 
 786:         $query = 'DELETE FROM ' . $this->_params['table'] .
 787:                  ' WHERE datatree_id = ?';
 788:         $values = array((int)$id);
 789: 
 790:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::remove(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 791:         $result = $this->_write_db->query($query, $values);
 792:         if (is_a($result, 'PEAR_Error')) {
 793:             return $result;
 794:         }
 795: 
 796:         $parents = $this->getParentIdString($object);
 797:         if (is_a($parents, 'PEAR_Error')) {
 798:             return $parents;
 799:         }
 800: 
 801:         $reorder = $this->reorder($parents, $order);
 802:         if (is_a($reorder, 'PEAR_Error')) {
 803:             return $reorder;
 804:         }
 805: 
 806:         return is_a(parent::remove($object), 'PEAR_Error') ? $id : true;
 807:     }
 808: 
 809:     /**
 810:      * Removes one or more objects by id.
 811:      *
 812:      * This function does *not* do the validation, reordering, etc. that
 813:      * remove() does. If you need to check for children, re-do ordering, etc.,
 814:      * then you must remove() objects one-by-one. This is for code that knows
 815:      * it's dealing with single (non-parented) objects and needs to delete a
 816:      * batch of them quickly.
 817:      *
 818:      * @param array $ids  The objects to remove.
 819:      */
 820:     function removeByIds($ids)
 821:     {
 822:         /* Sanitize input. */
 823:         if (!is_array($ids)) {
 824:             $ids = array((int)$ids);
 825:         } else {
 826:             array_walk($ids, 'intval');
 827:         }
 828: 
 829:         /* Removing zero objects always succeeds. */
 830:         if (!$ids) {
 831:             return true;
 832:         }
 833: 
 834:         /* Remove attributes for $ids. */
 835:         $query = 'DELETE FROM ' . $this->_params['table_attributes'] .
 836:                  ' WHERE datatree_id IN (?' . str_repeat(', ?', count($ids) - 1) . ')';
 837:         $values = $ids;
 838: 
 839:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::removeByIds(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 840:         $result = $this->_write_db->query($query, $values);
 841:         if (is_a($result, 'PEAR_Error')) {
 842:             return $result;
 843:         }
 844: 
 845:         $query = 'DELETE FROM ' . $this->_params['table'] .
 846:                  ' WHERE datatree_id IN (?' . str_repeat(', ?', count($ids) - 1) . ')';
 847:         $values = $ids;
 848: 
 849:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::removeByIds(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 850:         return $this->_write_db->query($query, $values);
 851:     }
 852: 
 853:     /**
 854:      * Removes one or more objects by name.
 855:      *
 856:      * This function does *not* do the validation, reordering, etc. that
 857:      * remove() does. If you need to check for children, re-do ordering, etc.,
 858:      * then you must remove() objects one-by-one. This is for code that knows
 859:      * it's dealing with single (non-parented) objects and needs to delete a
 860:      * batch of them quickly.
 861:      *
 862:      * @param array $names  The objects to remove.
 863:      */
 864:     function removeByNames($names)
 865:     {
 866:         if (!is_array($names)) {
 867:             $names = array($names);
 868:         }
 869: 
 870:         /* Removing zero objects always succeeds. */
 871:         if (!$names) {
 872:             return true;
 873:         }
 874: 
 875:         $query = 'SELECT datatree_id FROM ' . $this->_params['table'] .
 876:                  ' WHERE datatree_name IN (?' . str_repeat(', ?', count($names) - 1) . ')';
 877:         $values = $names;
 878: 
 879:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::removeByNames(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 880:         $ids = $this->_db->getCol($query, 0, $values);
 881:         if (is_a($ids, 'PEAR_Error')) {
 882:             return $ids;
 883:         }
 884: 
 885:         return $this->removeByIds($ids);
 886:     }
 887: 
 888:     /**
 889:      * Move an object to a new parent.
 890:      *
 891:      * @param mixed  $object     The object to move.
 892:      * @param string $newparent  The new parent object. Defaults to the root.
 893:      */
 894:     function move($object, $newparent = null)
 895:     {
 896:         $old_parent_path = $this->getParentIdString($object);
 897:         $result = parent::move($object, $newparent);
 898:         if (is_a($result, 'PEAR_Error')) {
 899:             return $result;
 900:         }
 901:         $id = $this->getId($object);
 902:         $new_parent_path = $this->getParentIdString($object);
 903: 
 904:         /* Fetch the object being moved and all of its children, since
 905:          * we also need to update their parent paths to avoid creating
 906:          * orphans. */
 907:         $query = 'SELECT datatree_id, datatree_parents' .
 908:                  ' FROM ' . $this->_params['table'] .
 909:                  ' WHERE datatree_parents = ? OR datatree_parents LIKE ?' .
 910:                  ' OR datatree_id = ?';
 911:         $values = array($old_parent_path . ':' . $id,
 912:                         $old_parent_path . ':' . $id . ':%',
 913:                         (int)$id);
 914: 
 915:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::move(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 916:         $rowset = $this->_db->query($query, $values);
 917:         if (is_a($rowset, 'PEAR_Error')) {
 918:             return $rowset;
 919:         }
 920: 
 921:         /* Update each object, replacing the old parent path with the
 922:          * new one. */
 923:         while ($row = $rowset->fetchRow(DB_FETCHMODE_ASSOC)) {
 924:             if (is_a($row, 'PEAR_Error')) {
 925:                 return $row;
 926:             }
 927: 
 928:             $oquery = '';
 929:             if ($row['datatree_id'] == $id) {
 930:                 $oquery = ', datatree_order = 0 ';
 931:             }
 932: 
 933:             /* Do str_replace() only if this is not a first level
 934:              * object. */
 935:             if (!empty($row['datatree_parents'])) {
 936:                 $ppath = str_replace($old_parent_path, $new_parent_path, $row['datatree_parents']);
 937:             } else {
 938:                 $ppath = $new_parent_path;
 939:             }
 940:             $query = sprintf('UPDATE %s SET datatree_parents = %s' . $oquery . ' WHERE datatree_id = %s',
 941:                              $this->_params['table'],
 942:                              $this->_write_db->quote($ppath),
 943:                              (int)$row['datatree_id']);
 944: 
 945:             Horde::logMessage('SQL Query by Horde_DataTree_Sql::move(): ' . $query, 'DEBUG');
 946:             $result = $this->_write_db->query($query);
 947:             if (is_a($result, 'PEAR_Error')) {
 948:                 return $result;
 949:             }
 950:         }
 951: 
 952:         $order = $this->getOrder($object);
 953: 
 954:         /* Shuffle down the old order positions. */
 955:         $reorder = $this->reorder($old_parent_path, $order);
 956: 
 957:         /* Shuffle up the new order positions. */
 958:         $reorder = $this->reorder($new_parent_path, 0, $id);
 959: 
 960:         return true;
 961:     }
 962: 
 963:     /**
 964:      * Change an object's name.
 965:      *
 966:      * @param mixed  $old_object       The old object.
 967:      * @param string $new_object_name  The new object name.
 968:      */
 969:     function rename($old_object, $new_object_name)
 970:     {
 971:         /* Do the cache renaming first */
 972:         $result = parent::rename($old_object, $new_object_name);
 973:         if (is_a($result, 'PEAR_Error')) {
 974:             return $result;
 975:         }
 976: 
 977:         /* Get the object id and set up the sql query. */
 978:         $id = $this->getId($old_object);
 979:         $query = 'UPDATE ' . $this->_params['table'] .
 980:                  ' SET datatree_name = ? WHERE datatree_id = ?';
 981:         $values = array(Horde_String::convertCharset($new_object_name, 'UTF-8', $this->_params['charset']),
 982:                         (int)$id);
 983: 
 984:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::rename(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
 985:         $result = $this->_write_db->query($query, $values);
 986: 
 987:         return is_a($result, 'PEAR_Error') ? $result : true;
 988:     }
 989: 
 990:     /**
 991:      * Retrieves data for an object from the datatree_data field.
 992:      *
 993:      * @param integer $cid  The object id to fetch, or an array of object ids.
 994:      */
 995:     function getData($cid)
 996:     {
 997:         if (is_array($cid)) {
 998:             if (!count($cid)) {
 999:                 return array();
1000:             }
1001: 
1002:             $query = sprintf('SELECT datatree_id, datatree_data, datatree_serialized FROM %s WHERE datatree_id IN (%s)',
1003:                              $this->_params['table'],
1004:                              implode(', ', $cid));
1005: 
1006:             Horde::logMessage('SQL Query by Horde_DataTree_Sql::getData(): ' . $query, 'DEBUG');
1007:             $result = $this->_db->getAssoc($query);
1008:             if (is_a($result, 'PEAR_Error')) {
1009:                 Horde::logMessage($result, 'ERR');
1010:                 return $result;
1011:             }
1012: 
1013:             $data = array();
1014:             foreach ($result as $id => $row) {
1015:                 $data[$id] = Horde_Serialize::unserialize($row[0], $row[1],
1016:                                                           'UTF-8');
1017:                 /* Convert old data to the new format. */
1018:                 if ($row[1] == Horde_Serialize::BASIC) {
1019:                     $data[$id] = Horde_String::convertCharset($data[$id],
1020:                                                               $GLOBALS['registry']->getLanguageCharset(),
1021:                                                               'UTF-8');
1022:                 }
1023: 
1024:                 $data[$id] = (is_null($data[$id]) || !is_array($data[$id]))
1025:                     ? array()
1026:                     : $data[$id];
1027:             }
1028: 
1029:             return $data;
1030:         } else {
1031:             $query = 'SELECT datatree_data, datatree_serialized' .
1032:                      ' FROM ' . $this->_params['table'] .
1033:                      ' WHERE datatree_id = ?';
1034:             $values = array((int)$cid);
1035: 
1036:             Horde::logMessage('SQL Query by Horde_DataTree_Sql::getData(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
1037:             $row = $this->_db->getRow($query, $values, DB_FETCHMODE_ASSOC);
1038: 
1039:             $data = Horde_Serialize::unserialize($row['datatree_data'],
1040:                                                  $row['datatree_serialized'],
1041:                                                  'UTF-8');
1042:             /* Convert old data to the new format. */
1043:             if ($row['datatree_serialized'] == Horde_Serialize::BASIC) {
1044:                 $data = Horde_String::convertCharset($data, $GLOBALS['registry']->getLanguageCharset(), 'UTF-8');
1045:             }
1046:             return (is_null($data) || !is_array($data)) ? array() : $data;
1047:         }
1048:     }
1049: 
1050:     /**
1051:      * Retrieves data for an object from the horde_datatree_attributes table.
1052:      *
1053:      * @param integer|array $cid  The object id to fetch, or an array of
1054:      *                            object ids.
1055:      * @param array $keys         The attributes keys to fetch.
1056:      *
1057:      * @return array  A hash of attributes, or a multi-level hash of object
1058:      *                ids => their attributes.
1059:      */
1060:     function getAttributes($cid, $keys = false)
1061:     {
1062:         if (empty($cid)) {
1063:             return array();
1064:         }
1065: 
1066:         if ($keys) {
1067:             $filter = sprintf(' AND attribute_key IN (\'%s\')',
1068:                               implode("', '", $keys));
1069:         } else {
1070:             $filter = '';
1071:         }
1072: 
1073:         if (is_array($cid)) {
1074:             $query = sprintf('SELECT datatree_id, attribute_name AS name, attribute_key AS "key", attribute_value AS value FROM %s WHERE datatree_id IN (%s)%s',
1075:                              $this->_params['table_attributes'],
1076:                              implode(', ', $cid),
1077:                              $filter);
1078: 
1079:             Horde::logMessage('SQL Query by Horde_DataTree_Sql::getAttributes(): ' . $query, 'DEBUG');
1080:             $rows = $this->_db->getAll($query, DB_FETCHMODE_ASSOC);
1081:             if (is_a($rows, 'PEAR_Error')) {
1082:                 return $rows;
1083:             }
1084: 
1085:             $data = array();
1086:             foreach ($rows as $row) {
1087:                 if (empty($data[$row['datatree_id']])) {
1088:                     $data[$row['datatree_id']] = array();
1089:                 }
1090:                 $data[$row['datatree_id']][] = array('name' => $row['name'],
1091:                                                      'key' => $row['key'],
1092:                                                      'value' => Horde_String::convertCharset($row['value'], $this->_params['charset'], 'UTF-8'));
1093:             }
1094:             return $data;
1095:         } else {
1096:             $query = sprintf('SELECT attribute_name AS name, attribute_key AS "key", attribute_value AS value FROM %s WHERE datatree_id = %s%s',
1097:                              $this->_params['table_attributes'],
1098:                              (int)$cid,
1099:                              $filter);
1100: 
1101:             Horde::logMessage('SQL Query by Horde_DataTree_Sql::getAttributes(): ' . $query, 'DEBUG');
1102:             $rows = $this->_db->getAll($query, DB_FETCHMODE_ASSOC);
1103:             for ($i = 0; $i < count($rows); $i++) {
1104:                 $rows[$i]['value'] = Horde_String::convertCharset($rows[$i]['value'],
1105:                                                                   $this->_params['charset'],
1106:                                                                   'UTF-8');
1107:             }
1108:             return $rows;
1109:         }
1110:     }
1111: 
1112:     /**
1113:      * Returns the number of objects matching a set of attribute criteria.
1114:      *
1115:      * @see buildAttributeQuery()
1116:      *
1117:      * @param array   $criteria   The array of criteria.
1118:      * @param string  $parent     The parent node to start searching from.
1119:      * @param boolean $allLevels  Return all levels, or just the direct
1120:      *                            children of $parent? Defaults to all levels.
1121:      * @param string  $restrict   Only return attributes with the same
1122:      *                            attribute_name or attribute_id.
1123:      */
1124:     function countByAttributes($criteria, $parent = DATATREE_ROOT,
1125:                                $allLevels = true, $restrict = 'name')
1126:     {
1127:         if (!count($criteria)) {
1128:             return 0;
1129:         }
1130: 
1131:         $aq = $this->buildAttributeQuery($criteria,
1132:                                          $parent,
1133:                                          $allLevels,
1134:                                          $restrict,
1135:                                          DATATREE_BUILD_COUNT);
1136:         if (is_a($aq, 'PEAR_Error')) {
1137:             return $aq;
1138:         }
1139:         list($query, $values) = $aq;
1140: 
1141:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::countByAttributes(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
1142: 
1143:         $result = $this->_db->query($query, $values);
1144:         if (is_a($result, 'PEAR_Error')) {
1145:             Horde::logMessage($result, 'ERR');
1146:             return $result;
1147:         }
1148:         $row = $result->fetchRow();
1149:         if (is_a($row, 'PEAR_Error')) {
1150:             Horde::logMessage($row, 'ERR');
1151:             return $row;
1152:         }
1153: 
1154:         return $row[0];
1155:     }
1156: 
1157:     /**
1158:      * Returns a set of object ids based on a set of attribute criteria.
1159:      *
1160:      * @see buildAttributeQuery()
1161:      *
1162:      * @param array   $criteria     The array of criteria.
1163:      * @param string  $parent       The parent node to start searching from.
1164:      * @param boolean $allLevels    Return all levels, or just the direct
1165:      *                              children of $parent? Defaults to all levels.
1166:      * @param string  $restrict     Only return attributes with the same
1167:      *                              attribute_name or attribute_id.
1168:      * @param integer $from         The object to start to fetching
1169:      * @param integer $count        The number of objects to fetch
1170:      * @param string  $sortby_name  Attribute name to use for sorting.
1171:      * @param string  $sortby_key   Attribute key to use for sorting.
1172:      * @param integer $direction    Sort direction:
1173:      *                                0 - ascending
1174:      *                                1 - descending
1175:      */
1176:     function getByAttributes($criteria, $parent = DATATREE_ROOT,
1177:                              $allLevels = true, $restrict = 'name', $from = 0,
1178:                              $count = 0, $sortby_name = null,
1179:                              $sortby_key = null, $direction = 0)
1180:     {
1181:         if (!count($criteria)) {
1182:             return PEAR::raiseError('no criteria');
1183:         }
1184: 
1185:         // If there are top-level OR criteria, process one at a time
1186:         // and return any results as soon as they're found...but only if
1187:         // there is no LIMIT requested.
1188:         if ($count == 0 && $from == 0) {
1189:             foreach ($criteria as $key => $vals) {
1190:                 if ($key == 'OR') {
1191:                     $rows = array();
1192:                     $num_or_statements = count($criteria[$key]);
1193:                     for ($i = 0; $i < $num_or_statements; $i++) {
1194:                         $criteria_or = $criteria['OR'][$i];
1195:                         list($query, $values) = $this->buildAttributeQuery(
1196:                                                     $criteria_or,
1197:                                                     $parent,
1198:                                                     $allLevels,
1199:                                                     $restrict,
1200:                                                     DATATREE_BUILD_SELECT,
1201:                                                     $sortby_name,
1202:                                                     $sortby_key,
1203:                                                     $direction);
1204:                         if ($count) {
1205:                             $query = $this->_db->modifyLimitQuery($query, $from, $count);
1206:                         }
1207: 
1208:                         Horde::logMessage('SQL Query by Horde_DataTree_Sql::getByAttributes(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
1209: 
1210:                         $result = $this->_db->query($query, $values);
1211:                         if (is_a($result, 'PEAR_Error')) {
1212:                             Horde::logMessage($result, 'ERR');
1213:                             return $result;
1214:                         }
1215:                         while ($row = $result->fetchRow()) {
1216:                             $rows[$row[0]] = Horde_String::convertCharset($row[1], $this->_params['charset'], 'UTF-8');
1217:                         }
1218:                     }
1219: 
1220:                     return $rows;
1221:                 }
1222:             }
1223:         }
1224:         // Process AND or other complex queries.
1225:         $aq = $this->buildAttributeQuery($criteria,
1226:                                          $parent,
1227:                                          $allLevels,
1228:                                          $restrict,
1229:                                          DATATREE_BUILD_SELECT,
1230:                                          $sortby_name,
1231:                                          $sortby_key,
1232:                                          $direction);
1233:         if (is_a($aq, 'PEAR_Error')) {
1234:             return $aq;
1235:         }
1236: 
1237:         list($query, $values) = $aq;
1238: 
1239:         if ($count) {
1240:             $query = $this->_db->modifyLimitQuery($query, $from, $count);
1241:         }
1242: 
1243:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::getByAttributes(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
1244:         $result = $this->_db->query($query, $values);
1245:         if (is_a($result, 'PEAR_Error')) {
1246:             Horde::logMessage($result, 'ERR');
1247:             return $result;
1248:         }
1249: 
1250:         $rows = array();
1251:         while ($row = $result->fetchRow()) {
1252:             $rows[$row[0]] = Horde_String::convertCharset($row[1], $this->_params['charset'], 'UTF-8');
1253:         }
1254: 
1255:         return $rows;
1256:     }
1257: 
1258:     /**
1259:      * Sorts IDs by attribute values. IDs without attributes will be added to
1260:      * the end of the sorted list.
1261:      *
1262:      * @param array $unordered_ids  Array of ids to sort.
1263:      * @param array $sortby_name    Attribute name to use for sorting.
1264:      * @param array $sortby_key     Attribute key to use for sorting.
1265:      * @param array $direction      Sort direction:
1266:      *                                0 - ascending
1267:      *                                1 - descending
1268:      *
1269:      * @return array  Sorted ids.
1270:      */
1271:     function sortByAttributes($unordered_ids, $sortby_name = null,
1272:                               $sortby_key = null, $direction = 0)
1273:     {
1274:         /* Select ids ordered by attribute value. */
1275:         $where = '';
1276:         if (!is_null($sortby_name)) {
1277:             $where = sprintf(' AND attribute_name = %s ',
1278:                              $this->_db->quote($sortby_name));
1279:         }
1280:         if (!is_null($sortby_key)) {
1281:             $where = sprintf('%s AND attribute_key = %s ',
1282:                              $where,
1283:                              $this->_db->quote($sortby_key));
1284:         }
1285: 
1286:         $query = sprintf('SELECT datatree_id FROM %s WHERE datatree_id IN (%s) %s ORDER BY attribute_value %s',
1287:                          $this->_params['table_attributes'],
1288:                          implode(',', $unordered_ids),
1289:                          $where,
1290:                          ($direction == 1) ? 'DESC' : 'ASC');
1291: 
1292:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::sortByAttributes(): ' . $query, 'DEBUG');
1293:         $ordered_ids = $this->_db->getCol($query);
1294: 
1295:         /* Make sure that some ids didn't get lost because has no such
1296:          * attribute name/key. Append them to the end. */
1297:         if (count($ordered_ids) != count($unordered_ids)) {
1298:             $ordered_ids = array_keys(array_flip(array_merge($ordered_ids, $unordered_ids)));
1299:         }
1300: 
1301:         return $ordered_ids;
1302:     }
1303: 
1304:     /**
1305:      * Returns the number of all of the available values matching the
1306:      * given criteria. Either attribute_name or attribute_key MUST be
1307:      * supplied, and both MAY be supplied.
1308:      *
1309:      * @see buildAttributeQuery()
1310:      *
1311:      * @param array   $criteria     The array of criteria.
1312:      * @param string  $parent       The parent node to start searching from.
1313:      * @param boolean $allLevels    Return all levels, or just the direct
1314:      *                              children of $parent? Defaults to all levels.
1315:      * @param string  $restrict     Only return attributes with the same
1316:      *                              attribute_name or attribute_id.
1317:      * @param string  $attribute_name  The name of the attribute.
1318:      * @param string  $attribute_key   The key value of the attribute.
1319:      */
1320:     function countValuesByAttributes($criteria, $parent = DATATREE_ROOT,
1321:                                      $allLevels = true, $restrict = 'name',
1322:                                      $key = null, $name = null)
1323:     {
1324:         if (!count($criteria)) {
1325:             return PEAR::raiseError('no criteria');
1326:         }
1327: 
1328:         $aq = $this->buildAttributeQuery($criteria,
1329:                                          $parent,
1330:                                          $allLevels,
1331:                                          $restrict,
1332:                                          DATATREE_BUILD_VALUES_COUNT);
1333: 
1334:         $aq[0] .= ' AND a.datatree_id = c.datatree_id';
1335: 
1336:         if ($key !== null) {
1337:             $aq[0] .=  ' AND a.attribute_key = ?';
1338:             $aq[1][] = $key;
1339:         }
1340: 
1341:         if ($name !== null) {
1342:             $aq[0] .=  ' AND a.attribute_name = ?';
1343:             $aq[1][] = $name;
1344:         }
1345: 
1346:         return $this->_db->getOne($aq[0], $aq[1]);
1347:     }
1348: 
1349:     /**
1350:      * Returns a list of all of the available values of the given criteria
1351:      * Either attribute_name or attribute_key MUST be
1352:      * supplied, and both MAY be supplied.
1353:      *
1354:      * @see buildAttributeQuery()
1355:      *
1356:      * @param array   $criteria     The array of criteria.
1357:      * @param string  $parent       The parent node to start searching from.
1358:      * @param boolean $allLevels    Return all levels, or just the direct
1359:      *                              children of $parent? Defaults to all levels.
1360:      * @param string  $restrict     Only return attributes with the same
1361:      *                              attribute_name or attribute_id.
1362:      * @param integer $from         The object to start to fetching
1363:      * @param integer $count        The number of objects to fetch
1364:      * @param string  $sortby_name  Attribute name to use for sorting.
1365:      * @param string  $sortby_key   Attribute key to use for sorting.
1366:      * @param integer $direction    Sort direction:
1367:      *                                0 - ascending
1368:      *                                1 - descending
1369:      * @param string  $attribute_name  The name of the attribute.
1370:      * @param string  $attribute_key   The key value of the attribute.
1371:      */
1372:     function getValuesByAttributes($criteria, $parent = DATATREE_ROOT,
1373:                                    $allLevels = true, $restrict = 'name', $from = 0,
1374:                                    $count = 0, $sortby_name = null,
1375:                                    $sortby_key = null, $direction = 0,
1376:                                    $key = null, $name = null)
1377:     {
1378:         if (!count($criteria)) {
1379:             return PEAR::raiseError('no criteria');
1380:         }
1381: 
1382:         $aq = $this->buildAttributeQuery($criteria,
1383:                                          $parent,
1384:                                          $allLevels,
1385:                                          $restrict,
1386:                                          DATATREE_BUILD_VALUES,
1387:                                          $sortby_name,
1388:                                          $sortby_key,
1389:                                          $direction);
1390: 
1391:         $aq[0] .=  ' AND a.datatree_id = c.datatree_id';
1392: 
1393:         if ($key !== null) {
1394:             $aq[0] .=  ' AND a.attribute_key = ?';
1395:             $aq[1][] = $key;
1396:         }
1397: 
1398:         if ($name !== null) {
1399:             $aq[0] .=  ' AND a.attribute_name = ?';
1400:             $aq[1][] = $name;
1401:         }
1402: 
1403:         if ($count) {
1404:             $aq[0] = $this->_db->modifyLimitQuery($aq[0], $from, $count);
1405:         }
1406: 
1407:         return $this->_db->getCol($aq[0], 0, $aq[1]);
1408:     }
1409: 
1410:     /**
1411:      * Returns a list of all of the available values of the given attribute
1412:      * name/key combination. Either attribute_name or attribute_key MUST be
1413:      * supplied, and both MAY be supplied.
1414:      *
1415:      * @param string  $attribute_name  The name of the attribute.
1416:      * @param string  $attribute_key   The key value of the attribute.
1417:      * @param string  $parent          The parent node to start searching from.
1418:      * @param boolean $allLevels       Return all levels, or just the direct
1419:      *                                 children of $parent? Defaults to all
1420:      *                                 levels.
1421:      *
1422:      * @return array  An array of all of the available values.
1423:      */
1424:     function getAttributeValues($attribute_name = null, $attribute_key = null,
1425:                                 $parent = DATATREE_ROOT, $allLevels = true)
1426:     {
1427:         // Build the name/key filter.
1428:         $where = '';
1429:         if (!is_null($attribute_name)) {
1430:             $where .= 'a.attribute_name = ' . $this->_db->quote($attribute_name);
1431:         }
1432:         if (!is_null($attribute_key)) {
1433:             if ($where) {
1434:                 $where .= ' AND ';
1435:             }
1436:             $where .= 'a.attribute_key = ' . $this->_db->quote($attribute_key);
1437:         }
1438: 
1439:         // Return if we have no criteria.
1440:         if (!$where) {
1441:             return PEAR::raiseError('no criteria');
1442:         }
1443: 
1444:         // Add filtering by parent, and for one or all levels.
1445:         $levelQuery = '';
1446:         if ($parent != DATATREE_ROOT) {
1447:             $parts = explode(':', $parent);
1448:             $parents = '';
1449:             $pstring = '';
1450:             foreach ($parts as $part) {
1451:                 $pstring .= (empty($pstring) ? '' : ':') . $part;
1452:                 $pid = $this->getId($pstring);
1453:                 if (is_a($pid, 'PEAR_Error')) {
1454:                     return $pid;
1455:                 }
1456:                 $parents .= ':' . $pid;
1457:             }
1458: 
1459:             if ($allLevels) {
1460:                 $levelQuery = sprintf('AND (datatree_parents = %s OR datatree_parents LIKE %s)',
1461:                                       $this->_db->quote($parents),
1462:                                       $this->_db->quote($parents . ':%'));
1463:             } else {
1464:                 $levelQuery = sprintf('AND datatree_parents = %s',
1465:                                       $this->_db->quote($parents));
1466:             }
1467:         } elseif (!$allLevels) {
1468:             $levelQuery = "AND datatree_parents = ''";
1469:         }
1470: 
1471:         // Build the FROM/JOIN clauses.
1472:         $joins = 'LEFT JOIN ' . $this->_params['table'] .
1473:             ' c ON a.datatree_id = c.datatree_id';
1474: 
1475:         $query = sprintf('SELECT DISTINCT a.attribute_value FROM %s a %s WHERE c.group_uid = %s AND %s %s',
1476:                          $this->_params['table_attributes'],
1477:                          $joins,
1478:                          $this->_db->quote($this->_params['group']),
1479:                          $where,
1480:                          $levelQuery);
1481: 
1482:         Horde::logMessage('SQL Query by Horde_DataTree_Sql::getAttributeValues(): ' . $query, 'DEBUG');
1483: 
1484:         $rows = $this->_db->getCol($query);
1485:         if (is_a($rows, 'PEAR_Error')) {
1486:             Horde::logMessage($rows, 'ERR');
1487:         }
1488: 
1489:         return $rows;
1490:     }
1491: 
1492:     /**
1493:      * Builds an attribute query. Here is an example $criteria array:
1494:      *
1495:      * <code>
1496:      * $criteria['OR'] = array(
1497:      *     array('AND' => array(
1498:      *         array('field' => 'name',
1499:      *               'op'    => '=',
1500:      *               'test'  => 'foo'),
1501:      *         array('field' => 'key',
1502:      *               'op'    => '=',
1503:      *               'test'  => 'abc'))),
1504:      *         array('AND' => array(
1505:      *             array('field' => 'name',
1506:      *                   'op'    => '=',
1507:      *                   'test'  => 'bar'),
1508:      *             array('field' => 'key',
1509:      *                   'op'    => '=',
1510:      *                   'test'  => 'xyz'))));
1511:      * </code>
1512:      *
1513:      * This would fetch all object ids where attribute name is "foo" AND key
1514:      * is "abc", OR "bar" AND "xyz".
1515:      *
1516:      * @param array   $criteria     The array of criteria.
1517:      * @param string  $parent       The parent node to start searching from.
1518:      * @param boolean $allLevels    Return all levels, or just the direct
1519:      *                              children of $parent? Defaults to all levels.
1520:      * @param string  $restrict     Only return attributes with the same
1521:      *                              attribute_name or attribute_id.
1522:      * @param integer $operation    Type of query to build
1523:      * @param string  $sortby_name  Attribute name to use for sorting.
1524:      * @param string  $sortby_key   Attribute key to use for sorting.
1525:      * @param integer $direction    Sort direction:
1526:      *                                0 - ascending
1527:      *                                1 - descending
1528:      *
1529:      * @return array  An SQL query and a list of values suitable for binding
1530:      *                as an array.
1531:      */
1532:     function buildAttributeQuery($criteria, $parent = DATATREE_ROOT,
1533:                                  $allLevels = true, $restrict = 'name',
1534:                                  $operation = DATATREE_BUILD_SELECT,
1535:                                  $sortby_name = null, $sortby_key = null,
1536:                                  $direction = 0)
1537:     {
1538:         if (!count($criteria)) {
1539:             return array('', array());
1540:         }
1541: 
1542:         /* Build the query. */
1543:         $this->_tableCount = 1;
1544:         $query = '';
1545:         $values = array();
1546:         foreach ($criteria as $key => $vals) {
1547:             if ($key == 'OR' || $key == 'AND') {
1548:                 if (!empty($query)) {
1549:                     $query .= ' ' . $key . ' ';
1550:                 }
1551:                 $binds = $this->_buildAttributeQuery($key, $vals);
1552:                 $query .= '(' . $binds[0] . ')';
1553:                 $values += $binds[1];
1554:             }
1555:         }
1556: 
1557:         // Add filtering by parent, and for one or all levels.
1558:         $levelQuery = '';
1559:         $levelValues = array();
1560:         if ($parent != DATATREE_ROOT) {
1561:             $parts = explode(':', $parent);
1562:             $parents = '';
1563:             $pstring = '';
1564:             foreach ($parts as $part) {
1565:                 $pstring .= (empty($pstring) ? '' : ':') . $part;
1566:                 $pid = $this->getId($pstring);
1567:                 if (is_a($pid, 'PEAR_Error')) {
1568:                     return $pid;
1569:                 }
1570:                 $parents .= ':' . $pid;
1571:             }
1572: 
1573:             if ($allLevels) {
1574:                 $levelQuery = 'AND (datatree_parents = ? OR datatree_parents LIKE ?)';
1575:                 $levelValues = array($parents, $parents . ':%');
1576:             } else {
1577:                 $levelQuery = 'AND datatree_parents = ?';
1578:                 $levelValues = array($parents);
1579:             }
1580:         } elseif (!$allLevels) {
1581:             $levelQuery = "AND datatree_parents = ''";
1582:         }
1583: 
1584:         // Build the FROM/JOIN clauses.
1585:         $joins = array();
1586:         $pairs = array();
1587:         for ($i = 1; $i <= $this->_tableCount; $i++) {
1588:             $joins[] = 'LEFT JOIN ' . $this->_params['table_attributes'] .
1589:                 ' a' . $i . ' ON a' . $i . '.datatree_id = c.datatree_id';
1590: 
1591:             if ($i != 1) {
1592:                 if ($restrict == 'name') {
1593:                     $pairs[] = 'AND a1.attribute_name = a' . $i . '.attribute_name';
1594:                 } elseif ($restrict == 'id') {
1595:                     $pairs[] = 'AND a1.datatree_id = a' . $i . '.datatree_id';
1596:                 }
1597:             }
1598:         }
1599: 
1600:         // Override sorting.
1601:         $sort = array();
1602:         if (!is_null($sortby_name) || !is_null($sortby_key)) {
1603:             $order_table = 'a' . $i;
1604:             $joins[] = 'LEFT JOIN ' . $this->_params['table_attributes'] .
1605:                 ' ' . $order_table . ' ON ' . $order_table .
1606:                 '.datatree_id = c.datatree_id';
1607: 
1608:             if (!is_null($sortby_name)) {
1609:                 $pairs[] = sprintf('AND %s.attribute_name = ? ', $order_table);
1610:                 $sort[] = $sortby_name;
1611:             }
1612:             if (!is_null($sortby_key)) {
1613:                 $pairs[] = sprintf('AND %s.attribute_key = ? ', $order_table);
1614:                 $sort[] = $sortby_key;
1615:             }
1616: 
1617:             $order = sprintf('%s.attribute_value %s',
1618:                              $order_table,
1619:                              ($direction == 1) ? 'DESC' : 'ASC');
1620:             $group_by = 'c.datatree_id, c.datatree_name, c.datatree_order, ' .
1621:                 $order_table . '.attribute_value';
1622:         } else {
1623:             $order = 'c.datatree_order, c.datatree_name, c.datatree_id';
1624:             $group_by = 'c.datatree_id, c.datatree_name, c.datatree_order';
1625:         }
1626: 
1627:         $joins = implode(' ', $joins);
1628:         $pairs = implode(' ', $pairs);
1629: 
1630:         switch ($operation) {
1631: 
1632:         case DATATREE_BUILD_VALUES_COUNT:
1633:             $what = 'COUNT(DISTINCT(a.attribute_value))';
1634:             $from = ' ' . $this->_params['table_attributes'] . ' a, ' . $this->_params['table'];
1635:             $tail = '';
1636:             break;
1637: 
1638:         case DATATREE_BUILD_VALUES:
1639:             $what = 'DISTINCT(a.attribute_value)';
1640:             $from = ' ' . $this->_params['table_attributes'] . ' a, ' . $this->_params['table'];
1641:             $tail = '';
1642:             break;
1643: 
1644:         case DATATREE_BUILD_COUNT:
1645:             $what = 'COUNT(DISTINCT c.datatree_id)';
1646:             $from = $this->_params['table'];
1647:             $tail = '';
1648:             break;
1649: 
1650:         default:
1651:             $what = 'c.datatree_id, c.datatree_name';
1652:             $from = $this->_params['table'];
1653:             $tail = sprintf('GROUP BY %s ORDER BY %s', $group_by, $order);
1654:             break;
1655:         }
1656: 
1657:         return array(sprintf('SELECT %s FROM %s c %s WHERE c.group_uid = ? AND %s %s %s %s',
1658:                              $what,
1659:                              $from,
1660:                              $joins,
1661:                              $query,
1662:                              $levelQuery,
1663:                              $pairs,
1664:                              $tail),
1665:                      array_merge(array($this->_params['group']),
1666:                                  $values,
1667:                                  $levelValues,
1668:                                  $sort));
1669:     }
1670: 
1671:     /**
1672:      * Builds a piece of an attribute query.
1673:      *
1674:      * @param string $glue     The glue to join the criteria (OR/AND).
1675:      * @param array $criteria  The array of criteria.
1676:      * @param boolean $join    Should we join on a clean
1677:      *                         horde_datatree_attributes table? Defaults to
1678:      *                         false.
1679:      *
1680:      * @return array  An SQL fragment and a list of values suitable for binding
1681:      *                as an array.
1682:      */
1683:     function _buildAttributeQuery($glue, $criteria, $join = false)
1684:     {
1685:         // Initialize the clause that we're building.
1686:         $clause = '';
1687:         $values = array();
1688: 
1689:         // Get the table alias to use for this set of criteria.
1690:         $alias = $this->_getAlias($join);
1691: 
1692:         foreach ($criteria as $key => $vals) {
1693:             if (!empty($clause)) {
1694:                 $clause .= ' ' . $glue . ' ';
1695:             }
1696:             if (!empty($vals['OR']) || !empty($vals['AND'])) {
1697:                 $binds = $this->_buildAttributeQuery($glue, $vals);
1698:                 $clause .= '(' . $binds[0] . ')';
1699:                 $values = array_merge($values, $binds[1]);
1700:             } elseif (!empty($vals['JOIN'])) {
1701:                 $binds = $this->_buildAttributeQuery($glue, $vals['JOIN'], true);
1702:                 $clause .= $binds[0];
1703:                 $values = array_merge($values, $binds[1]);
1704:             } else {
1705:                 if (isset($vals['field'])) {
1706:                     // All of the attribute_* fields are text, so make
1707:                     // sure we send strings to the database.
1708:                     if (is_array($vals['test'])) {
1709:                         for ($i = 0, $iC = count($vals['test']); $i < $iC; ++$i) {
1710:                             $vals['test'][$i] = (string)$vals['test'][$i];
1711:                         }
1712:                     } else {
1713:                         $vals['test'] = (string)$vals['test'];
1714:                     }
1715: 
1716:                     $binds = self::buildClause($this->_db, $alias . '.attribute_' . $vals['field'], $vals['op'], $vals['test'], true);
1717:                     $clause .= $binds[0];
1718:                     $values = array_merge($values, $binds[1]);
1719:                 } else {
1720:                     $binds = $this->_buildAttributeQuery($key, $vals);
1721:                     $clause .= $binds[0];
1722:                     $values = array_merge($values, $binds[1]);
1723:                 }
1724:             }
1725:         }
1726: 
1727:         return array($clause, $values);
1728:     }
1729: 
1730:     /**
1731:      * Get an alias to horde_datatree_attributes, incrementing it if
1732:      * necessary.
1733:      *
1734:      * @param boolean $increment  Increment the alias count? Defaults to no.
1735:      */
1736:     function _getAlias($increment = false)
1737:     {
1738:         static $seen = array();
1739: 
1740:         if ($increment && !empty($seen[$this->_tableCount])) {
1741:             $this->_tableCount++;
1742:         }
1743: 
1744:         $seen[$this->_tableCount] = true;
1745:         return 'a' . $this->_tableCount;
1746:     }
1747: 
1748:     /**
1749:      * Update the data in an object. Does not change the object's
1750:      * parent or name, just serialized data or attributes.
1751:      *
1752:      * @param Horde_DataTree $object  A Horde_DataTree object.
1753:      */
1754:     function updateData($object)
1755:     {
1756:         if (!is_a($object, 'Horde_DataTreeObject')) {
1757:             /* Nothing to do for non objects. */
1758:             return true;
1759:         }
1760: 
1761:         /* Get the object id. */
1762:         $id = $this->getId($object->getName());
1763:         if (is_a($id, 'PEAR_Error')) {
1764:             return $id;
1765:         }
1766: 
1767:         /* See if we can break the object out to datatree_attributes table. */
1768:         if (method_exists($object, '_toAttributes')) {
1769:             /* If we can, clear out the datatree_data field to make sure it
1770:              * doesn't get picked up by getData(). Intentionally don't check
1771:              * for errors here in case datatree_data goes away in the
1772:              * future. */
1773:             $query = 'UPDATE ' . $this->_params['table'] .
1774:                      ' SET datatree_data = ? WHERE datatree_id = ?';
1775:             $values = array(NULL, (int)$id);
1776: 
1777:             Horde::logMessage('SQL Query by Horde_DataTree_Sql::updateData(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
1778:             $this->_write_db->query($query, $values);
1779: 
1780:             /* Start a transaction. */
1781:             $this->_write_db->autoCommit(false);
1782: 
1783:             /* Delete old attributes. */
1784:             $query = 'DELETE FROM ' . $this->_params['table_attributes'] .
1785:                      ' WHERE datatree_id = ?';
1786:             $values = array((int)$id);
1787: 
1788:             Horde::logMessage('SQL Query by Horde_DataTree_Sql::updateData(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
1789:             $result = $this->_write_db->query($query, $values);
1790:             if (is_a($result, 'PEAR_Error')) {
1791:                 $this->_write_db->rollback();
1792:                 $this->_write_db->autoCommit(true);
1793:                 return $result;
1794:             }
1795: 
1796:             /* Get the new attribute set, and insert each into the DB. If
1797:              * anything fails in here, rollback the transaction, return the
1798:              * relevant error, and bail out. */
1799:             $attributes = $object->_toAttributes();
1800:             $query = 'INSERT INTO ' . $this->_params['table_attributes'] .
1801:                      ' (datatree_id, attribute_name, attribute_key, attribute_value)' .
1802:                      ' VALUES (?, ?, ?, ?)';
1803:             $statement = $this->_write_db->prepare($query);
1804:             foreach ($attributes as $attr) {
1805:                 $values = array((int)$id,
1806:                                 $attr['name'],
1807:                                 $attr['key'],
1808:                                 Horde_String::convertCharset($attr['value'], 'UTF-8', $this->_params['charset']));
1809: 
1810:                 Horde::logMessage('SQL Query by Horde_DataTree_Sql::updateData(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
1811: 
1812:                 $result = $this->_write_db->execute($statement, $values);
1813:                 if (is_a($result, 'PEAR_Error')) {
1814:                     $this->_write_db->rollback();
1815:                     $this->_write_db->autoCommit(true);
1816:                     return $result;
1817:                 }
1818:             }
1819: 
1820:             /* Commit the transaction, and turn autocommit back on. */
1821:             $result = $this->_write_db->commit();
1822:             $this->_write_db->autoCommit(true);
1823: 
1824:             return is_a($result, 'PEAR_Error') ? $result : true;
1825:         } else {
1826:             /* Write to the datatree_data field. */
1827:             $ser = Horde_Serialize::UTF7_BASIC;
1828:             $data = Horde_Serialize::serialize($object->getData(), $ser, 'UTF-8');
1829: 
1830:             $query = 'UPDATE ' . $this->_params['table'] .
1831:                      ' SET datatree_data = ?, datatree_serialized = ?' .
1832:                      ' WHERE datatree_id = ?';
1833:             $values = array($data,
1834:                             (int)$ser,
1835:                             (int)$id);
1836: 
1837:             Horde::logMessage('SQL Query by Horde_DataTree_Sql::updateData(): ' . $query . ', ' . var_export($values, true), 'DEBUG');
1838:             $result = $this->_write_db->query($query, $values);
1839: 
1840:             return is_a($result, 'PEAR_Error') ? $result : true;
1841:         }
1842:     }
1843: 
1844:     /**
1845:      * Attempts to open a connection to the SQL server.
1846:      *
1847:      * @return mixed  True or PEAR_Error.
1848:      */
1849:     function _init()
1850:     {
1851:         try {
1852:             $this->_db = $GLOBALS['injector']->getInstance('Horde_Core_Factory_DbPear')->create('read');
1853:             $this->_write_db = $GLOBALS['injector']->getInstance('Horde_Core_Factory_DbPear')->create('rw');
1854:         } catch (Horde_Exception $e) {
1855:             return PEAR::raiseError($e->getMessage());
1856:         }
1857: 
1858:         $this->_params = array_merge(array(
1859:             'table' => 'horde_datatree',
1860:             'table_attributes' => 'horde_datatree_attributes',
1861:         ), $this->_params);
1862: 
1863:         return true;
1864:     }
1865: 
1866:     /**
1867:      * Returns a boolean expression using the specified operator. Uses
1868:      * database-specific casting, if necessary.
1869:      *
1870:      * @param DB $dbh        The PEAR::DB database object.
1871:      * @param string $lhs    The column or expression to test.
1872:      * @param string $op     The operator.
1873:      * @param string $rhs    The comparison value.
1874:      * @param boolean $bind  If true, the method returns the query and a list
1875:      *                       of values suitable for binding as an array.
1876:      * @param array $params  Any additional parameters for the operator.
1877:      *
1878:      * @return mixed  The SQL test fragment, or an array containing the query
1879:      *                and a list of values if $bind is true.
1880:      */
1881:     static public function buildClause($dbh, $lhs, $op, $rhs, $bind = false, $params = array())
1882:     {
1883:         $type = $dbh instanceof Horde_Db_Adapter ? Horde_String::lower($dbh->adapterName()) : $dbh->phptype;
1884: 
1885:         switch ($op) {
1886:         case '|':
1887:         case '&':
1888:             switch ($type) {
1889:             case 'pgsql':
1890:             case 'pdo_postgresql':
1891:                 // Only PgSQL 7.3+ understands SQL99 'SIMILAR TO'; use
1892:                 // ~ for greater backwards compatibility.
1893:                 $query = 'CASE WHEN CAST(%s AS VARCHAR) ~ \'^-?[0-9]+$\' THEN (CAST(%s AS INTEGER) %s %s) <> 0 ELSE FALSE END';
1894:                 if ($bind) {
1895:                     return array(sprintf(self::escapePrepare($query),
1896:                                          self::escapePrepare($lhs),
1897:                                          self::escapePrepare($lhs),
1898:                                          self::escapePrepare($op),
1899:                                          '?'),
1900:                                  array((int)$rhs));
1901:                 } else {
1902:                     return sprintf($query, $lhs, $lhs, $op, (int)$rhs);
1903:                 }
1904: 
1905:             case 'oci8':
1906:                 // Oracle uses & for variables. We need to use the bitand
1907:                 // function that is available, but may be unsupported.
1908:                 $query = 'bitand(%s, %s) = %s';
1909:                 if ($bind) {
1910:                     return array(sprintf(self::escapePrepare($query),
1911:                                          self::escapePrepare($lhs), '?', '?'),
1912:                                  array((int)$rhs, (int)$rhs));
1913:                 } else {
1914:                     return sprintf($query, $lhs, (int)$rhs, (int)$rhs);
1915:                 }
1916: 
1917:             case 'mssql':
1918:                 // MSSQL must have a valid boolean expression
1919:                 $query = '(CASE WHEN ISNUMERIC(%s) = 1 THEN (%s & %s) ELSE %s END) = %s';
1920:                 if ($bind) {
1921:                     return array(sprintf(self::escapePrepare($query),
1922:                                          self::escapePrepare($lhs),
1923:                                          self::escapePrepare($lhs), '?', '?', '?'),
1924:                                  array((int)$rhs, (int)$rhs - 1, (int)$rhs));
1925:                 } else {
1926:                     return sprintf($query, $lhs, $lhs, (int)$rhs, (int)$rhs - 1, (int)$rhs);
1927:                 }
1928: 
1929:             case 'odbc':
1930:                 // ODBC must have a valid boolean expression
1931:                 $query = '(%s & %s) = %s';
1932:                 if ($bind) {
1933:                     return array(sprintf(self::escapePrepare($query),
1934:                                          self::escapePrepare($lhs), '?', '?'),
1935:                                  array((int)$rhs, (int)$rhs));
1936:                 } else {
1937:                     return sprintf($query, $lhs, (int)$rhs, (int)$rhs);
1938:                 }
1939: 
1940:             default:
1941:                 if ($bind) {
1942:                     return array($lhs . ' ' . self::escapePrepare($op) . ' ?',
1943:                                  array((int)$rhs));
1944:                 } else {
1945:                     return $lhs . ' ' . $op . ' ' . (int)$rhs;
1946:                 }
1947:             }
1948: 
1949:         case '~':
1950:             if ($type == 'mysql' || $type == 'mysqli' || $type == 'pdo_mysql') {
1951:                 $op = 'REGEXP';
1952:             }
1953:             if ($bind) {
1954:                 return array($lhs . ' ' . $op . ' ?', array($rhs));
1955:             } else {
1956:                 return $lhs . ' ' . $op . ' ' . $rhs;
1957:             }
1958: 
1959:         case 'IN':
1960:             if ($bind) {
1961:                 if (is_array($rhs)) {
1962:                     return array($lhs . ' IN (?' . str_repeat(', ?', count($rhs) - 1) . ')', $rhs);
1963:                 } else {
1964:                     // We need to bind each member of the IN clause
1965:                     // separately to ensure proper quoting.
1966:                     if (substr($rhs, 0, 1) == '(') {
1967:                         $rhs = substr($rhs, 1);
1968:                     }
1969:                     if (substr($rhs, -1) == ')') {
1970:                         $rhs = substr($rhs, 0, -1);
1971:                     }
1972: 
1973:                     $ids = preg_split('/\s*,\s*/', $rhs);
1974: 
1975:                     return array($lhs . ' IN (?' . str_repeat(', ?', count($ids) - 1) . ')', $ids);
1976:                 }
1977:             } else {
1978:                 if (is_array($rhs)) {
1979:                     return $lhs . ' IN ' . implode(', ', $rhs);
1980:                 } else {
1981:                     return $lhs . ' IN ' . $rhs;
1982:                 }
1983:             }
1984: 
1985:         case 'LIKE':
1986:             if ($type == 'pgsql' || $type == 'pdo_pgsql') {
1987:                 $query = '%s ILIKE %s';
1988:             } else {
1989:                 $query = 'LOWER(%s) LIKE LOWER(%s)';
1990:             }
1991:             if ($bind) {
1992:                 if (empty($params['begin'])) {
1993:                     return array(sprintf($query,
1994:                                          self::escapePrepare($lhs),
1995:                                          '?'),
1996:                                  array('%' . $rhs . '%'));
1997:                 } else {
1998:                     return array(sprintf('(' . $query . ' OR ' . $query . ')',
1999:                                          self::escapePrepare($lhs),
2000:                                          '?',
2001:                                          self::escapePrepare($lhs),
2002:                                          '?'),
2003:                                  array($rhs . '%', '% ' . $rhs . '%'));
2004:                 }
2005:             } else {
2006:                 if (empty($params['begin'])) {
2007:                     return sprintf($query,
2008:                                    $lhs,
2009:                                    $dbh->quote('%' . $rhs . '%'));
2010:                 } else {
2011:                     return sprintf('(' . $query . ' OR ' . $query . ')',
2012:                                    $lhs,
2013:                                    $dbh->quote($rhs . '%'),
2014:                                    $lhs,
2015:                                    $dbh->quote('% ' . $rhs . '%'));
2016:                 }
2017:             }
2018: 
2019:         default:
2020:             if ($bind) {
2021:                 return array($lhs . ' ' . self::escapePrepare($op) . ' ?', array($rhs));
2022:             } else {
2023:                 return $lhs . ' ' . $op . ' ' . $dbh->quote($rhs);
2024:             }
2025:         }
2026:     }
2027: 
2028:     /**
2029:      * Escapes all characters in a string that are placeholders for the
2030:      * prepare/execute methods of the DB package.
2031:      *
2032:      * @param string $query  A string to escape.
2033:      *
2034:      * @return string  The correctly escaped string.
2035:      */
2036:     static public function escapePrepare($query)
2037:     {
2038:         return preg_replace('/[?!&]/', '\\\\$0', $query);
2039:     }
2040: }
2041: 
API documentation generated by ApiGen