<?php
/*
 * $RCSfile: GalleryPermissionHelper_advanced.class,v $
 *
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2005 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.
 */
/**
 * @version $Revision: 1.26 $ $Date: 2005/08/23 03:49:04 $
 * @package GalleryCore
 * @author Bharat Mediratta <bharat@menalto.com>
 */

GalleryCoreApi::relativeRequireOnce(
    'modules/core/classes/helpers/GalleryPermissionHelper_simple.class');
GalleryCoreApi::relativeRequireOnce('modules/core/classes/GalleryAccessMap.class');
GalleryCoreApi::relativeRequireOnce('modules/core/classes/GalleryAccessSubscriberMap.class');

/**
 * The central registry for all permissions in the system
 *
 * @package GalleryCore
 * @subpackage Helpers
 */
class GalleryPermissionHelper_advanced {

    /**
     * Add the given itemid, userid, permission mapping
     *
     * @param int the id of the GalleryItem
     * @param int the id of the GalleryUser
     * @param string the permission id
     * @param whether or not this call applies to child items
     * @return object GalleryStatus a status code
     * @static
     */
    function addUserPermission($itemId, $userId, $permission, $applyToChildren=false) {
	$ret = GalleryPermissionHelper_advanced::_changePermission(
	    'add', $itemId, $userId, 0, $permission, $applyToChildren);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$event = GalleryCoreApi::newEvent('Gallery::ViewableTreeChange');
	$event->setData(array('userId' => $userId, 'itemId' => $itemId));
	list ($ret) = GalleryCoreApi::postEvent($event);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	return GalleryStatus::success();
    }

    /**
     * Add the given itemid, groupid, permission mapping
     *
     * @param int the id of the GalleryItem
     * @param int the id of the GalleryGroup
     * @param string the permission id
     * @param whether or not this call applies to child items
     * @return object GalleryStatus a status code
     * @static
     */
    function addGroupPermission($itemId, $groupId, $permission, $applyToChildren=false) {
	$ret = GalleryPermissionHelper_advanced::_changePermission(
	    'add', $itemId, 0, $groupId, $permission, $applyToChildren);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$ret = GalleryPermissionHelper_advanced::_postGroupEvent($groupId, $itemId);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	return GalleryStatus::success();
    }

    /**
     * Post Gallery::ViewableTreeChange event for change of group permissions.
     *
     * @param int group id
     * @param mixed item id or array of ids
     * @return object GalleryStatus a status code
     * @access private
     * @static
     */
    function _postGroupEvent($groupId, $itemId) {
	$userId = null;
	list ($ret, $group) = GalleryCoreApi::loadEntitiesById($groupId);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}
	if ($group->getGroupType() != GROUP_ALL_USERS && $group->getGroupType() != GROUP_EVERYBODY) {
	    list ($ret, $userData) = GalleryCoreApi::fetchUsersForGroup($groupId);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	    $userId = array_keys($userData);
	}

	$event = GalleryCoreApi::newEvent('Gallery::ViewableTreeChange');
	$event->setData(array('userId' => $userId, 'itemId' => $itemId));
	list ($ret) = GalleryCoreApi::postEvent($event);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	return GalleryStatus::success();
    }

    /**
     * Remove the given itemid, userid, permission mapping
     *
     * @param int the id of the GalleryItem
     * @param int the id of the GalleryUser
     * @param string the permission id
     * @param whether or not this call applies to child items
     * @return object GalleryStatus a status code
     * @static
     */
    function removeUserPermission($itemId, $userId, $permission, $applyToChildren=false) {
	$ret = GalleryPermissionHelper_advanced::_changePermission(
	    'remove', $itemId, $userId, 0, $permission, $applyToChildren);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$event = GalleryCoreApi::newEvent('Gallery::ViewableTreeChange');
	$event->setData(array('userId' => $userId, 'itemId' => $itemId));
	list ($ret) = GalleryCoreApi::postEvent($event);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	return GalleryStatus::success();
    }

    /**
     * Remove the given itemid, userid, permission mapping
     *
     * @param int the id of the GalleryItem
     * @param int the id of the GalleryGroup
     * @param string the permission id
     * @param whether or not this call applies to child items
     * @return object GalleryStatus a status code
     * @static
     */
    function removeGroupPermission($itemId, $groupId, $permission, $applyToChildren=false) {
	$ret = GalleryPermissionHelper_advanced::_changePermission(
	    'remove', $itemId, 0, $groupId, $permission, $applyToChildren);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$ret = GalleryPermissionHelper_advanced::_postGroupEvent($groupId, $itemId);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	return GalleryStatus::success();
    }

    /**
     * Make the appropriate permission change.
     *
     * @param string the type of change ('add' or 'remove')
     * @param int the id of the GalleryItem
     * @param int the id of the GalleryUser
     * @param int the id of the GalleryGroup
     * @param int the permission id
     * @param whether or not this call applies to child items
     * @return object GalleryStatus a status code
     * @access private
     * @static
     */
    function _changePermission($changeType, $itemId, $userId, $groupId,
			       $permission, $applyToChildren) {
	global $gallery;
	if (empty($itemId) || !isset($userId) || !isset($groupId) || empty($permission) ||
	    ($changeType != 'add') && ($changeType != 'remove')) {
	    return GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__);
	}
	$storage =& $gallery->getStorage();

	list ($ret, $lockId) =
	    GalleryPermissionHelper_advanced::_getAccessListCompacterLock('read');
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/* Convert the permission to its bits */
	list($ret, $deltaBits) = GalleryCoreApi::convertPermissionIdsToBits($permission);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/*
	 * We can always change the given item, whether or not it has core.changePermission but
	 * we can only propagate the change downwards to items that have core.changePermission.
	 */
	$data = array();
	if ($applyToChildren) {
	    list ($ret, $parentSequence) = GalleryCoreApi::fetchParentSequence($itemId);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	    $parentSequence[] = $itemId;
	    $parentSequence = join('/', $parentSequence);

	    list ($ret, $aclIds) = GalleryCoreApi::fetchAccessListIds(
		'core.changePermissions', $gallery->getActiveUserId());
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	    $aclMarkers = GalleryUtilities::makeMarkers(count($aclIds));

	    $query = sprintf('
            SELECT
              [GalleryAccessSubscriberMap::accessListId],
              [GalleryItemAttributesMap::itemId]
            FROM
              [GalleryItemAttributesMap],
              [GalleryAccessSubscriberMap]
            WHERE
              [GalleryItemAttributesMap::itemId] = [GalleryAccessSubscriberMap::itemId]
              AND
              ([GalleryItemAttributesMap::itemId] = ?
               OR
               ([GalleryItemAttributesMap::parentSequence] LIKE ?
                AND
                [GalleryAccessSubscriberMap::accessListId] IN (%s)))
            ', $aclMarkers);
	    $data[] = $itemId;
	    $data[] = "$parentSequence/%";
	    $data = array_merge($data, $aclIds);
	} else {
	    $query = '
            SELECT
              [GalleryAccessSubscriberMap::accessListId],
              [GalleryAccessSubscriberMap::itemId]
            FROM
              [GalleryAccessSubscriberMap]
            WHERE
              [GalleryAccessSubscriberMap::itemId] = ?
            ';
	    $data[] = $itemId;
	}

	list ($ret, $searchResults) = $gallery->search($query, $data);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/*
	 * Now we're getting back a series of acl id => item id pairs from the database, each of
	 * which we need to convert.  When we see an acl id we haven't seen before, create a new
	 * ACL for it with our permission change applied, and put that item id in a queue to
	 * be moved to that new acl.  We could probably do this more efficiently with subselects
	 * (can't use them until we raise the MySQL bar to 4.x) or temporary tables.
	 */
	$oldToNewAclMapping = array();
	$itemsToBeRemapped = array();
	$removeEventData = array();

	while ($result = $searchResults->nextResult()) {
	    list ($oldAclId, $targetItemId) = array((int)$result[0], (int)$result[1]);
	    if (!isset($oldToNewAclMapping[$oldAclId])) {
		/* Figure out what the current bits are */
		list ($ret, $currentBits) =
		    GalleryPermissionHelper_advanced::_fetchPermissionBitsForItem(
			$targetItemId, $userId, $groupId);
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}

		$newAclId = 0;
		if ($changeType == 'add') {
		    if (($currentBits | $deltaBits) == $currentBits) {
			/* The item's acl already has the bits we want to add */
		    } else {
			/* Clone the acl */
			list($ret, $newAclId) =
			    GalleryPermissionHelper_advanced::_copyAccessList($oldAclId);
			if ($ret->isError()) {
			    return $ret->wrap(__FILE__, __LINE__);
			}

			$newBits = $currentBits | $deltaBits;
			if (empty($currentBits)) {
			    /* Add a new entry in our map to reflect this change. */
			    $ret = GalleryAccessMap::addMapEntry(
				array('accessListId' => $newAclId,
				      'userId' => $userId,
				      'groupId' => $groupId,
				      'permission' => $newBits));
			} else {
			    /* Update the current entry in our map to reflect this change. */
			    $ret = GalleryAccessMap::updateMapEntry(
				array('accessListId' => $newAclId,
				      'userId' => $userId,
				      'groupId' => $groupId),
				array('permission' => $newBits));
			}
			if ($ret->isError()) {
			    return $ret->wrap(__FILE__, __LINE__);
			}
		    }
		} else {
		    $newBits = $currentBits & ~$deltaBits;
		    if (!$currentBits) {
			if ($gallery->getDebug()) {
			    $gallery->debug("Tried to remove a non-existant permission!");
			}
		    } else if ($currentBits == $newBits) {
			/* The item's acl doesn't have the bits we want to remove */
		    } else {
			/* Clone the acl */
			list($ret, $newAclId) =
			    GalleryPermissionHelper_advanced::_copyAccessList($oldAclId);
			if ($ret->isError()) {
			    return $ret->wrap(__FILE__, __LINE__);
			}

			if ($newBits == 0) {
			    /* Remove the map entry; there are no bits left! */
			    $ret = GalleryAccessMap::removeMapEntry(
				array('accessListId' => $newAclId,
				      'userId' => $userId,
				      'groupId' => $groupId));
			} else {
			    /* Update the current entry in our map to reflect this change. */
			    $ret = GalleryAccessMap::updateMapEntry(
				array('accessListId' => $newAclId,
				      'userId' => $userId,
				      'groupId' => $groupId),
				array('permission' => $newBits));
			}
			if ($ret->isError()) {
			    return $ret->wrap(__FILE__, __LINE__);
			}
		    }
		}

		$oldToNewAclMapping[$oldAclId] = $newAclId;
	    }

	    if ($newAclId) {
		$itemsToBeRemapped[$oldAclId][] = $targetItemId;

		if (count($itemsToBeRemapped[$oldAclId]) >= 200) {
		    $ret = GalleryAccessSubscriberMap::updateMapEntry(
			array('itemId' => $itemsToBeRemapped[$oldAclId]),
			array('accessListId' => $newAclId));
		    if ($ret->isError()) {
			return $ret->wrap(__FILE__, __LINE__);
		    }
		    $itemsToBeRemapped[$oldAclId] = array();
		}

		if ($changeType == 'remove') {
		    /* Keep track of all items with removed permissions for the postEvent call */
		    $removeEventData[$targetItemId] = $currentBits & ~$newBits;
		}
	    }
	}

	foreach (array_keys($itemsToBeRemapped) as $oldAclId) {
	    if ($oldToNewAclMapping[$oldAclId] && !empty($itemsToBeRemapped[$oldAclId])) {
		$ret = GalleryAccessSubscriberMap::updateMapEntry(
		    array('itemId' => $itemsToBeRemapped[$oldAclId]),
		    array('accessListId' => $oldToNewAclMapping[$oldAclId]));
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
	    }
	}

	GalleryPermissionHelper_simple::_clearCachedAccessListIds();
	if ($changeType == 'remove') {
	    GalleryDataCache::clearPermissionCache();
	}

	$ret = GalleryCoreApi::releaseLocks($lockId);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/* Post a RemovePermission event for all affected items if necessary */
	if (!empty($removeEventData)) {
	    /* user/groupId 0 has a special meaning in the RemovePermission event */
	    $eventUserId = $userId < 1 ? null : $userId;
	    $eventGroupId = $groupId < 1 ? null : $groupId;
	    $event = GalleryCoreApi::newEvent('Gallery::RemovePermission');
	    $event->setData(array('userId' => $eventUserId, 'groupId' => $eventGroupId,
				  'itemIdsAndBits' => $removeEventData));
	    list ($ret) = GalleryCoreApi::postEvent($event);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	}

	return GalleryStatus::success();
    }

    /**
     * Remove all permissions for the given itemid.  We do this by setting its access list id to
     * zero, which corresponds to no acl.  We leave the row around in the map, though so that we
     * don't have to worry about whether it exists or not.
     *
     * @param mixed item id or array of ids
     * @return object GalleryStatus a status code
     * @static
     */
    function removeItemPermissions($itemId) {
	global $gallery;
	if (empty($itemId) || !is_int($itemId)) {
	    return GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__);
	}

	$ret = GalleryAccessSubscriberMap::updateMapEntry(
	    array('itemId' => $itemId),
	    array('accessListId' => 0));
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$event = GalleryCoreApi::newEvent('Gallery::ViewableTreeChange');
	$event->setData(array('userId' => null, 'itemId' => $itemId));
	list ($ret) = GalleryCoreApi::postEvent($event);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/*
	 * For the RemovePermission event we have the choice between specifying the permission bits
	 * that were removed and specifying the new permission bits. In this case, we cannot
	 * specify the removed permission bits, because these bits are different for each user /
	 * group and the RemovePermission event only allows to specify multiple item => bits pairs
	 * and not multiple item => (user => bits) pairs. This is by design, more detail is not
	 * needed and would just make the RemovePermission event slower.
	 */
	/* Get the bits for the new permisions. Get it with the API call rather than just using 0 */
	list ($ret, $zeroBits) = GalleryCoreApi::convertPermissionIdsToBits(array());
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}
	$event = GalleryCoreApi::newEvent('Gallery::RemovePermission');
	$event->setData(array('userId' => 0, 'groupId' => 0,
			      'itemIdsAndBits' => array($itemId => $zeroBits),
			      'format' => 'newBits'));
	list ($ret) = GalleryCoreApi::postEvent($event);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	return GalleryStatus::success();
    }

    /**
     * Return a list of permissions for the given item id
     *
     * @param int the id of the item
     * @param boolean should we compress the permission list?
     * @return array object GalleryStatus a status code
     *               array array('userId' => ...,
     *                           'groupId' => ...,
     *                           'permission' => ...)
     * @static
     */
    function fetchAllPermissionsForItem($itemId, $compress=false) {
	global $gallery;

	if (empty($itemId)) {
	    return array(GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__), null);
	}

	$query = '
        SELECT
            [GalleryAccessMap::userId],
            [GalleryAccessMap::groupId],
            [GalleryAccessMap::permission]
        FROM
            [GalleryAccessMap],
            [GalleryAccessSubscriberMap]
        WHERE
            [GalleryAccessSubscriberMap::itemId] = ?
            AND
            [GalleryAccessSubscriberMap::accessListId] = [GalleryAccessMap::accessListId]
        ORDER BY
            [GalleryAccessMap::permission] ASC
        ';

	list($ret, $searchResults) = $gallery->search($query, array($itemId));
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	$storage =& $gallery->getStorage();

	$data = array();
	while ($result = $searchResults->nextResult()) {
	    $permissions = $storage->convertBitsToInt($result[2]);
	    GalleryCoreApi::relativeRequireOnce(
		'modules/core/classes/helpers/GalleryPermissionHelper_medium.class');
	    list ($ret, $permissions) =
		GalleryPermissionHelper_medium::convertBitsToIds($permissions, $compress);
	    if ($ret->isError()) {
		return array($ret->wrap(__FILE__, __LINE__), null);
	    }

	    foreach ($permissions as $permission) {
		$data[] = array('userId' => (int)$result[0],
				'groupId' => (int)$result[1],
				'permission' => $permission['id']);
	    }
	}
	return array(GalleryStatus::success(), $data);
    }

    /**
     * Return a permissions for the given item
     *
     * @param int GalleryItem id
     * @param int user id or null (user id and group id can't both be null)
     * @param int group id or null (user id and group id can't both be null)
     * @return array object GalleryStatus a status code
     *               int permissions or null if not found
     * @access private
     * @static
     */
    function _fetchPermissionBitsForItem($itemId, $userId, $groupId) {
	global $gallery;

	if (empty($itemId) || (empty($userId) && empty($groupId))) {
	    return array(GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__), null);
	}

	$query = '
        SELECT
            [GalleryAccessMap::permission]
        FROM
            [GalleryAccessMap],
            [GalleryAccessSubscriberMap]
        WHERE
            [GalleryAccessSubscriberMap::itemId] = ?
            AND
            [GalleryAccessSubscriberMap::accessListId] = [GalleryAccessMap::accessListId]
            AND
            [GalleryAccessMap::userId] = ?
            AND
            [GalleryAccessMap::groupId] = ?
        ';

	list($ret, $searchResults) =
	    $gallery->search($query, array($itemId, $userId, $groupId));
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	$storage =& $gallery->getStorage();

	$permission = null;
	if ($result = $searchResults->nextResult()) {
	    $permission = $storage->convertBitsToInt($result[0]);
	}
	return array(GalleryStatus::success(), $permission);
    }

    /**
     * Look up an item's access list.
     *
     * @param int the id of the source item
     * @return array object GalleryStatus a status code,
     *               int accessListId the associated item's list
     * @access private
     * @static
     */
    function _lookupAccessListId($itemId) {
	global $gallery;

	if(empty($itemId)) {
	    return array(GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__), null);
	}

	$query = '
	SELECT
	    [GalleryAccessSubscriberMap::accessListId]
	FROM
	    [GalleryAccessSubscriberMap]
	WHERE
	    [GalleryAccessSubscriberMap::itemId] = ?
	';

	$data = array($itemId);

	list($ret, $searchResults) =
	    $gallery->search($query, $data, array('limit' => array('count' => 1)));
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	$result = $searchResults->nextResult();

	return array(GalleryStatus::success(), empty($result) ? null : $result[0]);
    }

    /**
     * Create a duplicate access list.
     *
     * @param int the id of the source access list
     * @return array object GalleryStatus a status code,
     *               int AccessListId the new access list's id
     * @access private
     * @static
     */
    function _copyAccessList($oldAccessListId) {
	global $gallery;

	$storage =& $gallery->getStorage();
	list($ret, $newAccessListId) = $storage->getUniqueId();
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	if (!empty($oldAccessListId)) {
	    $query = '
            SELECT
                [GalleryAccessMap::userId],
                [GalleryAccessMap::groupId],
                [GalleryAccessMap::permission]
            FROM
                [GalleryAccessMap]
            WHERE
                [GalleryAccessMap::accessListId] = ?
            ';
	    $data = array($oldAccessListId);

	    list($ret, $searchResults) = $gallery->search($query, $data);
	    if ($ret->isError()) {
		return array($ret->wrap(__FILE__, __LINE__), null);
	    }

	    /* TODO: consider replacing this with an INSERT INTO..SELECT FROM */
	    while ($result = $searchResults->nextResult()) {
		$permission = $storage->convertBitsToInt($result[2]);
		$ret = GalleryAccessMap::addMapEntry(array('userId' => (int)$result[0],
							   'groupId' => (int)$result[1],
							   'permission' => $permission,
							   'accessListId' => (int)$newAccessListId));
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
	    }
	}

	return array(GalleryStatus::success(), $newAccessListId);
    }

    /**
     * Copy a set of permissions from one id to another
     *
     * @param int the id of the target item
     * @param int the id of the source item
     * @return object GalleryStatus a status code
     * @static
     */
    function copyPermissions($toId, $fromId) {
	global $gallery;

	if (empty($toId) || empty($fromId)) {
	    return GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__);
	}

	list ($ret, $lockId) =
	    GalleryPermissionHelper_advanced::_getAccessListCompacterLock('read');
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$query = '
	SELECT
            [GalleryAccessSubscriberMap::itemId],
            [GalleryAccessSubscriberMap::accessListId]
        FROM
            [GalleryAccessSubscriberMap]
        WHERE
            [GalleryAccessSubscriberMap::itemId] IN (?, ?)
        ';
	$data = array($toId, $fromId);

	list($ret, $searchResults) = $gallery->search($query, $data);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$map = array();
	while ($result = $searchResults->nextResult()) {
	    $map[$result[0]] = (int)$result[1];
	}

	$ret = GalleryAccessSubscriberMap::updateMapEntry(
	    array('itemId' => $toId), array('accessListId' => $map[$fromId]));
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$ret = GalleryCoreApi::releaseLocks($lockId);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	return GalleryStatus::success();
    }

    /**
     * Does the user/group combo have all needed permissions for the target item?
     *
     * @param int the id of the target item
     * @param array int user ids
     * @param array int group ids
     * @param array string target permissions
     * @return array object GalleryStatus a status code
     *               boolean true if yes
     * @static
     */
    function hasPermission($itemId, $userIds, $groupIds, $permission) {
	global $gallery;

	if (!isset($permission)) {
	    return array(GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__), null);
	}

	list ($ret, $bits) = GalleryCoreApi::convertPermissionIdsToBits($permission);
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	if (!empty($userIds)) {
	    foreach ($userIds as $userId) {
		$ands[] = '[GalleryAccessMap::userId] = ?';
	    }
	}

	if (!empty($groupIds)) {
	    foreach ($groupIds as $groupId) {
		$ands[] = '[GalleryAccessMap::groupId] = ?';
	    }
	}

	$storage =& $gallery->getStorage();
	list ($ret, $andPermission) = $storage->getFunctionSql('BITAND',
						array('[GalleryAccessMap::permission]', '?'));
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	$query = '
	SELECT
          [GalleryAccessSubscriberMap::itemId]
        FROM
          [GalleryAccessMap], [GalleryAccessSubscriberMap]
        WHERE
          [GalleryAccessSubscriberMap::itemId] = ?
	  AND [GalleryAccessMap::accessListId] = [GalleryAccessSubscriberMap::accessListId]
          AND (' . join(' OR ', $ands) . ')
          AND (' . $andPermission . ' = ?)
        ';
	$data = array($itemId);
	$data = array_merge($data, $userIds, $groupIds);
	$data[] = $storage->convertIntToBits($bits);
	$data[] = $storage->convertIntToBits($bits);

	list($ret, $searchResults) =
	    $gallery->search($query, $data, array('limit' => array('count' => 1)));
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	return array(GalleryStatus::success(),
		     $searchResults->resultCount() ? true : false);
    }

    /**
     * Register a new permission
     *
     * @param string the id of the module
     * @param string the id of the permission
     * @param string the non-localized description of the permission
     * @param int flags (of the GALLERY_PERMISSION_XXX variety)
     * @param array ids of other permissions that compose this one
     * @return object GalleryStatus a status code
     * @static
     */
    function registerPermission($module, $permissionId, $description,
	                        $flags=0, $composites=array()) {
	global $gallery;

	if (empty($module) || empty($permissionId) || empty($description)) {
	    return GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__);
	}

	GalleryCoreApi::relativeRequireOnce(
		'modules/core/classes/helpers/GalleryPermissionHelper_simple.class');
	list ($ret, $permissionTable) = GalleryPermissionHelper_simple::_fetchAllPermissions();
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	if (isset($permissionTable[$permissionId])) {
	    return GalleryStatus::error(ERROR_COLLISION, __FILE__, __LINE__,
					'Duplicate permission id: ' . $permissionId);
	}

	if ($flags & GALLERY_PERMISSION_ALL_ACCESS) {
	    /*
	     * This is a special case where we want to grant all possible
	     * permissions.  Convert it to a composite with all bits lit.
	     */
	    $bits = 0x7FFFFFFF;
	    $flags |= GALLERY_PERMISSION_COMPOSITE;
	} else if ($flags & GALLERY_PERMISSION_COMPOSITE) {
	    if (empty($composites)) {
		return GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
					    "Permission $permissionId is marked as a composite, " .
					    "but didn't specify any composites!");
	    }

	    /* Convert our composites to their associated values */
	    list ($ret, $bits) = GalleryPermissionHelper_simple::convertIdsToBits($composites);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	} else {
	    list ($ret, $bits) = GalleryPermissionHelper_advanced::_newPermissionBit();
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	}

	/* Add a new entry in our map to represent this relationship. */
	$ret = GalleryPermissionHelper_advanced::_setPermission(array('module' => $module,
								      'permission' => $permissionId,
								      'description' => $description,
								      'flags' => $flags,
								      'bits' => $bits));
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/* Clear the cache */
	GalleryDataCache::remove('GalleryPermissionHelper::allPermissions');

	return GalleryStatus::success();
    }

    /**
     * Get all the permission ids that match the specified flags
     * This will return any permissions that contain *all* the bits from flags.
     *
     * @param int flags
     * @return array object GalleryStatus a status code
     *               array (id => description, id => description, ...)
     * @static
     */
    function getPermissionIds($flags=0) {
	GalleryCoreApi::relativeRequireOnce(
		'modules/core/classes/helpers/GalleryPermissionHelper_simple.class');
	list ($ret, $allPermissions) = GalleryPermissionHelper_simple::_fetchAllPermissions();
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	$results = array();
	foreach ($allPermissions as $id => $permission) {
	    if (($permission['flags'] & $flags) == $flags) {
		$results[$id] = $permission['description'];
	    }
	}

	return array(GalleryStatus::success(), $results);
    }

    /**
     * Expand a single permission into all the possible permissions that it can
     * possibly be.  For example, convert 'core.viewAll' into:
     * ('core.viewAll', 'core.view', 'core.viewOriginal', 'core.viewResizes')
     *
     * @return array object GalleryStatus a status code
     *               array(array('id' => ..., 'description' => ...), ...)
     * @static
     */
    function getSubPermissions($permissionId) {
	list ($ret, $bits) =
	    GalleryCoreApi::convertPermissionIdsToBits(array($permissionId));
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	GalleryCoreApi::relativeRequireOnce(
		'modules/core/classes/helpers/GalleryPermissionHelper_medium.class');
	list ($ret, $ids) = GalleryPermissionHelper_medium::convertBitsToIds($bits);
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	return array(GalleryStatus::success(), $ids);
    }

    /**
     * Unregister all permission associated with a given module.
     *
     * @return object GalleryStatus a status code
     * @static
     */
    function unregisterModulePermissions($moduleId) {
	if (empty($moduleId)) {
	    return GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__);
	}

	GalleryCoreApi::relativeRequireOnce(
		'modules/core/classes/helpers/GalleryPermissionHelper_simple.class');
	list ($ret, $allPermissions) = GalleryPermissionHelper_simple::_fetchAllPermissions();
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$ids = array();
	foreach ($allPermissions as $key => $info) {
	    /* Remove this module's non-composite permissions from all entities.. */
	    if ($info['module'] == $moduleId && !($info['flags'] & GALLERY_PERMISSION_COMPOSITE)) {
		$ids[] = $key;
	    }
	}

	$ret = GalleryPermissionHelper_advanced::_removePermissionsFromAllItems($ids);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/* Remove the permission from the permission set */
	GalleryCoreApi::relativeRequireOnce('modules/core/classes/GalleryPermissionSetMap.class');
	$ret = GalleryPermissionSetMap::removeMapEntry(array('module' => $moduleId));
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	return GalleryStatus::success();
    }

    /**
     * Return an unused permission bit that we can use for our purposes
     *
     * @return array object GalleryStatus a status code
     *               int location of a bit (1, 2, 3, etc)
     * @access private
     * @static
     */
    function _newPermissionBit() {
	list ($ret, $allPermissions) = GalleryPermissionHelper_simple::_fetchAllPermissions();
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	$bitSet = 0;
	foreach ($allPermissions as $permission) {
	    /*
	     * Should we exclude composites from the scan?  We have to exclude
	     * the "all access" composite, since that covers all the bits.
	     * If we exclude composites then we may run afoul of a problem
	     * where a permission bit is removed but it's still part of a
	     * different permission's composite.
	     */
	    if ($permission['flags'] & GALLERY_PERMISSION_ALL_ACCESS) {
		continue;
	    }

	    $bitSet |= $permission['bits'];
	}

	/*
	 * Bitset now has all the bits that we're using.  Scan it for an
	 * available bit.
	 */
	$newBit = 0;
	for ($i = 0; $i < 31; $i++) {
	    $bit = 1 << $i;
	    if (!($bitSet & $bit)) {
		$newBit = $bit;
		break;
	    }
	}

	if ($newBit == 0) {
	    return array(GalleryStatus::error(ERROR_OUT_OF_SPACE, __FILE__, __LINE__), null);
	}

	return array(GalleryStatus::success(), $newBit);
    }

    /**
     * Add a permission to the database and to our permission cache.
     *
     * @param array the specific permission data
     * @return object GalleryStatus a status code
     * @access private
     * @static
     */
    function _setPermission($data) {
	$cacheKey = 'GalleryPermissionHelper::_allPermissions';
	if (GalleryDataCache::containsKey($cacheKey)) {
	    $permissions = GalleryDataCache::get($cacheKey);
	} else {
	    $permissions = array();
	}

	$permissions[$data['permission']] = $data;
	GalleryDataCache::put($cacheKey, $permissions);

	GalleryCoreApi::relativeRequireOnce('modules/core/classes/GalleryPermissionSetMap.class');
	$ret = GalleryPermissionSetMap::addMapEntry($data);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	return GalleryStatus::success();
    }

    /**
     * Remove the given permissions from all items.  Useful when we remove a
     * permission from the system.
     *
     * @param array permission ids
     * @return object GalleryStatus a status code
     */
    function _removePermissionsFromAllItems($permissionIds) {
	global $gallery;

	list ($ret, $removeBits) = GalleryCoreApi::convertPermissionIdsToBits($permissionIds);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	list ($ret, $allBits) = GalleryCoreApi::convertPermissionIdsToBits('core.all');
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$storage =& $gallery->getStorage();
	list ($ret, $andNotPermission) = $storage->getFunctionSql('BITAND',
	    array('[GalleryAccessMap::permission]', '?'));
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$query = '
        UPDATE
          [GalleryAccessMap]
        SET
	  [::permission] = ' . $andNotPermission . '
        WHERE
           [GalleryAccessMap::permission] != ?
        ';
	$data = array();
	$data[] = $storage->convertIntToBits(0x7FFFFFFF - $removeBits);
	$data[] = $storage->convertIntToBits($allBits);

	list ($results, $searchResults) = $gallery->search($query, $data);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/*
	 * TODO: What if view type permissions are removed? Then we need RemovePermission and
	 * ViewableTreeChange events here.
	 * In practice, these permissions cannot be removed, therefore this has low priority.
	 */

	return GalleryStatus::success();
    }

    /**
     * Acquire a read or write lock on our access list compacter semaphore.  While we
     * have this read lock, the access list can't be compacted.  While we have a write
     * lock, we're in the process of compacting so nobody else should be touching the
     * access map.
     *
     * @param string 'read' or 'write'
     * @return array object GalleryStatus a status code
     *         int lock id
     * @access private
     * @static
     */
    function _getAccessListCompacterLock($type) {
	list ($ret, $semaphoreId) =
	    GalleryCoreApi::getPluginParameter('module', 'core', 'id.accessListCompacterLock');
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	switch ($type) {
	case 'read':
	    if (GalleryCoreApi::isReadLocked($semaphoreId)) {
		$lockId = null;
	    } else {
		list ($ret, $lockId) = GalleryCoreApi::acquireReadLock($semaphoreId);
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
	    }
	    break;

	case 'write':
	    if (GalleryCoreApi::isReadLocked($semaphoreId)) {
		$lockId = null;
	    } else {
		list ($ret, $lockId) = GalleryCoreApi::acquireWriteLock($semaphoreId);
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
	    }
	    break;

	default:
	    return array(GalleryStatus::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__), null);
	}

	return array(GalleryStatus::success(), $lockId);
    }

    /**
     * Compact the access list map, if we deem that it's a good time to do so.
     *
     * @return object GalleryStatus a status code
     */
    function maybeCompactAccessLists() {
	/* We use a high tech genetic algorithm to make our decision */
	if (rand(1, 100) <= 50) {
	    $ret = GalleryCoreApi::compactAccessLists();
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	}
	return GalleryStatus::success();
    }

    /**
     * Compact the access map.  Remove any duplicate access maps and remap any subscribers from
     * the duplicates to the one remaining version.
     *
     * @return object GalleryStatus a status code
     */
    function compactAccessLists() {
	global $gallery;

	list ($ret, $lockId) =
	    GalleryPermissionHelper_advanced::_getAccessListCompacterLock('write');
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/* Load the entire access list into memory */
	$query = '
        SELECT
          [GalleryAccessMap::accessListId],
          [GalleryAccessMap::userId],
          [GalleryAccessMap::groupId],
          [GalleryAccessMap::permission]
        FROM
          [GalleryAccessMap]
        ORDER BY
          [GalleryAccessMap::userId] ASC,
          [GalleryAccessMap::groupId] ASC,
          [GalleryAccessMap::accessListId] DESC
        ';
	list ($ret, $searchResults) = $gallery->search($query);
	if ($ret->isError()) {
	    GalleryCoreApi::releaseLocks($lockId);
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$aclIdToKey = array();
	$gallery->guaranteeTimeLimit(60);
	while ($result = $searchResults->nextResult()) {
	    $aclId = $result[0];
	    $key = sprintf("%s|%s|%s,", $result[1], $result[2], $result[3]);
	    if (!isset($aclIdToKey[$aclId])) {
		$aclIdToKey[$aclId] = '';
	    }
	    $aclIdToKey[$aclId] .= $key;
	}

	$keyToAclId = array();
	foreach ($aclIdToKey as $key => $value) {
	    $keyToAclId[$value][] = $key;
	}

	/* We've now got buckets of duplicate acls.  Start compacting. */
	foreach ($keyToAclId as $key => $aclIds) {
	    $gallery->guaranteeTimeLimit(20);
	    if (count($aclIds) == 1) {
		continue;
	    }

	    $master = array_shift($aclIds);
	    $ret = GalleryAccessSubscriberMap::updateMapEntry(
		array('accessListId' => $aclIds), array('accessListId' => $master));
	    if ($ret->isError()) {
		GalleryCoreApi::releaseLocks($lockId);
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    $ret = GalleryAccessMap::removeMapEntry(array('accessListId' => $aclIds));
	    if ($ret->isError()) {
		GalleryCoreApi::releaseLocks($lockId);
		return $ret->wrap(__FILE__, __LINE__);
	    }
	}

	/* Eliminate any unused acls */
	$query = '
        SELECT DISTINCT
          [GalleryAccessMap::accessListId]
        FROM
          [GalleryAccessMap] LEFT JOIN [GalleryAccessSubscriberMap] ON
             [GalleryAccessMap::accessListId] = [GalleryAccessSubscriberMap::accessListId]
        WHERE
          [GalleryAccessSubscriberMap::accessListId] IS NULL
        ';
	list($ret, $searchResults) = $gallery->search($query);
	if ($ret->isError()) {
	    GalleryCoreApi::releaseLocks($lockId);
	    return $ret->wrap(__FILE__, __LINE__);
	}
	$unusedAclIds = array();
	while ($result = $searchResults->nextResult()) {
	    $unusedAclIds[] = $result[0];
	}

	if (!empty($unusedAclIds)) {
	    $ret = GalleryAccessMap::removeMapEntry(array('accessListId' => $unusedAclIds));
	    if ($ret->isError()) {
		GalleryCoreApi::releaseLocks($lockId);
		return $ret->wrap(__FILE__, __LINE__);
	    }
	}

	$ret = GalleryCoreApi::releaseLocks($lockId);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	GalleryPermissionHelper_simple::_clearCachedAccessListIds();

	return GalleryStatus::success();
    }
}
?>
