*/
class UpdateException extends \Exception
{
}
class updater
{
/** @var bool */
private $availableUpdate = false;
const DOWNLOAD_PATH = '../tmp_uploaded_update';
const ELIGIBLE_SESSION_KEY = 'phplist_updater_eligible';
const CONFIG_FILE = __DIR__ . '/../config/config.php';
private $excludedFiles = array(
'dl.php',
'index.php',
'index.html',
'lt.php',
'ut.php',
'api.php',
);
public function isAuthenticated()
{
session_start();
if (isset($_SESSION[self::ELIGIBLE_SESSION_KEY]) && $_SESSION[self::ELIGIBLE_SESSION_KEY] === true) {
return true;
}
return false;
}
public function deauthUpdaterSession()
{
unset($_SESSION[self::ELIGIBLE_SESSION_KEY]);
unlink(__DIR__ . '/../config/actions.txt');
}
/**
* Return true if there is an update available
* @return bool
*/
public function availableUpdate()
{
return $this->availableUpdate;
}
/**
* Returns current version of phpList.
*
* @return string
* @throws UpdateException
*/
public function getCurrentVersion()
{
$version = file_get_contents('../admin/init.php');
$matches = array();
preg_match_all('/define\(\"VERSION\",\"(.*)\"\);/', $version, $matches);
if (isset($matches[1][0])) {
return $matches[1][0];
}
throw new UpdateException('No production version found.');
}
/**
* Checks if there is an Update Available
* @return string
* @throws \Exception
*/
function checkIfThereIsAnUpdate()
{
$serverResponse = $this->getResponseFromServer();
$version = isset($serverResponse['version']) ? $serverResponse['version'] : '';
$versionString = isset($serverResponse['versionstring']) ? $serverResponse['versionstring'] : '';
if ($version !== '' && $version !== $this->getCurrentVersion() && version_compare($this->getCurrentVersion(), $version)) {
$this->availableUpdate = true;
$updateMessage = 'Update to ' . htmlentities($versionString) . ' is available. ';
} else {
$updateMessage = 'phpList is up-to-date.';
}
if ($this->availableUpdate && isset($serverResponse['autoupdater']) && !($serverResponse['autoupdater'] === 1 || $serverResponse['autoupdater'] === '1')) {
$this->availableUpdate = false;
$updateMessage .= '
The automatic updater is disabled for this update.';
}
return $updateMessage;
}
/**
* Return version data from server
* @return array
* @throws \Exception
*/
private function getResponseFromServer()
{
$serverUrl = "https://download.phplist.org/version.json";
$updateUrl = $serverUrl . '?version=' . $this->getCurrentVersion();
// create a new cURL resource
$ch = curl_init();
// set URL and other appropriate options
// Disable SSL verification
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// Will return the response, if false it print the response
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Set the url
curl_setopt($ch, CURLOPT_URL, $updateUrl);
// Execute
$responseFromServer = curl_exec($ch);
// Closing
curl_close($ch);
// decode json
$responseFromServer = json_decode($responseFromServer, true);
return $responseFromServer;
}
private function getDownloadUrl()
{
// todo: error handling
$response = $this->getResponseFromServer();
if (isset($response['url'])) {
return $response['url'];
}
// todo error handling
}
/**
* Checks write permissions and returns files that are not writable
* @return array
*/
function checkWritePermissions()
{
$directory = new \RecursiveDirectoryIterator(__DIR__ . '/../', \RecursiveDirectoryIterator::SKIP_DOTS); // Exclude dot files
/** @var SplFileInfo[] $iterator */
$iterator = new \RecursiveIteratorIterator($directory, RecursiveIteratorIterator::CHILD_FIRST);
$files = array();
foreach ($iterator as $info) {
if (!is_writable($info->getRealPath())) {
$files[] = $info->getRealPath();
}
}
return $files;
}
/**
* @return array
*/
function checkRequiredFiles()
{
$expectedFiles = array(
'.' => 1,
'..' => 1,
'admin' => 1,
'config' => 1,
'images' => 1,
'js' => 1,
'styles' => 1,
'texts' => 1,
'.htaccess' => 1,
'dl.php' => 1,
'index.html' => 1,
'index.php' => 1,
'lt.php' => 1,
'ut.php' => 1,
'updater' => 1,
'base' => 1,
'api.php' =>1,
);
$existingFiles = scandir(__DIR__ . '/../');
foreach ($existingFiles as $fileName) {
if (isset($expectedFiles[$fileName])) {
unset($expectedFiles[$fileName]);
} else {
$expectedFiles[$fileName] = 1;
}
}
return $expectedFiles;
}
/**
*
* Recursively delete a directory and all of it's contents
*
* @param string $dir absolute path to directory to delete
* @return bool
* @throws UpdateException
*/
private function rmdir_recursive($dir)
{
if (false === file_exists($dir)) {
throw new \UpdateException("$dir doesn't exist.");
}
/** @var SplFileInfo[] $files */
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
if ($fileinfo->isDir()) {
if (false === rmdir($fileinfo->getRealPath())) {
throw new \UpdateException("Could not delete $fileinfo");
}
} else {
if (false === unlink($fileinfo->getRealPath())) {
throw new \UpdateException("Could not delete $fileinfo");
}
}
}
return rmdir($dir);
}
/**
* Delete dirs/files except config and other files that we want to keep
* @throws UpdateException
*/
function deleteFiles()
{
$excludedFolders = array(
'config',
'tmp_uploaded_update',
'updater',
'.',
'..',
);
$filesTodelete = scandir(__DIR__ . '/../');
foreach ($filesTodelete as $fileName) {
$absolutePath = __DIR__ . '/../' . $fileName;
$is_dir = false;
if (is_dir($absolutePath)) {
$is_dir = true;
if (in_array($fileName, $excludedFolders)) {
continue;
}
} else if (is_file($absolutePath)) {
if (in_array($fileName, $this->excludedFiles)) {
continue;
}
}
if ($is_dir) {
$this->rmdir_recursive($absolutePath);
} else {
unlink($absolutePath);
}
}
}
/**
* Get a PDO connection
* @return PDO
* @throws UpdateException
*/
function getConnection()
{
$standardConfig = self::CONFIG_FILE;
if (isset($_SERVER['ConfigFile']) && is_file($_SERVER['ConfigFile'])) {
include $_SERVER['ConfigFile'];
} elseif (file_exists($standardConfig)) {
include $standardConfig;
} else {
throw new \UpdateException("Error: Cannot find config file");
}
$charset = 'utf8mb4';
/** @var string $database_host
* @var string $database_name
* @var string $database_user
* @var string $database_password
*/
$dsn = "mysql:host=$database_host;dbname=$database_name;charset=$charset";
$options = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
);
try {
$pdo = new PDO($dsn, $database_user, $database_password, $options);
} catch (\PDOException $e) {
throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
return $pdo;
}
/**
*Set the maintenance mode
* @return bool true - maintenance mode is set; false - maintenance mode could not be set because an update is already running
* @throws UpdateException
*/
function addMaintenanceMode()
{
$standardConfig = self::CONFIG_FILE;
if (isset($_SERVER['ConfigFile']) && is_file($_SERVER['ConfigFile'])) {
include $_SERVER['ConfigFile'];
} elseif (file_exists($standardConfig)) {
include $standardConfig;
} else {
throw new \UpdateException("Error: Cannot find config file");
}
if (isset($table_prefix)) {
$table_name = $table_prefix . 'config';
} else {
$table_name = 'phplist_config';
}
$prepStmt = $this->getConnection()->prepare("SELECT * FROM {$table_name} WHERE item=?");
$prepStmt->execute(array('update_in_progress'));
$result = $prepStmt->fetch(PDO::FETCH_ASSOC);
if ($result === false) {
// the row does not exist => no update running
$this->getConnection()
->prepare("INSERT INTO {$table_name}(`item`,`editable`,`value`) VALUES (?,0,?)")
->execute(array('update_in_progress', 1));
}
if ($result['update_in_progress'] == 0) {
$this->getConnection()
->prepare("UPDATE {$table_name} SET `value`=? WHERE `item`=?")
->execute(array(1, 'update_in_progress'));
} else {
// the row exists and is not 0 => there is an update running
return false;
}
$name = 'maintenancemode';
$value = "Update process";
$sql = "UPDATE {$table_name} SET value =?, editable =? where item =? ";
$this->getConnection()->prepare($sql)->execute(array($value, 0, $name));
}
/**
*Clear the maintenance mode and remove the update_in_progress lock
* @throws UpdateException
*/
function removeMaintenanceMode()
{
$standardConfig = self::CONFIG_FILE;
if (isset($_SERVER['ConfigFile']) && is_file($_SERVER['ConfigFile'])) {
include $_SERVER['ConfigFile'];
} elseif (file_exists($standardConfig)) {
include $standardConfig;
} else {
throw new \UpdateException("Error: Cannot find config file");
}
if (isset($table_prefix)) {
$table_name = $table_prefix . 'config';
} else {
$table_name = 'phplist_config';
}
$name = 'maintenancemode';
$value = '';
$sql = "UPDATE {$table_name} SET value =?, editable =? where item =? ";
$this->getConnection()->prepare($sql)->execute(array($value, 0, $name));
$this->getConnection()
->prepare("UPDATE {$table_name} SET `value`=? WHERE `item`=?")
->execute(array(0, "update_in_progress"));
}
/**
* Download and unzip phpList from remote server
*
* @throws UpdateException
*/
function downloadUpdate()
{
/** @var string $url */
$url = $this->getDownloadUrl();
$zipFile = tempnam(sys_get_temp_dir(), 'phplist-update');
if ($zipFile === false) {
throw new UpdateException("Error: Temporary file cannot be created");
}
// Get The Zip File From Server
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_FILE, fopen($zipFile, 'w+'));
$page = curl_exec($ch);
if (!$page) {
echo "Error :- " . curl_error($ch);
}
curl_close($ch);
// extract files
$this->unZipFiles($zipFile, self::DOWNLOAD_PATH);
}
/**
* Creates temporary dir
* @throws UpdateException
*/
function temp_dir()
{
$tempdir = mkdir(self::DOWNLOAD_PATH, 0700);
if ($tempdir === false) {
throw new UpdateException("Error: Could not create temporary file");
}
}
function cleanUp()
{
if (function_exists('opcache_reset')) {
opcache_reset();
}
}
/**
* @throws UpdateException
*/
function replacePHPEntryPoints()
{
$entryPoints = array(
'dl.php',
'index.html',
'index.php',
'lt.php',
'ut.php',
);
foreach ($entryPoints as $key => $fileName) {
$current = "Update in progress \n";
$content = file_put_contents(__DIR__ . '/../' . $fileName, $current);
if ($content === FALSE) {
throw new UpdateException("Error: Could not write to the $fileName");
}
}
}
/**
* Returns true if the file/dir is excluded otherwise false.
* @param $file
* @return bool
*/
function isExcluded($file)
{
$excludedFolders = array(
'config',
'tmp_uploaded_update',
'updater',
'.',
'..',
);
if (in_array($file, $excludedFolders)) {
return true;
} else if (in_array($file, $this->excludedFiles)) {
return true;
}
return false;
}
/**
* Move new files in place.
* @throws UpdateException
*/
function moveNewFiles()
{
$rootDir = __DIR__ . '/../tmp_uploaded_update/phplist/public_html/lists';
$downloadedFiles = scandir($rootDir);
if (count($downloadedFiles) <= 2) {
throw new UpdateException("Error: Download folder is empty!");
}
foreach ($downloadedFiles as $fileName) {
if ($this->isExcluded($fileName)) {
continue;
}
$oldFile = $rootDir . '/' . $fileName;
$newFile = __DIR__ . '/../' . $fileName;
$state = rename($oldFile, $newFile);
if ($state === false) {
throw new UpdateException("Error: Could not move new files");
}
}
}
/**
* Move entry points in place.
*/
function moveEntryPHPpoints()
{
$rootDir = __DIR__ . '/../tmp_uploaded_update/phplist/public_html/lists';
$downloadedFiles = scandir($rootDir);
foreach ($downloadedFiles as $filename) {
$oldFile = $rootDir . '/' . $filename;
$newFile = __DIR__ . '/../' . $filename;
if (in_array($filename, $this->excludedFiles)) {
rename($oldFile, $newFile);
}
}
}
/**
* Back up old files to the location specified by the user.
* @param $destination 'path' to backup zip
* @throws UpdateException
*/
function backUpFiles($destination)
{
$iterator = new \RecursiveDirectoryIterator(realpath(__DIR__ . '/../'), FilesystemIterator::SKIP_DOTS);
/** @var SplFileInfo[] $iterator */
/** @var $iterator */
$iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST);
$zip = new ZipArchive();
$resZip = $zip->open($destination, ZipArchive::CREATE);
if ($resZip === false) {
throw new \UpdateException("Error: Could not create back up of phpList directory. Please make sure that the argument is valid or writable and try again without reloading the page.");
}
$zip->addEmptyDir('lists');
foreach ($iterator as $file) {
$prefix = realpath(__DIR__ . '/../');
$name = 'lists/' . substr($file->getRealPath(), strlen($prefix) + 1);
if ($file->isDir()) {
$zip->addEmptyDir($name);
continue;
}
if ($file->isFile()) {
$zip->addFromString($name, file_get_contents($file->getRealPath()));
continue;
}
}
$state = $zip->close();
if ($state === false) {
throw new UpdateException('Error: Could not create back up of phpList directory. Please make sure that the argument is valid or is writable and try again without reloading the page.');
}
}
/**
* Extract Zip Files
* @param string $toBeExtracted
* @param string $extractPath
* @throws UpdateException
*/
function unZipFiles($toBeExtracted, $extractPath)
{
$zip = new ZipArchive;
/* Open the Zip file */
if ($zip->open($toBeExtracted) !== true) {
throw new \UpdateException("Error: Unable to open the Zip File");
}
/* Extract Zip File */
$zip->extractTo($extractPath);
$zip->close();
}
/**
* Delete temporary downloaded files
* @throws UpdateException
*/
function deleteTemporaryFiles()
{
$isTempDirDeleted = $this->rmdir_recursive(self::DOWNLOAD_PATH);
if ($isTempDirDeleted === false) {
throw new \UpdateException("Error: Could not delete temporary files!");
}
}
/**
* @throws UpdateException
*/
function recoverFiles()
{
$this->unZipFiles('backup.zip', self::DOWNLOAD_PATH);
}
/**
* @param int $action
* @throws UpdateException
*/
function writeActions($action)
{
$actionsdir = __DIR__ . '/../config/actions.txt';
if (!file_exists($actionsdir)) {
$actionsFile = fopen($actionsdir, "w+");
if ($actionsFile === false) {
throw new \UpdateException("Error: Could not create actions file in the config directory, please change permissions");
}
}
$written = file_put_contents($actionsdir, json_encode(array('continue' => false, 'step' => $action)));
if ($written === false) {
throw new \UpdateException("Error: Could not write on $actionsdir");
}
}
/**
* Return the current step
* @return mixed array of json data
* @throws UpdateException
*/
function currentUpdateStep()
{
$actionsdir = __DIR__ . '/../config/actions.txt';
if (file_exists($actionsdir)) {
$status = file_get_contents($actionsdir);
if ($status === false) {
throw new \UpdateException("Cannot read content from $actionsdir");
}
$decodedJson = json_decode($status, true);
if (!is_array($decodedJson)) {
throw new \UpdateException('JSON data cannot be decoded!');
}
} else {
return array('step' => 0, 'continue' => true);
}
return $decodedJson;
}
/**
* Check if config folder is writable. Required to be writable in order to write steps.
*/
function checkConfig()
{
$configdir = __DIR__ . '/../config/';
if (!is_dir($configdir) || !is_writable($configdir)) {
die("Cannot update because config directory is not writable.");
}
}
/**
* Check if required php modules are installed.
*/
function checkphpmodules()
{
$phpmodules = array('curl', 'pdo', 'zip');
$notinstalled = array();
foreach ($phpmodules as $value) {
if (!extension_loaded($value)) {
array_push($notinstalled, $value);
}
}
if (count($notinstalled) > 0) {
$message = "The following php modules are required. Please install them to continue." . '
';
foreach ($notinstalled as $value) {
$message .= $value . '
';
}
die($message);
}
}
/**
* Move plugins in temporary folder to prevent them from being overwritten.
* @throws UpdateException
*/
function movePluginsInTempFolder()
{
$oldDir = __DIR__ . '/../admin/plugins';
$newDir = __DIR__ . '/../tmp_uploaded_update/tempplugins';
$state = rename($oldDir, $newDir);
if ($state === false) {
throw new UpdateException("Could not move plugins directory");
}
}
/**
* Move plugins back in admin directory.
* @throws UpdateException
*/
function movePluginsInPlace()
{
$oldDir = realpath(__DIR__ . '/../tmp_uploaded_update/tempplugins');
$newDir = realpath(__DIR__ . '/../admin/plugins');
$this->rmdir_recursive($newDir);
$state = rename($oldDir, $newDir);
if ($state === false) {
throw new UpdateException("Could not move plugins directory to admin folder.");
}
}
/**
* Update updater to a new location before temp folder is deleted!
* @throws UpdateException
*/
function moveUpdater()
{
$rootDir = __DIR__ . '/../tmp_uploaded_update/phplist/public_html/lists';
$oldFile = $rootDir . '/updater';
$newFile = __DIR__ . '/../tempupdater';
$state = rename($oldFile, $newFile);
if ($state === false) {
throw new UpdateException("Could not move updater");
}
}
/**
* Replace new updater as the final step
* @throws UpdateException
*/
function replaceNewUpdater()
{
$newUpdater = realpath(__DIR__ . '/../tempupdater');
$oldUpdater = realpath(__DIR__ . '/../updater');
$this->rmdir_recursive($oldUpdater);
$state = rename($newUpdater, $oldUpdater);
if ($state === false) {
throw new UpdateException("Could not move the new updater in place");
}
}
}
try {
$update = new updater();
if (!$update->isAuthenticated()) {
die('No permission to access updater.');
}
$update->checkConfig();
$update->checkphpmodules();
} catch (\UpdateException $e) {
throw $e;
}
/**
*
*
*
*/
if (isset($_POST['action'])) {
set_time_limit(0);
//ensure that $action is integer
$action = (int)$_POST['action'];
header('Content-Type: application/json');
$writeStep = true;
switch ($action) {
case 0:
$statusJson = $update->currentUpdateStep();
echo json_encode(array('status' => $statusJson, 'autocontinue' => true));
break;
case 1:
$currentVersion = $update->getCurrentVersion();
$updateMessage = $update->checkIfThereIsAnUpdate();
$isThereAnUpdate = $update->availableUpdate();
if ($isThereAnUpdate === false) {
echo(json_encode(array('continue' => false, 'response' => $updateMessage)));
} else {
echo(json_encode(array('continue' => true, 'response' => $updateMessage)));
}
break;
case 2:
echo(json_encode(array('continue' => true, 'autocontinue' => true, 'response' => 'Starting integrity check')));
break;
case 3:
$unexpectedFiles = $update->checkRequiredFiles();
if (count($unexpectedFiles) !== 0) {
$elements = "Error: The following files are not expected or required. To continue please move or delete them. \n";;
foreach ($unexpectedFiles as $key => $fileName) {
$elements .= $key . "\n";
}
echo(json_encode(array('retry' => true, 'continue' => false, 'response' => $elements)));
} else {
echo(json_encode(array('continue' => true, 'response' => 'Integrity check successful', 'autocontinue' => true)));
}
break;
case 4:
$notWriteableFiles = $update->checkWritePermissions();
if (count($notWriteableFiles) !== 0) {
$notWriteableElements = "Error: No write permission for the following files: \n";;
foreach ($notWriteableFiles as $key => $fileName) {
$notWriteableElements .= $fileName . "\n";
}
echo(json_encode(array('retry' => true, 'continue' => false, 'response' => $notWriteableElements)));
} else {
echo(json_encode(array('continue' => true, 'response' => 'Write check successful.', 'autocontinue' => true)));
}
break;
case 5:
echo(json_encode(array('continue' => true, 'response' => 'Do you want a backup?
Happy with your existing installation? Paid support by independent consultants here.
Great value
Price $1
3000 Subscribers