Page MenuHomeIn-Portal Phabricator

in-portal
No OneTemporary

File Metadata

Created
Mon, Jan 6, 7:04 AM

in-portal

Index: branches/5.2.x/core/units/helpers/deployment_helper.php
===================================================================
--- branches/5.2.x/core/units/helpers/deployment_helper.php (revision 16571)
+++ branches/5.2.x/core/units/helpers/deployment_helper.php (revision 16572)
@@ -1,692 +1,708 @@
<?php
/**
* @version $Id$
* @package In-Portal
* @copyright Copyright (C) 1997 - 2011 Intechnic. All rights reserved.
* @license GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/
defined('FULL_PATH') or die('restricted access!');
class DeploymentHelper extends kHelper {
/**
* How many symbols from sql should be shown
*/
const SQL_TRIM_LENGTH = 120;
/**
* Name of module, that is processed right now
*
* @var string
* @access private
*/
private $moduleName = '';
/**
* List of sqls, associated with each revision (from project_upgrades.sql file)
*
* @var Array
* @access private
*/
private $revisionSqls = Array ();
/**
* List of revision titles as user typed them (from project_upgrades.sql file)
* @var Array
*/
private $revisionTitles = Array ();
/**
* Revision dependencies
*
* @var Array
* @access private
*/
private $revisionDependencies = Array ();
/**
* Numbers of revisions, that were already applied
*
* @var Array
* @access private
*/
private $appliedRevisions = Array ();
/**
* Don't change database, but only check syntax of project_upgrades.sql file and mark all revisions discovered as applied
*
* @var bool
* @access private
*/
private $dryRun = false;
/**
* Remembers script invocation method
*
* @var bool
* @access public
*/
public $isCommandLine = false;
/**
* IP Address of script invoker
*
* @var string
*/
public $ip = '';
/**
* Event, that triggered deployment
*
* @var kEvent
* @access private
*/
private $_event;
public function __construct()
{
parent::__construct();
kUtil::setResourceLimit();
$this->_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 '<pre style="font-size: 10pt; color: #BBB; background-color: black; border: 2px solid darkgreen; padding: 8px;">' . 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 '</pre>' . 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()
{
/** @var LanguageImportHelper $language_import_helper */
$language_import_helper = $this->Application->recallObject('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);
}
/** @var LanguageImportHelper $language_import_helper */
$language_import_helper = $this->Application->recallObject('LanguageImportHelper');
- $language_import_helper->performExport(EXPORT_PATH . '/' . $this->moduleName . '.lang', '|0|1|2|', $languages, '|' . $this->moduleName . '|');
+ $this->out('Exporting LanguagePack ... ');
+ $language_import_helper->performExport(
+ EXPORT_PATH . '/' . $this->moduleName . '.lang',
+ '|0|1|2|',
+ $languages,
+ '|' . $this->moduleName . '|'
+ );
+ $this->displayStatus('OK');
}
/**
* 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->out('Simulating Database Upgrade ... ', true);
+
+ foreach ( $this->revisionSqls as $revision => $sqls ) {
+ echo PHP_EOL . $this->colorText($this->revisionTitles[$revision], 'gray', true) . PHP_EOL;
+ echo '...' . PHP_EOL;
+ }
+
+ echo PHP_EOL;
+
$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 '<span style="color: ' . $html_color . '">' . kUtil::escape($text, kUtil::ESCAPE_HTML) . '</span>';
}
/**
* 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 : '');
}
}

Event Timeline