Index: branches/5.2.x/core/units/helpers/deployment_helper.php
===================================================================
--- branches/5.2.x/core/units/helpers/deployment_helper.php	(revision 15116)
+++ branches/5.2.x/core/units/helpers/deployment_helper.php	(revision 15117)
@@ -1,606 +1,606 @@
 <?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 = '';
 
 	public function __construct()
 	{
 		parent::__construct();
 
 		set_time_limit(0);
 		ini_set('memory_limit', -1);
 
 		$this->isCommandLine = isset($GLOBALS['argv']) && count($GLOBALS['argv']);
 
 		if ( !$this->isCommandLine ) {
 			$this->ip = $_SERVER['REMOTE_ADDR'];
 		}
 		elseif ( isset($GLOBALS['argv'][3]) ) {
 			$this->ip = $GLOBALS['argv'][3];
 		}
 	}
 
 	/**
 	 * 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 '</pre>' . PHP_EOL;
 		}
 
 		return $ret;
 	}
 
 	/**
 	 * 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;
 		}
 
 		if ( !$this->dryRun ) {
 			$this->importLanguagePack();
 		}
 
 		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 */
 
 		echo 'Importing LanguagePack ... ';
 		$filename = $this->getModuleFile('english.lang');
 		$language_import_helper->performImport($filename, '|0|1|2|', $this->moduleName, LANG_SKIP_EXISTING);
 		$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)
 		echo 'Resetting Unit Config Cache ... ';
 		$admin_event = new kEvent('adm:OnResetConfigsCache');
 		$this->Application->HandleEvent($admin_event);
 		$this->displayStatus('OK');
 
 		// 3. reset sections cache
 		echo 'Resetting Sections Cache ... ';
 		$admin_event = new kEvent('adm:OnResetSections');
 		$this->Application->HandleEvent($admin_event);
 		$this->displayStatus('OK');
 	}
 
 	/**
 	 * Rebuild theme files
 	 *
 	 * @return void
 	 * @access private
 	 */
 	private function refreshThemes()
 	{
 		echo 'Rebuilding Theme Files ... ';
 		$admin_event = new kEvent('adm:OnRebuildThemes');
 		$this->Application->HandleEvent($admin_event);
 		$this->displayStatus('OK');
 	}
 
 	/**
 	 * Runs database upgrade script
 	 *
 	 * @return bool
 	 * @access private
 	 */
 	private function upgradeDatabase()
 	{
 		$this->loadAppliedRevisions();
 		$this->Conn->errorHandler = Array (&$this, 'handleSqlError');
 
 		echo '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;
 		}
 
 		foreach ($matches as $index => $match) {
 			$revision = $match[1][0];
 
 			if ( $this->revisionApplied($revision) ) {
 				// skip applied revisions
 				continue;
 			}
 
 			if ( isset($this->revisionSqls[$revision]) ) {
 				// duplicate revision among non-applied ones
 				$this->displayStatus('FAILED' . PHP_EOL . 'Duplicate revision #' . $revision . ' found');
 
 				return false;
 			}
 
 			// 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;
 		}
 
 		echo 'Upgrading Database ... ' . PHP_EOL;
 
 		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);
 
 			foreach ($sqls as $sql) {
 				$this->toLog($sql);
 
 				if ( substr($sql, 0, 1) == '#' ) {
 					// output comment as is
 					echo $this->colorText($sql, 'purple') . PHP_EOL;
 					continue;
 				}
 				elseif ( $sql ) {
 					echo mb_substr(trim(preg_replace('/(\n|\t| )+/is', ' ', $sql)), 0, self::SQL_TRIM_LENGTH) . ' ... ';
 
 					$this->Conn->Query($sql);
 
 					if ( $this->Conn->hasError() ) {
 						// consider revisions with errors applied
 						$this->appliedRevisions[] = $revision;
 
 						return false;
 					}
 					else {
-						$this->displayStatus('OK');
+						$this->displayStatus('OK (' . $this->Conn->getAffectedRows() . ')');
 					}
 				}
 			}
 
 			$this->appliedRevisions[] = $revision;
 		}
 
 		echo PHP_EOL;
 
 		return true;
 	}
 
 	/**
 	 * Error handler for sql errors
 	 *
 	 * @param int $code
 	 * @param string $msg
 	 * @param string $sql
 	 * @return bool
 	 * @access public
 	 */
 	public function handleSqlError($code, $msg, $sql)
 	{
 		$this->toLog('SQL Error #' . $code . ': ' . $msg);
 
 		$this->displayStatus('FAILED' . PHP_EOL . 'SQL Error #' . $code . ': ' . $msg);
 		echo 'Please execute rest of SQLs in this Revision by hand and run deployment script again.' . PHP_EOL;
 
 		return true;
 	}
 
 	/**
 	 * 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 . '">' . $text . '</span>';
 	}
 
 	/**
 	 * Makes given text bold
 	 *
 	 * @param string $text
 	 * @return string
 	 * @access private
 	 */
 	private function boldText($text)
 	{
 		if ( $this->isCommandLine ) {
 			return "\033[1m" . $text . "\033[0m";
 		}
 
 		return '<strong>' . $text . '</strong>';
 	}
 
 	/**
 	 * 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;
 		}
 	}
 }
\ No newline at end of file