*/ class Update { const SQL_DIR = 'update/sql/'; const PHP_DIR = 'update/php/'; const INSTRUCTION_DIR = 'update/instruction/'; protected $version = null; /** @var bool */ protected $usePropel = null; /** @var null|Tlog */ protected $logger = null; /** @var array log messages */ protected $logs = []; /** @var array post instructions */ protected $postInstructions = []; /** @var array */ protected $updatedVersions = []; /** @var PDO */ protected $connection = null; /** @var string|null */ protected $backupFile = null; /** @var string */ protected $backupDir = 'local/backup/'; /** @var array */ protected $messages = []; /** @var Translator */ protected $translator; public function __construct($usePropel = true) { $this->usePropel = $usePropel; if ($this->usePropel) { $this->logger = Tlog::getInstance(); $this->logger->setLevel(Tlog::DEBUG); } else { $this->logs = []; } $dbConfig = null; try { $dbConfig = $this->getDatabaseConfig(); } catch (ParseException $ex) { throw new UpdateException("database.yml is not a valid file : " . $ex->getMessage()); } try { $this->connection = new \PDO( $dbConfig['dsn'], $dbConfig['user'], $dbConfig['password'], [PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'"] ); } catch (\PDOException $ex) { throw new UpdateException('Wrong connection information' . $ex->getMessage()); } $this->version = $this->getVersionList(); } /** * retrieve the database configuration * * @return array containing the database */ protected function getDatabaseConfig() { $configPath = THELIA_CONF_DIR . "database.yml"; if (!file_exists($configPath)) { throw new UpdateException("Thelia is not installed yet"); } $definePropel = new DefinePropel( new DatabaseConfiguration(), Yaml::parse(file_get_contents($configPath)), $this->getEnvParameters() ); return $definePropel->getConfig(); } /** * Gets the environment parameters. * * Only the parameters starting with "SYMFONY__" are considered. * * @return array An array of parameters */ protected function getEnvParameters() { $parameters = array(); foreach ($_SERVER as $key => $value) { if (0 === strpos($key, 'SYMFONY__')) { $parameters[strtolower(str_replace('__', '.', substr($key, 9)))] = $value; } } return $parameters; } public function isLatestVersion($version = null) { if (null === $version) { $version = $this->getCurrentVersion(); } $lastEntry = end($this->version); return $lastEntry == $version; } public function process() { $this->updatedVersions = array(); $currentVersion = $this->getCurrentVersion(); $this->log('debug', "start update process"); if (true === $this->isLatestVersion($currentVersion)) { $this->log('debug', "You already have the latest version. No update available"); throw new UpToDateException('You already have the latest version. No update available'); } $index = array_search($currentVersion, $this->version); $this->connection->beginTransaction(); $database = new Database($this->connection); $version = null; try { $size = count($this->version); for ($i = ++$index; $i < $size; $i++) { $version = $this->version[$i]; $this->updateToVersion($version, $database); $this->updatedVersions[] = $version; } $currentVersion = Version::parse(); $this->log('debug', sprintf('setting database configuration to %s', $currentVersion['version'])); $updateConfigVersion = [ 'thelia_version' => $currentVersion['version'], 'thelia_major_version' => $currentVersion['major'], 'thelia_minus_version' => $currentVersion['minus'], 'thelia_release_version' => $currentVersion['release'], 'thelia_extra_version' => $currentVersion['extra'], ]; foreach ($updateConfigVersion as $name => $value) { $stmt = $this->connection->prepare('SELECT * FROM `config` WHERE `name` = ?'); $stmt->execute([$name]); if ($stmt->rowCount()) { $stmt = $this->connection->prepare('UPDATE `config` SET `value` = ? WHERE `name` = ?'); $stmt->execute([$version, $value]); } else { $stmt = $this->connection->prepare('INSERT INTO `config` (?) VALUES (?)'); $stmt->execute([$version, $value]); } } $this->connection->commit(); $this->log('debug', 'update successfully'); } catch (\Exception $e) { $this->connection->rollBack(); $this->log('error', sprintf('error during update process with message : %s', $e->getMessage())); $ex = new UpdateException($e->getMessage(), $e->getCode(), $e->getPrevious()); $ex->setVersion($version); throw $ex; } $this->log('debug', 'end of update processing'); return $this->updatedVersions; } /** * Backup current DB to file local/backup/update.sql * @return bool if it succeeds, false otherwise * @throws \Exception */ public function backupDb() { $database = new Database($this->connection); if (! $this->checkBackupIsPossible()) { $message = 'Your database is too big for an automatic backup'; $this->log('error', $message); throw new UpdateException($message); } $this->backupFile = THELIA_ROOT . $this->backupDir . 'update.sql'; $backupDir = THELIA_ROOT . $this->backupDir; $fs = new Filesystem(); try { $this->log('debug', sprintf('Backup database to file : %s', $this->backupFile)); // test if backup dir exists if (!$fs->exists($backupDir)) { $fs->mkdir($backupDir); } if (!is_writable($backupDir)) { throw new \RuntimeException(sprintf('impossible to write in directory : %s', $backupDir)); } // test if backup file already exists if ($fs->exists($this->backupFile)) { // remove file $fs->remove($this->backupFile); } $database->backupDb($this->backupFile); } catch (\Exception $ex) { $this->log('error', sprintf('error during backup process with message : %s', $ex->getMessage())); throw $ex; } } /** * Restores file local/backup/update.sql to current DB * * @return bool if it succeeds, false otherwise */ public function restoreDb() { $database = new Database($this->connection); try { $this->log('debug', sprintf('Restore database with file : %s', $this->backupFile)); if (!file_exists($this->backupFile)) { return false; } $database->restoreDb($this->backupFile); } catch (\Exception $ex) { $this->log('error', sprintf('error during restore process with message : %s', $ex->getMessage())); print $ex->getMessage(); return false; } return true; } /** * @return null|string */ public function getBackupFile() { return $this->backupFile; } public function getLogs() { return $this->logs; } protected function log($level, $message) { if ($this->usePropel) { switch ($level) { case 'debug': $this->logger->debug($message); break; case 'info': $this->logger->info($message); break; case 'notice': $this->logger->notice($message); break; case 'warning': $this->logger->warning($message); break; case 'error': $this->logger->error($message); break; case 'critical': $this->logger->critical($message); break; } } else { $this->logs[] = [$level, $message]; } } protected function updateToVersion($version, Database $database) { // sql update $filename = sprintf( "%s%s%s", THELIA_SETUP_DIRECTORY, str_replace('/', DS, self::SQL_DIR), $version . '.sql' ); if (file_exists($filename)) { $this->log('debug', sprintf('inserting file %s', $version . '.sql')); $database->insertSql(null, [$filename]); $this->log('debug', sprintf('end inserting file %s', $version . '.sql')); } // php update $filename = sprintf( "%s%s%s", THELIA_SETUP_DIRECTORY, str_replace('/', DS, self::PHP_DIR), $version . '.php' ); if (file_exists($filename)) { $this->log('debug', sprintf('executing file %s', $version . '.php')); include_once($filename); $this->log('debug', sprintf('end executing file %s', $version . '.php')); } // instructions $filename = sprintf( "%s%s%s", THELIA_SETUP_DIRECTORY, str_replace('/', DS, self::INSTRUCTION_DIR), $version . '.md' ); if (file_exists($filename)) { $this->addPostInstructions($version, file_get_contents($filename)); } $this->setCurrentVersion($version); } public function getCurrentVersion() { $stmt = $this->connection->query("SELECT `value` FROM `config` WHERE name='thelia_version'"); return $stmt->fetchColumn(); } public function setCurrentVersion($version) { $currentVersion = null; if (null !== $this->connection) { try { $stmt = $this->connection->prepare('UPDATE config set value = ? where name = ?'); $stmt->execute([$version, 'thelia_version']); } catch (PDOException $e) { $this->log('error', sprintf('Error setting current version : %s', $e->getMessage())); throw $e; } } } /** * Returns the database size in Mo * @return float * @throws \Exception */ public function getDataBaseSize() { $stmt = $this->connection->query( "SELECT sum(data_length) / 1024 / 1024 'size' FROM information_schema.TABLES WHERE table_schema = DATABASE() GROUP BY table_schema" ); if ($stmt->rowCount()) { return floatval($stmt->fetch(PDO::FETCH_OBJ)->size); } throw new \Exception('Impossible to calculate the database size'); } /** * Checks whether it is possible to make a data base backup * * @return bool */ public function checkBackupIsPossible() { $size = 0; if (preg_match('/^(\d+)(.)$/', ini_get('memory_limit'), $matches)) { switch (strtolower($matches[2])) { case 'k': $size = $matches[1] / 1024; break; case 'm': $size = $matches[1]; break; case 'g': $size = $matches[1] * 1024; break; } } if ($this->getDataBaseSize() > ($size - 64) / 8) { return false; } return true; } public function getLatestVersion() { return end($this->version); } public function getVersions() { return $this->version; } /** * @return array */ public function getUpdatedVersions() { return $this->updatedVersions; } /** * @param array $updatedVersions */ public function setUpdatedVersions($updatedVersions) { $this->updatedVersions = $updatedVersions; } /** * Add a new post update instruction * * @param string $instructions content of the instruction un markdown format */ protected function addPostInstructions($version, $instructions) { if (!isset($this->postInstructions[$version])) { $this->postInstructions[$version] = []; } $this->postInstructions[$version][] = $instructions; } /** * Return the content of all instructions * * @param string $format the format of the export : plain (default) or html * @return string the instructions in plain text or html */ public function getPostInstructions($format = 'plain') { $content = []; if (count($this->postInstructions) == 0) { return null; } ksort($this->postInstructions); foreach ($this->postInstructions as $version => $instructions) { $content[] = sprintf("## %s", $version); foreach ($instructions as $instruction) { $content[] = sprintf("%s", $instruction); } } $content = implode("\n\n", $content); if ($format === 'html') { $content = Markdown::defaultTransform($content); } return $content; } public function hasPostInstructions() { return (count($this->postInstructions) !== 0); } public function getVersionList() { $list = []; $finder = new Finder(); $path = sprintf("%s%s", THELIA_SETUP_DIRECTORY, str_replace('/', DS, self::SQL_DIR)); $sort = function (\SplFileInfo $a, \SplFileInfo $b) { $a = strtolower(substr($a->getRelativePathname(), 0, -4)); $b = strtolower(substr($b->getRelativePathname(), 0, -4)); return version_compare($a, $b); }; $files = $finder->name('*.sql')->in($path)->sort($sort); foreach ($files as $file) { $list[] = substr($file->getRelativePathname(), 0, -4); } return $list; } /** * @param string $message * @param string $type * @return $this */ public function setMessage($message, $type = 'info') { $this->messages[] = [$message, $type]; return $this; } /** * @return array */ public function getMessages() { return $this->messages; } /** * @param string $string * @return string */ public function trans($string) { return $this->translator ? $this->translator->trans($string) : $string; } /** * @param Translator $translator * @return $this */ public function setTranslator(Translator $translator) { $this->translator = $translator; return $this; } public function getWebVersion() { $url = "http://thelia.net/version.php"; $curl = curl_init($url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5); curl_setopt($curl, CURLOPT_TIMEOUT, 5); $res = curl_exec($curl); try { if (Version::parse($res)) { return trim($res); } } catch (\Exception $e) { return null; } } }