_event = new kEvent('adm:OnDummy'); $this->isCommandLine = isset($GLOBALS['argv']) && count($GLOBALS['argv']); if ( !$this->isCommandLine ) { $this->ip = $this->Application->getClientIp(); } elseif ( isset($GLOBALS['argv'][3]) ) { $this->ip = $GLOBALS['argv'][3]; } } /** * Sets event, associated with deployment * * @param kEvent $event * @return void * @access public */ public function setEvent(kEvent $event) { $this->_event = $event; } /** * Adds message to script execution log * * @param string $message * @param bool $new_line * @return void * @access private */ private function toLog($message, $new_line = true) { $log_file = (defined('RESTRICTED') ? RESTRICTED : WRITEABLE) . '/project_upgrades.log'; $fp = fopen($log_file, 'a'); fwrite($fp, $message . ($new_line ? "\n" : '')); fclose($fp); chmod($log_file, 0666); } /** * Loads already applied revisions list of current module * * @return void * @access private */ private function loadAppliedRevisions() { $sql = 'SELECT AppliedDBRevisions FROM ' . TABLE_PREFIX . 'Modules WHERE Name = ' . $this->Conn->qstr($this->moduleName); $revisions = $this->Conn->GetOne($sql); $this->appliedRevisions = $revisions ? explode(',', $revisions) : Array (); } /** * Saves applied revision numbers to current module record * * @return void * @access private */ private function saveAppliedRevisions() { // maybe optimize sort($this->appliedRevisions); $fields_hash = Array ( 'AppliedDBRevisions' => implode(',', $this->appliedRevisions), ); $this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'Modules', '`Name` = ' . $this->Conn->qstr($this->moduleName)); } /** * Deploys changes from all installed modules * * @param bool $dry_run * @return bool * @access public */ public function deployAll($dry_run = false) { if ( !$this->isCommandLine ) { echo '
' . PHP_EOL;
		}

		$ret = true;
		$this->dryRun = $dry_run;

		$this->toLog(PHP_EOL . '[' . adodb_date('Y-m-d H:i:s') . '] === ' . $this->ip . ' ===');

		foreach ($this->Application->ModuleInfo as $module_name => $module_info) {
			$this->moduleName = $module_name;

			if ( !file_exists($this->getModuleFile('project_upgrades.sql')) ) {
				continue;
			}

			$ret = $ret && $this->deploy($module_name);
		}

		if ( $ret && !$this->dryRun ) {
			$this->resetCaches();
			$this->refreshThemes();
		}

		if ( !$this->isCommandLine ) {
			echo kUtil::escape($this->_runShellScript());
			echo '
' . PHP_EOL; } return $ret; } /** * Runs user-specific shell script when deployment happens from Web * * @return string * @access protected */ protected function _runShellScript() { if ( !$this->Application->isDebugMode(false) ) { return ''; } $wrapper_script = '/usr/local/bin/guest2host_server.sh'; $script_name = FULL_PATH .'/tools/' . ($this->dryRun ? 'synchronize.sh' : 'deploy.sh'); if ( file_exists($wrapper_script) && file_exists($script_name) ) { $script_name = preg_replace('/^.*\/web/', constant('DBG_LOCAL_BASE_PATH'), $script_name); return shell_exec($wrapper_script . ' ' . $script_name . ' 2>&1'); } return ''; } /** * Deploys pending changes to a site * * @param string $module_name * @return bool * @access private */ private function deploy($module_name) { echo $this->colorText('Deploying Module "' . $module_name . '":', 'cyan', true) . PHP_EOL; if ( !$this->upgradeDatabase() ) { return false; } try { if ( $this->dryRun ) { $this->exportLanguagePack(); } else { $this->importLanguagePack(); } } catch ( Exception $e ) { echo $this->colorText('Failed with Module "' . $module_name . '".', 'red', true) . PHP_EOL . PHP_EOL; return false; } echo $this->colorText('Done with Module "' . $module_name . '".', 'green', true) . PHP_EOL . PHP_EOL; return true; } /** * Import latest languagepack (without overwrite) * * @return void * @access private */ private function importLanguagePack() { $language_import_helper = $this->Application->recallObject('LanguageImportHelper'); /* @var $language_import_helper LanguageImportHelper */ $this->out('Importing LanguagePack ... '); $filename = $this->getModuleFile('english.lang'); $language_import_helper->performImport($filename, '|0|1|2|', $this->moduleName, LANG_SKIP_EXISTING); $this->displayStatus('OK'); } /** * Exports latest language pack * * @return void * @access private */ private function exportLanguagePack() { static $languages = null; if ( !isset($languages) ) { $sql = 'SELECT LanguageId FROM ' . $this->Application->getUnitOption('lang', 'TableName') . ' WHERE Enabled = 1'; $languages = $this->Conn->GetCol($sql); } $language_import_helper = $this->Application->recallObject('LanguageImportHelper'); /* @var $language_import_helper LanguageImportHelper */ $language_import_helper->performExport(EXPORT_PATH . '/' . $this->moduleName . '.lang', '|0|1|2|', $languages, '|' . $this->moduleName . '|'); } /** * Resets unit and section cache * * @return void * @access private */ private function resetCaches() { // 2. reset unit config cache (so new classes get auto-registered) $this->out('Resetting Configs Files Cache and Parsed System Data ... '); $this->_event->CallSubEvent('OnResetConfigsCache'); $this->displayStatus('OK'); // 3. reset sections cache $this->out('Resetting Admin Console Sections ... '); $this->_event->CallSubEvent('OnResetSections'); $this->displayStatus('OK'); // 4. reset mod-rewrite cache $this->out('Resetting ModRewrite Cache ... '); $this->_event->CallSubEvent('OnResetModRwCache'); $this->displayStatus('OK'); } /** * Rebuild theme files * * @return void * @access private */ private function refreshThemes() { $this->out('Refreshing Theme Files ... '); $this->_event->CallSubEvent('OnRebuildThemes'); $this->displayStatus('OK'); } /** * Runs database upgrade script * * @return bool * @access private */ private function upgradeDatabase() { $this->loadAppliedRevisions(); $this->Conn->errorHandler = Array (&$this, 'handleSqlError'); $this->out('Verifying Database Revisions ... '); if ( !$this->collectDatabaseRevisions() || !$this->checkRevisionDependencies() ) { return false; } $this->displayStatus('OK'); $applied = $this->applyRevisions(); $this->saveAppliedRevisions(); return $applied; } /** * Collects database revisions from "project_upgrades.sql" file. * * @return bool * @access private */ private function collectDatabaseRevisions() { $filename = $this->getModuleFile('project_upgrades.sql'); if ( !file_exists($filename) ) { return true; } $sqls = file_get_contents($filename); preg_match_all("/# r([\d]+)([^\:]*):.*?(\n|$)/s", $sqls, $matches, PREG_SET_ORDER + PREG_OFFSET_CAPTURE); if ( !$matches ) { $this->displayStatus('FAILED' . PHP_EOL . 'No Database Revisions Found'); return false; } $revision_numbers = array(); foreach ( $matches as $index => $match ) { $revision = $match[1][0]; if ( in_array($revision, $revision_numbers) ) { $this->displayStatus('FAILED' . PHP_EOL . 'Duplicate revision #' . $revision . ' found'); return false; } $revision_numbers[] = $revision; if ( $this->revisionApplied($revision) ) { // Skip applied revisions. continue; } // Get revision sqls. $start_pos = $match[0][1] + strlen($match[0][0]); $end_pos = isset($matches[$index + 1]) ? $matches[$index + 1][0][1] : strlen($sqls); $revision_sqls = substr($sqls, $start_pos, $end_pos - $start_pos); if ( !$revision_sqls ) { // resision without sqls continue; } $this->revisionTitles[$revision] = trim($match[0][0]); $this->revisionSqls[$revision] = $revision_sqls; $revision_lependencies = $this->parseRevisionDependencies($match[2][0]); if ( $revision_lependencies ) { $this->revisionDependencies[$revision] = $revision_lependencies; } } ksort($this->revisionSqls); ksort($this->revisionDependencies); return true; } /** * Checks that all dependent revisions are either present now OR were applied before * * @return bool * @access private */ private function checkRevisionDependencies() { foreach ($this->revisionDependencies as $revision => $revision_dependencies) { foreach ($revision_dependencies as $revision_dependency) { if ( $this->revisionApplied($revision_dependency) ) { // revision dependend upon already applied -> depencency fulfilled continue; } if ( $revision_dependency >= $revision ) { $this->displayStatus('FAILED' . PHP_EOL . 'Revision #' . $revision . ' has incorrect dependency to revision #' . $revision_dependency . '. Only dependencies to older revisions are allowed!'); return false; } if ( !isset($this->revisionSqls[$revision_dependency]) ) { $this->displayStatus('FAILED' . PHP_EOL . 'Revision #' . $revision . ' depends on missing revision #' . $revision_dependency . '!'); return false; } } } return true; } /** * Runs all pending sqls * * @return bool * @access private */ private function applyRevisions() { if ( !$this->revisionSqls ) { return true; } if ( $this->dryRun ) { $this->appliedRevisions = array_merge($this->appliedRevisions, array_keys($this->revisionSqls)); return true; } $this->out('Upgrading Database ... ', true); foreach ($this->revisionSqls as $revision => $sqls) { echo PHP_EOL . $this->colorText($this->revisionTitles[$revision], 'gray', true) . PHP_EOL; // 'Processing DB Revision: #' . $revision . ' ... '; $sqls = str_replace("\r\n", "\n", $sqls); // convert to linux line endings $no_comment_sqls = preg_replace("/#\s([^;]*?)\n/is", "# \\1;\n", $sqls); // add ";" to each comment end to ensure correct split $sqls = explode(";\n", $no_comment_sqls . "\n"); // ensures that last sql won't have ";" in it $sqls = array_map('trim', $sqls); try { foreach ($sqls as $sql) { if ( substr($sql, 0, 1) == '#' ) { // output comment as is $this->toLog($sql); echo $this->colorText($sql, 'purple') . PHP_EOL; continue; } elseif ( $sql ) { $this->toLog($sql . ' ... ', false); $escaped_sql = $this->isCommandLine ? $sql : kUtil::escape($sql); echo mb_substr(trim(preg_replace('/(\n|\t| )+/is', ' ', $escaped_sql)), 0, self::SQL_TRIM_LENGTH) . ' ... '; $this->Conn->Query($sql); $this->toLog('OK (' . $this->Conn->getAffectedRows() . ')'); $this->displayStatus('OK (' . $this->Conn->getAffectedRows() . ')'); } } } catch (Exception $e) { // consider revisions with errors applied $this->appliedRevisions[] = $revision; return false; } $this->appliedRevisions[] = $revision; } echo PHP_EOL; return true; } /** * Error handler for sql errors. * * @param integer $code Error code. * @param string $msg Error message. * @param string $sql SQL query. * * @return void * @throws Exception When SQL error happens. */ public function handleSqlError($code, $msg, $sql) { $this->toLog('FAILED' . PHP_EOL . 'SQL Error #' . $code . ': ' . $msg); $this->displayStatus('FAILED' . PHP_EOL . 'SQL Error #' . $code . ': ' . $msg); $this->out('Please execute rest of SQLs in this Revision by hand and run deployment script again.', true); throw new Exception($msg, $code); } /** * Checks if given revision was already applied * * @param int $revision * @return bool * @access private */ private function revisionApplied($revision) { foreach ($this->appliedRevisions as $applied_revision) { // revision range $applied_revision = explode('-', $applied_revision, 2); if ( !isset($applied_revision[1]) ) { // convert single revision to revision range $applied_revision[1] = $applied_revision[0]; } if ( $revision >= $applied_revision[0] && $revision <= $applied_revision[1] ) { return true; } } return false; } /** * Returns path to given file in current module install folder * * @param string $filename * @return string * @access private */ private function getModuleFile($filename) { $module_folder = $this->Application->findModule('Name', $this->moduleName, 'Path'); return FULL_PATH . DIRECTORY_SEPARATOR . $module_folder . 'install/' . $filename; } /** * Extracts revisions from string in format "(1,3,5464,23342,3243)" * * @param string $string * @return Array * @access private */ private function parseRevisionDependencies($string) { if ( !$string ) { return Array (); } $string = explode(',', substr($string, 1, -1)); return array_map('trim', $string); } /** * Applies requested color and bold attributes to given text string * * @param string $text * @param string $color * @param bool $bold * @return string * @access private */ private function colorText($text, $color, $bold = false) { if ( $this->isCommandLine ) { $color_map = Array ( 'black' => 30, // dark gray (in bold) 'blue' => 34, // light blue (in bold) 'green' => 32, // light green (in bold) 'cyan' => 36, // light cyan (in bold) 'red' => 31, // light red (in bold) 'purple' => 35, // light purple (in bold) 'brown' => 33, // yellow (in bold) 'gray' => 37, // white (in bold) ); return "\033[" . ($bold ? 1 : 0) . ";" . $color_map[$color] . "m" . $text . "\033[0m"; } $html_color_map = Array ( 'black' => Array ('normal' => '#000000', 'bold' => '#666666'), 'blue' => Array ('normal' => '#00009C', 'bold' => '#3C3CFF'), 'green' => Array ('normal' => '#009000', 'bold' => '#00FF00'), 'cyan' => Array ('normal' => '#009C9C', 'bold' => '#00FFFF'), 'red' => Array ('normal' => '#9C0000', 'bold' => '#FF0000'), 'purple' => Array ('normal' => '#900090', 'bold' => '#F99CF9'), 'brown' => Array ('normal' => '#C9C909', 'bold' => '#FFFF00'), 'gray' => Array ('normal' => '#909090', 'bold' => '#FFFFFF'), ); $html_color = $html_color_map[$color][$bold ? 'bold' : 'normal']; return '' . kUtil::escape($text, kUtil::ESCAPE_HTML) . ''; } /** * Displays last command execution status * * @param string $status_text * @param bool $new_line * @return void * @access private */ private function displayStatus($status_text, $new_line = true) { $color = substr($status_text, 0, 2) == 'OK' ? 'green' : 'red'; echo $this->colorText($status_text, $color, false); if ( $new_line ) { echo PHP_EOL; } } /** * Outputs a text and escapes it if necessary * * @param string $text * @param bool $new_line * @return void */ private function out($text, $new_line = false) { if ( !$this->isCommandLine ) { $text = kUtil::escape($text); } echo $text . ($new_line ? PHP_EOL : ''); } }