1143 lines
28 KiB
PHP
1143 lines
28 KiB
PHP
<?php
|
|
/**
|
|
* @package solo
|
|
* @copyright Copyright (c)2014-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
|
* @license GNU General Public License version 3, or later
|
|
*/
|
|
|
|
namespace Solo\Model;
|
|
|
|
use Akeeba\Engine\Platform;
|
|
use Awf\Container\Container;
|
|
use Awf\Date\Date;
|
|
use Awf\Download\Download;
|
|
use Awf\Mvc\Model;
|
|
use Awf\Registry\Registry;
|
|
use Awf\Session\Exception;
|
|
use Awf\Text\Text;
|
|
use Awf\Uri\Uri;
|
|
|
|
class Update extends Model
|
|
{
|
|
/** @var string The URL containing the INI update stream URL */
|
|
protected $updateStreamURL = '';
|
|
|
|
/** @var Registry A registry object holding the update information */
|
|
protected $updateInfo = null;
|
|
|
|
/** @var string The table where key-valueinformation is stored */
|
|
protected $tableName = '';
|
|
|
|
/** @var string The table field which stored the key of the key-value pairs */
|
|
protected $keyField = 'key';
|
|
|
|
/** @var string The table field which stored the value of the key-value pairs */
|
|
protected $valueField = 'value';
|
|
|
|
/** @var string The key tag for the live update serialised information */
|
|
protected $updateInfoTag = 'liveupdate';
|
|
|
|
/** @var string The key tag for the last check timestamp */
|
|
protected $lastCheckTag = 'liveupdate_lastcheck';
|
|
|
|
/** @var integer The last update check UNIX timestamp */
|
|
protected $lastCheck = null;
|
|
|
|
/** @var string Currently installed version */
|
|
protected $currentVersion = '';
|
|
|
|
/** @var string Currently installed version's date stamp */
|
|
protected $currentDateStamp = '';
|
|
|
|
/** @var string Minimum stability for reporting updates */
|
|
protected $minStability = 'alpha';
|
|
|
|
protected $downloadId = '';
|
|
|
|
/**
|
|
* How to determine if a new version is available. 'different' = if the version number is different,
|
|
* the remote version is newer, 'vcompare' = use version compare between the two versions, 'newest' =
|
|
* compare the release dates to find the newest. I suggest using 'different' on most cases.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $versionStrategy = 'smart';
|
|
|
|
/**
|
|
* Update constructor.
|
|
*
|
|
* @param Container $container The application container
|
|
*/
|
|
public function __construct(\Awf\Container\Container $container = null)
|
|
{
|
|
parent::__construct($container);
|
|
|
|
$this->currentVersion = defined('AKEEBABACKUP_VERSION') ? AKEEBABACKUP_VERSION : 'dev';
|
|
$this->currentDateStamp = defined('AKEEBABACKUP_DATE') ? AKEEBABACKUP_DATE : gmdate('Y-m-d');
|
|
$this->minStability = $this->container->appConfig->get('options.minstability', 'stable');
|
|
$this->downloadId = $this->container->appConfig->get('options.update_dlid', '');
|
|
|
|
// Set the update stream URL
|
|
if (isset($container['updateStreamURL']))
|
|
{
|
|
$this->updateStreamURL = $container->updateStreamURL;
|
|
}
|
|
else
|
|
{
|
|
$pro = AKEEBABACKUP_PRO ? 'pro' : 'core';
|
|
|
|
$this->updateStreamURL = 'http://cdn.akeeba.com/updates/solo' . $pro . '.ini';
|
|
}
|
|
|
|
// Testing updates in development versions: define AKEEBABACKUP_UPDATE_BASEURL in version.php
|
|
if (defined('AKEEBABACKUP_UPDATE_BASEURL'))
|
|
{
|
|
$pro = AKEEBABACKUP_PRO ? 'pro' : 'core';
|
|
|
|
$this->updateStreamURL = AKEEBABACKUP_UPDATE_BASEURL . $pro . '.ini';
|
|
}
|
|
|
|
$this->tableName = '#__ak_storage';
|
|
$this->keyField = 'tag';
|
|
$this->valueField = 'data';
|
|
$this->versionStrategy = 'smart';
|
|
|
|
$this->load(false);
|
|
}
|
|
|
|
/**
|
|
* Load the update information into the $this->updateInfo object. The update information will be returned from the
|
|
* cache. If the cache is expired, the $force flag is set or the APATH_BASE . 'update.ini' file is present the
|
|
* update information will be reloaded from the source. The update source normally is $this->updateStreamURL. If
|
|
* the APATH_BASE . 'update.ini' file is present it's used as the update source instead.
|
|
*
|
|
* In short, the APATH_BASE . 'update.ini' file allows you to override update sources for testing purposes.
|
|
*
|
|
* @param bool $force True to force reload the information from the source.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function load($force = false)
|
|
{
|
|
// Clear the update information and last update check timestamp
|
|
$this->lastCheck = null;
|
|
$this->updateInfo = null;
|
|
|
|
// Get a reference to the database
|
|
$db = $this->container->db;
|
|
|
|
// Get the last update timestamp
|
|
$query = $db->getQuery(true)
|
|
->select($db->qn($this->valueField))
|
|
->from($db->qn($this->tableName))
|
|
->where($db->qn($this->keyField) . '=' . $db->q($this->lastCheckTag));
|
|
$this->lastCheck = $db->setQuery($query)->loadResult();
|
|
|
|
if (is_null($this->lastCheck))
|
|
{
|
|
$this->lastCheck = 0;
|
|
}
|
|
|
|
/**
|
|
* Override for automated testing
|
|
*
|
|
* If the file update.ini exists (next to version.php) force reloading the update information.
|
|
*/
|
|
$fileTestingUpdates = APATH_BASE . '/update.ini';
|
|
|
|
if (file_exists($fileTestingUpdates))
|
|
{
|
|
$force = true;
|
|
}
|
|
|
|
// Do I have to forcible reload from a URL?
|
|
if (!$force)
|
|
{
|
|
// Force reload if more than 6 hours have elapsed
|
|
if (abs(time() - $this->lastCheck) >= 21600)
|
|
{
|
|
$force = true;
|
|
}
|
|
}
|
|
|
|
// Try to load from cache
|
|
if (!$force)
|
|
{
|
|
$query = $db->getQuery(true)
|
|
->select($db->qn($this->valueField))
|
|
->from($db->qn($this->tableName))
|
|
->where($db->qn($this->keyField) . '=' . $db->q($this->updateInfoTag));
|
|
$rawInfo = $db->setQuery($query)->loadResult();
|
|
|
|
if (empty($rawInfo))
|
|
{
|
|
$force = true;
|
|
}
|
|
else
|
|
{
|
|
$this->updateInfo = new Registry();
|
|
$this->updateInfo->loadString($rawInfo, 'JSON');
|
|
}
|
|
}
|
|
|
|
// If it's stuck and we are not forcibly retrying to reload, bail out
|
|
if (!$force && !empty($this->updateInfo) && $this->updateInfo->get('stuck', false))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Maybe we are forced to load from a URL?
|
|
// NOTE: DO NOT MERGE WITH PREVIOUS IF AS THE $force VARIABLE MAY BE MODIFIED THERE!
|
|
if ($force)
|
|
{
|
|
$this->updateInfo = new Registry();
|
|
$this->updateInfo->set('stuck', 1);
|
|
$this->lastCheck = time();
|
|
|
|
// Store last update check timestamp
|
|
$o = (object) array(
|
|
$this->keyField => $this->lastCheckTag,
|
|
$this->valueField => $this->lastCheck,
|
|
);
|
|
|
|
$result = false;
|
|
|
|
try
|
|
{
|
|
$result = $db->insertObject($this->tableName, $o, $this->keyField);
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
$result = false;
|
|
}
|
|
|
|
if (!$result)
|
|
{
|
|
try
|
|
{
|
|
$result = $db->updateObject($this->tableName, $o, $this->keyField);
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
$result = false;
|
|
}
|
|
}
|
|
|
|
// Store update information
|
|
$o = (object) array(
|
|
$this->keyField => $this->updateInfoTag,
|
|
$this->valueField => $this->updateInfo->toString('JSON'),
|
|
);
|
|
|
|
$result = false;
|
|
|
|
try
|
|
{
|
|
$result = $db->insertObject($this->tableName, $o, $this->keyField);
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
$result = false;
|
|
}
|
|
|
|
if (!$result)
|
|
{
|
|
try
|
|
{
|
|
$result = $db->updateObject($this->tableName, $o, $this->keyField);
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
$result = false;
|
|
}
|
|
}
|
|
|
|
// Simulate a PHP crash for automated testing
|
|
if (defined('AKEEBA_TESTS_SIMULATE_STUCK_UPDATE'))
|
|
{
|
|
die(sprintf('<p id="automated-testing-simulated-crash">This is a simulated crash for automated testing.</p></p>If you are seeing this outside of an automated testing scenario, please delete the line <code>define(\'AKEEBA_TESTS_SIMULATE_STUCK_UPDATE\', 1);</code> from the %s\version.php file</p>', APATH_BASE));
|
|
}
|
|
|
|
// Try to fetch the update information
|
|
try
|
|
{
|
|
/**
|
|
* Override for automated testing
|
|
*
|
|
* If the file update.ini exists (next to version.php) we use its contents as the update source, without
|
|
* accessing the update information URL at all. The file is immediately removed.
|
|
*/
|
|
if (is_file($fileTestingUpdates))
|
|
{
|
|
$rawInfo = @file_get_contents($fileTestingUpdates);
|
|
|
|
$this->container->fileSystem->delete($fileTestingUpdates);
|
|
}
|
|
else
|
|
{
|
|
$options = [];
|
|
$proxyParams = Platform::getInstance()->getProxySettings();
|
|
|
|
if ($proxyParams['enabled'])
|
|
{
|
|
$options['proxy'] = [
|
|
'host' => $proxyParams['host'],
|
|
'port' => $proxyParams['port'],
|
|
'user' => $proxyParams['user'],
|
|
'pass' => $proxyParams['pass'],
|
|
];
|
|
}
|
|
|
|
$download = new Download($this->container);
|
|
$download->setAdapterOptions($options);
|
|
|
|
$rawInfo = $download->getFromURL($this->updateStreamURL);
|
|
}
|
|
|
|
$this->updateInfo->loadString($rawInfo, 'INI');
|
|
$this->updateInfo->set('loadedUpdate', ($rawInfo !== false) ? 1 : 0);
|
|
$this->updateInfo->set('stuck', 0);
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
// We are stuck. Darn.
|
|
|
|
return;
|
|
}
|
|
|
|
// If not stuck, loadedUpdate is 1, version key exists and stability key does not exist / is empty, determine the version stability
|
|
$version = $this->updateInfo->get('version', '');
|
|
$stability = $this->updateInfo->get('stability', '');
|
|
if (
|
|
!$this->updateInfo->get('stuck', 0)
|
|
&& $this->updateInfo->get('loadedUpdate', 0)
|
|
&& !empty($version)
|
|
&& empty($stability)
|
|
)
|
|
{
|
|
$this->updateInfo->set('stability', $this->getStability($version));
|
|
}
|
|
|
|
// Since we had to load from a URL, commit the update information to db
|
|
$o = (object) array(
|
|
$this->keyField => $this->updateInfoTag,
|
|
$this->valueField => $this->updateInfo->toString('JSON'),
|
|
);
|
|
|
|
$result = false;
|
|
|
|
try
|
|
{
|
|
$result = $db->insertObject($this->tableName, $o, $this->keyField);
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
$result = false;
|
|
}
|
|
|
|
if (!$result)
|
|
{
|
|
try
|
|
{
|
|
$result = $db->updateObject($this->tableName, $o, $this->keyField);
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
$result = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if an update is available and push it to the update information registry
|
|
$this->updateInfo->set('hasUpdate', $this->hasUpdate());
|
|
|
|
// Post-process the download URL, appending the Download ID (if defined)
|
|
$link = $this->updateInfo->get('link', '');
|
|
|
|
if (!empty($link) && !empty($this->downloadId))
|
|
{
|
|
$link = new Uri($link);
|
|
$link->setVar('dlid', $this->downloadId);
|
|
$this->updateInfo->set('link', $link->toString());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is there an update available?
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasUpdate()
|
|
{
|
|
$this->updateInfo->set('minstabilityMatch', 1);
|
|
$this->updateInfo->set('platformMatch', 0);
|
|
|
|
// Validate the minimum stability
|
|
$stability = strtolower($this->updateInfo->get('stability'));
|
|
|
|
switch ($this->minStability)
|
|
{
|
|
case 'alpha':
|
|
default:
|
|
// Reports any stability level as an available update
|
|
break;
|
|
|
|
case 'beta':
|
|
// Do not report alphas as available updates
|
|
if (in_array($stability, array('alpha')))
|
|
{
|
|
$this->updateInfo->set('minstabilityMatch', 0);
|
|
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case 'rc':
|
|
// Do not report alphas and betas as available updates
|
|
if (in_array($stability, array('alpha', 'beta')))
|
|
{
|
|
$this->updateInfo->set('minstabilityMatch', 0);
|
|
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case 'stable':
|
|
// Do not report alphas, betas and rcs as available updates
|
|
if (in_array($stability, array('alpha', 'beta', 'rc')))
|
|
{
|
|
$this->updateInfo->set('minstabilityMatch', 0);
|
|
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Validate the platform compatibility
|
|
$platforms = explode(',', $this->updateInfo->get('platforms', ''));
|
|
|
|
if (!empty($platforms))
|
|
{
|
|
$phpVersionParts = explode('.', PHP_VERSION, 3);
|
|
$currentPHPVersion = $phpVersionParts[0] . '.' . $phpVersionParts[1];
|
|
|
|
$platformFound = false;
|
|
|
|
$requirePlatformName = $this->container->segment->get('platformNameForUpdates', 'php');
|
|
$currentPlatform = $this->container->segment->get('platformVersionForUpdates', $currentPHPVersion);
|
|
|
|
// Check for the platform
|
|
foreach ($platforms as $platform)
|
|
{
|
|
$platform = trim($platform);
|
|
$platform = strtolower($platform);
|
|
$platformParts = explode('/', $platform, 2);
|
|
|
|
if ($platformParts[0] != $requirePlatformName)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ((substr($platformParts[1], -1) == '+') && version_compare($currentPlatform, substr($platformParts[1], 0, -1), 'ge'))
|
|
{
|
|
$this->updateInfo->set('platformMatch', 1);
|
|
$platformFound = true;
|
|
}
|
|
elseif ($platformParts[1] == $currentPlatform)
|
|
{
|
|
$this->updateInfo->set('platformMatch', 1);
|
|
$platformFound = true;
|
|
}
|
|
}
|
|
|
|
// If we are running inside a CMS perform a second check for the PHP version
|
|
if ($platformFound && ($requirePlatformName != 'php'))
|
|
{
|
|
$this->updateInfo->set('platformMatch', 0);
|
|
$platformFound = false;
|
|
|
|
foreach ($platforms as $platform)
|
|
{
|
|
$platform = trim($platform);
|
|
$platform = strtolower($platform);
|
|
$platformParts = explode('/', $platform, 2);
|
|
|
|
if ($platformParts[0] != 'php')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ($platformParts[1] == $currentPHPVersion)
|
|
{
|
|
$this->updateInfo->set('platformMatch', 1);
|
|
$platformFound = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$platformFound)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If the user had the Core version but has entered a Download ID we will always display an update as being
|
|
// available
|
|
if (!AKEEBABACKUP_PRO && !empty($this->downloadId))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Apply the version strategy
|
|
$version = $this->updateInfo->get('version', null);
|
|
$date = $this->updateInfo->get('date', null);
|
|
|
|
if (empty($version) || empty($date))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
switch ($this->versionStrategy)
|
|
{
|
|
case 'newest':
|
|
return $this->hasUpdateByNewest($version, $date);
|
|
|
|
break;
|
|
|
|
case 'vcompare':
|
|
return $this->hasUpdateByVersion($version, $date);
|
|
|
|
break;
|
|
|
|
case 'different':
|
|
return $this->hasUpdateByDifferentVersion($version, $date);
|
|
|
|
break;
|
|
|
|
case 'smart':
|
|
return $this->hasUpdateByDateAndVersion($version, $date);
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the update information
|
|
*
|
|
* @param bool $force Should we force the fetch of new information?
|
|
*
|
|
* @return \Awf\Registry\Registry
|
|
*/
|
|
public function getUpdateInformation($force = false)
|
|
{
|
|
if (is_null($this->updateInfo))
|
|
{
|
|
$this->load($force);
|
|
}
|
|
|
|
return $this->updateInfo;
|
|
}
|
|
|
|
/**
|
|
* Try to prepare a world-writeable update.zip file in the temporary directory, or throw an exception if it's not
|
|
* possible.
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function prepareDownload()
|
|
{
|
|
$tmpDir = defined('AKEEBA_TESTS_UPDATE_TEMP_FOLDER') ? AKEEBA_TESTS_UPDATE_TEMP_FOLDER : $this->container['temporaryPath'];
|
|
$tmpFile = $tmpDir . '/update.zip';
|
|
|
|
$fs = $this->container->fileSystem;
|
|
|
|
if (!is_dir($tmpDir))
|
|
{
|
|
throw new \Exception(Text::sprintf('SOLO_UPDATE_ERR_DOWNLOAD_INVALIDTMPDIR', $tmpDir), 500);
|
|
}
|
|
|
|
$fs->delete($tmpFile);
|
|
|
|
$fp = @fopen($tmpFile, 'w');
|
|
|
|
if ($fp === false)
|
|
{
|
|
$nada = '';
|
|
$fs->write($tmpFile, $nada);
|
|
}
|
|
else
|
|
{
|
|
@fclose($fp);
|
|
}
|
|
|
|
$fs->chmod($tmpFile, 0777);
|
|
}
|
|
|
|
/**
|
|
* Step through the download of the update archive.
|
|
*
|
|
* If the file APATH_BASE . 'update.zip' file is present it is used instead (and removed immediately).
|
|
*
|
|
* @param bool $staggered Should I try a staggered (multi-step) download? Default is true.
|
|
*
|
|
* @return array A return array giving the status of the staggered download
|
|
*/
|
|
|
|
public function stepDownload($staggered = true)
|
|
{
|
|
$this->load();
|
|
|
|
// The restore script expects to find the update inside the temp directory
|
|
$tmpDir = defined('AKEEBA_TESTS_UPDATE_TEMP_FOLDER') ? AKEEBA_TESTS_UPDATE_TEMP_FOLDER : $this->container['temporaryPath'];
|
|
$tmpDir = rtrim($tmpDir, '/\\');
|
|
$localFilename = $tmpDir . '/update.zip';
|
|
|
|
/**
|
|
* Override for automated testing
|
|
*
|
|
* If the file APATH_BASE . 'update.zip' file is present it is used instead (and removed immediately).
|
|
*/
|
|
$fileOverride = APATH_BASE . 'update.zip';
|
|
|
|
if (is_file($fileOverride))
|
|
{
|
|
$size = filesize($localFilename);
|
|
$frag = $this->getState('frag', 0);
|
|
$frag++;
|
|
|
|
$ret = array(
|
|
"status" => true,
|
|
"error" => '',
|
|
"frag" => $frag,
|
|
"totalSize" => $size,
|
|
"doneSize" => $size,
|
|
"percent" => 100,
|
|
"errorCode" => 0,
|
|
);
|
|
|
|
// Fake stepped download: frag 1 causes 1 second delay, frag 2 moves the file
|
|
switch ($frag)
|
|
{
|
|
case 0:
|
|
sleep(1);
|
|
$ret['doneSize'] = (int) ($size / 2);
|
|
$ret['percent'] = 50;
|
|
$this->setState('frag', $frag);
|
|
|
|
break;
|
|
|
|
default:
|
|
$this->setState('frag', null);
|
|
$this->container->fileSystem->move($fileOverride, $localFilename);
|
|
|
|
break;
|
|
}
|
|
|
|
// Special case for automated tests: if the file is 0 bytes we will just throw an error :)
|
|
if ($size == 0)
|
|
{
|
|
$retArray['status'] = false;
|
|
$retArray['error'] = Text::sprintf('AWF_DOWNLOAD_ERR_LIB_COULDNOTDOWNLOADFROMURL', '@test_override_file@');
|
|
$retArray['errorCode'] = 500;
|
|
$this->container->fileSystem->delete($fileOverride);
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Back to our regular code. Set up the file import parameters.
|
|
*/
|
|
$params = array(
|
|
'file' => $this->updateInfo->get('link', ''),
|
|
'frag' => $this->getState('frag', -1),
|
|
'totalSize' => $this->getState('totalSize', -1),
|
|
'doneSize' => $this->getState('doneSize', -1),
|
|
'localFilename' => $localFilename,
|
|
);
|
|
|
|
$download = new Download($this->container);
|
|
|
|
if ($staggered)
|
|
{
|
|
// importFromURL expects the remote URL in the 'url' index
|
|
$params['url'] = $params['file'];
|
|
$retArray = $download->importFromURL($params);
|
|
|
|
// Better it
|
|
unset($params['url']);
|
|
}
|
|
else
|
|
{
|
|
$retArray = array(
|
|
"status" => true,
|
|
"error" => '',
|
|
"frag" => 1,
|
|
"totalSize" => 0,
|
|
"doneSize" => 0,
|
|
"percent" => 0,
|
|
"errorCode" => 0,
|
|
);
|
|
|
|
try
|
|
{
|
|
$result = $download->getFromURL($params['file']);
|
|
|
|
if ($result === false)
|
|
{
|
|
throw new Exception(Text::sprintf('AWF_DOWNLOAD_ERR_LIB_COULDNOTDOWNLOADFROMURL', $params['file']), 500);
|
|
}
|
|
|
|
$tmpDir = APATH_ROOT . '/tmp';
|
|
$tmpDir = rtrim($tmpDir, '/\\');
|
|
$localFilename = $tmpDir . '/update.zip';
|
|
|
|
$fs = $this->container->fileSystem;
|
|
|
|
$fs->write($localFilename, $result);
|
|
|
|
$retArray['status'] = true;
|
|
$retArray['totalSize'] = strlen($result);
|
|
$retArray['doneSize'] = $retArray['totalSize'];
|
|
$retArray['percent'] = 100;
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
$retArray['status'] = false;
|
|
$retArray['error'] = $e->getMessage();
|
|
$retArray['errorCode'] = $e->getCode();
|
|
}
|
|
}
|
|
|
|
return $retArray;
|
|
}
|
|
|
|
/**
|
|
* Creates the restoration.ini file which is used during the update package's extraction. This file tells Akeeba
|
|
* Restore which package to read and where and how to extract it.
|
|
*
|
|
* @return bool True on success
|
|
*/
|
|
public function createRestorationINI()
|
|
{
|
|
// Get a password
|
|
$password = base64_encode(random_bytes(32));
|
|
|
|
$fs = $this->container->fileSystem;
|
|
|
|
$this->setState('update_password', $password);
|
|
|
|
// Also save the update_password in the session, we'll need it if this page is reloaded
|
|
$this->container->segment->set('update_password', $password);
|
|
|
|
// Get the absolute path to site's root
|
|
$siteRoot = (isset($this->container['filesystemBase'])) ? $this->container['filesystemBase'] : APATH_BASE;
|
|
$siteRoot = str_replace('\\', '/', $siteRoot);
|
|
$siteRoot = str_replace('//', '/', $siteRoot);
|
|
|
|
// On WordPress we need to go one level up
|
|
if (defined('WPINC'))
|
|
{
|
|
$parts = explode('/', $siteRoot);
|
|
array_pop($parts);
|
|
$siteRoot = implode('/', $parts);
|
|
}
|
|
|
|
$tempdir = $this->container['temporaryPath'];
|
|
|
|
$data = "<?php\ndefined('_AKEEBA_RESTORATION') or die();\n";
|
|
$data .= '$restoration_setup = array(' . "\n";
|
|
|
|
$ftpOptions = $this->getFTPOptions();
|
|
$engine = $ftpOptions['enable'] ? 'hybrid' : 'direct';
|
|
$dryRun = defined('AKEEBABACKUP_UPDATE_DRYRUN') ? '1' : '0';
|
|
$destDir = defined('AKEEBABACKUP_UPDATE_DRYRUN') ? $tempdir : $siteRoot;
|
|
|
|
$data .= <<<ENDDATA
|
|
'kickstart.security.password' => '$password',
|
|
'kickstart.tuning.max_exec_time' => '5',
|
|
'kickstart.tuning.run_time_bias' => '75',
|
|
'kickstart.tuning.min_exec_time' => '0',
|
|
'kickstart.procengine' => '$engine',
|
|
'kickstart.setup.sourcefile' => '{$tempdir}/update.zip',
|
|
'kickstart.setup.destdir' => '$destDir',
|
|
'kickstart.setup.restoreperms' => '0',
|
|
'kickstart.setup.filetype' => 'zip',
|
|
'kickstart.setup.dryrun' => '$dryRun',
|
|
ENDDATA;
|
|
|
|
// On WordPress we need to remove the akeebabackupwp prefix from the package
|
|
if (defined('WPINC'))
|
|
{
|
|
$data .= "\n\t'kickstart.setup.removepath' => 'akeebabackupwp',\n";
|
|
}
|
|
|
|
if ($ftpOptions['enable'])
|
|
{
|
|
// Is the tempdir really writable?
|
|
$writable = @is_writeable($tempdir);
|
|
|
|
if ($writable)
|
|
{
|
|
// Let's be REALLY sure
|
|
$fp = @fopen($tempdir . '/test.txt', 'w');
|
|
if ($fp === false)
|
|
{
|
|
$writable = false;
|
|
}
|
|
else
|
|
{
|
|
fclose($fp);
|
|
unlink($tempdir . '/test.txt');
|
|
}
|
|
}
|
|
|
|
// If the tempdir is not writable, create a new writable subdirectory
|
|
if (!$writable)
|
|
{
|
|
$newTemp = APATH_BASE . '/tmp/update_tmp';
|
|
$fs->mkdir($newTemp, 0777);
|
|
|
|
$tempdir = $newTemp;
|
|
}
|
|
|
|
// If we still have no writable directory, we'll try /tmp and the system's temp-directory
|
|
$writable = @is_writeable($tempdir);
|
|
|
|
if (!$writable && function_exists('sys_get_temp_dir'))
|
|
{
|
|
$tempdir = sys_get_temp_dir();
|
|
}
|
|
|
|
$data .= <<<ENDDATA
|
|
'kickstart.ftp.ssl' => '0',
|
|
'kickstart.ftp.passive' => '1',
|
|
'kickstart.ftp.host' => '{$ftpOptions['host']}',
|
|
'kickstart.ftp.port' => '{$ftpOptions['port']}',
|
|
'kickstart.ftp.user' => '{$ftpOptions['user']}',
|
|
'kickstart.ftp.pass' => '{$ftpOptions['pass']}',
|
|
'kickstart.ftp.dir' => '{$ftpOptions['root']}',
|
|
'kickstart.ftp.tempdir' => '$tempdir',
|
|
ENDDATA;
|
|
}
|
|
|
|
$data .= ');';
|
|
|
|
|
|
$configPath = $siteRoot . '/restoration.php';
|
|
|
|
if (defined('WPINC'))
|
|
{
|
|
$configPath = $siteRoot . '/app/restoration.php';
|
|
}
|
|
|
|
clearstatcache(true, $configPath);
|
|
|
|
// Remove the old file, if it's there...
|
|
if (file_exists($configPath))
|
|
{
|
|
$fs->delete($configPath);
|
|
}
|
|
|
|
// Write the new file
|
|
$fs->write($configPath, $data);
|
|
|
|
// Clear opcode caches for the generated .php file
|
|
if (function_exists('opcache_invalidate'))
|
|
{
|
|
opcache_invalidate($configPath);
|
|
}
|
|
|
|
if (function_exists('apc_compile_file'))
|
|
{
|
|
apc_compile_file($configPath);
|
|
}
|
|
|
|
if (function_exists('wincache_refresh_if_changed'))
|
|
{
|
|
wincache_refresh_if_changed(array($configPath));
|
|
}
|
|
|
|
if (function_exists('xcache_asm'))
|
|
{
|
|
xcache_asm($configPath);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns an array with the configured FTP options
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getFTPOptions()
|
|
{
|
|
// Initialise from Joomla! Global Configuration
|
|
$config = $this->container->appConfig;
|
|
|
|
$retArray = array(
|
|
'enable' => $config->get('fs.driver', 'file') == 'ftp',
|
|
'host' => $config->get('fs.host', 'localhost'),
|
|
'port' => $config->get('fs.port', '21'),
|
|
'user' => $config->get('fs.username', ''),
|
|
'pass' => $config->get('fs.password', ''),
|
|
'root' => $config->get('fs.directory', ''),
|
|
'tempdir' => APATH_BASE . '/tmp',
|
|
);
|
|
|
|
return $retArray;
|
|
}
|
|
|
|
/**
|
|
* Finalises the update. Reserved for future use. DO NOT REMOVE.
|
|
*/
|
|
public function finalise()
|
|
{
|
|
// Reserved for future use. DO NOT REMOVE.
|
|
}
|
|
|
|
/**
|
|
* Get the currently used update stream URL
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getUpdateStreamURL()
|
|
{
|
|
return $this->updateStreamURL;
|
|
}
|
|
|
|
/**
|
|
* Normalise the version number to a PHP-format version string.
|
|
*
|
|
* @param string $version The whatever-format version number
|
|
*
|
|
* @return string A standard formatted version number
|
|
*/
|
|
public function sanitiseVersion($version)
|
|
{
|
|
$test = strtolower($version);
|
|
$alphaQualifierPosition = strpos($test, 'alpha-');
|
|
$betaQualifierPosition = strpos($test, 'beta-');
|
|
$betaQualifierPosition2 = strpos($test, '-beta');
|
|
$rcQualifierPosition = strpos($test, 'rc-');
|
|
$rcQualifierPosition2 = strpos($test, '-rc');
|
|
$rcQualifierPosition3 = strpos($test, 'rc');
|
|
$devQualifiedPosition = strpos($test, 'dev');
|
|
|
|
if ($alphaQualifierPosition !== false)
|
|
{
|
|
$betaRevision = substr($test, $alphaQualifierPosition + 6);
|
|
if (!$betaRevision)
|
|
{
|
|
$betaRevision = 1;
|
|
}
|
|
$test = substr($test, 0, $alphaQualifierPosition) . '.a' . $betaRevision;
|
|
}
|
|
elseif ($betaQualifierPosition !== false)
|
|
{
|
|
$betaRevision = substr($test, $betaQualifierPosition + 5);
|
|
if (!$betaRevision)
|
|
{
|
|
$betaRevision = 1;
|
|
}
|
|
$test = substr($test, 0, $betaQualifierPosition) . '.b' . $betaRevision;
|
|
}
|
|
elseif ($betaQualifierPosition2 !== false)
|
|
{
|
|
$betaRevision = substr($test, $betaQualifierPosition2 + 5);
|
|
|
|
if (!$betaRevision)
|
|
{
|
|
$betaRevision = 1;
|
|
}
|
|
|
|
$test = substr($test, 0, $betaQualifierPosition2) . '.b' . $betaRevision;
|
|
}
|
|
elseif ($rcQualifierPosition !== false)
|
|
{
|
|
$betaRevision = substr($test, $rcQualifierPosition + 5);
|
|
if (!$betaRevision)
|
|
{
|
|
$betaRevision = 1;
|
|
}
|
|
$test = substr($test, 0, $rcQualifierPosition) . '.rc' . $betaRevision;
|
|
}
|
|
elseif ($rcQualifierPosition2 !== false)
|
|
{
|
|
$betaRevision = substr($test, $rcQualifierPosition2 + 3);
|
|
|
|
if (!$betaRevision)
|
|
{
|
|
$betaRevision = 1;
|
|
}
|
|
|
|
$test = substr($test, 0, $rcQualifierPosition2) . '.rc' . $betaRevision;
|
|
}
|
|
elseif ($rcQualifierPosition3 !== false)
|
|
{
|
|
$betaRevision = substr($test, $rcQualifierPosition3 + 5);
|
|
|
|
if (!$betaRevision)
|
|
{
|
|
$betaRevision = 1;
|
|
}
|
|
|
|
$test = substr($test, 0, $rcQualifierPosition3) . '.rc' . $betaRevision;
|
|
}
|
|
elseif ($devQualifiedPosition !== false)
|
|
{
|
|
$betaRevision = substr($test, $devQualifiedPosition + 6);
|
|
if (!$betaRevision)
|
|
{
|
|
$betaRevision = '';
|
|
}
|
|
$test = substr($test, 0, $devQualifiedPosition) . '.dev' . $betaRevision;
|
|
}
|
|
|
|
return $test;
|
|
}
|
|
|
|
public function getStability($version)
|
|
{
|
|
$versionParts = explode('.', $version);
|
|
$lastVersionPart = array_pop($versionParts);
|
|
|
|
if (substr($lastVersionPart, 0, 1) == 'a')
|
|
{
|
|
return 'alpha';
|
|
}
|
|
|
|
if (substr($lastVersionPart, 0, 1) == 'b')
|
|
{
|
|
return 'beta';
|
|
}
|
|
|
|
if (substr($lastVersionPart, 0, 2) == 'rc')
|
|
{
|
|
return 'rc';
|
|
}
|
|
|
|
if (substr($lastVersionPart, 0, 3) == 'dev')
|
|
{
|
|
return 'alpha';
|
|
}
|
|
|
|
return 'stable';
|
|
}
|
|
|
|
/**
|
|
* Checks if there is an update taking into account only the release date. If the release date is the same then it
|
|
* takes into account the version.
|
|
*
|
|
* @param string $version
|
|
* @param string $date
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function hasUpdateByNewest($version, $date)
|
|
{
|
|
if (empty($this->currentDateStamp))
|
|
{
|
|
$mine = new Date('2000-01-01 00:00:00');
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
$mine = new Date($this->currentDateStamp);
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
$mine = new Date('2000-01-01 00:00:00');
|
|
}
|
|
}
|
|
|
|
$theirs = new Date($date);
|
|
|
|
/**
|
|
* Do we have the same time? This happens when we release two versions in the same day. In such cases we have to
|
|
* check vs the version number.
|
|
*/
|
|
if ($mine->toUnix() == $theirs->toUnix())
|
|
{
|
|
return $this->hasUpdateByVersion($version, $date);
|
|
}
|
|
|
|
return ($theirs->toUnix() > $mine->toUnix());
|
|
}
|
|
|
|
/**
|
|
* Checks if there is an update by comparing the version numbers using version_compare()
|
|
*
|
|
* @param string $version
|
|
* @param string $date
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function hasUpdateByVersion($version, $date)
|
|
{
|
|
$mine = $this->currentVersion;
|
|
|
|
if (empty($mine))
|
|
{
|
|
$mine = '0.0.0';
|
|
}
|
|
|
|
if (empty($version))
|
|
{
|
|
$version = '0.0.0';
|
|
}
|
|
|
|
return version_compare($version, $mine, 'gt');
|
|
}
|
|
|
|
/**
|
|
* Checks if there is an update by looking for a different version number
|
|
*
|
|
* @param string $version
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function hasUpdateByDifferentVersion($version, $date)
|
|
{
|
|
$mine = $this->currentVersion;
|
|
|
|
if (empty($mine))
|
|
{
|
|
$mine = '0.0.0';
|
|
}
|
|
|
|
if (empty($version))
|
|
{
|
|
$version = '0.0.0';
|
|
}
|
|
|
|
return ($version != $mine);
|
|
}
|
|
|
|
private function hasUpdateByDateAndVersion($version, $date)
|
|
{
|
|
$isCurrentDev = in_array(substr($this->currentVersion, 0, 3), array('dev', 'rev'));
|
|
$isUpdateDev = in_array(substr($version, 0, 3), array('dev', 'rev'));
|
|
|
|
// Development (rev*) to numbered version; numbered to development; or development to development: use the date
|
|
if ($isCurrentDev || $isUpdateDev)
|
|
{
|
|
return $this->hasUpdateByNewest($version, $date);
|
|
}
|
|
|
|
// Identical version number? Use the date
|
|
if ($version == $this->currentVersion)
|
|
{
|
|
return $this->hasUpdateByNewest($version, $date);
|
|
}
|
|
|
|
// Otherwise only by version number
|
|
return $this->hasUpdateByVersion($version, $date);
|
|
}
|
|
}
|