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('
This is a simulated crash for automated testing.
If you are seeing this outside of an automated testing scenario, please delete the linedefine(\'AKEEBA_TESTS_SIMULATE_STUCK_UPDATE\', 1); from the %s\version.php file', 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 = "getFTPOptions();
$engine = $ftpOptions['enable'] ? 'hybrid' : 'direct';
$dryRun = defined('AKEEBABACKUP_UPDATE_DRYRUN') ? '1' : '0';
$destDir = defined('AKEEBABACKUP_UPDATE_DRYRUN') ? $tempdir : $siteRoot;
$data .= <<