. * @author niel * @copyright 2014 nZEDb */ namespace Blacklight\db; use App\Models\Settings; use Blacklight\ColorCLI; use Blacklight\utility\Git; use Blacklight\utility\Utility; class DbUpdate { public $backedup; /** * @var \Blacklight\db\DB */ public $pdo; /** * @var mixed */ public $git; /** * @var object Instance variable for logging object. Currently only ColorCLI supported, * but expanding for full logging with agnostic API planned. */ public $log; /** * @var */ public $settings; /** * @var string */ protected $_DbSystem; /** * @var bool Has the Db been backed up? */ private $backedUp = false; /** * @var bool Should we perform a backup? */ private $backup; /** * DbUpdate constructor. * * @param array $options * @throws \Exception */ public function __construct(array $options = []) { $options += [ 'backup' => true, 'db' => null, 'git' => new Git(), 'logger' => new ColorCLI(), ]; $this->backup = $options['backup']; $this->git = $options['git']; $this->log = $options['logger']; // Must be DB not Settings because the Settings table may not exist yet. $this->pdo = (($options['db'] instanceof DB) ? $options['db'] : new DB()); $this->_DbSystem = strtolower($this->pdo->DbSystem()); } /** * Takes new files in the correct format from the patches directory and turns them into proper patches. * * The files should be name as '+x~.sql' where x is a number starting at 1 for your first * patch.
should be the name of the primary table affected. If you have to modify more * than one table, consider splitting into multiple patches using different patch modifier * numbers to order them. i.e. +1~settings.sql, +2~predb.sql, etc. * * @param array $options * * @throws \Exception */ public function newPatches(array $options = []): void { $defaults = [ 'ext' => 'sql', 'path' => NN_RES.'db'.DS.'patches'.DS.$this->_DbSystem, 'regex' => '#^'.Utility::PATH_REGEX.'\+(?P\d+)~(?P
\w+)\.sql$#', 'safe' => true, ]; $options += $defaults; $this->processPatches(['safe' => $options['safe']]); // Make sure we are completely up to date! ColorCLI::doEcho(ColorCLI::primaryOver('Looking for new patches...')); $files = Utility::getDirFiles($options); $count = \count($files); ColorCLI::doEcho(ColorCLI::header(" $count found"), true); if ($count > 0) { ColorCLI::doEcho(ColorCLI::header('Processing...'), true); natsort($files); $local = $this->pdo->isLocalDb() ? '' : 'LOCAL '; foreach ($files as $file) { if (! preg_match($options['regex'], $file, $matches)) { ColorCLI::error("$file does not match the pattern {$options['regex']}\nPlease fix this before continuing"); } else { ColorCLI::doEcho(ColorCLI::header('Processing patch file: '.$file), true); $this->splitSQL($file, ['local' => $local]); $current = Settings::settingValue('..sqlpatch'); $current++; Settings::query()->where('setting', '=', 'sqlpatch')->update(['value' => $current]); $newName = $matches['drive'].$matches['path']. str_pad($current, 4, '0', STR_PAD_LEFT).'~'. $matches['table'].'.sql'; rename($matches[0], $newName); $this->git->add($newName); if ($this->git->isCommited($this->git->getBranch().':'.str_replace(NN_ROOT, '', $matches[0]))) { $this->git->add(" -u {$matches[0]}"); // remove old filename from the index. } } } } } /** * @param array $options * * @return int * @throws \RuntimeException * @throws \Exception */ public function processPatches(array $options = []): int { $patched = 0; $defaults = [ 'ext' => 'sql', 'path' => NN_RES.'db'.DS.'patches'.DS.$this->_DbSystem, 'regex' => '#^'.Utility::PATH_REGEX.'(?P\d{4})~(?P
\w+)\.sql$#', 'safe' => true, ]; $options += $defaults; $currentVersion = Settings::settingValue('..sqlpatch'); if (! is_numeric($currentVersion)) { exit("Bad sqlpatch value: '$currentVersion'\n"); } $files = empty($options['files']) ? Utility::getDirFiles($options) : $options['files']; if (\count($files)) { natsort($files); $local = $this->pdo->isLocalDb() ? '' : 'LOCAL '; ColorCLI::doEcho(ColorCLI::primary('Looking for unprocessed patches...'), true); foreach ($files as $file) { $setPatch = false; $fp = fopen($file, 'r'); $patch = fread($fp, filesize($file)); if (preg_match($options['regex'], str_replace('\\', '/', $file), $matches)) { $patch = (int) $matches['patch']; $setPatch = true; } elseif (preg_match( '/UPDATE `?site`? SET `?value`? = \'?(?P\d+)\'? WHERE `?setting`? = \'sqlpatch\'/i', $patch, $matches ) ) { $patch = (int) $matches['patch']; } else { throw new \RuntimeException('No patch information available, stopping!!'); } if ($patch > $currentVersion) { ColorCLI::doEcho(ColorCLI::header('Processing patch file: '.$file), true); if (! $this->backedUp && $options['safe']) { $this->_backupDb(); } $this->splitSQL($file, ['local' => $local]); if ($setPatch) { Settings::query()->where('setting', '=', 'sqlpatch')->update(['value' => $patch]); } $patched++; } } } else { exit(ColorCLI::error("\nHave you changed the path to the patches folder, or do you have the right permissions?\n")); } if ($patched === 0) { ColorCLI::doEcho(ColorCLI::info("Nothing to patch, you are already on version $currentVersion"), true); } return $patched; } /** * @param $file * @param array $options */ public function splitSQL($file, array $options = []): void { $defaults = [ 'delimiter' => ';', 'local' => null, ]; $options += $defaults; if (! empty($options['vars'])) { extract($options['vars'], 'EXTR_OVERWRITE'); } set_time_limit(0); if (is_file($file)) { $file = fopen($file, 'r, b'); if (\is_resource($file)) { $query = []; $delimiter = $options['delimiter']; while (! feof($file)) { $line = fgets($file); if ($line === false) { continue; } // Skip comments. if (preg_match('!^\s*(#|--|//)\s*(.+?)\s*$!', $line, $matches)) { echo ColorCLI::info('COMMENT: '.$matches[2]); continue; } // Check for non default delimiters ($$ for example). if (preg_match('#^\s*DELIMITER\s+(?P.+)\s*$#i', $line, $matches)) { $delimiter = $matches['delimiter']; if ($delimiter !== $options['delimiter']) { continue; } } // Check if the line has delimiter that is non default ($$ for example). if ($delimiter !== $options['delimiter'] && preg_match('#^(.+?)'.preg_quote($delimiter).'\s*$#', $line, $matches)) { // Check if the line has also the default delimiter (;), remove it. if (preg_match('#^(.+?)'.preg_quote($options['delimiter']).'\s*$#', $matches[1], $matches2)) { $matches[1] = $matches2[1]; } // Change the non default delimiter ($$) to the default one(;). $line = $matches[1].$options['delimiter']; } $query[] = $line; if (preg_match('~'.preg_quote($delimiter, '~').'\s*$~iS', $line) === 1) { $query = trim(implode('', $query)); if ($options['local'] !== null) { $query = str_replace('{:local:}', $options['local'], $query); } try { $qry = $this->pdo->Prepare($query); $qry->execute(); ColorCLI::doEcho(ColorCLI::alternateOver('SUCCESS: ').ColorCLI::primary($query), true); } catch (\PDOException $e) { // Log the problem and the query. file_put_contents( NN_LOGS.'patcherrors.log', '['.date('r').'] [ERROR] ['. trim(preg_replace('/\s+/', ' ', $e->getMessage())).']'.PHP_EOL. '['.date('r').'] [QUERY] ['. trim(preg_replace('/\s+/', ' ', $query)).']'.PHP_EOL, FILE_APPEND ); if ( \in_array($e->errorInfo[1], [1091, 1060, 1061, 1071, 1146], false) || \in_array($e->errorInfo[0], [23505, 42701, 42703, '42P07', '42P16'], false) ) { if ($e->errorInfo[1] === 1060) { ColorCLI::doEcho(ColorCLI::warning( "$query The column already exists - No need to worry \{". $e->errorInfo[1]."}.\n" ), true); } else { ColorCLI::doEcho(ColorCLI::warning( "$query Skipped - No need to worry \{". $e->errorInfo[1]."}.\n" ), true); } } else { if (preg_match('/ALTER IGNORE/i', $query)) { $this->pdo->queryExec('SET SESSION old_alter_table = 1'); try { $this->pdo->exec($query); ColorCLI::doEcho(ColorCLI::alternateOver('SUCCESS: ').ColorCLI::primary($query)); } catch (\PDOException $e) { exit(ColorCLI::error("$query Failed \{".$e->errorInfo[1]."}\n\t".$e->errorInfo[2])); } } else { exit(ColorCLI::error("$query Failed \{".$e->errorInfo[1]."}\n\t".$e->errorInfo[2])); } } } while (ob_get_level() > 0) { ob_end_flush(); } flush(); } if (\is_string($query) === true) { $query = []; } } } } } protected function _backupDb(): void { $PHP = 'php'; system("$PHP ".NN_MISC.'testing'.DS.'DB'.DS.$this->_DbSystem. 'dump_tables.php db dump'); $this->backedup = true; } }