. * */ 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; } }