Sindbad~EG File Manager

Current Path : /var/www/web3/modules/core/classes/GalleryStorage/
Upload File :
Current File : /var/www/web3/modules/core/classes/GalleryStorage/GalleryStorageExtras.class

<?php
/*
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2007 Bharat Mediratta
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA  02110-1301, USA.
 */

/**
 * Extended functionality in GalleryStorage that's not generally required for
 * simply viewing albums and photos.
 * @package GalleryCore
 * @subpackage Storage
 * @author Bharat Mediratta <bharat@menalto.com>
 * @version $Revision: 15954 $
 */
class GalleryStorageExtras /* the other half of GalleryStorage */ {
    /**
     * @param object GalleryStorage $galleryStorage the database storage instance
     */
    function GalleryStorageExtras(&$galleryStorage) {
	$this->_gs =& $galleryStorage;
    }

    /**
     * Return a non transactional database connection.
     * On occasion we'll need a non-transactional connection to do things like locking and
     * sequence handling, since they have to be consistent across may concurrent requests.
     * @return array object GalleryStatus a status code
     *               object ADOdb a database connection
     */
    function _getNonTransactionalDatabaseConnection() {
	if ($this->_gs->_isTransactional) {
	    if (empty($this->_gs->_nonTransactionalDb)) {
		list ($ret, $this->_gs->_nonTransactionalDb) = $this->_gs->_getConnection(true);
		if ($ret) {
		    return array($ret, null);
		}
	    }
	    return array(null, $this->_gs->_nonTransactionalDb);
	} else {
	    $ret = $this->_dbInit();
	    if ($ret) {
		return array($ret, null);
	    }
	    return array(null, $this->_gs->_db);
	}
    }

    /**
     * Connect to database if needed and optionally guarantee db transaction.
     * @return object GalleryStatus a status code
     * @access private
     */
    function _dbInit($transaction=false) {
	if (!isset($this->_gs->_db)) {
	    list ($ret, $this->_gs->_db) = $this->_gs->_getConnection();
	    if ($ret) {
		return $ret;
	    }
	}
	if ($transaction) {
	    $ret = $this->_gs->_guaranteeTransaction();
	    if ($ret) {
		return $ret;
	    }
	}
	return null;
    }

    /**
     * @see GalleryStorage::loadEntities
     */
    function loadEntities($ids) {
	global $gallery;
	$ret = $this->_dbInit();
	if ($ret) {
	    return array($ret, null);
	}

	foreach ($ids as $idx => $id) {
	    $ids[$idx] = (int)$id;
	}

	/* Identify all the ids at once */
	list ($ret, $types) = $this->_identifyEntities($ids);
	if ($ret) {
	    return array($ret, null);
	}

	/* Separate the ids by type */
	$classNames = array();
	$gallery->guaranteeTimeLimit(5);
	for ($i = 0; $i < count($ids); $i++) {
	    if (empty($types[$i])) {
		return array(GalleryCoreApi::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__,
						  "Missing object for id $ids[$i]"), null);
	    }
	    $classNames[$types[$i]][$ids[$i]] = 1;
	}

	/* Load them in groups */
	foreach ($classNames as $className => $targetIdHash) {
	    $gallery->guaranteeTimeLimit(5);

	    /* Get unique target ids */
	    $targetIds = array_keys($targetIdHash);

	    /* Get our member info for this class */
	    list ($ret, $memberInfo) = $this->describeEntity($className);
	    if ($ret) {
		return array($ret, null);
	    }

	    $idCol = $this->_gs->_translateColumnName('id');

	    /* Build up our query */
	    $columns = $tables = $where = $memberData = $callbacks = array();
	    $markers = GalleryUtilities::makeMarkers(count($targetIds));
	    $target = $className;
	    while ($target) {
		foreach ($memberInfo[$target]['members'] as $columnName => $columnInfo) {
		    list ($tableName, $unused) = $this->_gs->_translateTableName($target);
		    $memberData[] = $columnInfo;
		    $callbacks[] = $columnName;
		    $columns[$tableName . '.' . $this->_gs->_translateColumnName($columnName)] = 1;
		    $tables[$tableName] = 1;
		}
		$target = $memberInfo[$target]['parent'];
	    }
	    $tables = array_keys($tables);
	    $columns = array_keys($columns);

	    for ($i = 0; $i < count($tables); $i++) {
		if ($i == 0) {
		    $where[] = $tables[$i] . '.' . $idCol . ' IN (' . $markers . ')';
		} else {
		    $where[] = $tables[$i] . '.' . $idCol . '=' . $tables[0] . '.' . $idCol;
		}
	    }

	    $query = 'SELECT ';
	    $query .= implode(', ', $columns);
	    $query .= ' FROM ';
	    $query .= implode(', ', $tables);
	    $query .= ' WHERE ';
	    $query .= implode(' AND ', $where);

	    /* Execute the query */
	    $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;

	    $this->_gs->_traceStart();
	    $recordSet = $this->_gs->_db->Execute($query, $targetIds);
	    $this->_gs->_traceStop();
	    if ($recordSet) {
		if ($recordSet->RecordCount() != count($targetIds)) {
		    return array(GalleryCoreApi::error(ERROR_MISSING_OBJECT),
				 null);
		}

		/* Process all the results */
		$j = 0;
		while ($row = $recordSet->FetchRow()) {
		    if (++$j % 20 == 0) {
			$gallery->guaranteeTimeLimit(5);
		    }

		    if (!class_exists($className)) {
			GalleryCoreApi::requireOnce(
			    "modules/{$memberInfo[$className]['module']}/classes/$className.class");
		    }
		    $entity = new $className;

		    if (empty($entity)) {
			return array(GalleryCoreApi::error(ERROR_BAD_DATA_TYPE),
				     null);
		    }

		    for ($i = 0; $i < count($callbacks); $i++) {
			$value = $this->_gs->_normalizeValue($row[$i], $memberData[$i], true);

			/* Store the value in the object */
			$entity->$callbacks[$i] = $value;
			$entity->_persistentStatus['originalValue'][$callbacks[$i]] = $value;
		    }

		    $entity->resetOriginalValues();
		    $entities[$entity->id] = $entity;
		}

		$recordSet->Close();
	    } else {
		return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE), null);
	    }
	}

	/* Assemble the entities in the right order and return them */
	$result = array();
	foreach ($ids as $id) {
	    $result[] = $entities[$id];
	}

	return array(null, $result);
    }

    /**
     * @see GalleryStorage::saveEntity
     */
    function saveEntity(&$entity) {
	$ret = $this->_dbInit(true);
	if ($ret) {
	    return $ret;
	}

	/* Update the serial number, but remember the original one */
	$originalSerialNumber = (int)$entity->serialNumber++;

	/* Get our member info for this class */
	list ($ret, $memberInfo) = $this->describeEntity($entity->entityType);
	if ($ret) {
	    return $ret;
	}
	$idColumn = null;

	/*
	 * Build up a complete picture of all the various changed fields, so
	 * that we can do an insert or update.
	 */
	$dataTable = array();
	$id = array();
	$target = $entity->getEntityType();

	while ($target) {
	    foreach ($memberInfo[$target]['members'] as $memberName => $memberData) {
		$type = $memberData['type'];
		list ($tableName, $unused) = $this->_gs->_translateTableName($target);

		/* If the member is modified, record the new value in our table */
		if ($entity->isModified($memberName)) {
		    $value = $entity->$memberName;

		    $entity->$memberName = $value =
			$this->_gs->_normalizeValue($value, $memberData);

		    $columnName = $this->_gs->_translateColumnName($memberName);
		    $dataTable[$tableName][$columnName] = $value;
		} else {
		    /*
		     * If we haven't set up a table for this class, do so now.
		     * Otherwise we don't have a complete list of tables that we
		     * need to insert into in order for this class to be completely
		     * serialized.
		     */
		    if (!isset($dataTable[$tableName])) {
			$dataTable[$tableName] = array();
		    }
		}

		if ($type & STORAGE_TYPE_ID) {
		    $value = $entity->$memberName;

		    $id['column'] = $this->_gs->_translateColumnName($memberName);
		    $id['value'] = $value;
		}
	    }
	    $target = $memberInfo[$target]['parent'];
	}

	if ($entity->testPersistentFlag(STORAGE_FLAG_NEWLY_CREATED)) {
	    /*
	     * Iterate through the data table and make up an INSERT statement
	     * for each table that requires one.
	     */
	    foreach ($dataTable as $tableName => $columnChanges) {

		/* Make sure that the id column is set for each table */
		if (empty($columnChanges[$id['column']])) {
		    $columnChanges[$id['column']] = $id['value'];
		}

		$columns = array_keys($columnChanges);
		$data = array_values($columnChanges);
		$markers = GalleryUtilities::makeMarkers(count($columnChanges));
		$query = 'INSERT INTO ' . $tableName . ' (';
		$query .= implode(', ', $columns);
		$query .= ') VALUES (' . $markers . ')';

		$this->_gs->_traceStart();
		$recordSet = $this->_gs->_db->Execute($query, $data);
		$this->_gs->_traceStop();

		if (!$recordSet) {
		    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
		}
	    }
	} else {
	    /*
	     * Iterate through the data table and make an UPDATE statement for
	     * each table that requires one.  Make sure that we do the table
	     * that has the serial number in it first, as we use the serial
	     * number to make sure that we're not hitting a concurrency issue.
	     */
	    list ($serialNumberTable) = $this->_gs->_translateTableName('GalleryEntity');

	    $queryList = array();
	    foreach ($dataTable as $tableName => $columnChanges) {
		$changeList = array();
		$data = array();

		foreach ($columnChanges as $columnName => $value) {
		    $changeList[] = $columnName . '=?';
		    $data[] = $value;
		}

		if (count($changeList)) {
		    $query = 'UPDATE ' . $tableName  .  ' SET';
		    $query .= ' ' . implode(',', $changeList);
		    $query .= ' WHERE ' . $id['column'] . '=?';
		    $data[] = $id['value'];

		    if (!strcmp($tableName, $serialNumberTable)) {
			$query .= ' AND ' .
				$this->_gs->_translateColumnName('serialNumber') .
				'=?';
			$data[] = $originalSerialNumber;
			array_unshift($queryList, array($query, $data));
		    } else {
			array_push($queryList, array($query, $data));
		    }
		}
	    }

	    /*
	     * Now apply each UPDATE statement in turn.  Make sure that we're
	     * only affecting one row each time.
	     */
	    foreach ($queryList as $queryAndData) {
		list ($query, $data) = $queryAndData;

		$this->_gs->_traceStart();
		$recordSet = $this->_gs->_db->Execute($query, $data);
		$this->_gs->_traceStop();

		if (!$recordSet) {
		    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
		} else {
		    $this->_gs->_traceStart();
		    $affectedRows = $this->_gs->_db->Affected_Rows();
		    $this->_gs->_traceStop();
		    if ($affectedRows == 0) {
			return GalleryCoreApi::error(ERROR_OBSOLETE_DATA, __FILE__, __LINE__,
						    "$query (" . implode('|', $data) . ')');
		    } else if ($affectedRows > 1) {
			/* We just updated more than one row!  What do we do now? */
			return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__,
					      "$query (" . implode('|', $data) . ") $affectedRows");
		    }
		}
	    }
	}

	$entity->clearPersistentFlag(STORAGE_FLAG_NEWLY_CREATED);
	$entity->resetOriginalValues();
	$ret = $entity->onSave();
	if ($ret) {
	    return $ret;
	}

	return null;
    }

    /**
     * @see GalleryStorage::deleteEntity
     */
    function deleteEntity(&$entity) {
	$ret = $this->_dbInit(true);
	if ($ret) {
	    return $ret;
	}

	/* If this object has not yet been saved in the database, don't bother saving it. */
	if ($entity->testPersistentFlag(STORAGE_FLAG_NEWLY_CREATED)) {
	    $entity->clearPersistentFlag(STORAGE_FLAG_NEWLY_CREATED);
	    $entity->setPersistentFlag(STORAGE_FLAG_DELETED);
	    return null;
	}

	/* Get our persistent and member info for this class */
	list ($ret, $memberInfo) = $this->describeEntity($entity->entityType);
	if ($ret) {
	    return $ret;
	}

	$idCol = $this->_gs->_translateColumnName('id');

	$tables = array();
	$target = $entity->entityType;
	while ($target) {
	    foreach ($memberInfo[$target]['members'] as $columnName => $columnInfo) {
		list ($tableName, $unused) = $this->_gs->_translateTableName($target);
		$tables[$tableName] = 1;
	    }
	    $target = $memberInfo[$target]['parent'];
	}

	/*
	 * XXX OPT:  Override this for specific database implementations that
	 * allow multi-table delete.
	 */
	foreach ($tables as $tableName => $junk) {
	    $query = 'DELETE FROM ' . $tableName . ' WHERE ' . $idCol . '=?';
	    $data = array((int)$entity->getId());

	    $this->_gs->_traceStart();
	    $recordSet = $this->_gs->_db->Execute($query, $data);
	    $this->_gs->_traceStop();
	    if (!$recordSet) {
		return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
	    }
	}

	$entity->setPersistentFlag(STORAGE_FLAG_DELETED);

	return null;
    }

    /**
     * @see GalleryStorage::newEntity
     */
    function newEntity(&$entity) {
	$ret = $this->_dbInit();
	if ($ret) {
	    return $ret;
	}

	list ($ret, $id) = $this->_gs->getUniqueId();
	if ($ret) {
	    return $ret;
	}

	$entity->id = $id;
	$entity->serialNumber = 0;
	$entity->setPersistentFlag(STORAGE_FLAG_NEWLY_CREATED);

	return null;
    }

    /**
     * @see GalleryStorage::getUniqueId
     */
    function getUniqueId() {
	$ret = $this->_dbInit();
	if ($ret) {
	    return array($ret, null);
	}

	/*
	 * Wrap _getUniqueIdWithConnection with the current connection.  This allows
	 * subclasses to use a different connection if necessary, like MySQL which
	 * wants a non-transactional connection.
	 */
	return $this->_getUniqueIdWithConnection($this->_gs->_db);
    }

    /**
     * @see GalleryStorage::getUniqueId
     */
    function _getUniqueIdWithConnection($dbConn) {
	/* In case we're embedded in an app that sets adodb hasGenID to false (xaraya/postnuke) */
	if (isset($dbConn->hasGenID) && !$dbConn->hasGenID) {
	    $dbConn->hasGenID = $setGenID = true;
	}

	/* Get the id of the next object from our sequence */
	$this->_gs->_traceStart();
	$id = (int)$dbConn->GenId($this->_gs->_tablePrefix . DATABASE_SEQUENCE_ID);
	$this->_gs->_traceStop();
	if (empty($id)) {
	    return array(GalleryCoreApi::error(
			     ERROR_STORAGE_FAILURE, __FILE__, __LINE__, 'Empty sequence id!'),
			 null);
	}

	if (isset($setGenID)) {
	    $dbConn->hasGenID = false;
	}

	return array(null, $id);
    }

    /**
     * @see GalleryStorage::refreshEntity
     */
    function refreshEntity($entity) {
	$ret = $this->_dbInit();
	if ($ret) {
	    return array($ret, null);
	}

	/*
	 * We could check the serial number against the database, or check to
	 * see if the entity is modified in order to figure out whether or not
	 * we should refresh.  But either way that requires a database hit so we
	 * might as well just retrieve the record every time
	 */
	list ($ret, list ($freshEntity)) = $this->_gs->loadEntities(array($entity->id));
	if ($ret) {
	    return array($ret, null);
	}

	/* Let entity do its post-load procedure */
	$ret = $freshEntity->onLoad();
	if ($ret) {
	    return array($ret, null);
	}

	return array(null, $freshEntity);
    }

    /**
     * @see GalleryStorage::acquireReadLock
     */
    function acquireReadLock($entityIds, $timeout) {
	/* It's ok to pass in a single id */
	if (!is_array($entityIds)) {
	    $entityIds = array($entityIds);
	}

	foreach ($entityIds as $idx => $id) {
	    $entityIds[$idx] = (int)$id;
	}

	/* Acquire a non-transactional connection to use for this request */
	list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
	if ($ret) {
	    return array($ret, null);
	}

	/* Know when to call it quits */
	$cutoffTime = time() + $timeout;

	/* Get the true name of the lock table */
	list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');

	/*
	 * Algorithm:
	 * 1. Get clearance to acquire locks (and get the lock id)
	 * 2. If any of the entities that we want to lock are currently write
	 *    locked, then clear the request and go back to step 1.
	 * 3. Acquire our read locks
	 */
	while (true) {
	    list ($ret, $lockId) = $this->_getLockClearance($cutoffTime);
	    if ($ret) {
		return array($ret, null);
	    }

	    /* Check to see if any of the ids that we care about are write locked */
	    $writeEntityIdCol = $this->_gs->_translateColumnName('writeEntityId');
	    $markers = GalleryUtilities::makeMarkers(count($entityIds));
	    $query = 'SELECT COUNT(*) FROM ' . $lockTable
		   . ' WHERE ' . $writeEntityIdCol . ' IN (' . $markers . ')';
	    $data = $entityIds;

	    $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;

	    $this->_gs->_traceStart();
	    $recordSet = $db->Execute($query, $data);
	    $this->_gs->_traceStop();
	    if (!$recordSet) {
		$this->releaseLocks($lockId);
		return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE), null);
	    }

	    $row = $recordSet->FetchRow();
	    if ($row[0] == 0 ) {
		/* Success */
		break;
	    } else {
		/* An entity that we want is write locked */
		$this->releaseLocks($lockId);

		if (time() > $cutoffTime) {
		    return array(GalleryCoreApi::error(ERROR_LOCK_TIMEOUT),
				 null);
		}

		/* Wait a second and try again */
		sleep(1);

		/* Expire any bogus locks */
		$ret = $this->_expireLocks();
		if ($ret) {
		    return array($ret, null);
		}
	    }
	}

	/* Put in a read lock for every entity id */
	$lockIdCol = $this->_gs->_translateColumnName('lockId');
	$readEntityIdCol = $this->_gs->_translateColumnName('readEntityId');
	$freshUntilCol = $this->_gs->_translateColumnName('freshUntil');
	$freshUntil = time() + 30;
	$lockInfo = array();
	foreach ($entityIds as $entityId) {
	    $query = sprintf('INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)',
			     $lockTable, $lockIdCol, $readEntityIdCol, $freshUntilCol);
	    $data = array($lockId, $entityId, $freshUntil);

	    $this->_gs->_traceStart();
	    $recordSet = $db->Execute($query, $data);
	    $this->_gs->_traceStop();
	    if (!$recordSet) {
		$this->releaseLocks($lockId);
		return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE), null);
	    }
	    $lockInfo[$entityId] = true;
	}

	/* Drop the lock request, now that we've got the read locks */
	$requestCol = $this->_gs->_translateColumnName('request');
	$query = 'DELETE FROM ' . $lockTable
	       . ' WHERE ' . $lockIdCol . '=? AND ' . $requestCol . '=1';
	$data = array($lockId);

	$this->_gs->_traceStart();
	$recordSet = $db->Execute($query, $data);
	$this->_gs->_traceStop();
	if (!$recordSet) {
	    $this->releaseLocks($lockId);
	    return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE), null);
	}

	return array(null,
		     array('lockId' => $lockId, 'type' => LOCK_READ, 'ids' => $lockInfo));
    }

    /**
     * @see GalleryStorage::acquireWriteLock
     */
    function acquireWriteLock($entityIds, $timeout) {
	/* It's ok to pass in a single id */
	if (!is_array($entityIds)) {
	    $entityIds = array($entityIds);
	}

	foreach ($entityIds as $idx => $id) {
	    $entityIds[$idx] = (int)$id;
	}

	/* Acquire a non-transactional connection to use for this request */
	list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
	if ($ret) {
	    return array($ret, null);
	}

	/* Know when to call it quits */
	$cutoffTime = time() + $timeout;

	/* Get the true name of the lock table */
	list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');

	/*
	 * Algorithm:
	 * 1. Get clearance to acquire locks (and get the lock id)
	 * 2. If any of the entities that we want to lock are currently locked,
	 *    then clear the request and go back to step 1.
	 * 3. Acquire our write locks
	 */
	while (true) {
	    list ($ret, $lockId) = $this->_getLockClearance($cutoffTime);
	    if ($ret) {
		return array($ret, null);
	    }
	    $lockId = (int)$lockId;

	    /* Check to see if any of the ids that we care about are locked */
	    $readEntityIdCol = $this->_gs->_translateColumnName('readEntityId');
	    $writeEntityIdCol = $this->_gs->_translateColumnName('writeEntityId');
	    $markers = GalleryUtilities::makeMarkers(count($entityIds));
	    $query = 'SELECT COUNT(*) FROM ' . $lockTable . ' ' .
		    'WHERE ' . $readEntityIdCol . ' IN (' . $markers . ') ' .
		    'OR ' . $writeEntityIdCol . ' IN (' . $markers . ')';
	    $data = $entityIds;
	    $data = array_merge($data, $entityIds);

	    $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;

	    $this->_gs->_traceStart();
	    $recordSet = $db->Execute($query, $data);
	    $this->_gs->_traceStop();
	    if (!$recordSet) {
		$this->releaseLocks($lockId);
		return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE), null);
	    }

	    $row = $recordSet->FetchRow();
	    if ($row[0] == 0 ) {
		/* Success */
		break;
	    } else {
		/* An entity that we want is still locked */
		$this->releaseLocks($lockId);

		if (time() > $cutoffTime) {
		    return array(GalleryCoreApi::error(ERROR_LOCK_TIMEOUT),
				 null);
		}

		/* Wait a second and try again */
		sleep(1);

		/* Expire any bogus locks */
		$ret = $this->_expireLocks();
		if ($ret) {
		    return array($ret, null);
		}
	    }
	}

	/* We are approved to acquire our write locks */
	$lockIdCol = $this->_gs->_translateColumnName('lockId');
	$writeEntityIdCol = $this->_gs->_translateColumnName('writeEntityId');
	$freshUntilCol = $this->_gs->_translateColumnName('freshUntil');
	$freshUntil = time() + 30;
	$lockInfo = array();
	foreach ($entityIds as $entityId) {
	    $query = sprintf('INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)',
			     $lockTable, $lockIdCol, $writeEntityIdCol, $freshUntilCol);
	    $data = array($lockId, $entityId, $freshUntil);

	    $this->_gs->_traceStart();
	    $recordSet = $db->Execute($query, $data);
	    $this->_gs->_traceStop();
	    if (!$recordSet) {
		$this->releaseLocks($lockId);
		return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE), null);
	    }
	    $lockInfo[$entityId] = true;
	}

	/* Drop the lock request, now that we've got the write locks */
	$requestCol = $this->_gs->_translateColumnName('request');
	$query = 'DELETE FROM ' . $lockTable
	       . ' WHERE ' . $lockIdCol . '=? AND ' . $requestCol . '=1';
	$data = array($lockId);

	$this->_gs->_traceStart();
	$recordSet = $db->Execute($query, $data);
	$this->_gs->_traceStop();
	if (!$recordSet) {
	    $this->releaseLocks($lockId);
	    return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE), null);
	}

	return array(null,
		     array('lockId' => $lockId, 'type' => LOCK_WRITE, 'ids' => $lockInfo));
    }

    /**
     * @see GalleryStorage::refreshLocks
     */
    function refreshLocks($lockIds, $freshUntil) {
	if (!empty($lockIds)) {
	    foreach ($lockIds as $idx => $id) {
		$lockIds[$idx] = (int)$id;
	    }

	    /* Acquire a non-transactional connection to use for this request */
	    list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
	    if ($ret) {
		return $ret;
	    }

	    list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
	    $lockIdCol = $this->_gs->_translateColumnName('lockId');
	    $freshUntilCol = $this->_gs->_translateColumnName('freshUntil');
	    $lockIdMarkers = GalleryUtilities::makeMarkers(count($lockIds));
	    $query = sprintf('UPDATE %s SET %s = ? WHERE %s in (%s)',
			     $lockTable, $freshUntilCol, $lockIdCol, $lockIdMarkers);

	    $this->_gs->_traceStart();
	    $data = array_merge(array($freshUntil), $lockIds);
	    $recordSet = $db->Execute($query, $data);
	    $this->_gs->_traceStop();

	    if (!$recordSet) {
		return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
	    }
	}

	return null;
    }

    /**
     * Delete all not-so-fresh locks.
     * @return object GalleryStatus a status code
     * @access private
     */
    function _expireLocks() {
	/* Acquire a non-transactional connection to use for this request */
	list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
	if ($ret) {
	    return $ret;
	}

	list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
	$freshUntilCol = $this->_gs->_translateColumnName('freshUntil');
	$query = sprintf('DELETE FROM %s WHERE %s < ?',
			 $lockTable, $freshUntilCol);

	$this->_gs->_traceStart();
	$recordSet = $db->Execute($query, array(time()));
	$this->_gs->_traceStop();

	if (!$recordSet) {
	    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
	}

	return null;
    }

    /**
     * @see GalleryStorage::releaseLocks
     */
    function releaseLocks($lockIds) {
	if (!is_array($lockIds)) {
	    $lockIds = array($lockIds);
	}
	foreach ($lockIds as $idx => $id) {
	    $lockIds[$idx] = (int)$id;
	}

	/* Acquire a non-transactional connection to use for this request */
	list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
	if ($ret) {
	    return $ret;
	}

	/* Get the true name of the lock table */
	list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');

	$lockIdCol = $this->_gs->_translateColumnName('lockId');
	$markers = GalleryUtilities::makeMarkers(count($lockIds));
	$query = 'DELETE FROM ' . $lockTable . ' WHERE ' . $lockIdCol . ' IN (' . $markers . ')';

	$this->_gs->_traceStart();
	$recordSet = $db->Execute($query, $lockIds);
	$this->_gs->_traceStop();
	if ($recordSet) {
	    return null;
	} else {
	    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
	}
    }

    /**
     * @see GalleryStorage::removeIdsFromLock
     */
    function removeIdsFromLock($lock, $ids) {
	list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
	if ($ret) {
	    return $ret;
	}

	$query = '
	DELETE FROM [Lock]
	WHERE [::lockId] = ?
	  AND [::' . ($lock['type'] == LOCK_WRITE ? 'write' : 'read') . 'EntityId] IN ('
	    . GalleryUtilities::makeMarkers(count($ids)) . ')';
	$query = $this->_gs->_translateQuery($query);

	$this->_gs->_traceStart();
	$recordSet = $db->Execute($query, array_merge(array($lock['lockId']), $ids));
	$this->_gs->_traceStop();
	if ($recordSet) {
	    return null;
	} else {
	    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
	}
    }

    /**
     * @see GalleryStorage::moveIdsBetweenLocks
     */
    function moveIdsBetweenLocks($relock, $newLockId, $lockType) {
	list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
	if ($ret) {
	    return $ret;
	}

	$query = '
	UPDATE [Lock] SET [::lockId] = ? WHERE [::lockId] = ? AND [::'
	    . ($lockType == LOCK_WRITE ? 'write' : 'read') . 'EntityId] IN (';
	$query = $this->_gs->_translateQuery($query);
	foreach ($relock as $lockId => $ids) {
	    $this->_gs->_traceStart();
	    $recordSet = $db->Execute($query . GalleryUtilities::makeMarkers(count($ids)) . ')',
				      array_merge(array($newLockId, $lockId), $ids));
	    $this->_gs->_traceStop();
	    if (!$recordSet) {
		return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
	    }
	}
	return null;
    }

    /**
     * @see GalleryStorage::newLockId
     */
    function newLockId() {
	list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
	if ($ret) {
	    return array($ret, null);
	}

	$this->_gs->_traceStart();
	$lockId = $db->GenId($this->_gs->_tablePrefix . DATABASE_SEQUENCE_LOCK);
	$this->_gs->_traceStop();

	if (empty($lockId)) {
	    return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE,
					       __FILE__, __LINE__, 'Empty lock sequence id!'),
			 null);
	}

	return array(null, (int)$lockId);
    }

    /**
     * @see GalleryStorage::execute
     */
    function execute($statement, $data=array()) {
	$ret = $this->_dbInit(true);
	if ($ret) {
	    return $ret;
	}

	$statement = $this->_gs->_translateQuery($statement);

	$this->_gs->_traceStart();
	$recordSet = $this->_gs->_db->Execute($statement, $data);
	$this->_gs->_traceStop();

	/* Direct SQL commands can undermine our memory cache, so reset it */
	GalleryDataCache::reset();

	return $recordSet ? null
			  : GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
    }

    /**
     * @see GalleryStorage::addMapEntry
     */
    function addMapEntry($mapName, $entry) {
	$ret = $this->_dbInit(true);
	if ($ret) {
	    return $ret;
	}

	list ($ret, $mapInfo) = $this->_gs->describeMap($mapName);
	if ($ret) {
	    return $ret;
	}
	list ($tableName) = $this->_gs->_translateTableName($mapName);
	$data = $columns = array();
	foreach ($mapInfo as $memberName => $memberData) {
	    if (!array_key_exists($memberName, $entry)) {
		return GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
					     'Missing parameter: ' . $memberName);
	    }

	    if (is_array($entry[$memberName])) {
		return $this->_addMapEntries($mapInfo, $tableName, $entry);
	    }
	    $columns[] = $this->_gs->_translateColumnName($memberName);
	    $data[] = $this->_gs->_normalizeValue($entry[$memberName], $memberData);
	}

	$markers = GalleryUtilities::makeMarkers(count($columns));
	$query = 'INSERT INTO ' . $tableName . ' (';
	$query .= implode(', ', $columns);
	$query .= ') VALUES (' . $markers . ')';

	$this->_gs->_traceStart();
	$recordSet = $this->_gs->_db->Execute($query, $data);
	$this->_gs->_traceStop();
	if ($recordSet === false) {
	    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
	}

	return null;
    }

    /**
     * Add new entries to a map. This utility takes the values from entry array
     * e.g., (parm1 => (p1val1, p1val2, p1val3), parm2 => (p2val1, p2val2, p2val3))
     * and inserts them into the map similar to the following:
     * INSERT INTO ... (PARM1, PARM2) VALUES (p1val1, p2val1), (p1val2, p2val2) ...
     *
     * @param array $mapInfo map we're working on
     * @param string $tableName the translated table name
     * @param array $entry an associative array of data about the entry
     *              each data element is an array of values
     * @return object GalleryStatus a status code
     * @access private
     */
    function _addMapEntries($mapInfo, $tableName, $entry) {
	$columns = array();
	foreach ($mapInfo as $memberName => $memberData) {
	    $columns[$memberName] = $this->_gs->_translateColumnName($memberName);
	}

	/* Now we transpose the entry matrix */
	$rows = count($entry[$memberName]);
	$data = array();
	for ($ind = 0; $ind < $rows; $ind++) {
	    foreach ($mapInfo as $memberName => $memberData) {
		$data[] = $this->_gs->_normalizeValue($entry[$memberName][$ind], $memberData);
	    }
	}

	list ($ret, $query) =
	    $this->_gs->getFunctionSql('MULTI_INSERT', array($tableName, $columns, $rows));
	if ($ret) {
	    return $ret;
	}

	$this->_gs->_traceStart();
	$recordSet = $this->_gs->_db->Execute($query, $data);
	$this->_gs->_traceStop();
	if ($recordSet === false) {
	    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
	}

	return null;
    }

    /**
     * @see GalleryStorage::removeMapEntry
     */
    function removeMapEntry($mapName, $match) {
	$ret = $this->_dbInit(true);
	if ($ret) {
	    return $ret;
	}

	list ($ret, $mapInfo) = $this->_gs->describeMap($mapName);
	if ($ret) {
	    return $ret;
	}
	list ($tableName, $unused) = $this->_gs->_translateTableName($mapName);
	$data = $where = array();

	foreach ($mapInfo as $memberName => $memberData) {
	    if (array_key_exists($memberName, $match)) {
		if (GalleryUtilities::isA($match[$memberName], 'GallerySqlFragment')) {
		    $where[] = $this->_gs->_translateColumnName($memberName) . ' '
			     . $this->_gs->_translateQuery($match[$memberName]->getFragment());
		    foreach ($match[$memberName]->getValues() as $value) {
			$data[] = $value;
		    }
		} else if (is_array($match[$memberName])) {
		    $qs = array();
		    foreach ($match[$memberName] as $value) {
			$qs[] = '?';
			$data[] = $this->_gs->_normalizeValue($value, $memberData);
		    }
		    $where[] = $this->_gs->_translateColumnName($memberName) . ' IN ('
			     . implode(',', $qs) . ')';
		} else {
		    $value = $this->_gs->_normalizeValue($match[$memberName], $memberData);
		    if (is_null($value)) {
			$where[] = $this->_gs->_translateColumnName($memberName) . ' IS NULL';
		    } else {
			$where[] = $this->_gs->_translateColumnName($memberName) . '=?';
			$data[] = $value;
		    }
		}
	    }
	}

	if (empty($where)) {
	    return GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
					'Missing where clause');
	}

	$query = 'DELETE FROM ' . $tableName . ' WHERE '  . implode(' AND ', $where);

	$this->_gs->_traceStart();
	$recordSet = $this->_gs->_db->Execute($query, $data);
	$this->_gs->_traceStop();
	if (!$recordSet) {
	    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
	}

	return null;
    }

    /**
     * @see GalleryStorage::removeAllMapEntries
     */
    function removeAllMapEntries($mapName) {
	$ret = $this->_dbInit(true);
	if ($ret) {
	    return $ret;
	}

	list ($tableName) = $this->_gs->_translateTableName($mapName);
	$query = 'DELETE FROM ' . $tableName;

	$this->_gs->_traceStart();
	$recordSet = $this->_gs->_db->Execute($query);
	$this->_gs->_traceStop();
	if ($recordSet === false) {
	    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
	}

	return null;
    }

    /*
     * Load up the table creation and alteration SQL files for the given module
     * @access private
     */
    function _getModuleSql($moduleId) {
	global $gallery;
	$platform =& $gallery->getPlatform();
	$sqlFile = sprintf('%smodules/%s/classes/GalleryStorage/schema.tpl',
	    GalleryCoreApi::getPluginBaseDir('module', $moduleId), $moduleId);

	if ($platform->file_exists($sqlFile)) {
	    $sqlData = $platform->file($sqlFile);
	    $moduleSql = GalleryStorageExtras::parseSqlTemplate($sqlData, $this->_gs->getType());
	} else {
	    $moduleSql = array('table' => array(), 'alter' => array(),
			       'remove' => array(), 'test' => array());
	}

	return array(null, $moduleSql);
    }

    /**
     * Parse the SQL template file and break it down by database and sql file type and return the
     * results in an array.  The best way to see how this is supposed to work is to look in the
     * unit test.
     *
     * @param array $sqlData the raw template data
     * @param string $dbType the database type
     * @return array the parsed results
     * @static
     */
    function parseSqlTemplate($sqlData, $dbType) {
	$info = array('table' => array(), 'alter' => array(),
		      'remove' => array(), 'test' => array());
	$dbname = $tablename = null;
	$record = false;
	foreach ($sqlData as $line) {
	    $line = rtrim($line);
	    if (preg_match('/^## (.*)$/', $line, $matches)) {
		$record = ($matches[1] == $dbType);
		continue;
	    }
	    if (!$record) {
		continue;
	    }

	    if (preg_match('/^# (.*)$/', $line, $matches)) {
		$tablename = $matches[1];
		if (preg_match('/^T_(.*)_(\d+)/', $tablename, $matches)) {
		    if (!isset($info['test'][$matches[1]][$matches[2]])) {
			$info['test'][$matches[1]][$matches[2]] = '';
		    }
		    $insertPointer =& $info['test'][$matches[1]][$matches[2]];
		} else if (preg_match('/^A_(.*)_(\d+)\.(\d+)/', $tablename, $matches)) {
		    if (!isset($info['alter'][$matches[1]][$matches[2]][$matches[3]])) {
			$info['alter'][$matches[1]][$matches[2]][$matches[3]] = '';
		    }
		    $insertPointer =& $info['alter'][$matches[1]][$matches[2]][$matches[3]];
		} else if (preg_match('/^R_(.*)_(\d+)\.(\d+)/', $tablename, $matches)) {
		    if (!isset($info['remove'][$matches[1]][$matches[2]][$matches[3]])) {
			$info['remove'][$matches[1]][$matches[2]][$matches[3]] = '';
		    }
		    $insertPointer =& $info['remove'][$matches[1]][$matches[2]][$matches[3]];
		} else {
		    if (!isset($info['table'][$tablename])) {
			$info['table'][$tablename] = '';
		    }
		    $insertPointer =& $info['table'][$tablename];
		}
		continue;
	    }
	    $insertPointer .= $line . "\n";
	}

	return $info;
    }

    /**
     * @see GalleryStorage::configureStore
     */
    function configureStore($moduleId, $upgradeInfo=array()) {
	global $gallery;
	$gallery->guaranteeTimeLimit(20);

	$this->_clearEntityAndMapCache();

	$ret = $this->_dbInit(true);
	if ($ret) {
	    return $ret;
	}

	list ($ret, $moduleSql) = $this->_getModuleSql($moduleId);
	if ($ret) {
	    return $ret;
	}

	/* Get the metabase info about this database */
	$this->_gs->_traceStart();
	$metatables = $this->_gs->_db->MetaTables();
	$this->_gs->_traceStop();

	/*
	 * Some databases (notably MySQL on Win32) don't support mixed case
	 * table names.  So, when we get the meta table list back, it's lower
	 * case.  Force all metatable listings to lower case and then expect
	 * them to be lowercase so that we're consistent.
	 */
	for ($i = 0; $i < count($metatables); $i++) {
	    $metatables[$i] = strtolower($metatables[$i]);
	}

	/* Do the schema table first */
	list ($schemaTableName, $unused) = $this->_gs->_translateTableName('Schema');
	if (!in_array(strtolower($schemaTableName), $metatables)) {
	    $ret = $this->_executeSql($moduleSql['table']['Schema']);
	    if ($ret) {
		return $ret;
	    }
	    unset($moduleSql['table']['Schema']);

	    /* Create our sequences now */
	    foreach (array(DATABASE_SEQUENCE_LOCK, DATABASE_SEQUENCE_ID) as $sequenceId) {
		$this->_gs->_traceStart();
		$result = $this->_gs->_db->CreateSequence($this->_gs->_tablePrefix . $sequenceId);
		$this->_gs->_traceStop();
		if (empty($result)) {
		    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
		}
	    }
	}

	/* Load all table versions */
	list ($ret, $tableVersions) = $this->_loadTableVersions();
	if ($ret) {
	    return $ret;
	}

	/*
	 * Now take care of the rest of the tables.  If the table doesn't exist, apply the current
	 * table definition.  If it already exists, check to see if there is an upgrade available
	 * for the given table version that we should apply based on $upgradeInfo.
	 */
	foreach ($moduleSql['table'] as $rawTableName => $sql) {
	    $gallery->guaranteeTimeLimit(20);
	    list ($tableName, $unused, $tableNameInSchema) =
		$this->_gs->_translateTableName($rawTableName);
	    if (!in_array(strtolower($tableName), $metatables)) {
		$ret = $this->_executeSql($sql);
		if ($ret) {
		    return $ret;
		}
	    } else {
		while (1) {
		    /* The table exists -- see if we have an upgrade for it */
		    if (empty($tableVersions[$tableNameInSchema])) {
			/*
			 * We've found a SQL file that matches a table in the
			 * database, but has no matching version info in the
			 * schema table.  How can this be?  Leave it alone.
			 */
			if ($gallery->getDebug()) {
			    $gallery->debug("Table $rawTableName: missing entry in Schema table");
			}
			break;
		    }

		    /* If we locate an appropriate upgrade, apply it. */
		    list ($major, $minor) = $tableVersions[$tableNameInSchema];
		    if (!empty($moduleSql['alter'][$rawTableName][$major][$minor]) &&
			    in_array("$rawTableName:$major.$minor", $upgradeInfo)) {
			$sql = $moduleSql['alter'][$rawTableName][$major][$minor];
			$ret = $this->_executeSql($sql);
			if ($ret) {
			    return $ret;
			}

			/* Remember altered tables for post-upgrade optimizations */
			$altered = array($rawTableName);
			$cacheKey = 'GalleryStorage::configureStore::alter';
			if (GalleryDataCache::containsKey($cacheKey)) {
			    $altered = array_merge(GalleryDataCache::get($cacheKey), $altered);
			}
			GalleryDataCache::put($cacheKey, $altered);

			/* Reload all table versions, cause one has now changed */
			list ($ret, $tableVersions) = $this->_loadTableVersions();
			if ($ret) {
			    return $ret;
			}
		    } else {
			/* No upgrade available */
			break;
		    }
		}
	    }
	}

	return null;
    }

    /**
     * @see GalleryStorage::configureStoreCleanup
     */
    function configureStoreCleanup($moduleId) {
	global $gallery;

	$ret = $this->_dbInit(true);
	if ($ret) {
	    return $ret;
	}

	list ($ret, $moduleSql) = $this->_getModuleSql($moduleId);
	if ($ret) {
	    return $ret;
	}

	/* Get the metabase info about this database */
	$this->_gs->_traceStart();
	$metatables = $this->_gs->_db->MetaTables();
	$this->_gs->_traceStop();

	/*
	 * Some databases (notably MySQL on Win32) don't support mixed case
	 * table names.  So, when we get the meta table list back, it's lower
	 * case.  Force all metatable listings to lower case and then expect
	 * them to be lowercase so that we're consistent.
	 */
	for ($i = 0; $i < count($metatables); $i++) {
	    $metatables[$i] = strtolower($metatables[$i]);
	}

	/* Load all table versions */
	list ($ret, $tableVersions) = $this->_loadTableVersions();
	if ($ret) {
	    return $ret;
	}

	/* Now locate any existing tables that should be removed and drop them. */
	foreach (array_keys($moduleSql['remove']) as $rawTableName) {
	    if ($rawTableName == 'Schema') {
		continue;
	    }

	    list ($tableName, $unused, $tableNameInSchema) =
		$this->_gs->_translateTableName($rawTableName);
	    if (in_array(strtolower($tableName), $metatables)) {
		/* The table exists -- see if we should delete it */
		if (empty($tableVersions[$tableNameInSchema])) {
		    /*
		     * We've found a SQL file that matches a table in the
		     * database, but has no matching version info in the
		     * schema table.  How can this be?  Leave it alone.
		     */
		    if ($gallery->getDebug()) {
			$gallery->debug("Table $rawTableName: missing entry in Schema table");
		    }
		} else {
		    $gallery->guaranteeTimeLimit(20);
		    list ($major, $minor) = $tableVersions[$tableNameInSchema];
		    if (!empty($moduleSql['remove'][$rawTableName][$major][$minor])) {
			$ret = $this->_executeSql(
			    $moduleSql['remove'][$rawTableName][$major][$minor]);
			if ($ret) {
			    return $ret;
			}
		    }
		}
	    }
	}

	return null;
    }

    /**
     * @see GalleryStorage::unconfigureStore
     */
    function unconfigureStore($moduleId) {
	global $gallery;
	$gallery->guaranteeTimeLimit(20);

	$ret = $this->_dbInit(true);
	if ($ret) {
	    return $ret;
	}

	list ($ret, $moduleSql) = $this->_getModuleSql($moduleId);
	if ($ret) {
	    return $ret;
	}

	/* Get the metabase info about this database */
	$this->_gs->_traceStart();
	$metatables = $this->_gs->_db->MetaTables();
	$this->_gs->_traceStop();

	/*
	 * Some databases (notably MySQL on Win32) don't support mixed case
	 * table names.  So, when we get the meta table list back, it's lower
	 * case.  Force all metatable listings to lower case and then expect
	 * them to be lowercase so that we're consistent.
	 */
	for ($i = 0; $i < count($metatables); $i++) {
	    $metatables[$i] = strtolower($metatables[$i]);
	}

	/*
	 * Now take care of the rest of the tables.  If the table doesn't
	 * exist, apply the current table definition.  If it already exists,
	 * check to see if there is an upgrade available for the given table
	 * version.  If so, apply it.
	 */
	list ($schemaTableName, $unused) = $this->_gs->_translateTableName('Schema');
	$schemaColumnName = $this->_gs->_translateColumnName('name');
	foreach ($moduleSql['table'] as $rawTableName => $ignored) {
	    /* Don't drop the schema table, it's part of the core. */
	    if ($rawTableName == 'Schema') {
		continue;
	    }

	    $this->_gs->_traceStart();
	    list ($tableName, $unused, $tableNameInSchema) =
		$this->_gs->_translateTableName($rawTableName);
	    if (in_array(strtolower($tableName), $metatables)) {
		/* Drop the table and yank it from the schema table */
		$dropQuery = sprintf('DROP TABLE %s', $tableName);
		$recordSet = $this->_gs->_db->Execute($dropQuery);
		if (empty($recordSet)) {
		    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
		}
	    }

	    $cleanQuery = sprintf('DELETE FROM %s where %s=?',
				  $schemaTableName, $schemaColumnName);
	    $recordSet = $this->_gs->_db->Execute($cleanQuery, array($tableNameInSchema));
	    if (empty($recordSet)) {
		return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
	    }
	    $this->_gs->_traceStop();
	}

	return null;
    }

    /**
     * Examine the schema table and return the version of all the Gallery tables
     *
     * @return array object GalleryStatus a status code
     *               array (name => (major, minor))
     * @access private
     */
    function _loadTableVersions() {
	$GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;

	list ($schemaTableName) = $this->_gs->_translateTableName('Schema');
	$query = 'SELECT ' . $this->_gs->_translateColumnName('name') . ', '
	       . $this->_gs->_translateColumnName('major') . ', '
	       . $this->_gs->_translateColumnName('minor') . ' FROM ' . $schemaTableName;

	$this->_gs->_traceStart();
	$recordSet = $this->_gs->_db->Execute($query);
	$this->_gs->_traceStop();

	if (empty($recordSet)) {
	    return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__,
					      'Error reading schema table'), null);
	}

	$tableVersions = array();
	while ($row = $recordSet->FetchRow()) {
	    $tableVersions[$row[0]] = array($row[1], $row[2]);
	}

	return array(null, $tableVersions);
    }

    /**
     * Execute a given SQL against the database.  Prefix table and column names
     * as necessary.  Split multiple commands in the file into separate Execute() calls.
     *
     * @return object GalleryStatus a status code
     * @access private
     */
    function _executeSql($buffer) {
	/*
	 * Split the file where semicolons are followed by a blank line..
	 * PL/SQL blocks will have other semicolons, so we can't split on every one.
	 */
	foreach (preg_split('/; *\r?\n *\r?\n/s', $buffer) as $query) {
	    $query = trim($query);
	    if (!empty($query)) {
		$query = str_replace('DB_TABLE_PREFIX', $this->_gs->_tablePrefix, $query);
		$query = str_replace('DB_COLUMN_PREFIX', $this->_gs->_columnPrefix, $query);

		/* Perform database specific replacements */
		$query = strtr($query, $this->_gs->_getSqlReplacements());

		$this->_gs->_traceStart();
		$recordSet = $this->_gs->_db->Execute($query);
		$this->_gs->_traceStop();
		if (empty($recordSet)) {
		    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__,
						"Error trying to run query: $query");
		}
	    }
	}

	return null;
    }

    /**
     * @see GalleryStorage::executeSqlFile
     */
    function executeSqlFile($fileName) {
	global $gallery;
	$platform =& $gallery->getPlatform();

	if (!$platform->file_exists($fileName)) {
	    return GalleryCoreApi::error(ERROR_BAD_PATH, __FILE__, __LINE__,
					 "File $fileName does not exist");
	}

	if (($buffer = $platform->file_get_contents($fileName)) === false) {
	    return GalleryCoreApi::error(ERROR_BAD_PATH, __FILE__, __LINE__,
					 "Unable to read file $fileName");
	}

	$ret = $this->_dbInit(true);
	if ($ret) {
	    return $ret;
	}

	$ret = $this->_executeSql($buffer);
	if ($ret) {
	    return $ret;
	}

	return null;
    }

    /**
     * @see GalleryStorage::cleanStore
     */
    function cleanStore() {
	global $gallery;
	$gallery->guaranteeTimeLimit(20);

	$ret = $this->_dbInit(true);
	if ($ret) {
	    return $ret;
	}

	/* Get the metabase info about this database */
	$this->_gs->_traceStart();
	$metatables = $this->_gs->_db->MetaTables();
	$this->_gs->_traceStop();

	/*
	 * Some databases (notably MySQL on Win32) don't support mixed case
	 * table names.  So, when we get the meta table list back, it's lower
	 * case.  Force all metatable listings to lower case and then expect
	 * them to be lowercase so that we're consistent.
	 */
	for ($i = 0; $i < count($metatables); $i++) {
	    $metatables[$i] = strtolower($metatables[$i]);
	}

	/* If the schema table exists then delete all the tables it lists */
	list ($schemaTableName, $unused) = $this->_gs->_translateTableName('Schema');
	if (in_array(strtolower($schemaTableName), $metatables)) {
	    /* Load all table versions */
	    list ($ret, $tableVersions) = $this->_loadTableVersions();
	    if ($ret) {
		return $ret;
	    }

	    foreach (array_keys($tableVersions) as $rawTableName) {
		list ($tableName, $unused) = $this->_gs->_translateTableName($rawTableName);
		$query = sprintf('DROP TABLE %s', $tableName);

		$this->_gs->_traceStart();
		$recordSet = $this->_gs->_db->Execute($query);
		$this->_gs->_traceStop();
		if (empty($recordSet)) {
		    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
		}
	    }

	    /* Get rid of our sequences */
	    foreach (array(DATABASE_SEQUENCE_LOCK, DATABASE_SEQUENCE_ID) as $sequenceId) {
		$this->_gs->_traceStart();
		$recordSet = $this->_gs->_db->DropSequence($this->_gs->_tablePrefix . $sequenceId);
		$this->_gs->_traceStop();
		if (empty($recordSet)) {
		    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
		}
	    }
	}

	return null;
    }

    /**
     * @see GalleryStorage::getProfilingHtml
     */
    function getProfilingHtml() {
	if (!isset($this->_gs->_db)) {
	    return '';
	}
	$this->_gs->_traceStart();
	$perf =& NewPerfMonitor($this->_gs->_db);
	$buf = $perf->SuspiciousSQL();
	$buf .= $perf->ExpensiveSQL();
	$this->_gs->_traceStop();
	return $buf;
    }

    /**
     * @see GalleryStorage::isInstalled
     */
    function isInstalled() {
	$ret = $this->_dbInit();
	if ($ret) {
	    return array($ret, null);
	}

	/* Get the metabase info about this database */
	$this->_gs->_traceStart();
	$metatables = $this->_gs->_db->MetaTables();
	$this->_gs->_traceStop();

	list ($schemaTableName) = $this->_gs->_translateTableName('Schema');
	$isInstalled = preg_match("/\b$schemaTableName\b/i", implode(' ', $metatables));
	return array(null, $isInstalled);
    }

    /**
     * @see GalleryStorage::optimize
     */
    function optimize($tableNames=null) {
	global $gallery;

	$ret = $this->_dbInit();
	if ($ret) {
	    return $ret;
	}

	/* Load all table versions */
	list ($ret, $tableVersions) = $this->_loadTableVersions();
	if ($ret) {
	    return $ret;
	}

	/* Filter the list of tables if requested */
	$tables = array_keys($tableVersions);
	if (is_array($tableNames)) {
	    $tables = array();
	    foreach ($tableNames as $rawTableName) {
		list ($tableName, $unused, $tableNameInSchema) =
		    $this->_gs->_translateTableName($rawTableName);
		if (isset($tableVersions[$tableNameInSchema])) {
		    $tables[] = $tableNameInSchema;
		}
	    }
	}

	$statements = $this->_gs->_getOptimizeStatements();
	if (!empty($statements)) {
	    foreach ($statements as $statement) {
		foreach ($tables as $tableName) {
		    $query = sprintf($statement, $this->_gs->_tablePrefix . $tableName);
		    $gallery->guaranteeTimeLimit(300);
		    $this->_gs->_traceStart();
		    $recordSet = $this->_gs->_db->Execute($query);
		    $this->_gs->_traceStop();

		    if (!$recordSet) {
			return GalleryCoreApi::error(ERROR_STORAGE_FAILURE);
		    }
		}
	    }
	}

	return null;
    }

    /**
     * @see GalleryStorage::getAffectedRows
     */
    function getAffectedRows() {
	$ret = $this->_dbInit(true);
	if ($ret) {
	    return array($ret, null);
	}

	$this->_gs->_traceStart();
	$affectedRows = $this->_gs->_db->Affected_Rows();
	$this->_gs->_traceStop();

	return array(null, $affectedRows);
    }

    /**
     * Internal function to get clearance to acquire locks
     *
     * Request clearance to acquire locks and then wait until it's our turn.
     *
     * @param int $cutoffTime the time to stop trying to get clearance
     * @return object GalleryStatus a status code
     */
    function _getLockClearance($cutoffTime) {
	/* Get the true name of the lock table */
	list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');

	/* Acquire a non-transactional connection to use for this request */
	list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
	if ($ret) {
	    return array($ret, null);
	}

	/* Get a new lock id */
	$this->_gs->_traceStart();
	$lockId = $db->GenId($this->_gs->_tablePrefix . DATABASE_SEQUENCE_LOCK);
	$this->_gs->_traceStop();
	if (empty($lockId)) {
	    return array(GalleryCoreApi::error(
			     ERROR_STORAGE_FAILURE, __FILE__, __LINE__, 'Empty sequence id!'),
			 null);
	}
	$lockId = (int)$lockId;

	/* Put in a lock request */
	$lockIdCol = $this->_gs->_translateColumnName('lockId');
	$requestCol = $this->_gs->_translateColumnName('request');
	$freshUntilCol = $this->_gs->_translateColumnName('freshUntil');
	$query = sprintf('INSERT INTO %s (%s, %s, %s) VALUES(?, 1, ?)',
			 $lockTable, $lockIdCol, $requestCol, $freshUntilCol);
	$data = array($lockId, time() + 30);

	$this->_gs->_traceStart();
	$recordSet = $db->Execute($query, $data);
	$this->_gs->_traceStop();
	if (!$recordSet) {
	    $this->releaseLocks($lockId);
	    return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE), null);
	}

	/* Wait till it's our turn */
	while (true) {
	    $query = 'SELECT ' . $lockIdCol . ' FROM ' . $lockTable
		   . ' WHERE ' . $requestCol . '=1 ORDER BY ' . $lockIdCol . ' ASC';

	    $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
	    $this->_gs->_traceStart();
	    $recordSet = $db->SelectLimit($query, 1);
	    $this->_gs->_traceStop();
	    if (!$recordSet) {
		$this->releaseLocks($lockId);
		return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE),
			     null);
	    }

	    $row = $recordSet->FetchRow();
	    if ($row[0] == $lockId) {
		break;
	    }

	    /* Wait a second and try again */
	    sleep(1);

	    /* Expire any bogus locks */
	    $ret = $this->_expireLocks();
	    if ($ret) {
		return array($ret, null);
	    }

	    if (time() > $cutoffTime) {
		$this->releaseLocks($lockId);
		return array(GalleryCoreApi::error(ERROR_LOCK_TIMEOUT), null);
	    }
	}

	return array(null, $lockId);
    }

    /**
     * Identify the type of entity associated with the id provided
     *
     * @param mixed $ids array of ids or single int id
     * @return array a GalleryStatus and a string class name
     */
    function _identifyEntities($ids) {
	assert('!empty($ids)');

	if (!is_array($ids)) {
	    $ids = array($ids);
	    $returnArray = false;
	} else {
	    $returnArray = true;
	}

	$checkIds = array();
	foreach ($ids as $id) {
	    if (!GalleryDataCache::containsKey("GalleryStorage::_identifyEntities($id)")) {
		$checkIds[] = $id;
	    }
	}

	$local = array();
	if (!empty($checkIds)) {
	    $idCol = $this->_gs->_translateColumnName('id');
	    $entityTypeCol = $this->_gs->_translateColumnName('entityType');
	    list ($table, $unused) = $this->_gs->_translateTableName('GalleryEntity');
	    $markers = GalleryUtilities::makeMarkers(count($checkIds));
	    $query = 'SELECT ' . $idCol . ', ' . $entityTypeCol
		   . ' FROM ' . $table . ' WHERE ' . $idCol . ' IN (' . $markers . ')';

	    $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;

	    $this->_gs->_traceStart();
	    $recordSet = $this->_gs->_db->Execute($query, $checkIds);
	    $this->_gs->_traceStop();

	    if ($recordSet) {
		while ($row = $recordSet->FetchRow()) {
		    if (empty($row[1])) {
			return array(
			    GalleryCoreApi::error(ERROR_MISSING_OBJECT), null);
		    } else {
			/*
			 * Save a copy locally, in case the global cache is disabled
			 * (like in the upgrader)
			 */
			$local[$row[0]] = $row[1];
			GalleryDataCache::put("GalleryStorage::_identifyEntities($row[0])",
					      $row[1], true);
		    }
		}
	    } else {
		return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE),
			     null);
	    }
	}

	if ($returnArray) {
	    $results = array();
	    foreach ($ids as $id) {
		if (isset($local[$id])) {
		    $results[] = $local[$id];
		} else if (GalleryDataCache::containsKey(
			       "GalleryStorage::_identifyEntities($id)")) {
		    $results[] = GalleryDataCache::get("GalleryStorage::_identifyEntities($id)");
		} else {
		    return array(GalleryCoreApi::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__,
						       "Missing object for $id"), null);
		}
	    }
	} else {
	    $results = GalleryDataCache::get("GalleryStorage::_identifyEntities($ids[0])");
	}

	return array(null, $results);
    }

    /**
     * Describe the members, modules and parent of an entity
     *
     * @param string $entityName a class name
     * @param boolean $tryAllModules true if we should scan all modules, not just the active ones
     * @access protected
     * @return array object GalleryStatus a status code
     *               entity associative array
     */
    function describeEntity($entityName, $tryAllModules=false) {
	global $gallery;

	/* Note: keep these cache keys in sync with _clearEntityAndMapCache() */
	$cacheKey = "GalleryStorage::describeEntity()";
	$cacheParams = array('type' => 'module',
			     'itemId' => 'GalleryStorage_describeEntity',
			     'id' => '_all');

	/* We only cache the results for active modules */
	if (!$tryAllModules) {
	    if (!GalleryDataCache::containsKey($cacheKey)) {
		$entityInfo =& GalleryDataCache::getFromDisk($cacheParams);
		if (!empty($entityInfo)) {
		    GalleryDataCache::put($cacheKey, $entityInfo);
		}
	    }  else {
		$entityInfo = GalleryDataCache::get($cacheKey);
	    }
	}

	if (!isset($entityInfo)) {
	    list ($ret, $moduleStatus) = GalleryCoreApi::fetchPluginStatus('module');
	    if ($ret) {
		return array($ret, null);
	    }

	    $entityInfo = array();
	    foreach ($moduleStatus as $moduleId => $moduleInfo) {
		if (!$tryAllModules && empty($moduleInfo['active'])) {
		    continue;
		}

		/*
		 * Don't use GalleryPlatform here because it can cause difficult-to-eliminate
		 * issues in the testing code when we use mock platforms.  Once we have an
		 * abstraction layer around GalleryCoreApi we can use the platform here.
		 */
		$moduleDir = GalleryCoreApi::getPluginBaseDir('module', $moduleId);
		if ($ret) {
		    return array($ret, null);
		}

		$entitiesFile = sprintf('%smodules/%s/classes/Entities.inc', $moduleDir, $moduleId);
		if (file_exists($entitiesFile)) {
		    include($entitiesFile);
		}
	    }

	    if (!$tryAllModules) {
		GalleryDataCache::putToDisk($cacheParams, $entityInfo);
		GalleryDataCache::put($cacheKey, $entityInfo);
	    }
	}

	/* Fall back to all available modules */
	if (!$tryAllModules && !isset($entityInfo[$entityName])) {
	    list ($ret, $entityInfo) = $this->describeEntity($entityName, true);
	    if ($ret) {
		return array($ret, null);
	    }
	}

	/*
	 * Fall back on the parent class for any entities we don't recognize.  This is mainly so
	 * that tests can create lightweight subclasses.  Because PHP4 doesn't have case sensitive
	 * class names we have to do a linear time lookup.
	 * Don't use strcasecmp or strtolower because they are affected by locale.
	 */
	if (!isset($entityInfo[$entityName])) {
	    $parentClass = get_parent_class($entityName);
	    foreach (array_keys($entityInfo) as $candidate) {
		if ($parentClass == $candidate || $parentClass == strtr($candidate,
			    'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijlkmnopqrstuvwxyz')) {
		    $entityInfo[$entityName] = array(
			'members' => array(),
			'parent' => $candidate,
			'module' => 'unknown');
		    break;
		}
	    }
	}

	if (!isset($entityInfo[$entityName])) {
	    return array(GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
					       "Unknown entity type: $entityName"), null);
	}

	return array(null, $entityInfo);
    }

    /**
     * Clear out the entity and map caches, which we should do any time we add or remove a table.
     */
    function _clearEntityAndMapCache() {
	/* Note: keep these cache keys in sync with describeMap() */
	GalleryDataCache::remove("GalleryStorage::describeMap()");
	GalleryDataCache::removeFromDisk(array('type' => 'module',
					       'itemId' => 'GalleryStorage_describeMap',
					       'id' => '_all'));

	/* Note: keep these cache keys in sync with describeEntity() */
	GalleryDataCache::remove("GalleryStorage::describeEntity()");
	GalleryDataCache::removeFromDisk(array('type' => 'module',
					       'itemId' => 'GalleryStorage_describeEntity',
					       'id' => '_all'));
    }
}
?>

Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists