Index: install/install_data.sql =================================================================== --- install/install_data.sql (revision 14318) +++ install/install_data.sql (working copy) @@ -1,5 +1,3 @@ -# place here only sql queries, that were executed on live AND dev sites !!! - INSERT INTO Permissions VALUES(DEFAULT, 'custom.view', 11, 1, 1, 0); INSERT INTO Permissions VALUES(DEFAULT, 'custom:widgets.delete', 11, 1, 1, 0); INSERT INTO Permissions VALUES(DEFAULT, 'custom:widgets.edit', 11, 1, 1, 0); @@ -7,13 +5,3 @@ INSERT INTO Permissions VALUES(DEFAULT, 'custom:widgets.view', 11, 1, 1, 0); INSERT INTO Modules VALUES ('Custom', 'modules/custom/', 'custom-sections', DEFAULT, 1, 10, 'custom/', 0, NULL); - - -# ===== SQLs above this line already on LIVE ================================================================================================ - -# place here only sql queries, that were executed on dev site !!! - - -# ===== SQLs above this line already on DEV ======================================================================================================== - -# place here only sql queries, that were executed on prod server !!! Index: install/install_schema.sql =================================================================== --- install/install_schema.sql (revision 14318) +++ install/install_schema.sql (working copy) @@ -1,5 +1,3 @@ -# place here only sql queries, that were executed on live AND dev sites !!! - CREATE TABLE Widgets ( WidgetId int(11) NOT NULL AUTO_INCREMENT, Title varchar(255) NOT NULL DEFAULT '', @@ -17,11 +15,4 @@ PRIMARY KEY (WidgetId) ); -# ===== SQLs above this line already on LIVE ================================================================================================ - -# place here only sql queries, that were executed on dev site !!! - - -# ===== SQLs above this line already on DEV ======================================================================================================== - -# place here only sql queries, that were executed on prod server !!! +ALTER TABLE Modules ADD AppliedDBRevisions TEXT NULL; Index: install/project_upgrades.sql =================================================================== --- install/project_upgrades.sql (revision 0) +++ install/project_upgrades.sql (revision 0) @@ -0,0 +1,14 @@ +# r1: task for revision 1 (#435345) +# строка один +# строка два +# r3: task for revision 2 (#435345) +# ла-ла +# r2: task for revision 2 (#435345) +ALTER TABLE MISSING; +SELECT * +FROM bf_Modules; +# r5(4): task for revision 2 (#435345) +SELECT * +FROM bf_Modules; +# r4(3,1): task for revision 2 (#435345) +# строка три \ No newline at end of file Index: install/upgrades.sql =================================================================== --- install/upgrades.sql (revision 14318) +++ install/upgrades.sql (working copy) @@ -42,4 +42,7 @@ # ===== v 1.1.2-RC1 ===== -# ===== v 1.1.2 ===== \ No newline at end of file +# ===== v 1.1.2 ===== + +# ===== v 1.1.3-B1 ===== +ALTER TABLE Modules ADD AppliedDBRevisions TEXT NULL; Index: units/sections/custom_eh.php =================================================================== --- units/sections/custom_eh.php (revision 14318) +++ units/sections/custom_eh.php (working copy) @@ -9,6 +9,21 @@ class CustomEventHandler extends kEventHandler { /** + * Allows to override standart permission mapping + * + */ + function mapPermissions() + { + parent::mapPermissions(); + + $permissions = Array ( + 'OnDeploy' => Array ('self' => 'debug'), + ); + + $this->permMapping = array_merge($this->permMapping, $permissions); + } + + /** * Connection to database * * @var kDBConnection @@ -47,4 +62,377 @@ $this->Application->setUnitOption($event->MasterEvent->Prefix, 'Fields', $fields); } + /** + * Deploy changes + * + * Usage: "php tools/run_event.php custom-sections:OnDeploy b674006f3edb1d9cd4d838c150b0567d" + * + * @param kEvent $event + */ + function OnDeploy(&$event) + { + $event->status = erSTOP; + + $deployment_helper = new DeploymentHelper($event->Prefix); + $deployment_helper->deploy(); + } + } + + + class DeploymentHelper extends kHelper { + + private $unitPrefix = ''; + + private $revisionSqls = Array (); + + private $revisionDependencies = Array (); + + private $appliedRevisions = Array (); + + public function DeploymentHelper($prefix) + { + parent::kHelper(); + + $this->unitPrefix = $prefix; + + set_time_limit(0); + ini_set('memory_limit', -1); + + $this->loadAppliedRevisions(); + } + + private function loadAppliedRevisions() + { + $sql = 'SELECT AppliedDBRevisions + FROM ' . TABLE_PREFIX . 'Modules + WHERE Name = ' . $this->Conn->qstr( $this->getModuleName() ); + $revisions = $this->Conn->GetOne($sql); + + $this->appliedRevisions = $revisions ? explode(',', $revisions) : Array (); + } + + 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->getModuleName())); + } + + /** + * Deploys pending changes to a site + * + */ + public function deploy() + { + echo 'Upgrading Database ... '; + if ( !$this->upgradeDatabase() ) { + return ; + } + + echo 'OK' . PHP_EOL; + + $this->importLanguagePack(); + $this->resetCaches(); + $this->refreshThemes(); + + echo 'Deployed' . PHP_EOL; + } + + /** + * Import latest languagepack (without overwrite) + * + */ + 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|', 'Custom', LANG_SKIP_EXISTING); + echo 'OK' . PHP_EOL; + } + + /** + * Resets unit and section cache + * + */ + 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); + echo 'OK' . PHP_EOL; + + // 3. reset sections cache + echo 'Resetting Sections Cache ... '; + $admin_event = new kEvent('adm:OnResetSections'); + $this->Application->HandleEvent($admin_event); + echo 'OK' . PHP_EOL; + } + + /** + * rebuild theme files + * + */ + private function refreshThemes() + { + echo 'Rebuilding Theme Files ... '; + $admin_event = new kEvent('adm:OnRebuildThemes'); + $this->Application->HandleEvent($admin_event); + echo 'OK' . PHP_EOL; + } + + /** + * Runs database upgrade script + * + * @return bool + */ + private function upgradeDatabase() + { + $this->Conn->errorHandler = Array(&$this, 'handleSqlError'); + + if ( !$this->collectDatabaseRevisions() || !$this->checkRevisionDependencies() ) { + return false; + } + + $applied = $this->applyRevisions(); + $this->saveAppliedRevisions(); + + return $applied; + } + + /** + * Collects database revisions from "project_upgrades.sql" file. + * + * @return bool + */ + 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) { + echo 'No Database Revisions Found' . PHP_EOL; + + 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 + echo 'Duplicate revision ' . $revision . ' found' . PHP_EOL; + + 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->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 + */ + 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) { + echo 'Revision ' . $revision . ' has incorrect dependency to revision ' . $revision_dependency . '. Only dependencies to older revisions are allowed!' . PHP_EOL; + + return false; + } + + if ( !isset($this->revisionSqls[$revision_dependency]) ) { + echo 'Revision ' . $revision . ' depends on missing revision ' . $revision_dependency . '!' . PHP_EOL; + + return false; + } + } + } + + return true; + } + + /** + * Runs all pending sqls + * + * @return bool + */ + private function applyRevisions() + { + if (!$this->revisionSqls) { + return true; + } + + echo PHP_EOL; + + foreach ($this->revisionSqls as $revision => $sqls) { + echo 'Processing DB Revision: #' . $revision . ' ... '; + + $sqls = str_replace("\r\n", "\n", $sqls); // convert to linux line endings + $no_comment_sqls = preg_replace("/#\s([^;]*?)\n/is", '', $sqls); // remove all comments "#" on new lines + + $sqls = explode(";\n", $no_comment_sqls . "\n"); // ensures that last sql won't have ";" in it + $sqls = array_map('trim', $sqls); + + foreach ($sqls as $index => $sql) { + if (!$sql || (substr($sql, 0, 1) == '#')) { + continue; // usually last line + } + + $this->Conn->Query($sql); + + if ( $this->Conn->hasError() ) { + // consider revisions with errors applied + $this->appliedRevisions[] = $revision; + + return false; + } + } + + $this->appliedRevisions[] = $revision; + echo 'OK' . PHP_EOL; + } + + return true; + } + + /** + * Error handler for sql errors + * + * @param int $code + * @param string $msg + * @param string $sql + * @return bool + */ + public function handleSqlError($code, $msg, $sql) + { + echo 'Error (#' . $code . ': ' . $msg . ') during SQL processing:' . PHP_EOL . $sql . PHP_EOL; + 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 + */ + 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 + */ + private function getModuleFile($filename) + { + static $module_folder = null; + + if ( !isset($module_folder) ) { + $module_folder = $this->Application->findModule('Name', $this->getModuleName(), 'Path'); + } + + return FULL_PATH . DIRECTORY_SEPARATOR . $module_folder . 'install/' . $filename; + } + + /** + * Returns module name, where this class is used + * + * @return string + */ + private function getModuleName() + { + static $module_name = null; + + if ( !isset($module_name) ) { + $module_folder = $this->Application->getUnitOption($this->unitPrefix, 'ModuleFolder'); + $module_name = $this->Application->findModule('Path', $module_folder . '/', 'Name'); + } + + return $module_name; + } + + /** + * Extracts revisions from string in format "(1,3,5464,23342,3243)" + * + * @param string $string + * @return Array + */ + private function parseRevisionDependencies($string) + { + if (!$string) { + return Array (); + } + + $string = explode(',', substr($string, 1, -1)); + + return array_map('trim', $string); + } } \ No newline at end of file