<?php
/*-------------------------------------------------------+
| PHPFusion Content Management System
| Copyright (C) PHP Fusion Inc
| https://phpfusion.com/
+--------------------------------------------------------+
| Filename: Update.php
| Author: RobiNN
+--------------------------------------------------------+
| This program is released as free software under the
| Affero GPL license. You can redistribute it and/or
| modify it under the terms of this license which you
| can read by viewing the included agpl.txt or online
| at www.gnu.org/licenses/agpl.html. Removal of this
| copyright header is strictly prohibited without
| written permission from the original author(s).
+--------------------------------------------------------*/
namespace PHPFusion;
use FilesystemIterator;
use PHPFusion\Installer\Batch;
class Update extends Installer\Infusions {
/**
* Temporary directory for downloads
*
* @var string
*/
private $temp_dir = BASEDIR.'temp/';
/**
* Install directory
*
* @var string
*/
private $install_dir = BASEDIR;
/**
* Url to the update folder on the server
*
* @var string
*/
private $update_url = 'https://raw.githubusercontent.com/PHPFusion/Archive/updates/';
/**
* Version filename on the server
*
* @var string
*/
private $update_file = '9.json';
/**
* The URL from which the translations will be downloaded
*
* @var string
*/
private $lang_url = 'https://www.php-fusion.co.uk/translations/tmp/v9/';
/**
* List of available languages
*
* @var string
*/
private $available_languages = 'https://www.php-fusion.co.uk/translations/languages.php?version=9';
/**
* @var string
*/
private $latest_version = '';
/**
* @var string
*/
protected $current_version = '';
/**
* @var array
*/
private $update = [];
/**
* @var array
*/
private $messages = [];
/**
* @var array|string
*/
private $locale;
/**
* AutoUpdate constructor
*/
public function __construct() {
$this->locale = fusion_get_locale('', LOCALE.LOCALESET.'admin/upgrade.php');
if (!is_dir($this->temp_dir) && !mkdir($this->temp_dir, 0755, TRUE)) {
$this->setError(sprintf('Could not create temporary directory %s.', $this->temp_dir));
}
ini_set('max_execution_time', 300);
$this->current_version = fusion_get_settings('version');
}
/**
* Set the update url
*
* @param string $update_url
*/
public function setUpdateUrl($update_url) {
$this->update_url = $update_url;
}
/**
* Set the update file
*
* @param string $update_file
*/
public function setUpdateFile($update_file) {
$this->update_file = $update_file;
}
/**
* Get the update url
*
* @return string
*/
public function getUpdateUrl() {
return $this->update_url.$this->update_file;
}
/**
* Get the number of the latest version
*
* @return string
*/
public function getLatestVersion() {
return $this->latest_version;
}
/**
* Check for a new version
*
* @param false $return_version
*
* @return array|bool
*/
public function checkUpdate($return_version = FALSE) {
if ($this->isValidUrl($this->getUpdateUrl())) {
if (function_exists('curl_version')) {
$update = $this->downloadCurl($this->getUpdateUrl());
if ($update === FALSE) {
$this->setError(sprintf('Could not download update file %s via curl!', $this->getUpdateUrl()));
}
} else {
$update = @file_get_contents($this->getUpdateUrl(), FALSE);
if ($update === FALSE) {
$this->setError(sprintf('Could not download update file %s via file_get_contents!', $this->getUpdateUrl()));
}
}
if ($update === FALSE) {
return FALSE; // Could not check for updates
}
$versions = (array)json_decode($update, FALSE);
if (is_array($versions)) {
foreach ($versions as $version => $url) {
if (version_compare($version, $this->current_version, '>')) {
$this->latest_version = $version;
$this->update = ['version' => $version, 'url' => $url];
}
}
}
if ($this->newVersionAvailable()) {
if ($return_version == TRUE) {
return $this->update['version'];
} else {
return TRUE;
}
}
}
return NULL; // No update available
}
/**
* Check if a new version is available.
*
* @return bool
*/
public function newVersionAvailable() {
return version_compare($this->latest_version, $this->current_version, '>');
}
/**
* Check if url is valid
*
* @param string $url
*
* @return bool
*/
protected function isValidUrl($url) {
if (filter_var($url, FILTER_VALIDATE_URL) !== FALSE) {
return TRUE;
} else {
$this->setError(sprintf('Url %s is not valid.', $url));
return FALSE;
}
}
/**
* Download file via curl
*
* @param string $url URL to file
*
* @return string|false
*/
protected function downloadCurl($url) {
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($curl, CURLOPT_TIMEOUT, 5);
$update = curl_exec($curl);
$success = TRUE;
if (curl_error($curl)) {
$success = FALSE;
}
curl_close($curl);
return ($success === TRUE) ? $update : FALSE;
}
/**
* Download update
*
* @param string $update_url Url where to download from
* @param string $update_file Path where to save the download
*
* @return bool
*/
protected function downloadZip($update_url, $update_file) {
$this->setMessage(sprintf($this->locale['U_009'], $update_url));
if ($this->isValidUrl($update_url)) {
if (function_exists('curl_version')) {
$update = $this->downloadCurl($update_url);
if ($update === FALSE) {
return FALSE;
}
} else if (ini_get('allow_url_fopen')) {
$update = @file_get_contents($update_url, FALSE);
if ($update === FALSE) {
$this->setError(sprintf('Could not download update "%s"!', $update_url));
}
}
$handle = fopen($update_file, 'wb');
if (!$handle) {
$this->setError(sprintf('Could not open file handle to save update to "%s"!', $update_file));
return FALSE;
}
if (!empty($update)) {
if (!fwrite($handle, $update)) {
$this->setError(sprintf('Could not write update to file "%s"!', $update_file));
fclose($handle);
return FALSE;
}
}
fclose($handle);
return TRUE;
}
return NULL;
}
/**
* @param string $zip_file Path to the update file
* @param string $dest Destination directory
*
* @return bool
*/
protected function extractFiles($zip_file, $dest) {
$this->setMessage($this->locale['U_010']);
$zip = new \ZipArchive();
$resource = $zip->open($zip_file);
if ($resource !== TRUE) {
$this->setError(sprintf('Could not open zip file "%s", error: %d', $zip_file, $resource));
return FALSE;
}
for ($i = 0; $i < $zip->numFiles; $i++) {
$file_stats = $zip->statIndex($i);
$filename = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_stats['name']);
$foldername = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $dest.dirname($filename));
$absolute_filename = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $dest);
if (!is_dir($foldername) && !mkdir($foldername, 0777, TRUE) && !is_dir($foldername)) {
$this->setError(sprintf('Directory "%s" has to be writeable!', $foldername));
return FALSE;
}
if ($filename[strlen($filename) - 1] === DIRECTORY_SEPARATOR) {
continue;
}
if ($zip->extractTo($absolute_filename, $file_stats['name']) === FALSE) {
$this->setError(sprintf('Coud not read zip entry "%s"', $file_stats['filename']));
}
}
$zip->close();
return TRUE;
}
/**
* Copy files
*
* @param string $source
* @param string $target
* @param array $ignore
*/
private function copyFiles($source, $target, $ignore = []) {
$this->setMessage($this->locale['U_011']);
$directoryIterator = new \RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS);
$iterator = new \RecursiveIteratorIterator($directoryIterator, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $item) {
if ($item->isDir()) {
$dir = $target.DIRECTORY_SEPARATOR.$iterator->getSubPathName();
if (!is_dir($dir)) {
mkdir($dir);
}
} else {
if (!in_array($item->getFilename(), $ignore)) {
copy($item, $target.DIRECTORY_SEPARATOR.$iterator->getSubPathName());
}
}
}
}
/**
* @param $method
* @param $code_array
*
* @return bool
*
* @uses adminpanel_infuse
* @uses dropcol_infuse
* @uses sitelink_infuse
* @uses mlt_insertdbrow_infuse
* @uses mlt_adminpanel_infuse
* @uses mlt_infuse
* @uses altertable_infuse
* @uses updatedbrow_infuse
* @uses newtable_infuse
* @uses newcol_infuse
* @uses insertdbrow_infuse
* @uses deldbrow_infuse
*/
protected function doUpgradeBatch($method, $code_array) {
try {
$method = $method.'_infuse';
return $this->$method($code_array);
} catch (\Exception $e) {
if (!is_file(BASEDIR.'installer_'.date('d-M-Y').'.errors.log')) {
touch(BASEDIR.'installer_'.date('d-M-Y').'.errors.log');
}
write_file(BASEDIR.'installer_'.date('d-M-Y').'.log.txt', $e->getMessage(), FILE_APPEND);
return FALSE;
}
}
/**
* Run upgrade scripts
*
* @return bool
*/
private function doDbUpgrade() {
$to_upgrade = Batch::getInstance()->checkUpgrades();
if (!empty($to_upgrade)) {
$this->setMessage($this->locale['U_012']);
foreach ($to_upgrade as $file_upgrades) {
if (!empty($file_upgrades)) {
foreach ($file_upgrades as $callback_method => $upgrades) {
if (!empty($upgrades)) {
$method = $callback_method.'_infuse';
if (method_exists($this, $method)) {
$this->setMessage('Running db function'.$method);
$this->doUpgradeBatch($callback_method, [$callback_method => $upgrades]);
}
}
}
}
}
return TRUE;
}
return NULL;
}
/**
* Get enabled languages
*
* @return false|string[]
*/
public function getEnabledLanguages() {
$enabled_languages = fusion_get_settings('enabled_languages');
if (!empty($enabled_languages) && $enabled_languages !== 'English') {
return explode('.', $enabled_languages);
}
return FALSE;
}
/**
* Update languages
*
* @return bool
*/
public function updateLanguages() {
$enabled_languages = $this->getEnabledLanguages();
if (is_array($enabled_languages)) {
foreach ($enabled_languages as $language) {
if ($language !== 'English') {
$this->downloadLanguage($language);
}
}
$this->setMessage($this->locale['U_017']);
return TRUE;
}
return NULL;
}
/**
* Download language
*
* @param string $language
*
* @return bool
*/
public function downloadLanguage($language) {
$this->setMessage(sprintf($this->locale['U_018'], $language));
$lang_pack_zip = $this->temp_dir.$language.'.zip';
if (!is_file($lang_pack_zip)) {
if (!$this->downloadZip($this->lang_url.$language.'.zip', $lang_pack_zip)) {
$this->setError(sprintf('Failed to download pack from %s to %s!', $this->lang_url.$language, $lang_pack_zip));
return FALSE;
}
}
$dest = $this->temp_dir.$language.'/';
if (is_file($lang_pack_zip)) {
$this->extractFiles($lang_pack_zip, $dest);
}
if (is_dir($dest)) {
$this->copyFiles($dest, $this->install_dir);
}
if (!unlink($lang_pack_zip)) {
$this->setError(sprintf('Could not delete lang pack "%s"!', $lang_pack_zip));
}
if (is_dir($dest)) {
rrmdir($dest);
}
return TRUE;
}
/**
* Update core
*
* @return bool
*/
private function updateCoreFiles() {
if ($this->newVersionAvailable()) {
if (empty($this->temp_dir) || !is_dir($this->temp_dir) || !is_writable($this->temp_dir)) {
$this->setError(sprintf('Temporary directory "%s" does not exist or is not writeable!', $this->temp_dir));
return FALSE;
}
if (empty($this->install_dir) || !is_dir($this->install_dir) || !is_writable($this->install_dir)) {
$this->setError(sprintf('Install directory "%s" does not exist or is not writeable!', $this->install_dir));
return FALSE;
}
$update_zip_file = $this->temp_dir.$this->update['version'].'.zip';
if (!is_file($update_zip_file)) {
if (!$this->downloadZip($this->update['url'], $update_zip_file)) {
$this->setError(sprintf('Failed to download update from %s to %s!', $this->update['url'], $update_zip_file));
return FALSE;
}
}
if (is_file($update_zip_file) && !$this->extractFiles($update_zip_file, $this->temp_dir)) {
return FALSE;
}
if (is_dir($this->temp_dir.'files/')) {
$this->copyFiles($this->temp_dir.'files/', $this->install_dir, ['robots.txt']);
}
if (!unlink($update_zip_file)) {
$this->setError(sprintf('Could not delete update file "%s"!', $update_zip_file));
}
if (is_array($this->getEnabledLanguages())) {
if (!$this->updateLanguages()) {
$this->setError('An error occurred while updating the translations. After the update, you can update the translations separately.');
}
}
if (!$this->doDbUpgrade()) {
$this->setError('An error occurred while upgrading the database.');
return FALSE;
}
if (is_dir($this->temp_dir)) {
rrmdir($this->temp_dir);
}
if (file_exists(BASEDIR.'install.php')) {
@unlink(BASEDIR.'install.php');
}
return TRUE;
}
return NULL;
}
/**
* Run upgrade
*/
public function upgradeCms() {
$result = $this->updateCoreFiles();
if ($result == TRUE) {
$this->setMessage($this->locale['U_014']);
} else if ($result == FALSE) {
$this->setMessage($this->locale['U_015']);
}
}
/**
* Get available languages
*
* @return array|false
*/
public function getAvailableLanguages() {
if ($this->isValidUrl($this->available_languages)) {
if (function_exists('curl_version')) {
$list = $this->downloadCurl($this->available_languages);
if ($list === FALSE) {
$this->setError(sprintf('Could not download update file %s via curl!', $this->available_languages));
}
} else {
$list = @file_get_contents($this->available_languages, FALSE);
if ($list === FALSE) {
$this->setError(sprintf('Could not download update file %s via file_get_contents!', $this->available_languages));
}
}
if ($list === FALSE) {
return FALSE; // Could not check for available languages
}
return (array)json_decode($list, FALSE);
}
return NULL;
}
/**
* Ajax checker
*/
public function ajaxChecker() {
$this->locale += fusion_get_locale('', LOCALE.LOCALESET.'admin/main.php');
$settings = fusion_get_settings();
if (
($settings['update_checker'] == 1 && ($settings['update_last_checked'] < (time() - 21600))) || // check every 6 hours
(check_get('force') && get('force') == 'true')
) {
dbquery("UPDATE ".DB_SETTINGS." SET settings_value=:time WHERE settings_name=:name", [':time' => time(), ':name' => 'update_last_checked']);
$version = $this->checkUpdate(TRUE);
if (!empty($version) && version_compare($version, $settings['version'], '>')) {
$text = sprintf($this->locale['new_update_avalaible'], $version);
$text .= ' <a class="btn btn-primary btn-sm m-l-10" href="'.$settings['siteurl'].'administration/upgrade.php'.fusion_get_aidlink().'">'.$this->locale['update_now'].'</a>';
$result = ['result' => $text];
} else {
if (check_get('force') && get('force') == 'true') {
$result = ['result' => $this->locale['U_006']];
}
}
if (!empty($result)) {
header('Content-Type: application/json');
echo json_encode($result);
}
}
}
/**
* Set message
*
* @param $message
*/
private function setMessage($message) {
$this->messages[] = $message;
}
/**
* @return array
*/
public function getMessages() {
return $this->messages;
}
/**
* @param $message
*/
private function setError($message) {
set_error(1, $message, debug_backtrace()[1]['file'], debug_backtrace()[1]['line']);
}
}