<?php
/*
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2008 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.
 */

/**
 * Provides repository-related utility functions. Some of them are also used by repository tools.
 *
 * @package GalleryCore
 * @subpackage Classes
 * @author Jozef Selesi <selesi at gmail dot com>
 * @version $Revision: 17666 $
 */
class GalleryRepositoryUtilities {

    /**
     * Extracts the revision number from a string generated by CVS' Id tag.
     *
     * The pattern is one of:
     *  <DOLLAR>Id: it.po 13690 2006-05-19 18:01:46Z mindless <DOLLAR>
     *  <DOLLAR>Revision: 13690 <DOLLAR>
     *
     * @param string $string to exract revision from
     * @return array GalleryStatus a status code
     *               string revision
     * @todo On next major api bump make private or inline in getFileRevision()
     */
    function extractRevision($string) {
	if (preg_match('/Id: \S+ (\d+) \d.*/U', $string, $revision)) {
	    $revision = $revision[1];
	} else if (preg_match('/Revision: (\d+) /U', $string, $revision)) {
	    $revision = $revision[1];
	} else if (preg_match('/Id: \S+,v (.*?) .*/U', $string, $revision)) {
	    /* support 2.1.2 pre-svn strings.raw */
	    $revision = 10212;
	} else if (preg_match('/crc32 crc32/', $string)) {
	    /* support 2.1.2 pre-svn MANIFEST files*/
	    $revision = 10212;
	} else {
	    return array(GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
					      "No revision found in [$string]"),
			 null);
	}
	return array(null, $revision);
    }

    /**
     * Extracts the revision number from a string generated by CVS' Id tag.
     *
     * The pattern is one of:
     *  <DOLLAR>Id: it.po 13690 2006-05-19 18:01:46Z mindless <DOLLAR>
     *  <DOLLAR>Revision: 13690 <DOLLAR>
     *
     * @param string $file to extract revision from
     * @return array GalleryStatus a status code
     *               string revision
     */
    function getFileRevision($file) {
	list ($ret, $firstLine) = $this->getFirstBytesFromFile($file, 128);
	if ($ret) {
	    return array($ret, null);
	}

	return $this->extractRevision($firstLine);
    }

    /**
     * Compares two specified versions and, optionally, build numbers (timestamps). Returns the
     * relation between the first and second specified versions.
     *
     * @param string $version1 first version (x[.y][.z][...])
     * @param string $version2 second version
     * @param int $build1 first build (yyyymmddhhmmss)
     * @param int $build2 second build
     * @return array GalleryStatus a status code
     *		      string 'older', 'equal', 'newer'
     */
    function compareVersions($version1, $version2, $build1=null, $build2=null) {
	$relation = '';

	/* Compare versions if they're different. */
	if ($version1 != $version2) {
	    $relation = $this->compareRevisions($version1, $version2);
	}

	/* Compare builds if they're specified and if versions are identical. */
	if (!empty($build1) && !empty($build2) && empty($relation)) {
	    if ($build1 != $build2) {
		$relation = $build1 > $build2 ? 'newer' : 'older';
	    }
	}

	/* If no differences have been found, versions/builds are equal. */
	if (empty($relation)) {
	    $relation = 'equal';
	}

	return array(null, $relation);
    }

    /**
     * Compares two specified revisions.
     *
     * @param string $revision1 first revision (x[.y][.z][...])
     * @param string $revision2 second revision
     * @return string 'equal', 'older' or 'newer'
     */
    function compareRevisions($revision1, $revision2) {
	if ($revision1 == $revision2) {
	    return 'equal';
	}

	$revision1 = explode('.', $revision1);
	$revision2 = explode('.', $revision2);

	$subRevisions = max(count($revision1), count($revision2));
	for ($i = 0; $i < $subRevisions; $i ++) {
	    if (!isset($revision2[$i])) {
		$relation = 'newer';
		break;
	    }
	    if (!isset($revision1[$i])) {
		$relation = 'older';
		break;
	    }
	    if ($revision1[$i] != $revision2[$i]) {
		$relation = $revision1[$i] > $revision2[$i] ? 'newer' : 'older';
		break;
	    }
	}
	return $relation;
    }

    /**
     * Reads specified number of bytes from the file's beginning.
     *
     * @param string $path file path
     * @param int $bytes bytes to read
     * @return array GalleryStatus a status code
     *               string first line from file
     * @todo On next major api bump make private or inline in getFileRevision()
     */
    function getFirstBytesFromFile($path, $bytes) {
	global $gallery;
	$platform =& $gallery->getPlatform();

	if ($file = $platform->fopen($path, 'r')) {
	    $data = $platform->fread($file, $bytes);
	    $platform->fclose($file);
	} else {
	    return array(GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
					      "Error reading file [$path]"),
			 '');
	}
	return array(null, $data);
    }

    /**
     * Reads the strings.raw revision of the specified plugin.
     *
     * @param string $pluginType
     * @param string $pluginId
     * @return array GalleryStatus a status code
     *		     string strings.raw timestamp
     */
    function getLanguageBaseRevision($pluginType, $pluginId) {
	global $gallery;

	/* Make sure that strings.raw exists where we expect it. */
	$platform =& $gallery->getPlatform();
	$pluginBaseDir = GalleryCoreApi::getCodeBasePath();
	$pluginLanguageBasePath =
	    sprintf('%s%ss/%s/po/strings.raw', $pluginBaseDir, $pluginType, $pluginId);
	if (!$platform->file_exists($pluginLanguageBasePath)) {
	    return array(GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
					      "strings.raw not found [$pluginLanguageBasePath]"),
			 null);
	}

	/* Get the strings.raw file's first line which contains the timestamp. */
	list ($ret, $line) = $this->getFirstBytesFromFile($pluginLanguageBasePath, 128);
	if ($ret) {
	    return array($ret, null);
	}

	list ($ret, $revision) = $this->extractRevision($line);
	if ($ret) {
	    return array($ret, null);
	}

	return array(null, $revision);
    }

    /**
     * Determines whether the specified plugin is available in the local Gallery.
     *
     * @param string $pluginType
     * @param string $pluginId
     * @return array GalleryStatus a status code
     *		     boolean availability
     */
    function isPluginAvailable($pluginType, $pluginId) {
	list ($ret, $plugins) = GalleryCoreApi::getAllPluginIds($pluginType);
	if ($ret) {
	    return array($ret, null);
	}

	$isAvailable = in_array($pluginId, $plugins);
	return array(null, $isAvailable);
    }

    /**
     * Checks plugin compatibility with a certain version of Gallery.
     *
     * Determines whether the specified plugin type's required APIs are compatible with specified
     * provided APIs. If provided APIs are omitted, currently installed API versions will be
     * used.
     *
     * @param string $pluginType
     * @param array $requiredCoreApi required core API version
     * @param array $requiredPluginApi required theme/module API version
     * @param array $providedApis provided core API versions
     *              ('core', 'module', 'theme' => array(Major, Minor))
     * @return boolean compatibility
     */
    function isPluginCompatible($pluginType, $requiredCoreApi, $requiredPluginApi,
	    $providedApis=null) {

	/*
	 * We must explicitly convert version numbers to integers because
	 * GalleryUtilities::isCompatibleWithApi only works with integers.
	 */
	$requiredCoreApi[0] = (int)$requiredCoreApi[0];
	$requiredCoreApi[1] = (int)$requiredCoreApi[1];
	$requiredPluginApi[0] = (int)$requiredPluginApi[0];
	$requiredPluginApi[1] = (int)$requiredPluginApi[1];

	/* If no provided core API versions were specified, get versions from installed core. */
	list ($providedCoreApi, $providedPluginApi) =
	    $this->getProvidedApis($pluginType, $providedApis);

	return (GalleryUtilities::isCompatibleWithApi($requiredCoreApi, $providedCoreApi)
	    && GalleryUtilities::isCompatibleWithApi($requiredPluginApi, $providedPluginApi));
    }

    /**
     * Returns the provided APIs relevant to the specified plugin type.
     *
     * If no provided APIs are specified, currently installed API versions will be used.
     *
     * @param string $pluginType
     * @param array $providedApis provided core API versions
     *              ('core', 'module', 'theme' => array(Major, Minor))
     * @return array array provided core API version
     *		     array provided plugin (based on its type) API version
     */
    function getProvidedApis($pluginType, $providedApis=null) {
	if (empty($providedApis)) {
	    $providedCoreApi = GalleryCoreApi::getApiVersion();
	    /*
	     * GalleryModule will be loaded, but GalleryTheme may not be, yet because this
	     * is typically called from controller code.
	     */
	    GalleryCoreApi::requireOnce('modules/core/classes/GalleryTheme.class');
	    $providedPluginApi = $pluginType == 'module' ? GalleryModule::getApiVersion()
							 : GalleryTheme::getApiVersion();
	} else {
	    $providedCoreApi[0] = (int)$providedApis['core'][0];
	    $providedCoreApi[1] = (int)$providedApis['core'][1];
	    $providedPluginApi[0] = $pluginType == 'module' ? (int)$providedApis['module'][0]
							    : (int)$providedApis['theme'][0];
	    $providedPluginApi[1] = $pluginType == 'module' ? (int)$providedApis['module'][1]
							    : (int)$providedApis['theme'][1];
	}

	return array($providedCoreApi, $providedPluginApi);
    }

    /**
     * Gets version and build information about the installed packages of the specified plugin.
     *
     * @param string $pluginType
     * @param string $pluginId
     * @return array GalleryStatus a status code
     *		     array[$packageName] => array('version' => $version, 'build' => $build)
     */
    function getPluginPackages($pluginType, $pluginId) {
	if (empty($pluginType) || empty($pluginId)) {
	    return array(GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
					 "Missing plugin type [$pluginType] and/or ID [$pluginId]"),
			 null);
	}
	list ($ret, $searchResults) = GalleryCoreApi::getMapEntry('GalleryPluginPackageMap',
            array('locked', 'packageVersion', 'packageBuild', 'packageName'),
	    array('pluginType' => $pluginType, 'pluginId' => $pluginId));
	if ($ret) {
	    return array($ret, null);
	}

	$data = array();
	while ($result = $searchResults->nextResult()) {
	    $data[$result[3]] =
		array('locked' => $result[0], 'version' => $result[1], 'build' => $result[2]);
	}

	return array(null, $data);
    }

    /**
     * Returns the language description of the specified language-country code.
     * eg. en_US => English (US)
     *
     * @param string $languageCode
     * @return array GalleryStatus a status code
     *		     string language description
     * @deprecated use GalleryCoreApi::getLanguageDescription() remove after next api bump.
     */
    function getLanguageDescription($languageCode) {
	return GalleryCoreApi::getLanguageDescription($languageCode);
    }

    /**
     * Returns the version of the specified plugin.
     *
     * @param string $pluginType
     * @param string $pluginId
     * @return array GalleryStatus a status code
     *		     string version
     */
    function getPluginVersion($pluginType, $pluginId) {
	list ($ret, $plugin) = GalleryCoreApi::loadPlugin($pluginType, $pluginId, true);
	if ($ret) {
	    return array($ret, null);
	}

	return array(null, $plugin->getVersion());
    }

    /**
     * Downloads a file from the specified URL.
     *
     * It currently calls GalleryCoreApi::fetchWebPage to do all the work.  If the gzinflate()
     * function is available, then download the gzipped version of the file and unpack it locally
     * to save bandwidth.
     *
     * @param string $url url to download from
     * @param string $ignoreCompression don't try to get the compressed version of the file
     * @return array boolean file was successfully downloaded
     *		     string file contents
     */
    function downloadFile($url, $ignoreCompression=false) {
	global $gallery;

	$phpVm = $gallery->getPhpVm();
	$needsInflation = false;
	if (!$ignoreCompression && $phpVm->function_exists('gzinflate')) {
	    $url .= '.gz';
	    $needsInflation = true;
	}

	$url .= '?' . $this->_getApiQueryParams();
	list ($successful, $contents, $response, $headers, $actualUrl) =
	    GalleryCoreApi::fetchWebPage($url);
	if (empty($contents) || !$successful) {
	    return array(false, null);
	}

	if ($needsInflation) {
	    $contents = $phpVm->gzinflate($contents);
	}
	return array(true, $contents);
    }

    /**
     * Saves the specified package meta data into the database.
     *
     * @param string $pluginType
     * @param string $pluginId
     * @param string $packageName
     * @param string $packageVersion
     * @param string $packageBuild
     * @param string $locked
     * @return GalleryStatus a status code
     */
    function updatePackageMetaData(
	$pluginType, $pluginId, $packageName, $packageVersion, $packageBuild, $locked) {

	/* Check if specified package exists in the database. */
	list ($ret, $searchResults) = GalleryCoreApi::getMapEntry('GalleryPluginPackageMap',
	    array('packageVersion', 'packageBuild'),
	    array('pluginType' => $pluginType, 'pluginId' => $pluginId,
		  'packageName' => $packageName));
	if ($ret) {
	    return $ret;
	}

	if ($searchResults->resultCount() > 1) {
	    return GalleryCoreApi::error(
		ERROR_STORAGE_FAILURE, __FILE__, __LINE__,
		"Multiple records found [$pluginType] [$pluginId] [$package]");
	}
	$existsInDatabase = $searchResults->resultCount() > 0;

	/* Add or update package data. */
	if ($existsInDatabase) {
	    $ret = GalleryCoreApi::updateMapEntry(
		'GalleryPluginPackageMap',
		array('pluginType' => $pluginType, 'pluginId' => $pluginId,
		      'packageName' => $packageName),
		array('packageVersion' => $packageVersion, 'packageBuild' => $packageBuild,
		      'locked' => $locked));
	} else {
	    $ret = GalleryCoreApi::addMapEntry(
		'GalleryPluginPackageMap',
		array('pluginType' => $pluginType, 'pluginId' => $pluginId,
		      'packageName' => $packageName, 'packageVersion' => $packageVersion,
		      'packageBuild' => $packageBuild, 'locked' => $locked));
	}
	if ($ret) {
	    return $ret;
	}

	return null;
    }

    /**
     * Verify that a package will install cleanly by examining all of its paths and making sure
     * that any file operations that we intend to make will be successful.
     *
     * @param string $pluginType the plugin type (eg. module, theme)
     * @param string $pluginId the plugin id
     * @param string $packageName name of the package to check
     * @param array $descriptor descriptor of the plugin the package belongs to
     * @return array of files that can't be overwritten (empty array if everything is ok)
     */
    function preVerifyPackage($pluginType, $pluginId, $packageName, $descriptor) {
	global $gallery;
	$platform =& $gallery->getPlatform();

	$errorMessages = array();

	$pluginOutputDir = sprintf(
	    '%s%ss/%s/', GalleryCoreApi::getCodeBasePath(), $pluginType, $pluginId);

	if ($platform->file_exists($pluginOutputDir)) {
	    if (!$platform->is_writeable($pluginOutputDir) ||
		!$platform->is_dir($pluginOutputDir)) {
		return array($pluginOutputDir);
	    }
	} else {
	    return array();
	}

	foreach ($descriptor['contents']['files'] as $relativePath => $metaData) {
	    foreach ($metaData['packages'] as $descriptorPackage) {
		$gallery->guaranteeTimeLimit(10);

		if ($descriptorPackage == $packageName) {
		    $filePath = $pluginOutputDir . $relativePath;

		    if ($platform->file_exists($filePath)) {
			if (!$platform->is_writeable($filePath)) {
			    $errorMessages[$filePath] = 1;
			}
		    } else {
			$checkPath = dirname($filePath) . '/';
			while ($checkPath != $pluginOutputDir && $checkPath != '.') {
			    if ($platform->file_exists($checkPath) &&
				    (!$platform->is_writeable($checkPath) ||
				     !$platform->is_dir($checkPath))) {
				$errorMessages[$filePath] = 1;
				break;
			    }
			    $checkPath = dirname($checkPath) . '/';
			}
		    }
		}
	    }
	}

	return array_keys($errorMessages);
    }

    /**
     * Verifies the integrity of the specified packages' unpacked files.
     *
     * @param string $pluginType
     * @param string $pluginId
     * @param string $packageName name of the package to check
     * @param array $descriptor descriptor of the plugin the package belongs to
     * @return GalleryStatus a status code
     */
    function verifyPackageIntegrity($pluginType, $pluginId, $packageName, $descriptor) {
	global $gallery;
	$errorMessages = array();

	$pluginOutputDir = sprintf(
	    '%s%ss/%s/', GalleryCoreApi::getCodeBasePath(), $pluginType, $pluginId);

	$platform =& $gallery->getPlatform();
	foreach ($descriptor['files'] as $filePath => $metaData) {
	    foreach ($metaData['packages'] as $descriptorPackage) {
		if ($descriptorPackage == $packageName) {
		    $filePath = $pluginOutputDir . $filePath;
		    $contents = $platform->file_get_contents($filePath);
		    $checksum = strlen($metaData['hash']) == 32
			? md5($contents)
			: sprintf("%u", crc32($contents));

		    /**
		     * hash_crlf and bytes_crlf were introduced in r17072 (2.3) so they don't
		     * exist in older packages.  Set them to null if they don't exist so that we
		     * don't get errors below.
		     *
		     * @todo: remove this when all of our DP packages have been generated these
		     * fields.
		     */
		    if (empty($metaData['hash_crlf'])) {
			$metaData['hash_crlf'] = null;
		    }
		    if (empty($metaData['bytes_crlf'])) {
			$metaData['bytes_crlf'] = null;
		    }

		    if (!$platform->file_exists($filePath)) {
			$errorMessage = "'$filePath' doesn't exist.";
		    } else if (!in_array($platform->filesize($filePath),
					 array($metaData['bytes'], $metaData['bytes_crlf']))) {
			$errorMessage = "Size of '$filePath' did not match package descriptor.";
		    } else if (false === $contents) {
			$errorMessage = "Couldn't read '$filePath'.";
		    } else if (!in_array($checksum,
					 array($metaData['hash'], $metaData['hash_crlf']))) {
			$errorMessage = "Integrity check failed for '$filePath'.";
		    }

		    if (!empty($errorMessage)) {
			return GalleryCoreApi::error(ERROR_PLATFORM_FAILURE, __FILE__, __LINE__,
						    $errorMessage);
		    }
		}
	    }
	}

	return null;
    }

    /**
     * This method reads the aggregate download file and extracts the package files. 
     * The format of the file is:
     * file: relative filename(.gz) lengthCRLF
     * binary data if compressed or text if not compressed
     * 
     * file: relative filename(.gz) lengthCRLF
     * binary data if compressed or text if not compressed
     *
     * @param string $source name of the repository to be accessed
     * @param string $outputFile name of the temporary file containing the downloaded packages
     * @param array $callback progress notification callback
     * @return array GalleryStatus a status code
     * 		     array of packages that were downloaded
     */
    function splitAggregatePackage($source, $outputFile, &$callback) {
	global $gallery;
	$platform =& $gallery->getPlatform();
	$phpVm = $gallery->getPhpVm();

	$packages = array('module' => array(), 'theme' => array());

	/* Sample: file: modules/albumselect-lang-en_GB-12345-12345.package.gz 12345 */
	$languagePackPattern = '#^file:\s+((module|theme)s/([a-zA-Z0-9]+)-'
	    . '(?:(lang-[a-z]{2}(?:_[A-Z]{2})?)(?:-\d+){2})\.package)(\.gz)?\s+(\d+)$#';

	/* Sample: file: modules/albumselect-1.0.10-12345-base.package.gz 12345 */
	 $basePattern = '#^file:\s+((module|theme)s/([a-zA-Z0-9]+)-(?:\.?\d+){2,}-\d+-'
	    . 'base\.package)(\.gz)?\s+(\d+)$#';

	/* Sample: file: modules/albumselect-1.0.10-12345.descriptor.gz 12345 */
	$descriptorPattern = '#^file:\s+((module|theme)s/([a-zA-Z0-9]+)-(?:\.?\d+){2,}-\d+'
	    . '\.descriptor)(\.gz)?\s+(\d+)$#';

	$handle = $platform->fopen($outputFile, 'r');
	if (empty($handle)) {
	    return array(GalleryCoreApi::error(ERROR_PLATFORM_FAILURE, __FILE__, __LINE__,
		'Couldn\'t open aggregate package file'), null);
	}
	$cacheDir = $gallery->getConfig('repository.cache') . $source . '/';
	while (($line = $platform->fgets($handle, 4096)) !== false) {
	    $gallery->guaranteeTimeLimit(30);

	    $line = trim($line);
	    if (preg_match($languagePackPattern, $line, $matches)) {
		list ($value, $file, $pluginType, $pluginId, $packageName, $needsInflation,
		    $byteCount) = $matches;
		$isDescriptor = false;
	    } else if (preg_match($basePattern, $line, $matches)) {
		list ($value, $file, $pluginType, $pluginId, $needsInflation, $byteCount) = 
		    $matches;
		$packageName = 'base';
		$isDescriptor = false;
	    } else if (preg_match($descriptorPattern, $line, $matches)) {
		list ($value, $file, $pluginType, $pluginId, $needsInflation, $byteCount) = 
		    $matches;
		$packageName = null;
		$isDescriptor = true;
	    } else {
		$platform->fclose($handle);
		return array(GalleryCoreApi::error(ERROR_UNSUPPORTED_FILE_TYPE, null, null,
		    "Unexpected data encountered on download \"$line\""), null);
	    }

	    $contents = $platform->fread($handle, $byteCount);

	    if (!empty($needsInflation)) {
		if ($phpVm->function_exists('gzinflate')) {
		    $contents = $phpVm->gzinflate($contents);
		} else {
		    $platform->fclose($handle);
		    return array(GalleryCoreApi::error(ERROR_UNSUPPORTED_FILE_TYPE, null, null,
			"gzinflate not installed and is required to inflate: $file"), null);
		}
	    }

	    if (empty($packages[$pluginType][$pluginId])) {
		$packages[$pluginType][$pluginId] = array('files' => array());
	    }

	    /* Write package to the local repository cache directory */
	    $absolutePackagePath = $cacheDir . $file;
	    if ($platform->file_put_contents($absolutePackagePath, $contents) === false) {
		$platform->fclose($handle);
		return array(GalleryCoreApi::error(ERROR_PLATFORM_FAILURE, null, null,
		    "Error writing package: $absolutePackagePath"), null);
	    }

	    if ($isDescriptor) {
		$packages[$pluginType][$pluginId]['descriptor'] = unserialize($contents);
	    } else {
		$packages[$pluginType][$pluginId]['files'][] = array($packageName, $file);
	    }

	    call_user_func($callback['method'], $callback['title'], '', 
		++$callback['current'] / $callback['total']);
	}
	$platform->fclose($handle);
	return array(null, $packages);
    }

    /*
     * Unpack and install a downloaded package file.
     * This is primarily to allow unit testing.
     * @param string $packageFile The file containing the package
     * @param string $outputDirecory Directory the package should be installed to
     */
    function unpackPackage($packageFile, $outputDirecory) {
	include($packageFile);
	call_user_func($unpackFunction, $outputDirecory);
    }

    /**
     * Retrieve the core, module and theme API versions.
     * @return string the API version numbers in http query format.
     * @access private
     */
    function _getApiQueryParams() {
	static $apiQueryParams;
	if (empty($apiQueryParams)) {
	    GalleryCoreApi::requireOnce('modules/core/classes/GalleryTheme.class');
	    $apiQueryParams = sprintf('coreApi=%s&moduleApi=%s&themeApi=%s',
				      join('.', GalleryCoreApi::getApiVersion()),
				      join('.', GalleryModule::getApiVersion()),
				      join('.', GalleryTheme::getApiVersion()));
	}

	return $apiQueryParams;
    }

    /**
     * Contact the repository and download the specified files with one request.
     *
     * @param string $source repository source
     * @param array $filesToDownload list of files to download
     * @return array GalleryStatus a status code
     * 		     string name of the temporary file containing the aggregated download
     */
    function downloadAggregatePackages($source, $filesToDownload) {
	global $gallery;
	$platform =& $gallery->getPlatform();
	$phpVm = $gallery->getPhpVm();

	/* Create a temporary file for the multipart download file */
	$tmpDir = $gallery->getConfig('data.gallery.tmp');
	$outputFile = $platform->tempnam($tmpDir, 'multipart');
	if (!$platform->file_exists($outputFile)) {
	    return array(GalleryCoreApi::error(ERROR_PLATFORM_FAILURE, __FILE__, __LINE__,
		'Couldn\'t create temporary file'), null);
	}

	$postDataArray = array();
	$useCompression = $phpVm->function_exists('gzinflate');

	foreach ($filesToDownload as $key => $file) {
	    $postDataArray["files[$key]"] = $file . ($useCompression ? '.gz' : '');
	}

	$url = $gallery->getConfig('repository.url') . $source . '/multipart/';
	$url .= '?' . $this->_getApiQueryParams();

	$gallery->guaranteeTimeLimit(60);

	list ($success, $response, $headers, $location) = 
	    GalleryCoreApi::fetchWebFile($url, $outputFile, array(), $postDataArray);

	if (empty($success)) {
	    $platform->unlink($outputFile);
	    return array(GalleryCoreApi::error(ERROR_BAD_PATH, null, null, 
		"Failed to download packages: \"$response\"."), null);
	}
	return array(null, $outputFile);
    }
}
?>
