<?php
/*
 * $RCSfile: GalleryItemHelper_medium.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.64 $ $Date: 2005/08/23 03:49:04 $
 * @package GalleryCore
 * @author Bharat Mediratta <bharat@menalto.com>
 */

/**
 * Helper class for GalleryItems
 *
 * @package GalleryCore
 * @subpackage Helpers
 * @abstract
 */
class GalleryItemHelper_medium /* extends GalleryEventListener */ {

    /**
     * Return the ids of all items that match the given type and have the
     * given permission (defaults to 'core.view')
     *
     * @access public
     * @param string an item type (eg, 'GalleryAlbumItem')
     * @return array object GalleryStatus a status code
     *               array(id, id, id, ...)
     * @static
     */
    function fetchAllItemIds($itemType, $permission='core.view') {
	global $gallery;

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

	list ($ret, $aclIds) =
	    GalleryCoreApi::fetchAccessListIds($permission, $gallery->getActiveUserId());
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}
	if (empty($aclIds)) {
	    return array(GalleryStatus::success(), array());
	}
	$aclMarkers  = GalleryUtilities::makeMarkers(count($aclIds));

	$query = sprintf('
	SELECT
	  [GalleryItem::id]
	FROM
	  [GalleryEntity], [GalleryItem], [GalleryAccessSubscriberMap]
	WHERE
	  [GalleryEntity::entityType] = ?
	  AND
	  [GalleryItem::id] = [GalleryEntity::id]
	  AND
	  [GalleryAccessSubscriberMap::itemId] = [GalleryEntity::id]
	  AND
	  [GalleryAccessSubscriberMap::accessListId] IN (%s)
	', $aclMarkers);

	$data = array();
	$data[] = $itemType;
	$data = array_merge($data, $aclIds);

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

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

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

    /**
     * Return the ids of all items which are owned by the given user id
     * This function does NOT obey permissions!
     *
     * @access public
     * @param int the user id of the owner of the items
     * @return array object GalleryStatus a status code
     *               array(id, id, id, ...)
     * @static
     */
    function fetchAllItemIdsByOwnerId($ownerId) {
	global $gallery;

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

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

	/* Search the item table for all items with owner = $ownerId */
	$query = '
	SELECT
	  [GalleryItem::id]
	FROM
	  [GalleryItem]
	WHERE
	  [GalleryItem::ownerId] = ?
	';
	list ($ret, $queryResults) = $gallery->search($query, array($ownerId));
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}
	$itemIds = array();
	while ($result = $queryResults->nextResult()) {
	    $itemIds[] = $result[0];
	}

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

    /**
     * Return the appropriate GalleryItem instance for the mime type provided.
     *
     * Use the GalleryFactory to try to find an exact match to the mime type.
     * Failing that, fall back to the major type, then fall back to '*'.
     *
     * @param string the mime type
     * @return array object GalleryStatus a status code
     *               object GalleryItem an item
     * @static
     */
    function newItemByMimeType($mimeType) {

	/* Try the whole mime type first, fallback to major type only */
	list ($ret, $instance) = GalleryCoreApi::newFactoryInstanceByHint('GalleryItem',
	    array($mimeType, substr($mimeType, 0, strpos($mimeType, '/')) . '/*'));
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

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

    /**
     * Add a new data item to an album from a data file.
     *
     * @param string the path to the file on the local disk
     * @param string the name of the new item
     * @param string the title of the new item
     * @param string the summary of the new item
     * @param string the description of the new item
     * @param string the mime type of the new item
     * @param int the id of the target album
     * @param boolean (optional) a boolean true if we should symlink instead
     *        of copy (default is false).
     * @return array object GalleryStatus a status code
     *               object GalleryDataItem a new item
     * @static
     */
    function addItemToAlbum($fileName, $itemName, $title, $summary,
			    $description, $mimeType, $albumId, $symlink=false) {
	global $gallery;

	$platform = $gallery->getPlatform();

	/*
	 * Assume that we've got a working mime type, go ahead and
	 * get an appropriate GalleryDataItem from the GalleryFactory.
	 */
	list ($ret, $newItem) = GalleryCoreApi::newItemByMimeType($mimeType);
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	$ret = $newItem->create($albumId, $fileName, $mimeType, $itemName, $symlink);
	if ($ret->isError()) {
	    if ($ret->getErrorCode() & ERROR_BAD_DATA_TYPE) {
		/* Well, it wasn't what we thought it was.  Make it an unknown */
		$gallery->debug(sprintf('Failed to create item type %s, falling back on unknown',
					get_class($newItem)));
		list ($ret, $newItem) =
		    GalleryCoreApi::newFactoryInstanceById('GalleryEntity', 'GalleryUnknownItem');
		if (!isset($newItem)) {
		    return array(GalleryStatus::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__,
			'Unable to get a GalleryUnknownItem instance'), null);
		}

		$ret = $newItem->create($albumId, $fileName, $mimeType, $itemName, $symlink);
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
	    } else {
		return array($ret->wrap(__FILE__, __LINE__), null);
	    }
	}

	$newItem->setTitle($title);
	$newItem->setSummary($summary);
	$newItem->setDescription($description);

	/* Try to get an originationTimestamp for the just added item */
	list ($ret, $originationTimestamp) = GalleryCoreApi::fetchOriginationTimestamp($newItem);
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}
	if (!empty($originationTimestamp)) {
	    $newItem->setOriginationTimestamp($originationTimestamp);
	}

	$ret = $newItem->save();
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	$ret = GalleryCoreApi::addExistingItemToAlbum($newItem, $albumId, true);
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

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

    /**
     * Fetch the originationTimestamp through our known toolkits
     *
     * @param object the GalleryItem
     * @return array object GalleryStatus a status code
     *               int a timestamp or null if nothing was found
     * @static
     */
    function fetchOriginationTimestamp($item) {
	if (! GalleryUtilities::isA($item, 'GalleryDataItem')) {
	    return array(GalleryStatus::success(), null);
	}
	list ($ret, $toolkits) =
	    GalleryCoreApi::getToolkitsByProperty($item->getMimeType(), 'originationTimestamp');
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}
	if ($toolkits) {
	    list ($ret, $path) = $item->fetchPath();
	    if ($ret->isError()) {
		return array($ret->wrap(__FILE__, __LINE__), null);
	    }
	    foreach ($toolkits as $toolkit) {
		if (isset($toolkit)) {
		    list ($ret, $originationTimestamp) =
			$toolkit->getProperty($item->getMimeType(), 'originationTimestamp', $path);
		    if ($ret->isError()) {
			return array($ret->wrap(__FILE__, __LINE__), null);
		    }
		    if (is_array($originationTimestamp) && !empty($originationTimestamp[0])) {
			return array(GalleryStatus::success(), $originationTimestamp[0]);
		    }
		}
	    }
	}
	return array(GalleryStatus::success(), null);
    }

    /**
     * Add an existing data item to an album
     *
     * @param object GalleryItem the source item
     * @param int the id of the target album
     * @param boolean if true, skip check for existing derivatives
     * @return object GalleryStatus a status code
     * @static
     */
    function addExistingItemToAlbum($item, $albumId, $isNew=false) {

	/* Set the order weight */
	list ($ret, $maxWeight) = GalleryCoreApi::fetchExtremeChildWeight($albumId, HIGHER_WEIGHT);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$ret = GalleryCoreApi::setItemOrderWeight($item->getId(), $maxWeight + 1000);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/*
	 * Now create thumbnails and resizes according to the
	 * wishes of the parent album.
	 */
	list ($ret, $preferences) = GalleryCoreApi::fetchDerivativePreferencesForItem($albumId);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	list ($ret, $source) = GalleryCoreApi::fetchPreferredSource($item);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$mimeType = $source->getMimeType();

	/* Make the file type viewable, if we need to. */
	list ($ret, $isViewable) = GalleryCoreApi::isViewableMimeType($mimeType);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}
	if (!$isViewable && !strncmp($mimeType, 'image/', 6)) {
	    if (GalleryUtilities::isA($source, 'GalleryDerivative')) {
		$operations = $source->getDerivativeOperations();
	    } else {
		$operations = '';
	    }
	    list ($ret, $newOperations, $outputMimeType) =
		GalleryCoreApi::makeSupportedViewableOperationSequence(
		    $mimeType, $operations, false);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    if ($newOperations != $operations) {
		/*
		 * We now have operations to make it viewable.  If we don't have a preferred, then
		 * create one with those operations.  If we already have a preferred then just
		 * update it to use these operations.
		 */
		if (GalleryUtilities::isA($source, 'GalleryItem')) {
		    /* No preferred.  Create one */
		    list ($ret, $preferred) = GalleryCoreApi::newFactoryInstanceByHint(
			'GalleryDerivative', $item->getEntityType());
		    if ($ret->isError()) {
			return $ret->wrap(__FILE__, __LINE__);
		    }

		    if (!isset($preferred)) {
			return GalleryStatus::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__);
		    }

		    $ret = $preferred->create($source->getId(), DERIVATIVE_TYPE_IMAGE_PREFERRED);
		    if ($ret->isError()) {
			return $ret->wrap(__FILE__, __LINE__);
		    }

		    $preferred->setDerivativeSourceId($source->getId());
		    $preferred->setMimeType($outputMimeType);

		    $ret = GalleryCoreApi::remapSourceIds($source->getId(), $preferred->getId());
		    if ($ret->isError()) {
			return $ret->wrap(__FILE__, __LINE__);
		    }
		    $source = $preferred;
		} else {
		    /* Lock the preferred so that we can modify it */
		    list ($ret, $lockIds[]) = GalleryCoreApi::acquireWriteLock($source->getId());
		    if ($ret->isError()) {
			return $ret->wrap(__FILE__, __LINE__);
		    }

		    $ret = $source->expireCache();
		    if ($ret->isError()) {
			return $ret->wrap(__FILE__, __LINE__);
		    }
		}

		/* Either way, now $source is a preferred */
		$source->setDerivativeOperations($newOperations);

		/* Let our change ripple down the derivative tree, if necessary */
		$ret = GalleryCoreApi::adjustDependentDerivatives($source->getId(), $newOperations);
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}

		$ret = $source->save();
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}

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

		/* Mime type must have changed so that it can be viewable. */
		$mimeType = $outputMimeType;
	    }
	}

	/* Get existing derivatives */
	$derivs = array(DERIVATIVE_TYPE_IMAGE_THUMBNAIL => array(),
			DERIVATIVE_TYPE_IMAGE_RESIZE => array());
	if (!$isNew) {
	    list ($ret, $tmp) = GalleryCoreApi::fetchThumbnailsByItemIds(array($item->getId()));
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	    if (!empty($tmp)) {
		$derivs[DERIVATIVE_TYPE_IMAGE_THUMBNAIL][] = array_shift($tmp);
	    }
	    list ($ret, $tmp) = GalleryCoreApi::fetchResizesByItemIds(array($item->getId()));
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	    if (!empty($tmp)) {
		foreach (array_shift($tmp) as $resize) {
		    $derivs[DERIVATIVE_TYPE_IMAGE_RESIZE][] = $resize;
		}
	    }
	}
	$itemHasThumbnail = !empty($derivs[DERIVATIVE_TYPE_IMAGE_THUMBNAIL]);

	foreach ($preferences as $preference) {
	    $operations = $preference['derivativeOperations'];
	    $type = $preference['derivativeType'];

	    /* Special case to make sure that we don't upsample photos */
	    if (GalleryUtilities::isA($item, 'GalleryPhotoItem')) {
		if (preg_match('/(^|;)scale\|(\d+)/', $operations, $matches)) {
		    $maxDimension = max($item->getWidth(), $item->getHeight());
		    if ($matches[2] >= $maxDimension) {
			continue;
		    }
		}
	    }

	    if (!empty($derivs[$type])) {
		/*
		 * Check if we already have a derivative with matching operations.
		 * If so, keep it.
		 */
		foreach ($derivs[$type] as $i => $derivative) {
		    if (preg_match('/(^|;)' . preg_quote($operations) . '(;|$)/',
				   $derivative->getDerivativeOperations())) {
			unset($derivs[$type][$i]);
			continue 2;
		    }
		}

		/*
		 * We still have a derivative of the right type we can reuse.
		 * Save operation below will clear the derivative cache.
		 */
		$derivative = array_shift($derivs[$type]);
		list ($ret, $lockId) = GalleryCoreApi::acquireWriteLock($derivative->getId());
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}

		/* Preserve any existing derivative operations on a thumbnail */
		if ($type == DERIVATIVE_TYPE_IMAGE_THUMBNAIL) {
		    $list = explode('|', $operations);
		    if (count($list) == 2) {
			$tmpOperations = preg_replace('/((^|;)' . $list[0] . ')\|.*?(;|$)/',
						      '$1|' . $list[1] . '$3',
						      $derivative->getDerivativeOperations());
			if ($tmpOperations != $derivative->getDerivativeOperations()) {
			    $operations = $tmpOperations;
			}
		    }
		}
	    } else {
		/* Create a new derivative */
		list ($ret, $derivative) = GalleryCoreApi::newFactoryInstanceByHint(
		    'GalleryDerivative', $source->getEntityType());
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}

		if (!isset($derivative)) {
		    return GalleryStatus::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__);
		}

		$ret = $derivative->create($item->getId(), $type);
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
	    }

	    /*
	     * Validate/update the operations to make sure that they're supported
	     * by an active toolkit and produce a viewable mime type.
	     */
	    list ($ret, $operations, $outputMimeType) =
		GalleryCoreApi::makeSupportedViewableOperationSequence(
		    $mimeType, $operations, $type == DERIVATIVE_TYPE_IMAGE_THUMBNAIL);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    if (empty($operations)) {
		/* Oh well -- try the next preference */
		if (isset($lockId)) {
		    GalleryCoreApi::releaseLocks(array($lockId));
		    unset($lockId);
		}
		continue;
	    }

	    $derivative->setMimeType($outputMimeType);
	    $derivative->setDerivativeSourceId($source->getId());
	    $derivative->setDerivativeOperations($operations);

	    $derivative->setWidth(0);
	    $derivative->setHeight(0);
	    $ret = GalleryCoreApi::estimateDerivativeDimensions($derivative, $source);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    $ret = $derivative->save();
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

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

	    if ($type == DERIVATIVE_TYPE_IMAGE_THUMBNAIL) {
		$itemHasThumbnail = true;
	    }
	}

	/* Delete any old derivatives that have not been reused */
	foreach ($derivs as $tmp) {
	    foreach ($tmp as $derivative) {
		$ret = GalleryCoreApi::deleteEntityById($derivative->getId());
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
	    }
	}

	if ($itemHasThumbnail) {
	    /* See if the album has a thumbnail.  If not, try to use this item as the thumbnail. */
	    list ($ret, $thumbnailTable) =
		GalleryCoreApi::fetchThumbnailsByItemIds(array($albumId));
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    if (empty($thumbnailTable)) {
		list ($ret, $success) =
		    GalleryCoreApi::setThumbnailFromItem($albumId, $item->getId());
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
	    }
	}

	return GalleryStatus::success();
    }


    /**
     * Set the thumbnail for an album from an item, according to the thumbnail
     * preferences for the album.
     *
     * @param int the album id
     * @param int the item id
     * @return array object GalleryStatus a status code
     *               boolean true if successful
     * @static
     */
    function setThumbnailFromItem($itemId, $fromItemId) {
	global $gallery;

	/* Load the current album thumbnail. */
	list ($ret, $thumbnailTable) =
	    GalleryCoreApi::fetchThumbnailsByItemIds(array($itemId, $fromItemId));
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), false);
	}

	/*
	 * Find the right source id to create our thumbnail from.  Search for
	 * it in this order:
	 * 1. The fromItem's preferred derivative's source id
	 * 2. The fromItem's thumbnail's source id
	 * 3. The fromItem data itself
	 * 4. no thumbnail
	 */
	$source = null;
	$sourceMimeType = null;
	$parentId = null;
	$createdNewParentThumbnail = false;
	$success = false;

	/* 1. The fromItem's preferred derivative's source id */
	list ($ret, $derivatives) = GalleryCoreApi::fetchPreferredsByItemIds(array($fromItemId));
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}
	if (!empty($derivatives[$fromItemId])) {
	    $source = $derivatives[$fromItemId];
	    $sourceMimeType = $source->getMimeType();
	}

	/* 2. The item's thumbnail's source id */
	if (!$sourceMimeType) {
	    if (!empty($thumbnailTable[$fromItemId])) {
		$source = $thumbnailTable[$fromItemId];
		$sourceMimeType = $source->getMimeType();
	    }
	}

	/* 3. The item data itself */
	if (!$sourceMimeType) {
	    list ($ret, $fromItem) = GalleryCoreApi::loadEntitiesById($fromItemId);
	    if ($ret->isError()) {
		return array($ret->wrap(__FILE__, __LINE__), null);
	    }

	    if (GalleryUtilities::isA($fromItem, 'GalleryDataItem')) {
		$source = $fromItem;
		$sourceMimeType = $fromItem->getMimeType();
	    }
	}

	/*
	 * If at this point $source is undefined then we have no source
	 * thumbnail and that means that we're done.
	 */
	$lockIds = array();
	if (isset($source)) {
	    list ($ret, $lockIds[]) = GalleryCoreApi::acquireReadLock($source->getId());
	    if ($ret->isError()) {
		if (!empty($lockIds)) {
		    /* Ignore errors -- we're already in an error handler */
		    GalleryCoreApi::releaseLocks($lockIds);
		}
		return array($ret->wrap(__FILE__, __LINE__), null);
	    }

	    /* Reuse the existing derivative, if one exists */
	    if (isset($thumbnailTable[$itemId])) {
		$derivative = $thumbnailTable[$itemId];

		/* Modify existing thumbnail */
		list ($ret, $lockIds[]) = GalleryCoreApi::acquireWriteLock($derivative->getId());
		if ($ret->isError()) {
		    if (!empty($lockIds)) {
			/* Ignore errors -- we're already in an error handler */
			GalleryCoreApi::releaseLocks($lockIds);
		    }
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}

		list ($ret, $derivative) = $derivative->refresh();
		if ($ret->isError()) {
		    if (!empty($lockIds)) {
			/* Ignore errors -- we're already in an error handler */
			GalleryCoreApi::releaseLocks($lockIds);
		    }
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}

		$derivativeOperations = $derivative->getDerivativeOperations();
	    } else {
		/* Create a new one */
		list ($ret, $derivative) = GalleryCoreApi::newFactoryInstanceByHint(
		    'GalleryDerivative', $source->getEntityType());
		if ($ret->isError()) {
		    if (!empty($lockIds)) {
			/* Ignore errors -- we're already in an error handler */
			GalleryCoreApi::releaseLocks($lockIds);
		    }
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}

		if (!isset($derivative)) {
		    if (!empty($lockIds)) {
			/* Ignore errors -- we're already in an error handler */
			GalleryCoreApi::releaseLocks($lockIds);
		    }
		    return array(GalleryStatus::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__),
				 null);
		}

		$ret = $derivative->create($itemId, DERIVATIVE_TYPE_IMAGE_THUMBNAIL);
		if ($ret->isError()) {
		    if (!empty($lockIds)) {
			/* Ignore errors -- we're already in an error handler */
			GalleryCoreApi::releaseLocks($lockIds);
		    }
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}

		$createdNewParentThumbnail = true;

		/*
		 * The dimensions of the album's thumbnail are governed by the
		 * album's parent, so load the album, get its parent's id, get
		 * that album's preferences and use those for the thumbnail.
		 */
		list ($ret, $item) = GalleryCoreApi::loadEntitiesById($itemId);
		if ($ret->isError()) {
		    if (!empty($lockIds)) {
			/* Ignore errors -- we're already in an error handler */
			GalleryCoreApi::releaseLocks($lockIds);
		    }
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}

		$parentId = $item->getParentId();
		/* Use derivative preferences of self for root album highlight */
		if (empty($parentId)) {
		    $parentId = $item->getId();
		}
		list ($ret, $preferences) =
		    GalleryCoreApi::fetchDerivativePreferencesForItem($parentId);
		if ($ret->isError()) {
		    if (!empty($lockIds)) {
			/* Ignore errors -- we're already in an error handler */
			GalleryCoreApi::releaseLocks($lockIds);
		    }
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}

		foreach ($preferences as $preference) {
		    if ($preference['derivativeType'] == DERIVATIVE_TYPE_IMAGE_THUMBNAIL) {
			$derivativeOperations = $preference['derivativeOperations'];
			break;
		    }
		}

		if (empty($derivativeOperations)) {
		    if (!empty($lockIds)) {
			GalleryCoreApi::releaseLocks($lockIds);
		    }
		    return array(GalleryStatus::error(ERROR_MISSING_VALUE, __FILE__, __LINE__),
				 null);
		}
	    }

	    if (isset($derivativeOperations)) {
		/*
		 * Validate the stored preferences to make sure that
		 * they're supported by an active toolkit.
		 */
		list ($ret, $isSupported, $outputMimeType) =
		    GalleryCoreApi::isSupportedOperationSequence(
			$sourceMimeType, $derivativeOperations);
		if ($ret->isError()) {
		    if (!empty($lockIds)) {
			/* Ignore errors -- we're already in an error handler */
			GalleryCoreApi::releaseLocks($lockIds);
		    }
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}

		if ($isSupported) {
		    $success = true;
		    $derivative->setDerivativeOperations($derivativeOperations);
		    $derivative->setMimeType($outputMimeType);
		}
	    }

	    if ($success && isset($derivative)) {
		$derivative->setDerivativeSourceId($source->getId());

		/* After changing sources, the derivative dimensions are no longer valid. */
		$derivative->setWidth(0);
		$derivative->setHeight(0);
		$ret = GalleryCoreApi::estimateDerivativeDimensions($derivative, $source);
		if ($ret->isError()) {
		    if (!empty($lockIds)) {
			/* Ignore errors -- we're already in an error handler */
			GalleryCoreApi::releaseLocks($lockIds);
		    }
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}

		$ret = $derivative->save();
		if ($ret->isError()) {
		    if (!empty($lockIds)) {
			/* Ignore errors -- we're already in an error handler */
			GalleryCoreApi::releaseLocks($lockIds);
		    }
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
	    }

	    if (!empty($lockIds)) {
		$ret = GalleryCoreApi::releaseLocks($lockIds);
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
	    }
	}

	/*
	 * If we created a new thumbnail for the album, we should seek to
	 * propagate the thumbnail up to the parent album if it does not have a
	 * thumbnail.
	 */
	if ($success && !empty($parentId) && $createdNewParentThumbnail) {
	    list ($ret, $thumbTable) = GalleryCoreApi::fetchThumbnailsByItemIds(array($parentId));
	    if ($ret->isError()) {
		return array($ret->wrap(__FILE__, __LINE__), null);
	    }

	    if (empty($thumbTable)) {
		list ($ret, $success) =
		    GalleryItemHelper_medium::setThumbnailFromItem($parentId, $itemId);
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
	    }
	}

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

    /**
     * Invalidate our descendent count cache, any time we make a change that
     * would possibly affect any of our counts:
     * <ul>
     * <li> adding a new item
     * <li> deleting an item
     * <li> moving an item
     * <li> changing view permissions on an item
     * <li> adding a user to a group
     * <li> removing a user from a group
     * </ul>
     *
     * If you provide just a user id, it will invalidate all cached values for
     * that user.  If you provide just an item id, it'll invalidate that id and
     * all of its parents for all users.  If you provide both, it will only
     * invalidate for the intersection of the user and the item ids.
     *
     * @param mixed user id or array of ids or null
     * @param mixed item id or array of ids or null
     * @return object GalleryStatus a status code
     */
    function invalidateDescendentCountCaches($userId, $itemId) {
	GalleryCoreApi::relativeRequireOnce(
		'modules/core/classes/GalleryDescendentCountsMap.class');

	if (!empty($userId) && empty($itemId)) {
	    $ret = GalleryDescendentCountsMap::removeMapEntry(array('userId' => $userId));
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	}

	if (!empty($itemId)) {
	    if (!is_array($itemId)) {
		$itemId = array($itemId);
	    }
	    /* Gather up all ancestors of all specified ids */
	    $ids = array();
	    foreach ($itemId as $id) {
		list ($ret, $parentSequence) = GalleryCoreApi::fetchParentSequence($id);
		if ($ret->isError()) {
		    if ($ret->getErrorCode() & ERROR_MISSING_OBJECT) {
			/*
			 * It's legal to invalidate for objects that are in the
			 * process of being created
			 */
			continue;
		    }
		    return $ret->wrap(__FILE__, __LINE__);
		}
		foreach ($parentSequence as $parentId) {
		    $ids[$parentId] = true;
		}
		$ids[$id] = true;
	    }

	    if (!empty($ids)) {
		$data = array('itemId' => array_keys($ids));
		if (!empty($userId)) {
		    $data['userId'] = $userId;
		}
		$ret = GalleryDescendentCountsMap::removeMapEntry($data);
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
	    }
	}

	return GalleryStatus::success();
    }

    /**
     * Handler for Gallery::ViewableTreeChange event.
     * Event sends array('userId'=>id or array of ids or null, 'itemId'=>id or array of ids or null)
     *
     * @see GalleryEventListener::handleEvent
     */
    function handleEvent($event) {
	$param = $event->getData();
	if ($event->getEventName() == 'Gallery::ViewableTreeChange') {
	    $ret = $this->invalidateDescendentCountCaches($param['userId'], $param['itemId']);
	    if ($ret->isError()) {
		return array($ret->wrap(__FILE__, __LINE__), null);
	    }
	} else if ($event->getEventName() == 'Gallery::RemovePermission') {
	    /*
	     * RemovePermission event definition:
	     * @param 'userId' a user id or 0 for all users or null to specify no user at all
	     * @param 'groupId' a group id or 0 for all groups or null to specify no group at all
	     * @param 'itemIdsAndBits' array of itemId => permissionBits pairs
	     * @param 'format' either removeBits or newBits, defaults to removeBits
	     *
	     * With a single RemovePermission event one can handle an unlimited number of items.
	     * Sample usage:
	     *     $event = GalleryCoreApi::newEvent('Gallery::RemovePermission');
	     *     $event->setData(array('userId' => null, 'groupId' => $groupId,
	     *		                 'itemIdsAndBits' => array($itemId1 => $removeBits1,
	     *                                                     $itemId2 => $removeBits2)));
	     *     list ($ret) = GalleryCoreApi::postEvent($event);
	     *
	     * If the RemovePermission event affects multiple groupIds or multiple userIds, e.g.
	     * for GalleryCoreApi::removeItemPermission($itemId), the removedBits would be
	     * different for each userId / groupId. Thus, the event is simplified in such a case
	     * and you only specify the newBits of the item, because the newBits are the same for
	     * all userIds / groupIds. Use the 'format' => 'newBits' in this case.
	     *     $event = GalleryCoreApi::newEvent('Gallery::RemovePermission');
	     *     $event->setData(array('userId' => 0, 'groupId' => 0, 'format' => 'newBits',
	     *		                 'itemIdsAndBits' => array($itemId1 => $newBits1,
	     *                                                     $itemId2 => $newBits2)));
	     */

	    /* Here, we just check if the FastDownload files have to be removed */
	    foreach (array('core.view', 'core.viewResizes', 'core.viewSource') as $permissionId) {
		list ($ret, $bits[]) = GalleryCoreApi::convertPermissionIdsToBits($permissionId);
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
	    }

	    /* At the moment, we only have FastDownload files for the guest/anonymous user */
	    list ($ret, $anonymousUserId) =
		GalleryCoreApi::getPluginParameter('module', 'core', 'id.anonymousUser');
	    if ($ret->isError()) {
		return array($ret->wrap(__FILE__, __LINE__), null);
	    }

	    list ($ret, $groupIds) = GalleryCoreApi::fetchGroupsForUser($anonymousUserId);
	    if ($ret->isError()) {
		return array($ret->wrap(__FILE__, __LINE__), null);
	    }
	    $groupIds = array_keys($groupIds);
	    $groupIntersect = array_intersect(array($param['groupId']), $groupIds);
	    
	    if (isset($param['userId']) && ($param['userId'] == 0 ||
					    $param['userId'] == $anonymousUserId) ||
		isset($param['groupId']) && ($param['groupId'] == 0 || !empty($groupIntersect))) {

		$thumbnailIds = array();
		$resizeIds = array();
		$preferredIds = array();
		$targetDerivatives = array();

		/* Convert from newBits format to removeBits, if necessary */
		if (isset($param['format']) && $param['format'] == 'newBits') {
		    foreach ($param['itemIdsAndBits'] as $itemId => $newBits) {
			/*
			 * Potentially, all but $newBits were removed,
			 * thus removeBits := not newBits
			 */
			$param['itemIdsAndBits'][$itemId] = ~$newBits;
		    }
		}
		
		foreach ($param['itemIdsAndBits'] as $itemId => $removeBits) {
		    /* If core.view was removed then delete the thumbnail fast download */
		    if (($removeBits & $bits[0]) == $bits[0]) {
			$thumbnailIds[] = $itemId;
		    }
		    /* If core.viewResizes was removed then delete the resize fast downloads */
		    if (($removeBits & $bits[1]) == $bits[1]) {
			$resizeIds[] = $itemId;
		    }
		    /* If core.viewSource was removed then delete the preferred fast download */
		    if (($removeBits & $bits[2]) == $bits[2]) {
			$preferredIds[] = $itemId;
		    }
		}

		/* Load the thumbnails, resizes, and preferreds */
		list ($ret, $thumbnails) =
		    GalleryCoreApi::fetchThumbnailsByItemIds($thumbnailIds);
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
		$targetDerivatives = $thumbnails;
		
		list ($ret, $resizes) = GalleryCoreApi::fetchResizesByItemIds($resizeIds);
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
		foreach ($resizes as $resize) {
		    $targetDerivatives = array_merge($targetDerivatives, $resize);
		}
		
		list ($ret, $preferreds) = GalleryCoreApi::fetchPreferredsByItemIds($preferredIds);
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null);
		}
		$targetDerivatives = array_merge($targetDerivatives, $preferreds);
		
		foreach ($targetDerivatives as $derivative) {
		    $derivative->deleteFastDownloadFile();
		}
	    }
	}
	return array(GalleryStatus::success(), null);
    }
}
?>
