/** * Imports the SQL statements from the db_sync.sql file, or from the file provided through file upload. * This function can only be called from the Symphony backend, the CDI extension must be enabled and running in Database Synchronisation mode. */ public static function import() { // We should not be processing any queries when the extension is disabled or when it is in 'Continuous Database Integration' mode if((!class_exists('Administration')) || !CdiUtil::isEnabled() || !CdiUtil::isCdiDBSyncSlave()) { throw new Exception("You can only import the Database Synchroniser file from the Preferences page. The CDI extension must be enabled and should be a 'Slave' instance in 'Database Synchronisation' mode."); } // Prevent the CdiLogQuery::log() from persisting queries that are executed by CDI itself // This should not be possible anyway because we can only import in "Slave" mode, but just to be sure! CdiLogQuery::isUpdating(true); // Handle file upload $syncFile = CDI_DB_SYNC_FILE; if(!empty($_FILES['cdi_import_file']['tmp_name'])) { $syncFile = $_FILES['cdi_import_file']['tmp_name']; } //Execute the queries from file try { if(file_exists($syncFile)) { $contents = file_get_contents($syncFile); $queries = explode(';',$contents); foreach($queries as $query) { $query = trim($query); // ommit comments and empty statements if(!preg_match('/^--/i', $query) && !$query=='') { Symphony::Database()->query($query); } } if(isset($_POST['settings']['cdi']['deleteSyncFile'])) { unlink($syncFile); } } } catch (Exception $e) { // Re-enable CdiLogQuery::log() to persist queries CdiLogQuery::isUpdating(false); throw $e; } // Save the last update date to configuration Symphony::Configuration()->set('last-update', time(), 'cdi'); Symphony::Configuration()->write(); // Re-enable CdiLogQuery::log() to persist queries CdiLogQuery::isUpdating(false); }
/** * The rollback() function removes the query either from file (MASTER) or from the CDI log (SLAVE) * @param String $hash The MD5 hash created using the SQL statement, the timestamp and the execution order * @param Timestamp $timestamp The UNIX timestamp on which the query was originally logged * @param Integer $order The execution order of the query (in case of multiple executions with the same timestamp) */ public static function rollback($hash,$timestamp,$order) { // do not rollback erronous changes to tbl_cdi_log $tbl_prefix = Symphony::Configuration()->get('tbl_prefix', 'database'); if (preg_match("/{$tbl_prefix}cdi_log/i", $query)) return true; try { if(CdiUtil::isCdiSlave()) { // On the SLAVE instance we need to remove the execution log entry from the database Symphony::Database()->query("DELETE FROM `tbl_cdi_log` WHERE `query_hash` LIKE '" . $hash . "'"); } else if(CdiUtil::isCdiMaster()) { // On the MASTER instance we need to remove the persisted SQL Statement from disk (if it exists) $entries = self::getCdiLogEntries(); unset($entries[$hash]); file_put_contents(CDI_FILE, json_encode($entries)); } else { throw new Exception("Invalid value for the CDI 'mode' configuration option, 'CdiMaster' or 'CdiSlave' expected."); } } catch(Exception $e) { //TODO: think of some smart way of dealing with errors, perhaps through the preference screen or a CDI Status content page? //In this case it is perhaps better to simply throw the exception because the rollback failed. throw $e; } }
public static function restore() { // We should only backup the database when the extension is enabled and version 1.09 of the Dump_DB extension is installed. if((!class_exists('Administration')) || !CdiUtil::isEnabled()) { throw new Exception("You can only restore the Symphony database from the Preferences page"); } if(!CdiUtil::hasDumpDBInstalled()) { throw new Exception('No valid version of <a href="http://symphony-cms.com/download/extensions/view/40986/">Dump DB</a> found. Please make sure it is installed.'); } else { require_once(EXTENSIONS . '/dump_db/lib/class.mysqlrestore.php'); // Prevent the CdiLogQuery::log() from persisting queries that are executed by CDI itself CdiLogQuery::isUpdating(true); // COPIED FROM Dump_DB version 1.09 // Adjust to only support FULL database dump $restore = new MySQLRestore(Symphony::Database()); $filename = CDI_BACKUP_ROOT . '/' . $_POST["ref"]; if(isset($_FILES['dumpdb_restore_file'])) { $filename = self::getFileName('manual'); rename($_FILES['dumpdb_restore_file']['tmp_name'],$filename); } if(file_exists($filename)) { $data = file_get_contents($filename); $data = str_replace('tbl_', Symphony::Configuration()->get('tbl_prefix', 'database'), $data); $restore->import($data); } else { throw new Exception("The provided restore file '" . $filename . "' could not be found."); } // Re-enable CdiLogQuery::log() to persist queries CdiLogQuery::isUpdating(false); } }
if((!class_exists('Administration')) || !CdiUtil::isEnabled()) { $result["status"] = "error"; $result["message"] = "You can only execute actions from within Symphony and when the CDI extension is enabled"; Symphony::Log()->pushToLog('[CDI] You can only execute actions from within Symphony and when the CDI extension is enabled', E_NOTICE, true); } // Clean the database and log files when the cdi_clear action is called if(isset($_POST["action"]["cdi_clear"])) { try { if(CdiUtil::isCdiMaster()) { CdiMaster::uninstall(); CdiMaster::install(); } else if (CdiUtil::isCdiSlave()) { CdiSlave::uninstall(); CdiSlave::install(); } else if(CdiUtil::isCdiDBSync()) { CdiDBSync::uninstall(); CdiDBSync::install(); } $result["status"] = 'success'; } catch(Exception $e) { $result["status"] = "error"; $result["message"] = $e->getMessage(); Symphony::Log()->pushToLog('[CDI] ' . $e->getMessage(), E_ERROR, true); } } // Clean the database backup list when cdi_clear_restore action is called else if(isset($_POST["action"]["cdi_clear_restore"])) { try { CdiDumpDB::uninstall();
public function savePreferences($context){ if(CdiPreferences::save()) { // apply config changes if(CdiUtil::isCdiSlave()) { CdiSlave::install(); } else if (CdiUtil::isCdiMaster()) { CdiMaster::install(); } else { CdiDBSync::install(); } } else { Administration::instance()->Page->pageAlert(_('An unknown error occurred while saving preferences for CDI. Your changes have not been saved.')); return false; } }
<?php // We should only allow download of the database from the administration interface when the extension is enabled. if((!class_exists('Administration')) || !Administration::instance()->isLoggedIn() || !CdiUtil::isEnabled()) { throw new Exception("You can only download CDI content from the Preferences page"); } require_once(EXTENSIONS . '/cdi/lib/class.cdiutil.php'); $filename = $_REQUEST["ref"]; if($filename == CDI_FILENAME || $filename == CDI_DB_SYNC_FILENAME) { $file = CDIROOT . '/' . $filename; } else { $file = CDI_BACKUP_ROOT . '/' . $filename; } if(file_exists($file)) { $data = file_get_contents($file); header("Pragma: public"); header("Expires: 0"); header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); header("Content-Type: application/octet-stream"); header("Content-Transfer-Encoding: binary"); header("Content-Disposition: attachment; filename=" . $filename); echo $data; die(); } else { throw new Exception("The provided backup file '" . $file . "' could not be found.");
public static function appendClearLog() { $div = new XMLElement('div',NULL,array('class' => 'cdiClear')); $div->appendChild(new XMLElement('h3','Clear Log Entries',array('style' => 'margin-bottom: 5px;'))); $button = new XMLElement('div',NULL,array('style' => 'margin: 10px 0;')); $button->appendChild(new XMLElement('input', null, array('value' => 'Clear', 'name' => 'action[cdi_clear]', 'type' => 'button', 'class' => 'cdi_clear_action'))); if(CdiUtil::isCdi()) { $button->appendChild(new XMLElement('span',' Press "Clear" to remove all CDI log entries from disk and/or Symphony Database')); $div->appendChild($button); $div->appendChild(new XMLElement('p', 'You can use the "Clear" button to clean up old CDI logs. Ensure that all your Symphony have been updated either by CDI (check the last executed queries list above) or by manually restoring the same database backup on all instances. Make sure that you clear the log files on every instance (including the "Master" instance). It is important that the database schemas are synchronized before starting with a clean sheet.', array('class' => 'help'))); } else { $button->appendChild(new XMLElement('span',' Press "Clear" to remove <em>' . CDI_DB_SYNC_FILENAME . '</em> from disk')); $div->appendChild($button); $div->appendChild(new XMLElement('p', 'You can use the "Clear" button to remove current <em>' . CDI_DB_SYNC_FILENAME . '</em> file. Ensure that all your Symphony have been updated either by CDI or by manually restoring the same database backup on all instances. Make sure that you clear the <em>' . CDI_DB_SYNC_FILENAME . '</em> files on every instance (including the "Master" instance). It is important that the database schemas are synchronized before starting with a clean sheet.', array('class' => 'help'))); } return $div; }
<?php require_once(EXTENSIONS . '/cdi/lib/class.cdiutil.php'); require_once(EXTENSIONS . '/cdi/lib/class.cdislave.php'); // We should not be processing any queries when the extension is disabled or when we are the Master instance if((!class_exists('Administration')) || !CdiUtil::isEnabled() || (CdiUtil::isCdiMaster() || CdiUtil::isCdiDBSync())) { echo "WARNING: You are not calling this page from Symphony, the CDI extension is disabled or you are running the queryies on the Master instance. No queries have been executed."; } else { $callback = Administration::getPageCallback(); if(Symphony::Configuration()->get('api_key','cdi') !== $callback['context'][0]){ echo "WARNING: Invalid API key. The correct key can be found in the configuration page."; die(); } else{ CdiSlave::update(); } } die();
/** * The executeQueries() function will try to execute all SQL statements that are available in the manifest folder. * It will check the CDI log to see if the statement has already been executed based on the MD5 hash. * This function can only be called by SLAVE instances */ public static function update() { // We should not be processing any queries when the extension is disabled or when we are the Master instance // Check also exists on content page, but just to be sure! if((!class_exists('Administration')) || !CdiUtil::isEnabled() || !CdiUtil::isCdiSlave()) { echo "WARNING: CDI is disabled or you are running the queryies on the Master instance. No queries have been executed."; return; } // Prevent the CdiLogQuery::log() from persisting queries that are executed by CDI itself // Technically this should not be possible because it will not log queries on a SLAVE instance // and you can only run the executeQueries when in SLAVE mode. This is just to be sure. CdiLogQuery::isUpdating(true); // Switch to maintenance mode if(Symphony::Configuration()->get('maintenance-enabled', 'cdi') == 'yes') { Symphony::Configuration()->set('enabled', 'yes', 'maintenance_mode'); Symphony::Configuration()->write(); } // Implement automatic backup before processing structural changes if(Symphony::Configuration()->get('backup-enabled', 'cdi') == 'yes') { try { if(CdiUtil::hasDumpDBInstalled()) { require_once(EXTENSIONS . '/cdi/lib/class.cdidumpdb.php'); $currentBackup = CdiDumpDB::backup("automatic"); } else { throw new Exception('You can only enable automatic backup files when the "Dump DB" extension (version 1.10 or higher) is installed and enabled.'); } }catch (Exception $e) { echo "ERROR: " . $e->getMessage() , ". Failed to backup database before update, aborting."; die(); } } try { $skipped = 0; $executed = 0; $entries = CdiLogQuery::getCdiLogEntries(); foreach($entries as $entry) { $ts = $entry[0]; $order = $entry[1]; $hash = $entry[2]; $query = $entry[3]; $date = date('Y-m-d H:i:s', $ts); // Replace the table prefix in the query // Rename the generic table prefix to the prefix used by this instance $tbl_prefix = Symphony::Configuration()->get('tbl_prefix', 'database'); $query = str_replace('tbl_', $tbl_prefix, $query); try { // Look for available CDI log entries based on the provided MD5 hash $cdiLogEntry = Symphony::Database()->fetchRow(0,"SELECT * FROM tbl_cdi_log WHERE `query_hash` LIKE '" . $hash . "'"); if(empty($cdiLogEntry)) { // The query has not been found in the log, thus it has not been executed // So let's execute the query and add it to the log! Symphony::Database()->query("INSERT INTO `tbl_cdi_log` (`query_hash`,`author`,`url`,`date`,`order`) VALUES ('" . $hash . "','" . CdiUtil::getAuthor() . "','" . CdiUtil::getURL() . "','" . $date . "'," . $order . ")"); Symphony::Database()->query($query); $executed++; } else { // The query has already been executed, let's do nothing; $skipped++; } } catch (Exception $e) { //TODO: think of some smart way of dealing with errors, perhaps through the preference screen or a CDI Status content page? //Due to the error we need to perform a rollback to allow this query to be executed at a later stage. CdiLogQuery::rollback($hash,$ts,$order); // Implement automatic restore on error if((Symphony::Configuration()->get('backup-enabled', 'cdi') == 'yes') && (Symphony::Configuration()->get('restore-enabled', 'cdi') == 'yes')) { try { CdiDumpDB::restore($currentBackup); }catch (Exception $r) { echo "ERROR: " . $r->getMessage() , ". Failed to restore latest database backup. This instance needs immediate attention!"; die(); } // Restore was succesful, at least we can jump out of maintenance mode if(Symphony::Configuration()->get('maintenance-enabled', 'cdi') == 'yes') { Symphony::Configuration()->set('enabled', 'no', 'maintenance_mode'); Symphony::Configuration()->write(); } echo "ERROR: " . $e->getMessage() , ". Rollback & Restore have been executed."; } else { if(Symphony::Configuration()->get('maintenance-enabled', 'cdi') == 'yes') { echo "ERROR: " . $e->getMessage() , ". Rollback has been executed. This instance is now in maintenance mode and needs immediate attention!"; } else { echo "ERROR: " . $e->getMessage() , ". Rollback has been executed."; } } // Stop processing die(); } } echo "OK: " . $executed . " queries executed, " . $skipped . " skipped."; } catch (Exception $e) { //TODO: think of some smart way of dealing with errors, perhaps through the preference screen or a CDI Status content page? echo "ERROR: " . $e->getMessage(); } // Save the last update date to configuration Symphony::Configuration()->set('last-update', time(), 'cdi'); // No more maintenance mode if(Symphony::Configuration()->get('maintenance-enabled', 'cdi') == 'yes') { Symphony::Configuration()->set('enabled', 'no', 'maintenance_mode'); } Symphony::Configuration()->write(); // Re-enable CdiLogQuery::log() to persist queries CdiLogQuery::isUpdating(false); }