mirror of https://github.com/Mowie/Mowie
489 lines
10 KiB
PHP
Executable File
489 lines
10 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Mowie\Updater;
|
|
|
|
use Alchemy\Zippy\Zippy;
|
|
|
|
class updater
|
|
{
|
|
private $currentVersion;
|
|
private $oldVersion;
|
|
private $newVersion;
|
|
|
|
private $updateServer;
|
|
private $updateDir;
|
|
|
|
private $newUpdateFiles;
|
|
|
|
/**
|
|
* Every file/folder, including subdirectories which should not be updated or deleted after the update.
|
|
* @var string
|
|
*/
|
|
public $thingsToNotUpdate;
|
|
|
|
public function __construct()
|
|
{
|
|
// Bypass memory error
|
|
ini_set('memory_limit', '-1');
|
|
|
|
// Bypass Max execution time
|
|
set_time_limit(0);
|
|
}
|
|
|
|
/**
|
|
* Sets the server to use. Can be an array, in this case it will loop through all servers and check if there is an update
|
|
* @param $server
|
|
*/
|
|
public function setServer($server)
|
|
{
|
|
$this->updateServer = $server;
|
|
}
|
|
|
|
/**
|
|
* Sets the update directory on which the update is applied on.
|
|
* @param $dir
|
|
*/
|
|
public function setUpdateDir($dir)
|
|
{
|
|
$this->updateDir = $dir;
|
|
}
|
|
|
|
/**
|
|
* Sets the current version. Checks if it is properly formatted, throws an exeption if not.
|
|
* @param $version
|
|
* @throws \Exception
|
|
*/
|
|
public function setCurrentVersion($version)
|
|
{
|
|
// Check if version string is properly formatted
|
|
if (preg_match('/[0-9]+\.[0-9]+\.[0-9]+/', $version))
|
|
{
|
|
$this->currentVersion = $version;
|
|
} else
|
|
{
|
|
throw new \Exception('Version string not properly formatted! Please format it following the conventions on http://semver.org');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the current version
|
|
* @return string
|
|
*/
|
|
public function getCurrentVersion()
|
|
{
|
|
return $this->currentVersion;
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////
|
|
/// Check if an Update is available. ///
|
|
/////////////////////////////////////////
|
|
|
|
|
|
/**
|
|
* Checks if an update is available. If there is, returns an array with all informations for said update.
|
|
* @return array|null
|
|
*/
|
|
public function checkUpdateAvailable()
|
|
{
|
|
// Loop through all servers
|
|
foreach ($this->updateServer as $server)
|
|
{
|
|
// Get update infos
|
|
$updateInfos = $this->getNewestVersionFromServer($server);
|
|
if (isset($updateInfos))
|
|
{
|
|
$this->newVersion = $updateInfos['version'];
|
|
if (version_compare($updateInfos['version'], $this->currentVersion, '>'))
|
|
{
|
|
$updateInfos['server'] = $server;
|
|
return $updateInfos;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the newest version from a server.
|
|
* @param $server
|
|
* @return array|null
|
|
*/
|
|
public function getNewestVersionFromServer($server)
|
|
{
|
|
$fullUpdateList = $this->getUpdateInfoFromServer($server);
|
|
if (isset($fullUpdateList))
|
|
{
|
|
return end($fullUpdateList);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks if an update exists on the remote server. If there is, returns an array with update informations.
|
|
* Otherwise returns an empty array.
|
|
* @param $server
|
|
* @return array|null
|
|
*/
|
|
public function getUpdateInfoFromServer($server)
|
|
{
|
|
/**
|
|
* The update.json needs to have a form like this:
|
|
* [
|
|
* {
|
|
* "version": "0.96.1",
|
|
* "download": "v0.96.1.zip",
|
|
* "hash": "3437df76568baa21f8d4d1dfe562bc23f723001b97fac9be05515f60330d463b"
|
|
* }
|
|
* ]
|
|
*/
|
|
$updateInfosUrl = $server . 'update.json';
|
|
if ($this->remote_file_exists($updateInfosUrl))
|
|
{
|
|
$updateInfo = json_decode(urldecode(file_get_contents($updateInfosUrl)), true);
|
|
return $updateInfo;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function getChangelog($changelogUrl)
|
|
{
|
|
if ($this->remote_file_exists($changelogUrl))
|
|
{
|
|
return file_get_contents($changelogUrl);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
|
|
//////////////////////////////////
|
|
/// Check writing permissions ///
|
|
//////////////////////////////////
|
|
|
|
|
|
/**
|
|
* Checks if the given update folder is writable.
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
public function updateFolderIsWritable($updateFolder = '')
|
|
{
|
|
if ($updateFolder == '') $updateFolder = $this->updateDir;
|
|
if (is_dir($updateFolder))
|
|
{
|
|
if (is_writable($updateFolder))
|
|
{
|
|
$objects = scandir($updateFolder);
|
|
foreach ($objects as $object)
|
|
{
|
|
if (is_dir($object) && $object != "." && $object != ".." && substr($object, 0, 1) != '.')
|
|
{
|
|
if (!$this->updateFolderIsWritable($updateFolder . "/" . $object))
|
|
{
|
|
return false;
|
|
} else
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
} else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ($updateFolder == $this->updateDir && !is_dir($this->updateDir))
|
|
{
|
|
throw new \Exception('Update folder is not a folder!');
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////
|
|
/// Download Update ///
|
|
////////////////////////
|
|
|
|
|
|
/**
|
|
* Download the update to a temporary path
|
|
* @param $updateInfos
|
|
* @return bool
|
|
*/
|
|
public function downloadUpdate($updateInfos)
|
|
{
|
|
return copy($updateInfos['server'] . $updateInfos['download'], '.update.tmp.zip');
|
|
}
|
|
|
|
/**
|
|
* Verifies the downloaded update file
|
|
* @param $updateInfos
|
|
* @return bool
|
|
*/
|
|
public function verifyUpdate($updateInfos)
|
|
{
|
|
$hash = hash_file('sha256', '.update.tmp.zip');
|
|
if ($hash === $updateInfos['hash'])
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
////////////////////////////////
|
|
/// Backup the udate folder ///
|
|
////////////////////////////////
|
|
|
|
|
|
/**
|
|
* Creates a backup of the current state to use later in case we screw something up later
|
|
* @return bool
|
|
*/
|
|
public function backupUpdateFolder()
|
|
{
|
|
$zippy = Zippy::load();
|
|
$archive = $zippy->create('.update-backup.zip', array(
|
|
'folder' => $this->updateDir
|
|
), true);
|
|
|
|
// Check if the creation was successfull
|
|
if (file_exists('.update-backup.zip'))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
//////////////////////////
|
|
/// The actual update ///
|
|
//////////////////////////
|
|
|
|
|
|
/**
|
|
* Extracts all new files to the update folder, cleans all files which don't exist in the update
|
|
* @throws \Exception
|
|
*/
|
|
public function rollTheUpdate()
|
|
{
|
|
// Update Versions
|
|
$this->oldVersion = $this->currentVersion;
|
|
$this->currentVersion = $this->newVersion;
|
|
|
|
// Upzip the downloaded update
|
|
$zippy = Zippy::load();
|
|
$update = $zippy->open('.update.tmp.zip');
|
|
|
|
foreach ($update as $file)
|
|
{
|
|
$name = $this->updateDir . '/' . $file->getLocation();
|
|
if ($file->isDir())
|
|
{
|
|
$name = substr($name, 0, strlen($name) - 1);
|
|
}
|
|
|
|
$this->newUpdateFiles[] = $name;
|
|
}
|
|
|
|
if (!$update->extract($this->updateDir))
|
|
{
|
|
throw new \Exception('Could not extract the update!');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes the migration script if found in the root update directory
|
|
*/
|
|
public function migrate()
|
|
{
|
|
if (file_exists($this->updateDir . 'migrations.php'))
|
|
{
|
|
require_once $this->updateDir . 'migrations.php';
|
|
unlink($this->updateDir . 'migrations.php');
|
|
}
|
|
}
|
|
|
|
|
|
////////////////
|
|
/// Cleanup ///
|
|
////////////////
|
|
|
|
|
|
/**
|
|
* Deletes all unessesary files.
|
|
* @return bool
|
|
*/
|
|
public function cleanup()
|
|
{
|
|
// Delete all Files which don't exist anymore in the update. Ommit everything in $filesToNotUpdate
|
|
$diff = array_diff($this->getDirContents($this->updateDir), $this->newUpdateFiles);
|
|
|
|
foreach ($diff as $file)
|
|
{
|
|
if (is_dir($file))
|
|
{
|
|
$success = $this->rrmdir($file);
|
|
} else
|
|
{
|
|
// Empty files wont show up in the zip, which is why we check this here.
|
|
if(filesize($file) != 0)
|
|
{
|
|
$success = unlink($file);
|
|
}
|
|
}
|
|
}
|
|
|
|
// cleanup downloaded zips
|
|
$success = unlink('.update.tmp.zip');
|
|
$success = unlink('.update-backup.zip');
|
|
|
|
return $success;
|
|
}
|
|
|
|
|
|
/////////////////////////
|
|
/// Helper functions ///
|
|
/////////////////////////
|
|
|
|
|
|
/**
|
|
* Checks if a remote file exists
|
|
* @param $url
|
|
* @return bool
|
|
*/
|
|
private function remote_file_exists($url)
|
|
{
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_NOBODY, true);
|
|
curl_exec($ch);
|
|
$retcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($retcode == 200)
|
|
{
|
|
return true;
|
|
} else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a list of all files and folders inside a given directory, recursivly. Excluding $thingsNotToUpdate.
|
|
* @param $dir
|
|
* @param array $results
|
|
* @return array
|
|
*/
|
|
private function getDirContents($dir, &$results = array())
|
|
{
|
|
$results = $this->getFolderContentsList($dir);
|
|
|
|
// Only include it if it should not be ignored
|
|
foreach ($results as $in => $path)
|
|
{
|
|
$p = $this->str_replace_first($dir, '', $path);
|
|
$p = $this->str_replace_first('/', '', $p);
|
|
if ($this->stringMatchesInArray($p, $this->thingsToNotUpdate))
|
|
{
|
|
unset($results[$in]);
|
|
}
|
|
}
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Gets a list with all files and folders in a given directory, including all subdirectories
|
|
* @param $dir
|
|
* @param array $results
|
|
* @return array
|
|
*/
|
|
private function getFolderContentsList($dir, &$results = array())
|
|
{
|
|
$files = scandir($dir);
|
|
|
|
foreach ($files as $key => $value)
|
|
{
|
|
$path = $dir . DIRECTORY_SEPARATOR . $value;
|
|
|
|
// Don't show the directory if it's hidden
|
|
if (substr($value, 0, 1) !== ".")
|
|
{
|
|
if (!is_dir($path))
|
|
{
|
|
$results[] = $path;
|
|
} else if ($value != "." && $value != "..")
|
|
{
|
|
$this->getFolderContentsList($path, $results);
|
|
$results[] = $path;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* @param $string
|
|
* @param $array
|
|
* @return bool
|
|
*/
|
|
private function stringMatchesInArray($string, $array)
|
|
{
|
|
foreach ($array as $item)
|
|
{
|
|
$itm = '/' . preg_quote($item, '/') . '/';
|
|
if (preg_match($itm, $string))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
//if it matches without a slash at the end
|
|
if (substr($item, 0, (strlen($item) - 1)) === $string)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Replaces only the first occurence of a string
|
|
* @param $search
|
|
* @param $replace
|
|
* @param $subject
|
|
* @return mixed
|
|
*/
|
|
private function str_replace_first($search, $replace, $subject)
|
|
{
|
|
$search = '/' . preg_quote($search, '/') . '/';
|
|
|
|
return preg_replace($search, $replace, $subject, 1);
|
|
}
|
|
|
|
/**
|
|
* Deletes a non-empty folder
|
|
* @param $dir
|
|
* @return bool
|
|
*/
|
|
private function rrmdir($dir)
|
|
{
|
|
if (is_dir($dir))
|
|
{
|
|
$objects = scandir($dir);
|
|
foreach ($objects as $object)
|
|
{
|
|
if ($object != "." && $object != "..")
|
|
{
|
|
if (filetype($dir . "/" . $object) == "dir") $this->rrmdir($dir . "/" . $object); else unlink($dir . "/" . $object);
|
|
}
|
|
}
|
|
reset($objects);
|
|
return rmdir($dir);
|
|
} else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
} |