<?php
require_once(dirname(__FILE__) . "/../lib/LocalizableException.php");
require_once(dirname(__FILE__) . "/../file_sources/PathOperations.php");
require_once(dirname(__FILE__) . "/MonstaInstallContext.php");
class MonstaUpdateInstallContext extends MonstaInstallContext {
private static $monstaTestItems = array(
"application",
"license",
"settings",
"application/api",
"application/frontend",
"index.php"
);
/**
* @param $extractDir
* @param $extractGroupRootItem
* @return mixed
*/
private static function getBackupPath($extractDir, $extractGroupRootItem) {
$destinationBackupPath = PathOperations::join($extractDir, PathOperations::stripTrailingSlash($extractGroupRootItem) . ".bak");
return $destinationBackupPath;
}
/**
* @param $rootItem
* @return bool
*/
private static function getItemIsDirectory($rootItem) {
return substr($rootItem, -1) === "/";
}
/**
* @param $extractDir
* @param $version
* @param $rootItem
* @return mixed
*/
private static function buildItemExtractDirPath($extractDir, $version, $rootItem) {
$itemIsDirectory = self::getItemIsDirectory($rootItem);
$rootItemName = $itemIsDirectory ? substr($rootItem, 0, strlen($rootItem) - 1) : $rootItem;
return PathOperations::join($extractDir, $rootItemName . "-" . $version);
}
/**
* @param $updateManifest
* @param $archiveFileName
* @return mixed
*/
private static function getManifestIndexForFileRoot($updateManifest, $archiveFileName) {
$relativeArchiveFileName = self::getRelativeArchivePath($archiveFileName);
for ($manifestIndex = 0; $manifestIndex < count($updateManifest); ++$manifestIndex) {
$manifestItem = $updateManifest[$manifestIndex];
if (substr($relativeArchiveFileName, 0, strlen($manifestItem)) == $manifestItem)
return $manifestIndex;
}
return false;
}
private function validateMonstaItemExists($installDirectory, $itemRelativePath) {
$itemPath = PathOperations::join($installDirectory, $itemRelativePath);
if (@!file_exists($itemPath)) {
throw new LocalizableException("Could not update in $installDirectory as it does not appear to be a Monsta FTP install; missing $itemPath",
LocalizableExceptionDefinition::$INSTALL_DIRECTORY_INVALID_ERROR, array("installPath" => $installDirectory, "itemPath" => $itemPath));
}
}
public function validateInstallDirectory($installDirectory) {
$errorPath = basename(dirname($installDirectory)) . "/" . basename($installDirectory);
if (@!file_exists($installDirectory))
throw new LocalizableException("Could not update in $errorPath as the directory does not exist",
LocalizableExceptionDefinition::$INSTALL_DIRECTORY_DOES_NOT_EXIST_ERROR, array("path" => $errorPath));
foreach (self::$monstaTestItems as $item) {
$this->validateMonstaItemExists($installDirectory, $item);
}
if (@!is_writable($installDirectory)) {
throw new LocalizableException("Could not update $errorPath as the directory is not writable",
LocalizableExceptionDefinition::$INSTALL_PATH_NOT_WRITABLE_ERROR, array("path" => $errorPath));
}
}
private function extractVersionFromArchive($archivePath, $archiveHandle) {
$version = $archiveHandle->getFromName(self::$archiveParentPath . "application/api/VERSION");
if ($version === FALSE)
$this->throwInvalidArchiveError($archivePath, $archiveHandle);
return trim($version);
}
private function processExtractGroup($archiveHandle, $extractDir, $version, $rootItem, $files) {
$fullExtractDir = self::buildItemExtractDirPath($extractDir, $version, $rootItem);
return $archiveHandle->extractTo($fullExtractDir, $files);
}
private function extractAllGroups($archiveHandle, $installDirectory, $version, $extractGroups) {
foreach ($extractGroups as $extractGroupRootItem => $extractFiles) {
if (!$this->processExtractGroup($archiveHandle, $installDirectory, $version, $extractGroupRootItem,
$extractFiles)
) {
return false;
}
}
return true;
}
private function restoreBackups($extractDir, $extractGroups) {
// go back and move backup dirs back into place if something fails
$errorOccurred = false;
foreach ($extractGroups as $extractGroupRootItem => $extractFiles) {
$originalPath = PathOperations::join($extractDir, $extractGroupRootItem);
$destinationBackupPath = self::getBackupPath($extractDir, $extractGroupRootItem);
if (!@file_exists($destinationBackupPath)) {
continue; // don't restore as there was not one originally
}
if (@file_exists($originalPath)) {
if (!PathOperations::recursiveDelete($originalPath)) {
$errorOccurred = true;
continue;
}
}
if (!@rename($destinationBackupPath, $originalPath)) {
$errorOccurred = true;
}
}
if ($errorOccurred) {
// things have gone pretty wrong here so here's a hail mary
throw new LocalizableException("Restoring backup after failed install failed.",
LocalizableExceptionDefinition::$INSTALL_SETUP_BACKUP_RESTORE_ERROR);
}
}
private function cleanUpBackups($extractDir, $extractGroups) {
$cleanupSuccess = true;
foreach ($extractGroups as $extractGroupRootItem => $extractFiles) {
$destinationBackupPath = self::getBackupPath($extractDir, $extractGroupRootItem);
if (!file_exists($destinationBackupPath)) {
continue; // don't delete as it wasn't backed up
}
if (!PathOperations::recursiveDelete($destinationBackupPath)) {
$cleanupSuccess = false;
}
}
return $cleanupSuccess;
}
private function moveItemsIntoPlace($extractDir, $fullExtractDir, $extractGroupRootItem) {
$source = PathOperations::join($fullExtractDir, self::$archiveParentPath, $extractGroupRootItem);
$destination = PathOperations::join($extractDir, $extractGroupRootItem);
$destinationBackupPath = self::getBackupPath($extractDir, $extractGroupRootItem);
if (@file_exists($destination) && !@rename($destination, $destinationBackupPath)) {
throw new LocalizableException("Install setup failed moving '$destination' to '$destinationBackupPath'.",
LocalizableExceptionDefinition::$INSTALL_SETUP_RENAME_ERROR, array(
"source" => $destination, // lmao looks weird but is correct
"destination" => $destinationBackupPath
));
}
if (!@rename($source, $destination)) {
throw new LocalizableException("Install setup failed moving '$source to '$destination'.",
LocalizableExceptionDefinition::$INSTALL_SETUP_RENAME_ERROR, array(
"source" => $source,
"destination" => $destination
));
}
PathOperations::recursiveDelete($fullExtractDir);
}
private function moveExtractGroupsIntoPlace($extractDir, $version, $extractGroups) {
foreach ($extractGroups as $extractGroupRootItem => $extractFiles) {
$fullExtractDir = self::buildItemExtractDirPath($extractDir, $version, $extractGroupRootItem);
try {
$this->moveItemsIntoPlace($extractDir, $fullExtractDir, $extractGroupRootItem);
} catch (Exception $e) {
$this->restoreBackups($extractDir, $extractGroups);
throw $e;
}
}
if (!$this->cleanUpBackups($extractDir, $extractGroups))
$this->setWarning("BACKUP_CLEANUP_ERROR", "Cleaning up of backups created during update failed.");
}
public function install($archivePath, $installDirectory) {
list($archiveHandle, $updateManifest) = $this->getArchiveHandleAndUpdateManifest($archivePath);
$newMonstaVersion = $this->extractVersionFromArchive($archivePath, $archiveHandle);
$extractGroups = $this->buildExtractGroupsFromArchive($archiveHandle, $updateManifest);
// for each item in the manifest, extract to directory with new version appended
if (!$this->extractAllGroups($archiveHandle, $installDirectory, $newMonstaVersion, $extractGroups)) {
/* cleanup as some of the files might have been extracted OK.
* It shouldn't fail if the directories to remove aren't there
*/
$this->cleanUpAfterExtract($installDirectory, $newMonstaVersion, $extractGroups);
throw new LocalizableException("Extract of install archive failed.",
LocalizableExceptionDefinition::$INSTALL_ARCHIVE_EXTRACT_ERROR);
}
try {
$this->moveExtractGroupsIntoPlace($installDirectory, $newMonstaVersion, $extractGroups);
} catch (Exception $e) {
$this->cleanUpAfterExtract($installDirectory, $newMonstaVersion, $extractGroups);
throw $e;
}
}
/**
* @param $archiveHandle ZipArchive
* @param $updateManifest array
* @return mixed
*/
private function buildExtractGroupsFromArchive($archiveHandle, $updateManifest) {
$extractGroups = array();
foreach (self::listArchive($archiveHandle) as $archiveFileName) {
$manifestIndex = self::getManifestIndexForFileRoot($updateManifest, $archiveFileName);
if ($manifestIndex === false) {
continue;
}
$originalManifestEntry = $updateManifest[$manifestIndex];
if (!isset($extractGroups[$originalManifestEntry])) {
$extractGroups[$originalManifestEntry] = array();
}
$extractGroups[$originalManifestEntry][] = $archiveFileName;
}
return $extractGroups;
}
private function cleanUpAfterExtract($extractDir, $version, $extractGroups) {
foreach (array_keys($extractGroups) as $extractGroupRootItem) {
$fullExtractDir = self::buildItemExtractDirPath($extractDir, $version, $extractGroupRootItem);
if (file_exists($fullExtractDir) && is_dir($fullExtractDir))
@rmdir($fullExtractDir);
}
}
}