<?php
/*
 * $RCSfile: GalleryDerivative.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.60 $ $Date: 2005/09/10 06:11:20 $
 * @package GalleryCore
 * @author Bharat Mediratta <bharat@menalto.com>
 */

/**
 * Load the parent class
 */
GalleryCoreApi::relativeRequireOnce('modules/core/classes/GalleryChildEntity.class');

/**
 * A container for a data source that Gallery manages.
 *
 * A container for any data that Gallery can manage.  Known
 * implementations include ImageContainer, MovieContainer or
 * UnknownContainer.
 *
 * @g2 <class-name>GalleryDerivative</class-name>
 * @g2 <parent-class-name>GalleryChildEntity</parent-class-name>
 * @g2 <schema>
 * @g2   <schema-major>1</schema-major>
 * @g2   <schema-minor>1</schema-minor>
 * @g2 </schema>
 * @g2 <requires-id/>
 *
 * @package GalleryCore
 * @subpackage Classes
 */
class GalleryDerivative_core extends GalleryChildEntity {

    /*
     * ****************************************
     *                 Members
     * ****************************************
     */

    /**
     * What's the source of this derivative?  The source must be
     * the id of another DataContainer.
     *
     * @g2 <member>
     * @g2   <member-name>derivativeSourceId</member-name>
     * @g2   <member-type>INTEGER</member-type>
     * @g2   <indexed/>
     * @g2   <required/>
     * @g2 </member>
     *
     * @var int $_derivativeSourceId
     * @access private
     */
    var $_derivativeSourceId;

    /**
     * A sequence of operations used to derive this data from the original.
     * Can be empty, because the derivative can have only a postfilter.
     *
     * @g2 <member>
     * @g2   <member-name>derivativeOperations</member-name>
     * @g2   <member-type>STRING</member-type>
     * @g2   <member-size>LARGE</member-size>
     * @g2 </member>
     *
     * @var int $_derivativeOperations
     * @access private
     */
    var $_derivativeOperations;

    /**
     * The order of this derivative relative to others
     *
     * @g2 <member>
     * @g2   <member-name>derivativeOrder</member-name>
     * @g2   <member-type>INTEGER</member-type>
     * @g2   <indexed/>
     * @g2   <required/>
     * @g2 </member>
     */
    var $_derivativeOrder;

    /**
     * The size of the derived object
     *
     * @g2 <member>
     * @g2   <member-name>derivativeSize</member-name>
     * @g2   <member-type>INTEGER</member-type>
     * @g2 </member>
     *
     * @var int $_derivativeSize
     * @access private
     */
    var $_derivativeSize;

    /**
     * The type of this derivative (eg, DERIVATIVE_TYPE_IMAGE_THUMBNAIL)
     *
     * @g2 <member>
     * @g2   <member-name>derivativeType</member-name>
     * @g2   <member-type>INTEGER</member-type>
     * @g2   <indexed/>
     * @g2   <required/>
     * @g2 </member>
     */
    var $_derivativeType;

    /**
     * The mime type of data file
     *
     * @g2 <member>
     * @g2   <member-name>mimeType</member-name>
     * @g2   <member-type>STRING</member-type>
     * @g2   <member-size>MEDIUM</member-size>
     * @g2   <required/>
     * @g2 </member>
     *
     * @var string $_mimeType
     * @access private
     */
    var $_mimeType;

    /**
     * More operations that are applied at the very end of the derivative operations,
     * and are not carried down to derivatives that depend on this one.  Useful for
     * operations like watermarking that change the derivative in a way that we don't
     * want to propagate.
     *
     * Can be empty, because the derivative can have only a regular derivative operations.
     *
     * @g2 <member>
     * @g2   <member-name>postFilterOperations</member-name>
     * @g2   <member-type>STRING</member-type>
     * @g2   <member-size>LARGE</member-size>
     * @g2 </member>
     *
     * @var int $_postFilterOperations
     * @access private
     */
    var $_postFilterOperations;

    /**
     * Is the derivative broken?
     * We set this to true if a toolkit operation failed, or for similar reasons.
     *
     * Can be empty, which is interpreted as false
     *
     * @g2 <member>
     * @g2   <member-name>isBroken</member-name>
     * @g2   <member-type>BOOLEAN</member-type>
     * @g2 </member>
     *
     * @var int $_isBroken
     * @access private
     */
    var $_isBroken;

    /*
     * ****************************************
     *                 Methods
     * ****************************************
     */

    /**
     * Data items that can be viewed inline (photos, movies, etc) should return
     * true.  Items that can't be viewed inline (word documents, text, etc)
     * should return false.
     *
     * Classes that return true for this query must implement getWidth() and getHeight()
     *
     * @return boolean true if this data item can be viewed inline
     */
    function canBeViewedInline() {
	return false;
    }

    /**
     * Delete this GalleryEntity
     *
     * @access public
     * @return object GalleryStatus a status code
     */
    function delete() {
	global $gallery;

	/* Find all derivatives for whom I am the source and whack them too */
	/* TODO: just fetch the ids, instead of the entire entities */
	list ($ret, $derivativesTable) =
	    GalleryCoreApi::fetchDerivativesBySourceIds(array($this->getId()));
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	foreach ($derivativesTable as $itemId => $derivatives) {
	    foreach ($derivatives as $derivative) {
		$ret = GalleryCoreApi::deleteEntityById($derivative->getId());
		/*
		 * Deletes can cascade in interesting ways.  For example,
		 * deleting a derivative will get rid of any other derivatives
		 * that are sourced to it, so it's possible that deleting
		 * children here can lead to a MISSING_OBJECT result unless we
		 * re-run the parent/child query each time.  Easier to just
		 * ignore the MISSING_OBJECT error since we only care that it's
		 * gone.
		 */
		if ($ret->isError() && !($ret->getErrorCode() & ERROR_MISSING_OBJECT)) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
	    }
	}

	/* Delete myself */
	$ret = parent::delete();
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/* Expire our cache, and don't abort if this fails */
	$ret = $this->expireCache();

	GalleryDataCache::removeFromDisk(
	    array('type' => 'derivative-meta', 'itemId' => $this->getId()));

	return GalleryStatus::success();
    }

    /**
     * Create a new GalleryDerivative
     *
     * @param int the id of the parent GalleryItem
     * @return object GalleryStatus a status code
     */
    function create($parentId) {

	$ret = parent::create($parentId);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$this->setDerivativeOrder(0);
	$this->setIsBroken(false);

	return GalleryStatus::success();
    }

    /**
     * @see GalleryEntity::save()
     */
    function save($expire=true) {
	if ($this->testPersistentFlag(STORAGE_FLAG_NEWLY_CREATED)) {
	    $newItem = true;
	} else {
	    $newItem = false;
	}

	/* Save myself */
	$ret = parent::save();
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	if (!$newItem) {
	    /* Expire myself */
	    if ($expire) {
		$ret = $this->expireCache();
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
	    }

	    /* Expire anything that depends on me */
	    $ret = GalleryCoreApi::expireDerivativeTreeBySourceIds(array($this->getId()));
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	}

	return GalleryStatus::success();
    }

    /**
     * Rebuild the cache.  This should never be called directly; instead you
     * should call GalleryCoreApi::rebuildDerivativeCacheIfNotCurrent($derivativeId)
     *
     * Rebuilds the cache and marks the derivative as broken if we failed to rebuild the
     * derivative.
     *
     * @access public
     * @return object GalleryStatus a status code
     */
    function rebuildCache() {
	global $gallery;

	$ret = $this->_rebuildCache();

	if ($ret->isError()) {
	    /*
	     * Something went wrong with the toolkit
	     * Mark the derivative as broken for later repair attempts
	     */
	    $this->setIsBroken(true);
	    if ($gallery->getDebug()) {
		$gallery->debug("\n" . $ret->getAsText() . "\n");
	    }

	    /*
	     * Use our "busted" image / derivative instead.
	     * Figure out our target path
	     */
	    list($ret, $destPath) = $this->fetchPath();
	    if ($ret->isSuccess()) {
		/* Copy the broken derivative placeholder */
		$platform = $gallery->getPlatform();
		if ($platform->copy($this->getBrokenDerivativePath(), $destPath)) {
		    /* Get the mime type of the placeholder */
		    list ($ret, $mimeType) =
			GalleryCoreApi::getMimeType($this->getBrokenDerivativePath());
		    if ($ret->isSuccess()) {
			$this->setMimeType($mimeType);
			if ($platform->file_exists($destPath)) {
			    $size = $platform->filesize($destPath);
			    $this->setDerivativeSize($size);
			    $status = GalleryStatus::success();
			} else {
			    $status =
				GalleryStatus::error(ERROR_PLATFORM_FAILURE, __FILE__, __LINE__);
			}
		    } else {
			$status = $ret->wrap(__FILE__, __LINE__);
		    }
		} else {
		    $status = GalleryStatus::error(ERROR_PLATFORM_FAILURE, __FILE__, __LINE__);
		}
	    } else {
		$status = $ret->wrap(__FILE__, __LINE__);
	    }

	    /*
	     * It's important to remember that the derivative is broken and save wouldn't be
	     * called if we sent an error as a return value
	     */
	    $ret = $this->save(false);
	    if ($status->isError()) {
		return $status->wrap(__FILE__, __LINE__);
	    } elseif ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    /* Pretend the operation was successful */
	    $ret = GalleryStatus::success();
	} elseif ($this->getIsBroken()) {
	    $this->setIsBroken(false);
	}

	return $ret;
    }

    /**
     * Rebuild the cache.  This should never be called directly; instead you
     * should call GalleryCoreApi::rebuildDerivativeCacheIfNotCurrent($derivativeId)
     *
     * @access private
     * @return object GalleryStatus a status code
     */
    function _rebuildCache() {
	global $gallery;

	/* Figure out our target path */
	list($ret, $destPath) = $this->fetchPath();
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	/* Make sure our path is legit */
	GalleryUtilities::guaranteeDirExists(dirname($destPath));

	list ($ret, $source, $operations) = $this->fetchFinalOperations();
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	if (GalleryUtilities::isA($source, 'GalleryDerivative')) {
	    list ($ret, $rebuild) =
		GalleryCoreApi::rebuildDerivativeCacheIfNotCurrent($source->getId());
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    if ($rebuild) {
		/* Fetch the updated version back from our entity cache */
		list ($ret, $source) = GalleryCoreApi::loadEntitiesById($source->getId());
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
	    }

	    list ($ret, $sourcePath) = $source->fetchPath();
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	} else {
	    /* Get the path of the source file */
	    if ($source->isLinked()) {
		$linkedEntity = $source->getLinkedEntity();
		list($ret, $sourcePath) = $linkedEntity->fetchPath();
	    } else {
		list($ret, $sourcePath) = $source->fetchPath();
	    }
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	}

	$context = array();
	if (method_exists($source, 'getWidth') && method_exists($source, 'getHeight')) {
	    $context['width'] = $source->getWidth();
	    $context['height'] = $source->getHeight();
	    if ($context['width'] == 0 && $context['height'] == 0) {
		/* Don't put unknown size into context */
		$context = array();
	    }
	}

	/* Now apply our derivative commands to create the cache file */
	$mimeType = $source->getMimeType();
	for ($i = 0; $i < count($operations); $i++) {
	    if (strpos($operations[$i], '|') === false) {
		list($operationName, $operationArgs) = array($operations[$i], null);
	    } else {
		list($operationName, $operationArgs) = explode('|', $operations[$i]);
	    }

	    /* Get the appropriate toolkit */
	    list ($ret, $toolkit, $nextMimeType) =
		GalleryCoreApi::getToolkitByOperation($mimeType, $operationName);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    if (!isset($toolkit)) {
		return GalleryStatus::error(ERROR_UNSUPPORTED_OPERATION, __FILE__, __LINE__,
					    "$operationName $mimeType");
	    }

	    /* Put look-ahead info in context, so toolkit can decide if it can
	     * queue up parameters in the context for later processing, or just do it.
	     */
	    if ($i + 1 == count($operations)) {
		$context['next.toolkit'] = null;
		$context['next.operation'] = null;
	    } else {
		list ($nextOperationName) = explode('|', $operations[$i+1]);
		list ($ret, $nextToolkit) =
		    GalleryCoreApi::getToolkitByOperation($nextMimeType, $nextOperationName);
		if ($ret->isError()) {
		    return $ret->wrap(__FILE__, __LINE__);
		}
		$context['next.toolkit'] = isset($nextToolkit) ? $nextToolkit : null;
		$context['next.operation'] = $nextOperationName;
	    }

	    /* Perform the operation */
	    list ($ret, $outputMimeType, $context) = $toolkit->performOperation(
		$mimeType, $operationName, $sourcePath, $destPath,
		explode(',', $operationArgs), $context);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    /* Prepare for the next operation */
	    $sourcePath = $destPath;
	    $mimeType = $outputMimeType;
	}

	/* Get the size of the file */
	$platform = $gallery->getPlatform();
	if ($platform->file_exists($destPath)) {
	    $size = $platform->filesize($destPath);
	} else {
	    $size = -1;
	}

	/* Update our info */
	$this->setMimeType($mimeType);
	$this->setDerivativeSize($size);

	$data = array('derivativePath' => $destPath,
		      'derivativeType' => $this->getDerivativeType(),
		      'mimeType' => $this->getMimeType(),
		      'parentId' => $this->getParentId());
	list ($ret, $data['pseudoFileName']) = GalleryUtilities::getPseudoFileName($this);
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	GalleryDataCache::putToDisk(
	    array('type' => 'derivative-meta', 'itemId' => $this->getId()), $data);

	$this->deleteFastDownloadFile();
	$ret = $this->createFastDownloadFile();
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	return GalleryStatus::success();
    }

    /**
     * Delete our fast download file.
     */
    function deleteFastDownloadFile() {
	global $gallery;

	$fastDownloadFilePath = GalleryDataCache::getCachePath(
	    array('type' => 'fast-download', 'itemId' => $this->getId()));
	$platform = $gallery->getPlatform();
	if ($platform->file_exists($fastDownloadFilePath)) {
	    $platform->unlink($fastDownloadFilePath);
	}
    }

    /**
     * Create a small PHP file containing all the information we need to send
     * this derivative to the browser.
     *
     * @param bool force this to run, even inside the unit test framework
     *
     * @return object GalleryStatus a status code
     */
    function createFastDownloadFile($runEvenInUnitTest=false) {
	global $gallery;

	/* Disable this for unit tests, for now */
	if (!$runEvenInUnitTest && class_exists('GalleryTestCase')) {
	    return GalleryStatus::success();
	}

	/* Make sure this derivative is publicly viewable */
	list ($ret, $anonymousUserId) =
	    GalleryCoreApi::getPluginParameter('module', 'core', 'id.anonymousUser');
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	$hasPermission = false;
	$requiredPermission = null;
	switch ($this->getDerivativeType()) {
	case DERIVATIVE_TYPE_IMAGE_THUMBNAIL:
	    $requiredPermission = 'core.view';
	    break;

	case DERIVATIVE_TYPE_IMAGE_RESIZE:
	    $requiredPermission = 'core.viewResizes';
	    break;

	case DERIVATIVE_TYPE_IMAGE_PREFERRED:
	    $requiredPermission = 'core.viewSource';
	    break;
	}

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

	if ($requiredPermission) {
	    list ($ret, $permissions) = GalleryCoreApi::fetchPermissionsForItems(
		array($this->getParentId()), $anonymousUserId);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }
	    $hasPermission = isset($permissions[$this->getParentId()][$requiredPermission]);
	}

	if ($hasPermission) {
	    /* Write the fast download file */
	    list ($ret, $pseudoFileName) = GalleryUtilities::getPseudoFileName($this);
	    if ($ret->isError()) {
		return $ret->wrap(__FILE__, __LINE__);
	    }

	    $fullPath = GalleryDataCache::getCachePath(
		array('type' => 'derivative', 'itemId' => $this->getId()));
	    $relativePath = GalleryDataCache::getCachePath(
		array('type' => 'derivative-relative', 'itemId' => $this->getId()));
	    $platform = $gallery->getPlatform();
	    $stats = $platform->stat($fullPath);
	    $contentLength = $stats[7];
	    $lastModified = GalleryUtilities::getHttpDate($stats[9]);

	    $fastDownloadFilePath = GalleryDataCache::getCachePath(
		array('type' => 'fast-download', 'itemId' => $this->getId()));
	    $buf = sprintf('<?php function GalleryFastDownload() { ' .
			   'return $GLOBALS[\'gallery\']->fastDownload(\'%s\', \'%s\', ' .
			   '\'%s\', \'%s\', %d);} ?>',
			   $relativePath, $pseudoFileName, $lastModified,
			   $this->getMimeType(), $contentLength);
	    $platform->atomicWrite($fastDownloadFilePath, $buf);
	}

	return GalleryStatus::success();
    }

    /**
     * Get the complete set of operations required by this derivative.  This will return
     * the original source GalleryDataItem or preferred GalleryDerivative and an array of
     * all the operations that must be performed in order to create the correct output
     * file, including the post filter.
     *
     * @return array object GalleryStatus a status code
     *               object GalleryDataItem data item or GalleryDerivative preferred derivative
     *               array the operations
     */
    function fetchFinalOperations() {
	/* Load the source */
	list($ret, $source) = GalleryCoreApi::loadEntitiesById($this->getDerivativeSourceId());
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null, null);
	}

	if (GalleryUtilities::isA($source, 'GalleryDerivative')) {
	    /*
	     * In order to build our derivative, we need to complete set of
	     * commands from the original source file.  We can't rely on
	     * intervening files for two reasons:
	     *  1.  Larger derivatives sourced on derivatives would lead to upsampling
	     *  2.  The source derivative's cached data file may have postfilters applied, which we
	     *      don't want for our derivative
	     *
	     * So seek backwards to the source file, then merge and apply all
	     * the derivative operations from the intervening parents to
	     * discover the correct operations for this derivative.
	     */
	    $sources = array($source, $this);
	    while (GalleryUtilities::isA($sources[0], 'GalleryDerivative')) {
		list ($ret, $tmp) =
		    GalleryCoreApi::loadEntitiesById($sources[0]->getDerivativeSourceId());
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null, null);
		}

		if (!GalleryUtilities::isA($tmp, 'GalleryDerivative')) {
		    /* We've found our true source */
		    $source = $tmp;
		    break;
		}

		array_unshift($sources, $tmp);
	    }

	    /*
	     * If we have a preferred at the head of the chain, and that preferred has no
	     * postfilter operations then use that as the source.  We won't run any risk of
	     * upsampling, and it is as good a source as the original (better in fact, since we
	     * skip at least one operation).
	     */
	    if (count($sources) > 1 && /* we have at least one derivative in the chain */
		    $sources[0]->getDerivativeType() == DERIVATIVE_TYPE_IMAGE_PREFERRED &&
		    strlen($sources[0]->getPostFilterOperations()) == 0) {
		$source = array_shift($sources);  /* the preferred is now our source */
	    }

	    /*
	     * Now gather up all the remaining operations, and reduce them to the smallest
	     * possible sequence.
	     */
	    $operations = $sources[0]->getDerivativeOperations();
	    for ($i = 1; $i < sizeof($sources); $i++) {
		foreach (explode(';', $sources[$i]->getDerivativeOperations()) as $newOperation) {
		    list ($ret, $operations) =
			GalleryCoreApi::mergeDerivativeOperations($operations, $newOperation);
		    if ($ret->isError()) {
			return array($ret->wrap(__FILE__, __LINE__), null, null);
		    }
		}
	    }
	} else {
	    $operations = $this->getDerivativeOperations();
	}

	/* Merge in the postfilter operations */
	$postFilters = $this->getPostFilterOperations();
	if (empty($operations)) {
	    $operations = explode(';', $postFilters);
	} else {
	    foreach (explode(';', $postFilters) as $newOperation) {
		list ($ret, $operations) =
		    GalleryCoreApi::mergeDerivativeOperations($operations, $newOperation);
		if ($ret->isError()) {
		    return array($ret->wrap(__FILE__, __LINE__), null, null);
		}
	    }

	    $operations = explode(';', $operations);
	}

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

    /**
     * Is the cache for this item still current?
     *
     * If the cache is expired, it can be rebuilt with rebuildCache()
     *
     * @access public
     * @return array object GalleryStatus a status code,
     *               boolean false if the item is expired (ie, empty cache)
     */
    function isCacheCurrent() {
	global $gallery;
	$platform = $gallery->getPlatform();

	list($ret, $path) = $this->fetchPath();
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), false);
	}

	$bool = $platform->file_exists($path);
	return array(GalleryStatus::success(), $bool);
    }

    /**
     * Expire the cache.
     *
     * @access public
     * @return object GalleryStatus a status code
     */
    function expireCache() {
	global $gallery;
	$platform = $gallery->getPlatform();

	list($ret, $path) = $this->fetchPath();
	if ($ret->isError()) {
	    return $ret->wrap(__FILE__, __LINE__);
	}

	if ($platform->file_exists($path)) {
	    $platform->unlink($path);
	}

	$this->deleteFastDownloadFile();

	return GalleryStatus::success();
    }

    /**
     * Get the full path to the data file.
     *
     * @access public
     * @return array object GalleryStatus a status code,
     *               string a path where children can store their data files
     */
    function fetchPath() {
	global $gallery;

	$cacheFile = GalleryDataCache::getCachePath(array('type' => 'derivative',
							  'itemId' => $this->getId()));
	return array(GalleryStatus::success(), $cacheFile);
    }

    /**
     * Render this item in the given format.  For example,
     * GalleryDerivativeImage may want to render as an <img> tag in the HTML
     * format.
     *
     * @param string the format (eg, "HTML")
     * @param object GalleryDataItem the data item
     * @param array format specific key value pairs
     */
    function render($format, $item, $params) {
	return null;
    }

    /**
     * Return true if we have no derivative or postfilter operations
     *
     * @param bool true if there are no operations
     */
    function hasNoOperations() {
	$derivativeOperations = $this->getDerivativeOperations();
	$postfilterOperations = $this->getPostFilterOperations();
	return (empty($derivativeOperations) && empty($postfilterOperations));
    }

    /**
     * Return path for broken derivative placeholder which is shown when we fail to generate a
     * derivative item
     * Descendent classes can override this method to use their own broken derivative placeholder,
     * which can be of any mime type, e.g. a wav file for broken audio derivatives, etc.
     *
     * @return string the path of the broken derivative medium
     */
    function getBrokenDerivativePath() {
	/* Default to the broken-image.gif */
	return dirname(__FILE__) . '/../data/broken-image.gif';
    }
}

include(dirname(__FILE__) . '/interfaces/GalleryDerivative.inc');
?>
