445 lines
11 KiB
PHP
445 lines
11 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Vvveb
|
|
*
|
|
* Copyright (C) 2022 Ziadin Givan
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
namespace Vvveb\System;
|
|
|
|
use function Vvveb\download;
|
|
use function Vvveb\getUrl;
|
|
use function Vvveb\pregMatch;
|
|
use function Vvveb\rcopy;
|
|
use function Vvveb\rrmdir;
|
|
|
|
define('CHMOD_DIR', (0755 & ~umask()));
|
|
define('CHMOD_FILE', (0644 & ~umask()));
|
|
|
|
class Update {
|
|
protected $url = 'https://www.vvveb.com/update.json';
|
|
|
|
protected $workDir = DIR_STORAGE . 'upgrade';
|
|
|
|
protected $zipFile = false;
|
|
|
|
function checkUpdates($type = 'core', $force = false) {
|
|
if ($force) {
|
|
//delete update cache
|
|
$cacheKey = md5($this->url);
|
|
$cacheDriver = Cache :: getInstance();
|
|
$cacheDriver->delete('url', $cacheKey);
|
|
}
|
|
|
|
$error = '';
|
|
$result = false;
|
|
//cache results for one week
|
|
try {
|
|
$result = getUrl($this->url, true);
|
|
} catch (\Exception $e) {
|
|
if (DEBUG) {
|
|
$error = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
$info = ['hasUpdate' => false];
|
|
|
|
if ($result) {
|
|
$info = json_decode($result, true);
|
|
|
|
if ($type == 'core') {
|
|
$info['hasUpdate'] = max(version_compare($info['version'] ?? 0, V_VERSION), 0);
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
if ($error) {
|
|
$info['error'] = $error;
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
private function checkFolderPermissions($dir) {
|
|
$skip = ['install', 'locale', 'vendor', 'plugins', 'config'];
|
|
$unwritable = [];
|
|
|
|
$handle = @opendir($dir);
|
|
|
|
while (false !== ($file = readdir($handle))) {
|
|
$full = $dir . DS . $file;
|
|
|
|
if (($file != '.') &&
|
|
($file != '..') && ! in_array($file, $skip)) {
|
|
if (is_dir($full)) {
|
|
if (! is_writable($full)) {
|
|
if (! @chmod($full, CHMOD_DIR)) {
|
|
return $full;
|
|
}
|
|
}
|
|
|
|
$result = $this->checkFolderPermissions($full);
|
|
|
|
if ($result !== true) {
|
|
return $result;
|
|
}
|
|
}
|
|
//if (str_ends_with($full, '.php') && ! is_writable($full)) {
|
|
if ((substr_compare($full,'.php', -4) === 0) && ! is_writable($full)) {
|
|
if (! @chmod($full, CHMOD_FILE)) {
|
|
return $full;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function checkPermissions() {
|
|
$check = $this->checkFolderPermissions(DIR_ROOT);
|
|
|
|
return $check;
|
|
}
|
|
|
|
static function backup() {
|
|
$skipFolders = ['plugins', 'public', 'storage', 'install'];
|
|
$backupFolder = DIR_BACKUP . 'update-' . V_VERSION . '-' . date('Y-m-d_H:i:s');
|
|
|
|
rcopy(DIR_ROOT, $backupFolder, $skipFolders);
|
|
}
|
|
|
|
static function download($url) {
|
|
//$temp = tmpfile();
|
|
$f = false;
|
|
$temp = tempnam(sys_get_temp_dir(), 'vvveb_update');
|
|
|
|
if ($content = download($url)) {
|
|
$f = file_put_contents($temp, $content, LOCK_EX);
|
|
|
|
return $temp;
|
|
}
|
|
|
|
return $f;
|
|
}
|
|
|
|
function setPermissions() {
|
|
$folders = [DIR_ROOT . 'app', DIR_ROOT . 'admin', DIR_ROOT . 'system'];
|
|
$files = [DIR_ROOT . 'index.php', DIR_ROOT . 'admin' . DS . 'index.php', DIR_ROOT . 'install' . DS . 'index.php'];
|
|
|
|
foreach ($folders as $folder) {
|
|
@chmod($folder, CHMOD_DIR);
|
|
}
|
|
|
|
foreach ($files as $file) {
|
|
@chmod($file, CHMOD_FILE);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function copyFolder($src, $dest, $skipFolders = []) {
|
|
ignore_user_abort(true);
|
|
|
|
if (! is_writable($dest)) {
|
|
throw new \Exception(sprintf('Folder "%s" not writable!', $dest));
|
|
}
|
|
|
|
return rcopy($src, $dest, $skipFolders);
|
|
}
|
|
|
|
function copyAdmin() {
|
|
//$skipFolders = ['plugins', 'public', 'storage'];
|
|
return $this->copyFolder($this->workDir . DS . 'admin', DIR_ROOT . DS . 'admin');
|
|
}
|
|
|
|
function copyApp() {
|
|
//$skipFolders = ['plugins', 'public', 'storage'];
|
|
return $this->copyFolder($this->workDir . DS . 'app', DIR_ROOT . DS . 'app');
|
|
}
|
|
|
|
function copySystem() {
|
|
return $this->copyFolder($this->workDir . DS . 'system', DIR_ROOT . DS . 'system');
|
|
}
|
|
|
|
function copyInstall() {
|
|
//$skipFolders = ['plugins', 'public', 'storage'];
|
|
return $this->copyFolder($this->workDir . DS . 'install', DIR_ROOT . DS . 'install');
|
|
}
|
|
|
|
function copyCore() {
|
|
$skipFolders = ['plugins', 'public', 'storage', 'system', 'app', 'admin', 'install', 'config', 'vendor', 'env.php'];
|
|
|
|
return $this->copyFolder($this->workDir, DIR_ROOT, $skipFolders);
|
|
}
|
|
|
|
function copyConfig() {
|
|
$skip = ['plugins.php', 'mail.php', 'sites.php', 'app.php', 'admin.php', 'app-routes.php'];
|
|
|
|
return $this->copyFolder($this->workDir . DS . 'config', DIR_ROOT . DS . 'config', $skip);
|
|
}
|
|
|
|
function copyPublic() {
|
|
$skipFolders = ['plugins', 'themes', 'admin', 'media'];
|
|
|
|
return $this->copyFolder($this->workDir . DS . 'public', DIR_PUBLIC, $skipFolders);
|
|
}
|
|
|
|
function copyPublicAdmin() {
|
|
ignore_user_abort(true);
|
|
$skipFolders = ['plugins'];
|
|
|
|
return $this->copyFolder($this->workDir . DS . 'public' . DS . 'admin', DIR_PUBLIC . DS . 'admin', $skipFolders);
|
|
}
|
|
|
|
function copyPublicMedia() {
|
|
$skipFolders = ['plugins', 'themes', 'admin'];
|
|
|
|
return $this->copyFolder($this->workDir . DS . 'public' . DS . 'media', DIR_PUBLIC . DS . 'media', $skipFolders);
|
|
}
|
|
|
|
function createNewTables() {
|
|
$db = \Vvveb\System\Db::getInstance();
|
|
$tableNames = $db->getTableNames();
|
|
|
|
$driver = DB_ENGINE;
|
|
$sqlPath = DIR_ROOT . "install/sql/$driver/";
|
|
$files = \Vvveb\globBrace($sqlPath, ['', '*/*/'], '*.sql');
|
|
|
|
$diff = [];
|
|
//if the number of tables is less than in the install dir
|
|
if (count($tableNames) < count($files)) {
|
|
$tableSql = [];
|
|
//get table names from sql files
|
|
foreach ($files as $filename) {
|
|
$tableSql[] = basename($filename, '.sql');
|
|
}
|
|
//get the names of missing tables
|
|
$diff = array_diff($tableSql, $tableNames);
|
|
}
|
|
|
|
$sqlFiles = [];
|
|
//get files for missing tables
|
|
foreach ($diff as $key => $tableName) {
|
|
$sqlFiles[] = $files[$key];
|
|
}
|
|
|
|
//create missing tables
|
|
if ($sqlFiles) {
|
|
$sqlImport = new \Vvveb\System\Import\Sql();
|
|
$sqlImport->createTables($sqlFiles);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function tableColumns($sql) {
|
|
$cols = [];
|
|
$tableName = '';
|
|
|
|
if (preg_match('/CREATE TABLE ([^\s]+?)\s*\((.+)\).*;/ms', $sql, $matches)) {
|
|
$tableName = trim($matches[1], ' "\'`');
|
|
//$columns = explode("\n", $matches[2]);
|
|
$columns = preg_split('/\r\n|\r|\n/', trim($matches[2]));
|
|
|
|
foreach ($columns as $key => &$column) {
|
|
$column = preg_replace('/-- .+$/', '', $column); //remove comments
|
|
$column = trim($column, ' ,');
|
|
|
|
if ($column && in_array($column[0], ['`', '"', '\''])) {
|
|
$colName = pregMatch('/^[\'`"](.+?)[\'`"]/', $column, 1);
|
|
$cols[$colName] = trim($column, ',');
|
|
}
|
|
}
|
|
|
|
ksort($cols);
|
|
}
|
|
|
|
return [$tableName, $cols];
|
|
}
|
|
|
|
function addNewColumns() {
|
|
$db = \Vvveb\System\Db::getInstance();
|
|
$tableNames = $db->getTableNames();
|
|
|
|
$driver = DB_ENGINE;
|
|
$sqlPath = DIR_ROOT . "install/sql/$driver/";
|
|
$newSqlPath = $this->workDir . DS . "install/sql/$driver/";
|
|
$files = \Vvveb\globBrace($sqlPath, ['', '*/*/'], '*.sql');
|
|
|
|
$diff = [];
|
|
$tableSql = [];
|
|
|
|
//get table names from sql files
|
|
foreach ($files as $filename) {
|
|
$tableName = basename($filename, '.sql');
|
|
$newFilename = str_replace($sqlPath, $newSqlPath, $filename);
|
|
|
|
if (file_exists($newFilename)) {
|
|
$currentSql = file_get_contents($filename);
|
|
$currentTable = $this->tableColumns($currentSql);
|
|
$tableName = $currentTable[0];
|
|
$columns = $currentTable[1];
|
|
|
|
if (! $columns) {
|
|
continue;
|
|
}
|
|
|
|
$newSql = file_get_contents($newFilename);
|
|
$newColumns = $this->tableColumns($newSql)[1];
|
|
|
|
if (! $newColumns) {
|
|
continue;
|
|
}
|
|
|
|
//newly added columns
|
|
$tableColumns = $db->getColumnsMeta($tableName);
|
|
//if table does not exist yet skip, it will be created
|
|
if (! $tableColumns) {
|
|
continue;
|
|
}
|
|
|
|
$addedColumns = array_diff_key($newColumns, $tableColumns);
|
|
|
|
//changed columns
|
|
$changedColumns = [];
|
|
$deletedColumns = [];
|
|
|
|
foreach ($columns as $name => $column) {
|
|
if (isset($newColumns[$name])) {
|
|
if ($column != $newColumns[$name]) {
|
|
$changedColumns[$name] = $newColumns[$name];
|
|
}
|
|
} else {
|
|
$deletedColumns[$name] = $column[$name];
|
|
}
|
|
}
|
|
|
|
//check deleted columns agains existing table
|
|
$deletedColumns += array_diff_key($tableColumns, $newColumns);
|
|
$tableColumns = [];
|
|
//add new columns
|
|
if ($addedColumns) {
|
|
if (! $db) {
|
|
//don't connect to db unless we need to alter table
|
|
$db = Db::getInstance();
|
|
}
|
|
|
|
foreach ($addedColumns as $name => $definition) {
|
|
if (isset($tableColumns[$name])) {
|
|
//column already exists skip
|
|
continue;
|
|
}
|
|
$definition = $newColumns[$name];
|
|
|
|
$query = "ALTER TABLE $tableName ADD $definition";
|
|
$result = $db->query($query);
|
|
}
|
|
}
|
|
|
|
//change columns
|
|
if ($changedColumns) {
|
|
if (! $db) {
|
|
//don't connect to db unless we need to alter table
|
|
$db = Db::getInstance();
|
|
}
|
|
|
|
foreach ($changedColumns as $name => $definition) {
|
|
if (! isset($tableColumns[$name])) {
|
|
//column does not exist skip
|
|
continue;
|
|
}
|
|
|
|
$query = "ALTER TABLE $tableName MODIFY COLUMN $definition";
|
|
$result = $db->query($query);
|
|
}
|
|
}
|
|
|
|
//delete columns
|
|
//disabled to avoid deleting user added columns
|
|
if (false && $deletedColumns) {
|
|
if (! $db) {
|
|
//don't connect to db unless we need to alter table
|
|
$db = Db::getInstance();
|
|
}
|
|
|
|
foreach ($deletedColumns as $name => $definition) {
|
|
if (! isset($tableColumns[$name])) {
|
|
//column does not exist skip
|
|
continue;
|
|
}
|
|
|
|
$query = "ALTER TABLE $tableName DROP COLUMN $name";
|
|
$result = $db->query($query);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function clearCache() {
|
|
return CacheManager::delete();
|
|
}
|
|
|
|
function cleanup() {
|
|
rrmdir($this->workDir);
|
|
|
|
if ($this->zipFile) {
|
|
unlink($this->zipFile);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function install($zipFile) {
|
|
$this->unzip($zipFile, $this->workDir);
|
|
|
|
$success = true;
|
|
//$dest = substr(DIR_ROOT,0, -1); //remove trailing slash
|
|
rrmdir($this->workDir);
|
|
//plugins and themes are updated individually
|
|
$skipFolders = ['plugins', 'public' . DS . 'themes', 'storage', 'vendor'];
|
|
|
|
rcopy($this->workDir, DIR_ROOT, $skipFolders);
|
|
rrmdir($this->workDir);
|
|
unlink($zipFile);
|
|
CacheManager::delete();
|
|
}
|
|
|
|
function unzip($zipFile) {
|
|
if (! is_dir($this->workDir)) {
|
|
mkdir($this->workDir);
|
|
}
|
|
|
|
$result = false;
|
|
$this->zipFile = $zipFile;
|
|
$zip = new \ZipArchive();
|
|
|
|
if ($zip->open($zipFile) === true) {
|
|
$result = $zip->extractTo($this->workDir);
|
|
}
|
|
|
|
Event :: trigger(__CLASS__, __FUNCTION__, $zipFile, $result);
|
|
|
|
return $result;
|
|
}
|
|
}
|