Page Menu
Home
In-Portal Phabricator
Search
Configure Global Search
Log In
Files
F727121
in-portal
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Mon, Jan 6, 7:04 AM
Size
18 KB
Mime Type
text/x-diff
Expires
Wed, Jan 8, 7:04 AM (2 d, 3 h ago)
Engine
blob
Format
Raw Data
Handle
537183
Attached To
rINP In-Portal
in-portal
View Options
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
Log In to Comment