diff --git a/CHANGELOG.md b/CHANGELOG.md index 90bc9d3cb518d0522bcd99b0a3b00dbe330f6be4..ba91000bc509cf2ad9e128eca77d48f26de17748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ This is a changelog for Piwik platform developers. All changes for our HTTP API' ### Deprecations * `API` classes should no longer have a protected constructor. Classes with a protected constructor will generate a notice in the logs and should expose a public constructor instead. +* Update classes should not declare static `getSql()` and `update()` methods anymore. It is still supported to use those, but developers should instead override the `Updates::getMigrationQueries()` and `Updates::doUpdate()` instance methods. ### New features * `API` classes can now use dependency injection in their constructor to inject other instances. diff --git a/core/Columns/Updater.php b/core/Columns/Updater.php index 4272f46f566c88ac161106515371068bac6153fb..e1345a2dc2bb80ef1e06657dcc0589002fedaf35 100644 --- a/core/Columns/Updater.php +++ b/core/Columns/Updater.php @@ -7,6 +7,7 @@ * */ namespace Piwik\Columns; + use Piwik\Common; use Piwik\DbHelper; use Piwik\Plugin\Dimension\ActionDimension; @@ -25,86 +26,78 @@ class Updater extends \Piwik\Updates private static $cacheId = 'AllDimensionModifyTime'; /** - * @var Updater + * @var VisitDimension[] */ - private static $updater; + public $visitDimensions; /** - * Return SQL to be executed in this update - * - * @return array( - * 'ALTER .... ' => '1234', // if the query fails, it will be ignored if the error code is 1234 - * 'ALTER .... ' => false, // if an error occurs, the update will stop and fail - * // and user will have to manually run the query - * ) + * @var ActionDimension[] */ - public static function getSql() + private $actionDimensions; + + /** + * @var ConversionDimension[] + */ + private $conversionDimensions; + + /** + * @param VisitDimension[]|null $visitDimensions + * @param ActionDimension[]|null $actionDimensions + * @param ConversionDimension[]|null $conversionDimensions + */ + public function __construct(array $visitDimensions = null, array $actionDimensions = null, array $conversionDimensions = null) + { + $this->visitDimensions = $visitDimensions === null ? VisitDimension::getAllDimensions() : $visitDimensions; + $this->actionDimensions = $actionDimensions === null ? ActionDimension::getAllDimensions() : $actionDimensions; + $this->conversionDimensions = $conversionDimensions === null ? ConversionDimension::getAllDimensions() : $conversionDimensions; + } + + public function getMigrationQueries(PiwikUpdater $updater) { $sqls = array(); - $changingColumns = self::getUpdates(); + $changingColumns = $this->getUpdates($updater); foreach ($changingColumns as $table => $columns) { if (empty($columns) || !is_array($columns)) { continue; } - $sqls["ALTER TABLE `" . Common::prefixTable($table) . "` " . implode(', ', $columns)] = false; + $sqls["ALTER TABLE `" . Common::prefixTable($table) . "` " . implode(', ', $columns)] = array('1091', '1060'); } return $sqls; } - /** - * Incremental version update - */ - public static function update() - { - foreach (self::getSql() as $sql => $errorCode) { - try { - Db::exec($sql); - } catch (\Exception $e) { - if (!Db::get()->isErrNo($e, '1091') && !Db::get()->isErrNo($e, '1060')) { - PiwikUpdater::handleQueryError($e, $sql, false, __FILE__); - } - } - } - } - - public static function setUpdater($updater) + public function doUpdate(PiwikUpdater $updater) { - self::$updater = $updater; + $updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater)); } - private static function hasComponentNewVersion($component) - { - return empty(self::$updater) || self::$updater->hasNewVersion($component); - } - - private static function getUpdates() + private function getUpdates(PiwikUpdater $updater) { $visitColumns = DbHelper::getTableColumns(Common::prefixTable('log_visit')); $actionColumns = DbHelper::getTableColumns(Common::prefixTable('log_link_visit_action')); $conversionColumns = DbHelper::getTableColumns(Common::prefixTable('log_conversion')); - $changingColumns = array(); + $allUpdatesToRun = array(); - foreach (self::getVisitDimensions() as $dimension) { - $updates = self::getUpdatesForDimension($dimension, 'log_visit.', $visitColumns, $conversionColumns); - $changingColumns = self::mixinUpdates($changingColumns, $updates); + foreach ($this->visitDimensions as $dimension) { + $updates = $this->getUpdatesForDimension($updater, $dimension, 'log_visit.', $visitColumns, $conversionColumns); + $allUpdatesToRun = $this->mixinUpdates($allUpdatesToRun, $updates); } - foreach (self::getActionDimensions() as $dimension) { - $updates = self::getUpdatesForDimension($dimension, 'log_link_visit_action.', $actionColumns); - $changingColumns = self::mixinUpdates($changingColumns, $updates); + foreach ($this->actionDimensions as $dimension) { + $updates = $this->getUpdatesForDimension($updater, $dimension, 'log_link_visit_action.', $actionColumns); + $allUpdatesToRun = $this->mixinUpdates($allUpdatesToRun, $updates); } - foreach (self::getConversionDimensions() as $dimension) { - $updates = self::getUpdatesForDimension($dimension, 'log_conversion.', $conversionColumns); - $changingColumns = self::mixinUpdates($changingColumns, $updates); + foreach ($this->conversionDimensions as $dimension) { + $updates = $this->getUpdatesForDimension($updater, $dimension, 'log_conversion.', $conversionColumns); + $allUpdatesToRun = $this->mixinUpdates($allUpdatesToRun, $updates); } - return $changingColumns; + return $allUpdatesToRun; } /** @@ -114,12 +107,12 @@ class Updater extends \Piwik\Updates * @param array $conversionColumns * @return array */ - private static function getUpdatesForDimension($dimension, $componentPrefix, $existingColumnsInDb, $conversionColumns = array()) + private function getUpdatesForDimension(PiwikUpdater $updater, $dimension, $componentPrefix, $existingColumnsInDb, $conversionColumns = array()) { $column = $dimension->getColumnName(); $componentName = $componentPrefix . $column; - if (!self::hasComponentNewVersion($componentName)) { + if (!$updater->hasNewVersion($componentName)) { return array(); } @@ -136,22 +129,22 @@ class Updater extends \Piwik\Updates return $sqlUpdates; } - private static function mixinUpdates($changingColumns, $updatesFromDimension) + private function mixinUpdates($allUpdatesToRun, $updatesFromDimension) { if (!empty($updatesFromDimension)) { foreach ($updatesFromDimension as $table => $col) { - if (empty($changingColumns[$table])) { - $changingColumns[$table] = $col; + if (empty($allUpdatesToRun[$table])) { + $allUpdatesToRun[$table] = $col; } else { - $changingColumns[$table] = array_merge($changingColumns[$table], $col); + $allUpdatesToRun[$table] = array_merge($allUpdatesToRun[$table], $col); } } } - return $changingColumns; + return $allUpdatesToRun; } - public static function getAllVersions() + public function getAllVersions(PiwikUpdater $updater) { // to avoid having to load all dimensions on each request we check if there were any changes on the file system // can easily save > 100ms for each request @@ -169,16 +162,16 @@ class Updater extends \Piwik\Updates $actionColumns = DbHelper::getTableColumns(Common::prefixTable('log_link_visit_action')); $conversionColumns = DbHelper::getTableColumns(Common::prefixTable('log_conversion')); - foreach (self::getVisitDimensions() as $dimension) { - $versions = self::mixinVersions($dimension, 'log_visit.', $visitColumns, $versions); + foreach ($this->visitDimensions as $dimension) { + $versions = $this->mixinVersions($updater, $dimension, 'log_visit.', $visitColumns, $versions); } - foreach (self::getActionDimensions() as $dimension) { - $versions = self::mixinVersions($dimension, 'log_link_visit_action.', $actionColumns, $versions); + foreach ($this->actionDimensions as $dimension) { + $versions = $this->mixinVersions($updater, $dimension, 'log_link_visit_action.', $actionColumns, $versions); } - foreach (self::getConversionDimensions() as $dimension) { - $versions = self::mixinVersions($dimension, 'log_conversion.', $conversionColumns, $versions); + foreach ($this->conversionDimensions as $dimension) { + $versions = $this->mixinVersions($updater, $dimension, 'log_conversion.', $conversionColumns, $versions); } return $versions; @@ -191,10 +184,11 @@ class Updater extends \Piwik\Updates * @param array $versions * @return array The modified versions array */ - private static function mixinVersions($dimension, $componentPrefix, $columns, $versions) + private function mixinVersions(PiwikUpdater $updater, $dimension, $componentPrefix, $columns, $versions) { $columnName = $dimension->getColumnName(); + // dimensions w/o columns do not need DB updates if (!$columnName || !$dimension->hasColumnType()) { return $versions; } @@ -202,10 +196,15 @@ class Updater extends \Piwik\Updates $component = $componentPrefix . $columnName; $version = $dimension->getVersion(); + // if the column exists in the table, but has no associated version, and was one of the core columns + // that was moved when the dimension refactor took place, then: + // - set the installed version in the DB to the current code version + // - and do not check for updates since we just set the version to the latest if (array_key_exists($columnName, $columns) - && false === PiwikUpdater::getCurrentRecordedComponentVersion($component) - && self::wasDimensionMovedFromCoreToPlugin($component, $version)) { - PiwikUpdater::recordComponentSuccessfullyUpdated($component, $version); + && false === $updater->getCurrentComponentVersion($component) + && self::wasDimensionMovedFromCoreToPlugin($component, $version) + ) { + $updater->markComponentSuccessfullyUpdated($component, $version); return $versions; } @@ -224,7 +223,10 @@ class Updater extends \Piwik\Updates public static function wasDimensionMovedFromCoreToPlugin($name, $version) { - $dimensions = array ( + // maps names of core dimension columns that were part of the original dimension refactor with their + // initial "version" strings. The '1' that is sometimes appended to the end of the string (sometimes seen as + // NULL1) is from individual dimension "versioning" logic (eg, see VisitDimension::getVersion()) + $initialCoreDimensionVersions = array ( 'log_visit.config_resolution' => 'VARCHAR(9) NOT NULL', 'log_visit.config_device_brand' => 'VARCHAR( 100 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL', 'log_visit.config_device_model' => 'VARCHAR( 100 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL', @@ -285,14 +287,14 @@ class Updater extends \Piwik\Updates 'log_conversion.revenue_tax' => 'float default NULL', ); - if (!array_key_exists($name, $dimensions)) { + if (!array_key_exists($name, $initialCoreDimensionVersions)) { return false; } - return strtolower($dimensions[$name]) === strtolower($version); + return strtolower($initialCoreDimensionVersions[$name]) === strtolower($version); } - public static function onNoUpdateAvailable($versionsThatWereChecked) + public function onNoUpdateAvailable($versionsThatWereChecked) { if (!empty($versionsThatWereChecked)) { // invalidate cache only if there were actually file changes before, otherwise we write the cache on each @@ -337,25 +339,4 @@ class Updater extends \Piwik\Updates return array(); } - - private static function getVisitDimensions() - { - return VisitDimension::getAllDimensions(); - } - - /** - * @return mixed|Dimension[] - */ - private static function getActionDimensions() - { - return ActionDimension::getAllDimensions(); - } - - /** - * @return mixed|Dimension[] - */ - private static function getConversionDimensions() - { - return ConversionDimension::getAllDimensions(); - } -} +} \ No newline at end of file diff --git a/core/DbHelper.php b/core/DbHelper.php index 0c5ed7905973ca7f37f325c962b45350bcf63858..fdc1597cf69be2b1ad4186b8088588c9ee245f3e 100644 --- a/core/DbHelper.php +++ b/core/DbHelper.php @@ -166,7 +166,7 @@ class DbHelper /** * Get the SQL to create a specific Piwik table * - * @param string $tableName + * @param string $tableName Unprefixed table name. * @return string SQL */ public static function getTableCreateSql($tableName) diff --git a/core/FrontController.php b/core/FrontController.php index 7c3740d93d7d0253e39ed216c91a50cbe0e7e24b..f9dd3259cb14dd420b26e03cb8c762903c5227f5 100644 --- a/core/FrontController.php +++ b/core/FrontController.php @@ -14,6 +14,7 @@ use Piwik\API\Request; use Piwik\API\ResponseBuilder; use Piwik\Container\StaticContainer; use Piwik\Exception\AuthenticationFailedException; +use Piwik\Exception\DatabaseSchemaIsNewerThanCodebaseException; use Piwik\Http\ControllerResolver; use Piwik\Http\Router; use Piwik\Plugin\Report; @@ -318,7 +319,7 @@ class FrontController extends Singleton */ Piwik::postEvent('Request.dispatchCoreAndPluginUpdatesScreen'); - Updater::throwIfPiwikVersionIsOlderThanDBSchema(); + $this->throwIfPiwikVersionIsOlderThanDBSchema(); \Piwik\Plugin\Manager::getInstance()->installLoadedPlugins(); @@ -556,4 +557,23 @@ class FrontController extends Singleton return $result; } + /** + * This method ensures that Piwik Platform cannot be running when using a NEWER database. + */ + private function throwIfPiwikVersionIsOlderThanDBSchema() + { + $updater = new Updater(); + + $dbSchemaVersion = $updater->getCurrentComponentVersion('core'); + $current = Version::VERSION; + if (-1 === version_compare($current, $dbSchemaVersion)) { + $messages = array( + Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebase', array($current, $dbSchemaVersion)), + Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebaseWait'), + // we cannot fill in the Super User emails as we are failing before Authentication was ready + Piwik::translate('General_ExceptionContactSupportGeneric', array('', '')) + ); + throw new DatabaseSchemaIsNewerThanCodebaseException(implode(" ", $messages)); + } + } } diff --git a/core/Plugin/Manager.php b/core/Plugin/Manager.php index fb11f0a6640e0fb5357abc403ec34a72b37c28dd..22eb0296d1e8f8340e79c108f0015f7eebe59851 100644 --- a/core/Plugin/Manager.php +++ b/core/Plugin/Manager.php @@ -1048,7 +1048,8 @@ class Manager extends Singleton $this->executePluginInstall($plugin); $pluginsInstalled[] = $pluginName; $this->updatePluginsInstalledConfig($pluginsInstalled); - Updater::recordComponentSuccessfullyUpdated($plugin->getPluginName(), $plugin->getVersion()); + $updater = new Updater(); + $updater->markComponentSuccessfullyUpdated($plugin->getPluginName(), $plugin->getVersion()); $saveConfig = true; } diff --git a/core/Updater.php b/core/Updater.php index c9872d34817795d7b448b079e4341f4a2b9b990c..bccddff3f22084b647c9eb3c82d9d268a45d2cd0 100644 --- a/core/Updater.php +++ b/core/Updater.php @@ -7,8 +7,12 @@ * */ namespace Piwik; + use Piwik\Columns\Updater as ColumnUpdater; +use Piwik\Container\StaticContainer; use Piwik\Exception\DatabaseSchemaIsNewerThanCodebaseException; +use Piwik\Updater\UpdateObserver; +use Zend_Db_Exception; /** * Load and execute all relevant, incremental update scripts for Piwik core and plugins, and bump the component version numbers for completed updates. @@ -19,41 +23,66 @@ class Updater const INDEX_CURRENT_VERSION = 0; const INDEX_NEW_VERSION = 1; - public $pathUpdateFileCore; - public $pathUpdateFilePlugins; - private $componentsToCheck = array(); + private $pathUpdateFileCore; + private $pathUpdateFilePlugins; private $hasMajorDbUpdate = false; private $updatedClasses = array(); + private $componentsWithNewVersion = array(); + private $componentsWithUpdateFile = array(); + + /** + * @var UpdateObserver[] + */ + private $updateObservers = array(); + + /** + * @var Columns\Updater + */ + private $columnsUpdater; + + /** + * Currently used Updater instance, set on construction. This instance is used to provide backwards + * compatibility w/ old code that may use the deprecated static methods in Updates. + * + * @var Updater + */ + private static $activeInstance; /** - * Constructor + * Constructor. + * + * @param string|null $pathUpdateFileCore The path to core Update files. + * @param string|null $pathUpdateFilePlugins The path to plugin update files. Should contain a `'%s'` placeholder + * for the plugin name. + * @param Columns\Updater|null $columnsUpdater The dimensions updater instance. */ - public function __construct() + public function __construct($pathUpdateFileCore = null, $pathUpdateFilePlugins = null, Columns\Updater $columnsUpdater = null) { - $this->pathUpdateFileCore = PIWIK_INCLUDE_PATH . '/core/Updates/'; - $this->pathUpdateFilePlugins = PIWIK_INCLUDE_PATH . '/plugins/%s/Updates/'; + $this->pathUpdateFileCore = $pathUpdateFileCore ?: PIWIK_INCLUDE_PATH . '/core/Updates/'; + $this->pathUpdateFilePlugins = $pathUpdateFilePlugins ?: PIWIK_INCLUDE_PATH . '/plugins/%s/Updates/'; + $this->columnsUpdater = $columnsUpdater ?: new Columns\Updater(); - ColumnUpdater::setUpdater($this); + self::$activeInstance = $this; } /** - * Add component to check + * Adds an UpdateObserver to the internal list of listeners. * - * @param string $name - * @param string $version + * @param UpdateObserver $listener */ - public function addComponentToCheck($name, $version) + public function addUpdateObserver(UpdateObserver $listener) { - $this->componentsToCheck[$name] = $version; + $this->updateObservers[] = $listener; } /** - * Record version of successfully completed component update + * Marks a component as successfully updated to a specific version in the database. Sets an option + * that looks like `"version_$componentName"`. * - * @param string $name - * @param string $version + * @param string $name The component name. Eg, a plugin name, `'core'` or dimension column name. + * @param string $version The component version (should use semantic versioning). */ - public static function recordComponentSuccessfullyUpdated($name, $version) + public function markComponentSuccessfullyUpdated($name, $version) { try { Option::set(self::getNameInOptionTable($name), $version, $autoLoad = 1); @@ -63,12 +92,13 @@ class Updater } /** - * Retrieve the current version of a recorded component - * @param string $name - * @return false|string + * Returns the currently installed version of a Piwik component. + * + * @param string $name The component name. Eg, a plugin name, `'core'` or dimension column name. + * @return string A semantic version. * @throws \Exception */ - public static function getCurrentRecordedComponentVersion($name) + public function getCurrentComponentVersion($name) { try { $currentVersion = Option::get(self::getNameInOptionTable($name)); @@ -86,44 +116,19 @@ class Updater return $currentVersion; } - /** - * Returns the flag name to use in the option table to record current schema version - * @param string $name - * @return string - */ - private static function getNameInOptionTable($name) - { - return 'version_' . $name; - } - - - /** - * This method ensures that Piwik Platform cannot be running when using a NEWER database - */ - public static function throwIfPiwikVersionIsOlderThanDBSchema() - { - $dbSchemaVersion = self::getCurrentRecordedComponentVersion('core'); - $current = Version::VERSION; - if(-1 === version_compare($current, $dbSchemaVersion)) { - $messages = array( - Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebase', array($current, $dbSchemaVersion)), - Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebaseWait'), - // we cannot fill in the Super User emails as we are failing before Authentication was ready - Piwik::translate('General_ExceptionContactSupportGeneric', array('', '')) - ); - throw new DatabaseSchemaIsNewerThanCodebaseException(implode(" ", $messages)); - } - } - - /** * Returns a list of components (core | plugin) that need to run through the upgrade process. * + * @param string[] $componentsToCheck An array mapping component names to the latest locally available version. + * If the version is later than the currently installed version, the component + * must be upgraded. + * + * Example: `array('core' => '2.11.0')` * @return array( componentName => array( file1 => version1, [...]), [...]) */ - public function getComponentsWithUpdateFile() + public function getComponentsWithUpdateFile($componentsToCheck) { - $this->componentsWithNewVersion = $this->getComponentsWithNewVersion(); + $this->componentsWithNewVersion = $this->getComponentsWithNewVersion($componentsToCheck); $this->componentsWithUpdateFile = $this->loadComponentsWithUpdateFile(); return $this->componentsWithUpdateFile; } @@ -136,8 +141,7 @@ class Updater */ public function hasNewVersion($componentName) { - return isset($this->componentsWithNewVersion) && - isset($this->componentsWithNewVersion[$componentName]); + return isset($this->componentsWithNewVersion[$componentName]); } /** @@ -177,7 +181,8 @@ class Updater $classNames[] = $className; - $queriesForComponent = call_user_func(array($className, 'getSql')); + $update = StaticContainer::getContainer()->make($className); + $queriesForComponent = call_user_func(array($update, 'getMigrationQueries'), $this); foreach ($queriesForComponent as $query => $error) { $queries[] = $query . ';'; } @@ -214,29 +219,45 @@ class Updater { $warningMessages = array(); + $this->executeListenerHook('onComponentUpdateStarting', array($componentName)); + foreach ($this->componentsWithUpdateFile[$componentName] as $file => $fileVersion) { try { require_once $file; // prefixed by PIWIK_INCLUDE_PATH $className = $this->getUpdateClassName($componentName, $fileVersion); - if (!in_array($className, $this->updatedClasses) && class_exists($className, false)) { - // update() - call_user_func(array($className, 'update')); + if (!in_array($className, $this->updatedClasses) + && class_exists($className, false) + ) { + $this->executeListenerHook('onComponentUpdateFileStarting', array($componentName, $file, $className, $fileVersion)); + + $this->executeSingleUpdateClass($className); + + $this->executeListenerHook('onComponentUpdateFileFinished', array($componentName, $file, $className, $fileVersion)); + // makes sure to call Piwik\Columns\Updater only once as one call updates all dimensions at the same // time for better performance $this->updatedClasses[] = $className; } - self::recordComponentSuccessfullyUpdated($componentName, $fileVersion); + $this->markComponentSuccessfullyUpdated($componentName, $fileVersion); } catch (UpdaterErrorException $e) { + $this->executeListenerHook('onError', array($componentName, $fileVersion, $e)); + throw $e; } catch (\Exception $e) { $warningMessages[] = $e->getMessage(); + + $this->executeListenerHook('onWarning', array($componentName, $fileVersion, $e)); } } - // to debug, create core/Updates/X.php, update the core/Version.php, throw an Exception in the try, and comment the following line - self::recordComponentSuccessfullyUpdated($componentName, $this->componentsWithNewVersion[$componentName][self::INDEX_NEW_VERSION]); + // to debug, create core/Updates/X.php, update the core/Version.php, throw an Exception in the try, and comment the following lines + $updatedVersion = $this->componentsWithNewVersion[$componentName][self::INDEX_NEW_VERSION]; + $this->markComponentSuccessfullyUpdated($componentName, $updatedVersion); + + $this->executeListenerHook('onComponentUpdateFinished', array($componentName, $updatedVersion, $warningMessages)); + return $warningMessages; } @@ -284,7 +305,7 @@ class Updater uasort($componentsWithUpdateFile[$name], "version_compare"); } else { // there are no update file => nothing to do, update to the new version is successful - self::recordComponentSuccessfullyUpdated($name, $newVersion); + $this->markComponentSuccessfullyUpdated($name, $newVersion); } } @@ -294,29 +315,34 @@ class Updater /** * Construct list of outdated components * + * @param string[] $componentsToCheck An array mapping component names to the latest locally available version. + * If the version is later than the currently installed version, the component + * must be upgraded. + * + * Example: `array('core' => '2.11.0')` * @throws \Exception * @return array array( componentName => array( oldVersion, newVersion), [...]) */ - public function getComponentsWithNewVersion() + public function getComponentsWithNewVersion($componentsToCheck) { $componentsToUpdate = array(); // we make sure core updates are processed before any plugin updates - if (isset($this->componentsToCheck['core'])) { - $coreVersions = $this->componentsToCheck['core']; - unset($this->componentsToCheck['core']); - $this->componentsToCheck = array_merge(array('core' => $coreVersions), $this->componentsToCheck); + if (isset($componentsToCheck['core'])) { + $coreVersions = $componentsToCheck['core']; + unset($componentsToCheck['core']); + $componentsToCheck = array_merge(array('core' => $coreVersions), $componentsToCheck); } - $recordedCoreVersion = self::getCurrentRecordedComponentVersion('core'); - if ($recordedCoreVersion === false) { + $recordedCoreVersion = $this->getCurrentComponentVersion('core'); + if (empty($recordedCoreVersion)) { // This should not happen $recordedCoreVersion = Version::VERSION; - self::recordComponentSuccessfullyUpdated('core', $recordedCoreVersion); + $this->markComponentSuccessfullyUpdated('core', $recordedCoreVersion); } - foreach ($this->componentsToCheck as $name => $version) { - $currentVersion = self::getCurrentRecordedComponentVersion($name); + foreach ($componentsToCheck as $name => $version) { + $currentVersion = $this->getCurrentComponentVersion($name); if (ColumnUpdater::isDimensionComponent($name)) { $isComponentOutdated = $currentVersion !== $version; @@ -332,9 +358,192 @@ class Updater ); } } + return $componentsToUpdate; } + /** + * Updates multiple components, while capturing & returning errors and warnings. + * + * @param string[] $componentsWithUpdateFile Component names mapped with arrays of update files. Same structure + * as the result of `getComponentsWithUpdateFile()`. + * @return array Information about the update process, including: + * + * * **warnings**: The list of warnings that occurred during the update process. + * * **errors**: The list of updater exceptions thrown during individual component updates. + * * **coreError**: True if an exception was thrown while updating core. + * * **deactivatedPlugins**: The list of plugins that were deactivated due to an error in the + * update process. + */ + public function updateComponents($componentsWithUpdateFile) + { + $warnings = array(); + $errors = array(); + $deactivatedPlugins = array(); + $coreError = false; + + if (!empty($componentsWithUpdateFile)) { + $currentAccess = Access::getInstance(); + $hasSuperUserAccess = $currentAccess->hasSuperUserAccess(); + + if (!$hasSuperUserAccess) { + $currentAccess->setSuperUserAccess(true); + } + + // if error in any core update, show message + help message + EXIT + // if errors in any plugins updates, show them on screen, disable plugins that errored + CONTINUE + // if warning in any core update or in any plugins update, show message + CONTINUE + // if no error or warning, success message + CONTINUE + foreach ($componentsWithUpdateFile as $name => $filenames) { + try { + $warnings = array_merge($warnings, $this->update($name)); + } catch (UpdaterErrorException $e) { + $errors[] = $e->getMessage(); + if ($name == 'core') { + $coreError = true; + break; + } elseif (\Piwik\Plugin\Manager::getInstance()->isPluginActivated($name)) { + \Piwik\Plugin\Manager::getInstance()->deactivatePlugin($name); + $deactivatedPlugins[] = $name; + } + } + } + + if (!$hasSuperUserAccess) { + $currentAccess->setSuperUserAccess(false); + } + } + + Filesystem::deleteAllCacheOnUpdate(); + + $result = array( + 'warnings' => $warnings, + 'errors' => $errors, + 'coreError' => $coreError, + 'deactivatedPlugins' => $deactivatedPlugins + ); + + /** + * Triggered after Piwik has been updated. + */ + Piwik::postEvent('CoreUpdater.update.end'); + + return $result; + } + + /** + * Returns any updates that should occur for core and all plugins that are both loaded and + * installed. Also includes updates required for dimensions. + * + * @return string[]|null Returns the result of `getComponentsWithUpdateFile()`. + */ + public function getComponentUpdates() + { + $componentsToCheck = array( + 'core' => Version::VERSION + ); + + $manager = \Piwik\Plugin\Manager::getInstance(); + $plugins = $manager->getLoadedPlugins(); + foreach ($plugins as $pluginName => $plugin) { + if ($manager->isPluginInstalled($pluginName)) { + $componentsToCheck[$pluginName] = $plugin->getVersion(); + } + } + + $columnsVersions = $this->columnsUpdater->getAllVersions($this); + foreach ($columnsVersions as $component => $version) { + $componentsToCheck[$component] = $version; + } + + $componentsWithUpdateFile = $this->getComponentsWithUpdateFile($componentsToCheck); + + if (count($componentsWithUpdateFile) == 0) { + $this->columnsUpdater->onNoUpdateAvailable($columnsVersions); + + if (!$this->hasNewVersion('core')) { + return null; + } + } + + return $componentsWithUpdateFile; + } + + /** + * Execute multiple migration queries from a single Update file. + * + * @param string $file The path to the Updates file. + * @param array $migrationQueries An array mapping SQL queries w/ one or more MySQL errors to ignore. + */ + public function executeMigrationQueries($file, $migrationQueries) + { + foreach ($migrationQueries as $update => $ignoreError) { + $this->executeSingleMigrationQuery($update, $ignoreError, $file); + } + } + + /** + * Execute a single migration query from an update file. + * + * @param string $migrationQuerySql The SQL to execute. + * @param int|int[]|null An optional error code or list of error codes to ignore. + * @param string $file The path to the Updates file. + */ + public function executeSingleMigrationQuery($migrationQuerySql, $errorToIgnore, $file) + { + try { + $this->executeListenerHook('onStartExecutingMigrationQuery', array($file, $migrationQuerySql)); + + Db::exec($migrationQuerySql); + } catch (\Exception $e) { + $this->handleUpdateQueryError($e, $migrationQuerySql, $errorToIgnore, $file); + } + + $this->executeListenerHook('onFinishedExecutingMigrationQuery', array($file, $migrationQuerySql)); + } + + /** + * Handle an update query error. + * + * @param \Exception $e The error that occurred. + * @param string $updateSql The SQL that was executed. + * @param int|int[]|null An optional error code or list of error codes to ignore. + * @param string $file The path to the Updates file. + * @throws \Exception + */ + public function handleUpdateQueryError(\Exception $e, $updateSql, $errorToIgnore, $file) + { + if (($errorToIgnore === false) + || !self::isDbErrorOneOf($e, $errorToIgnore) + ) { + $message = $file . ":\nError trying to execute the query '" . $updateSql . "'.\nThe error was: " . $e->getMessage(); + throw new UpdaterErrorException($message); + } + } + + private function executeListenerHook($hookName, $arguments) + { + foreach ($this->updateObservers as $listener) { + call_user_func_array(array($listener, $hookName), $arguments); + } + } + + private function executeSingleUpdateClass($className) + { + $update = StaticContainer::getContainer()->make($className); + try { + call_user_func(array($update, 'doUpdate'), $this); + } catch (\Exception $e) { + // if an Update file executes PHP statements directly, DB exceptions be handled by executeSingleMigrationQuery, so + // make sure to check for them here + if ($e instanceof Zend_Db_Exception) { + throw new UpdaterErrorException($e->getMessage(), $e->getCode(), $e); + } else { + throw $e; + } + } + } + /** * Performs database update(s) * @@ -344,9 +553,7 @@ class Updater */ static function updateDatabase($file, $sqlarray) { - foreach ($sqlarray as $update => $ignoreError) { - self::executeMigrationQuery($update, $ignoreError, $file); - } + self::$activeInstance->executeMigrationQueries($file, $sqlarray); } /** @@ -358,11 +565,7 @@ class Updater */ public static function executeMigrationQuery($updateSql, $errorToIgnore, $file) { - try { - Db::exec($updateSql); - } catch (\Exception $e) { - self::handleQueryError($e, $updateSql, $errorToIgnore, $file); - } + self::$activeInstance->executeSingleMigrationQuery($updateSql, $errorToIgnore, $file); } /** @@ -376,12 +579,29 @@ class Updater */ public static function handleQueryError($e, $updateSql, $errorToIgnore, $file) { - if (($errorToIgnore === false) - || !self::isDbErrorOneOf($e, $errorToIgnore) - ) { - $message = $file . ":\nError trying to execute the query '" . $updateSql . "'.\nThe error was: " . $e->getMessage(); - throw new UpdaterErrorException($message); - } + self::$activeInstance->handleQueryError($e, $updateSql, $errorToIgnore, $file); + } + + /** + * Record version of successfully completed component update + * + * @param string $name + * @param string $version + */ + public static function recordComponentSuccessfullyUpdated($name, $version) + { + self::$activeInstance->markComponentSuccessfullyUpdated($name, $version); + } + + /** + * Retrieve the current version of a recorded component + * @param string $name + * @return false|string + * @throws \Exception + */ + public static function getCurrentRecordedComponentVersion($name) + { + return self::$activeInstance->getCurrentComponentVersion($name); } /** @@ -401,6 +621,16 @@ class Updater } return false; } + + /** + * Returns the flag name to use in the option table to record current schema version + * @param string $name + * @return string + */ + private static function getNameInOptionTable($name) + { + return 'version_' . $name; + } } /** diff --git a/core/Updater/UpdateObserver.php b/core/Updater/UpdateObserver.php new file mode 100644 index 0000000000000000000000000000000000000000..b8a91562f55cc33be9b20debfc2d34f8b1620494 --- /dev/null +++ b/core/Updater/UpdateObserver.php @@ -0,0 +1,114 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Updater; + +/** + * UpdateObservers can be used to inject logic into the component updating process. Derive + * from this base class and add an instance of the derived class to a Updater instance. When + * Updater::update() is called, the methods in the added UpdateListeners will be executed + * accordingly. + */ +abstract class UpdateObserver +{ + /** + * Executed when a component is about to be updated. At this point, no SQL queries or other + * updating logic has been executed. + * + * @param string $name The name of the component being updated. + */ + public function onComponentUpdateStarting($name) + { + // empty + } + + /** + * Executed after a component has been successfully updated. + * + * @param string $name The name of the component being updated. + * @param string $version The version of the component that was updated. + * @param string[] $warnings Any warnings that occurred during the component update process. + */ + public function onComponentUpdateFinished($name, $version, $warnings) + { + // empty + } + + /** + * Executed before the update method of an Updates class is executed. + * + * @param string $componentName The name of the component being updated. + * @param string $file The path to the Updates file being executed. + * @param string $updateClassName The name of the Update class that should exist within the Update file. + * @param string $version The version the Updates file belongs to. + */ + public function onComponentUpdateFileStarting($componentName, $file, $updateClassName, $version) + { + // empty + } + + /** + * Executed after the update method of an Updates class is successfully executed. + * + * @param string $componentName The name of the component being updated. + * @param string $file The path to the Updates file being executed. + * @param string $updateClassName The name of the Update class that should exist within the Update file. + * @param string $version The version the Updates file belongs to. + */ + public function onComponentUpdateFileFinished($componentName, $file, $updateClassName, $version) + { + // empty + } + + /** + * Executed before a migration query is executed. + * + * @param string $updateFile The path to the Updates file being executed. + * @param string $sql The SQL query that is about to be executed. + */ + public function onStartExecutingMigrationQuery($updateFile, $sql) + { + // empty + } + + /** + * Executed after a migration query is executed. + * + * @param string $updateFile The path to the Updates file being executed. + * @param string $sql The SQL query that has finished executing. + */ + public function onFinishedExecutingMigrationQuery($updateFile, $sql) + { + // empty + } + + /** + * Executed when a warning occurs during the update process. A warning occurs when an Updates file + * throws an exception that is not a UpdaterErrorException. + * + * @param string $componentName The name of the component being updated. + * @param string $version The version of the Updates file during which the warning occurred. + * @param \Exception $exception The exception that generated the warning. + */ + public function onWarning($componentName, $version, \Exception $exception) + { + // empty + } + + /** + * Executed when an error occurs during the update process. An error occurs when an Updates file + * throws a UpdaterErrorException. + * + * @param string $componentName The name of the component being updated. + * @param string $version The version of the Updates file during which the error occurred. + * @param \Exception $exception The exception that generated the error. + */ + public function onError($componentName, $version, \Exception $exception) + { + // empty + } +} \ No newline at end of file diff --git a/core/Updates.php b/core/Updates.php index 85e7e589ab07b7dc5f823074c49551cfc989b8bc..2ef3e0836da08148bf2b53905b0a4638c3ee8597 100644 --- a/core/Updates.php +++ b/core/Updates.php @@ -15,6 +15,21 @@ namespace Piwik; */ abstract class Updates { + /** + * @deprecated since v2.12.0 use getMigrationQueries() instead + */ + static function getSql() + { + return array(); + } + + /** + * @deprecated since v2.12.0 use doUpdate() instead + */ + static function update() + { + } + /** * Return SQL to be executed in this update * @@ -24,16 +39,17 @@ abstract class Updates * // and user will have to manually run the query * ) */ - static function getSql() + public function getMigrationQueries(Updater $updater) { - return array(); + return static::getSql(); } /** * Incremental version update */ - static function update() + public function doUpdate(Updater $updater) { + static::update(); } /** @@ -45,7 +61,7 @@ abstract class Updates * * @return bool */ - static function isMajorUpdate() + public static function isMajorUpdate() { return false; } @@ -53,7 +69,7 @@ abstract class Updates /** * Helper method to enable maintenance mode during large updates */ - static function enableMaintenanceMode() + public static function enableMaintenanceMode() { $config = Config::getInstance(); @@ -71,7 +87,7 @@ abstract class Updates /** * Helper method to disable maintenance mode after large updates */ - static function disableMaintenanceMode() + public static function disableMaintenanceMode() { $config = Config::getInstance(); diff --git a/core/Updates/2.10.0-b5.php b/core/Updates/2.10.0-b5.php index 1cbcb0e3532206dc303ba73c5c1ee6ac831809b9..51663a9d0a26e388880fed6264217c87af415ab6 100644 --- a/core/Updates/2.10.0-b5.php +++ b/core/Updates/2.10.0-b5.php @@ -42,6 +42,7 @@ use Piwik\Plugins\Dashboard\Model AS DashboardModel; */ class Updates_2_10_0_b5 extends Updates { + public static $archiveBlobTables; static function getSql() { @@ -120,21 +121,18 @@ class Updates_2_10_0_b5 extends Updates */ public static function getAllArchiveBlobTables() { - static $archiveBlobTables; - - if (empty($archiveBlobTables)) { - + if (empty(self::$archiveBlobTables)) { $archiveTables = ArchiveTableCreator::getTablesArchivesInstalled(); - $archiveBlobTables = array_filter($archiveTables, function($name) { + self::$archiveBlobTables = array_filter($archiveTables, function($name) { return ArchiveTableCreator::getTypeFromTableName($name) == ArchiveTableCreator::BLOB_TABLE; }); // sort tables so we have them in order of their date - rsort($archiveBlobTables); + rsort(self::$archiveBlobTables); } - return (array) $archiveBlobTables; + return (array) self::$archiveBlobTables; } /** diff --git a/plugins/CoreUpdater/Commands/Update.php b/plugins/CoreUpdater/Commands/Update.php index 9eb8309e45d491efe66b6df8e0b660a45a265542..0f5566e3946a944026dd0e931585a5bbb70a914c 100644 --- a/plugins/CoreUpdater/Commands/Update.php +++ b/plugins/CoreUpdater/Commands/Update.php @@ -8,22 +8,31 @@ */ namespace Piwik\Plugins\CoreUpdater\Commands; -use Piwik\Container\StaticContainer; +use Piwik\Version; +use Piwik\Config; +use Piwik\DbHelper; use Piwik\Filesystem; +use Piwik\Piwik; use Piwik\Plugin\ConsoleCommand; -use Piwik\Plugins\CoreUpdater\Controller; +use Piwik\Plugins\CoreUpdater\Commands\Update\CliUpdateObserver; use Piwik\Plugins\CoreUpdater\NoUpdatesFoundException; use Piwik\Plugins\UserCountry\LocationProvider; +use Piwik\Updater; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; /** - * @package CloudAdmin + * @package CoreUpdater */ class Update extends ConsoleCommand { + /** + * @var string[] + */ + private $migrationQueries; + protected function configure() { $this->setName('core:update'); @@ -62,10 +71,6 @@ class Update extends ConsoleCommand } catch(NoUpdatesFoundException $e) { // Do not fail if no updates were found $this->writeSuccessMessage($output, array($e->getMessage())); - } catch (\Exception $e) { - // Fail in case of any other error during upgrade - $output->writeln("<error>" . $e->getMessage() . "</error>"); - throw $e; } } @@ -86,9 +91,245 @@ class Update extends ConsoleCommand { $this->checkAllRequiredOptionsAreNotEmpty($input); - $updateController = StaticContainer::get('Piwik\Plugins\CoreUpdater\Controller'); - $content = $updateController->runUpdaterAndExit($doDryRun); + $updater = $this->makeUpdaterInstance($output); + + $componentsWithUpdateFile = $updater->getComponentUpdates(); + if (empty($componentsWithUpdateFile)) { + throw new NoUpdatesFoundException("Everything is already up to date."); + } + + $output->writeln(array( + "", + " *** " . Piwik::translate('CoreUpdater_UpdateTitle') . " ***" + )); + + // handle case of existing database with no tables + if (!DbHelper::isInstalled()) { + $this->handleCoreError($output, Piwik::translate('CoreUpdater_EmptyDatabaseError', Config::getInstance()->database['dbname'])); + return; + } + + $output->writeln(array( + "", + " " . Piwik::translate('CoreUpdater_DatabaseUpgradeRequired'), + "", + " " . Piwik::translate('CoreUpdater_YourDatabaseIsOutOfDate') + )); + + if ($this->isUpdatingCore($componentsWithUpdateFile)) { + $currentVersion = $this->getCurrentVersionForCore($updater); + $output->writeln(array( + "", + " " . Piwik::translate('CoreUpdater_PiwikWillBeUpgradedFromVersionXToVersionY', array($currentVersion, Version::VERSION)) + )); + } + + $pluginsToUpdate = $this->getPluginsToUpdate($componentsWithUpdateFile); + if (!empty($pluginsToUpdate)) { + $output->writeln(array( + "", + " " . Piwik::translate('CoreUpdater_TheFollowingPluginsWillBeUpgradedX', implode(', ', $pluginsToUpdate)) + )); + } + + $dimensionsToUpdate = $this->getDimensionsToUpdate($componentsWithUpdateFile); + if (!empty($dimensionsToUpdate)) { + $output->writeln(array( + "", + " " . Piwik::translate('CoreUpdater_TheFollowingDimensionsWillBeUpgradedX', implode(', ', $dimensionsToUpdate)) + )); + } + + $output->writeln(""); + + if ($doDryRun) { + $this->doDryRun($updater, $output); + } else { + $this->doRealUpdate($updater, $componentsWithUpdateFile, $output); + } + } + + private function doDryRun(Updater $updater, OutputInterface $output) + { + $migrationQueries = $this->getMigrationQueriesToExecute($updater); + + $output->writeln(array(" *** Note: this is a Dry Run ***", "")); + + foreach ($migrationQueries as $query) { + $output->writeln(" " . $query); + } + + $output->writeln(array("", " *** End of Dry Run ***", "")); + } + + private function doRealUpdate(Updater $updater, $componentsWithUpdateFile, OutputInterface $output) + { + $output->writeln(array(" " . Piwik::translate('CoreUpdater_TheUpgradeProcessMayTakeAWhilePleaseBePatient'), "")); + + $updaterResult = $updater->updateComponents($componentsWithUpdateFile); + + if (@$updaterResult['coreError']) { + $this->handleCoreError($output, $updaterResult['errors'], $includeDiyHelp = true); + return; + } + + if (!empty($updaterResult['warnings'])) { + $this->outputUpdaterWarnings($output, $updaterResult['warnings']); + } + + if (!empty($updaterResult['errors'])) { + $this->outputUpdaterErrors($output, $updaterResult['errors'], $updaterResult['deactivatedPlugins']); + } + + if (!empty($updaterResult['warnings']) + || !empty($updaterResult['errors']) + ) { + $output->writeln(array( + " " . Piwik::translate('CoreUpdater_HelpMessageIntroductionWhenWarning'), + "", + " * " . $this->getUpdateHelpMessage() + )); + } + } + + private function handleCoreError(OutputInterface $output, $errors, $includeDiyHelp = false) + { + if (!is_array($errors)) { + $errors = array($errors); + } + + $output->writeln(array( + "", + " [X] " . Piwik::translate('CoreUpdater_CriticalErrorDuringTheUpgradeProcess'), + "", + )); + + foreach ($errors as $errorMessage) { + $errorMessage = trim($errorMessage); + $errorMessage = str_replace("\n", "\n ", $errorMessage); + + $output->writeln(" * $errorMessage"); + } + + $output->writeln(array( + "", + " " . Piwik::translate('CoreUpdater_HelpMessageIntroductionWhenError'), + "", + " * " . $this->getUpdateHelpMessage() + )); + + if ($includeDiyHelp) { + $output->writeln(array( + "", + " " . Piwik::translate('CoreUpdater_ErrorDIYHelp'), + "", + " * " . Piwik::translate('CoreUpdater_ErrorDIYHelp_1'), + " * " . Piwik::translate('CoreUpdater_ErrorDIYHelp_2'), + " * " . Piwik::translate('CoreUpdater_ErrorDIYHelp_3'), + " * " . Piwik::translate('CoreUpdater_ErrorDIYHelp_4'), + " * " . Piwik::translate('CoreUpdater_ErrorDIYHelp_5') + )); + } + + throw new \RuntimeException("Piwik could not be updated! See above for more information."); + } + + private function outputUpdaterWarnings(OutputInterface $output, $warnings) + { + $output->writeln(array( + "", + " [!] " . Piwik::translate('CoreUpdater_WarningMessages'), + "" + )); + + foreach ($warnings as $message) { + $output->writeln(" * $message"); + } + } + + private function outputUpdaterErrors(OutputInterface $output, $errors, $deactivatedPlugins) + { + $output->writeln(array( + "", + " [X] " . Piwik::translate('CoreUpdater_ErrorDuringPluginsUpdates'), + "" + )); + + foreach ($errors as $message) { + $output->writeln(" * $message"); + } + + if (!empty($deactivatedPlugins)) { + $output->writeln(array( + "", + " [!] " . Piwik::translate('CoreUpdater_WeAutomaticallyDeactivatedTheFollowingPlugins', implode(', ', $deactivatedPlugins)) + )); + } + } + + private function getUpdateHelpMessage() + { + return Piwik::translate('CoreUpdater_HelpMessageContent', array('[',']',"\n *")); + } + + private function isUpdatingCore($componentsWithUpdateFile) + { + foreach ($componentsWithUpdateFile as $componentName => $updates) { + if ($componentName == 'core') { + return true; + } + } + return false; + } + + private function getCurrentVersionForCore(Updater $updater) + { + $currentVersion = $updater->getCurrentComponentVersion('core'); + if ($currentVersion === false) { + $currentVersion = "<= 0.2.9"; + } + return $currentVersion; + } + + private function getPluginsToUpdate($componentsWithUpdateFile) + { + $plugins = array(); + foreach ($componentsWithUpdateFile as $componentName => $updates) { + if ($componentName !== 'core' + && 0 !== strpos($componentName, 'log_') + ) { + $plugins[] = $componentName; + } + } + return $plugins; + } + + private function getDimensionsToUpdate($componentsWithUpdateFile) + { + $dimensions = array(); + foreach ($componentsWithUpdateFile as $componentName => $updates) { + if (0 === strpos($componentName, 'log_')) { + $dimensions[] = $componentName; + } + } + return $dimensions; + } + + private function getMigrationQueriesToExecute(Updater $updater) + { + if (empty($this->migrationQueries)) { + $this->migrationQueries = $updater->getSqlQueriesToExecute(); + } + return $this->migrationQueries; + } + + private function makeUpdaterInstance(OutputInterface $output) + { + $updater = new Updater(); + + $migrationQueryCount = count($this->getMigrationQueriesToExecute($updater)); + $updater->addUpdateObserver(new CliUpdateObserver($output, $migrationQueryCount)); - $output->writeln($content); + return $updater; } } diff --git a/plugins/CoreUpdater/Commands/Update/CliUpdateObserver.php b/plugins/CoreUpdater/Commands/Update/CliUpdateObserver.php new file mode 100644 index 0000000000000000000000000000000000000000..a8c95dc6047a56cdc42aa375d37bb99ba11dd76e --- /dev/null +++ b/plugins/CoreUpdater/Commands/Update/CliUpdateObserver.php @@ -0,0 +1,54 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\CoreUpdater\Commands\Update; + +use Piwik\Updater\UpdateObserver; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * UpdateObserver used to output progress of an update initiated on the command line. Prints the currently + * executing query and the total number of queries to run. + * + * @package CoreUpdater + */ +class CliUpdateObserver extends UpdateObserver +{ + /** + * @var OutputInterface + */ + private $output; + + /** + * @var int + */ + private $totalMigrationQueryCount; + + /** + * @var int + */ + private $currentMigrationQueryExecutionCount = 0; + + public function __construct(OutputInterface $output, $totalMigrationQueryCount) + { + $this->output = $output; + $this->totalMigrationQueryCount = $totalMigrationQueryCount; + } + + public function onStartExecutingMigrationQuery($updateFile, $sql) + { + $this->output->write(" Executing <comment>$sql</comment>... "); + + ++$this->currentMigrationQueryExecutionCount; + } + + public function onFinishedExecutingMigrationQuery($updateFile, $sql) + { + $this->output->writeln("Done. <info>[{$this->currentMigrationQueryExecutionCount} / {$this->totalMigrationQueryCount}]</info>"); + } +} \ No newline at end of file diff --git a/plugins/CoreUpdater/Controller.php b/plugins/CoreUpdater/Controller.php index 49cb1080a26c784503e4f691a07a871a1036d018..d15dc231e6178337233078d85a97d3f367e82b4b 100644 --- a/plugins/CoreUpdater/Controller.php +++ b/plugins/CoreUpdater/Controller.php @@ -110,7 +110,7 @@ class Controller extends \Piwik\Plugin\Controller return $view->render(); } - protected function redirectToDashboardWhenNoError($updater) + protected function redirectToDashboardWhenNoError(DbUpdater $updater) { if (count($updater->getSqlQueriesToExecute()) == 1 && !$this->coreError @@ -146,16 +146,15 @@ class Controller extends \Piwik\Plugin\Controller public function runUpdaterAndExit($doDryRun = null) { $updater = new DbUpdater(); - $componentsWithUpdateFile = CoreUpdater::getComponentUpdates($updater); + $componentsWithUpdateFile = $updater->getComponentUpdates(); if (empty($componentsWithUpdateFile)) { throw new NoUpdatesFoundException("Everything is already up to date."); } SettingsServer::setMaxExecutionTime(0); - $cli = Common::isPhpCliMode() ? '_cli' : ''; - $welcomeTemplate = '@CoreUpdater/runUpdaterAndExit_welcome' . $cli; - $doneTemplate = '@CoreUpdater/runUpdaterAndExit_done' . $cli; + $welcomeTemplate = '@CoreUpdater/runUpdaterAndExit_welcome'; + $doneTemplate = '@CoreUpdater/runUpdaterAndExit_done'; $viewWelcome = new View($welcomeTemplate); $this->addCustomLogoInfo($viewWelcome); @@ -176,21 +175,6 @@ class Controller extends \Piwik\Plugin\Controller return $viewWelcome->render(); } - // CLI - if (Common::isPhpCliMode()) { - $this->doWelcomeUpdates($viewWelcome, $componentsWithUpdateFile); - $output = $viewWelcome->render(); - - // Proceed with upgrade in CLI only if user specifically asked for it, or if running console command - $isUpdateRequested = Common::isRunningConsoleCommand() || Piwik::getModule() == 'CoreUpdater'; - - if (!$this->coreError && $isUpdateRequested) { - $this->doExecuteUpdates($viewDone, $updater, $componentsWithUpdateFile); - $output .= $viewDone->render(); - } - return $output; - } - // Web if ($doExecuteUpdates) { $this->warningMessages = array(); @@ -255,9 +239,9 @@ class Controller extends \Piwik\Plugin\Controller $view->coreToUpdate = $coreToUpdate; } - private function doExecuteUpdates($view, $updater, $componentsWithUpdateFile) + private function doExecuteUpdates($view, DbUpdater $updater, $componentsWithUpdateFile) { - $result = CoreUpdater::updateComponents($updater, $componentsWithUpdateFile); + $result = $updater->updateComponents($componentsWithUpdateFile); $this->coreError = $result['coreError']; $this->warningMessages = $result['warnings']; diff --git a/plugins/CoreUpdater/CoreUpdater.php b/plugins/CoreUpdater/CoreUpdater.php index f6f197db5df39de5d7cbaa50ebf3fe5510dc8ce5..eb5ce6c622e695efa31f6aefa7f00ef77a2cfe30 100644 --- a/plugins/CoreUpdater/CoreUpdater.php +++ b/plugins/CoreUpdater/CoreUpdater.php @@ -14,7 +14,6 @@ use Piwik\Common; use Piwik\Filesystem; use Piwik\FrontController; use Piwik\Piwik; -use Piwik\Columns\Updater as ColumnsUpdater; use Piwik\UpdateCheck; use Piwik\Updater; use Piwik\UpdaterErrorException; @@ -36,89 +35,20 @@ class CoreUpdater extends \Piwik\Plugin ); } + /** + * @deprecated + */ public static function updateComponents(Updater $updater, $componentsWithUpdateFile) { - $warnings = array(); - $errors = array(); - $deactivatedPlugins = array(); - $coreError = false; - - if (!empty($componentsWithUpdateFile)) { - $currentAccess = Access::getInstance(); - $hasSuperUserAccess = $currentAccess->hasSuperUserAccess(); - - if (!$hasSuperUserAccess) { - $currentAccess->setSuperUserAccess(true); - } - - // if error in any core update, show message + help message + EXIT - // if errors in any plugins updates, show them on screen, disable plugins that errored + CONTINUE - // if warning in any core update or in any plugins update, show message + CONTINUE - // if no error or warning, success message + CONTINUE - foreach ($componentsWithUpdateFile as $name => $filenames) { - try { - $warnings = array_merge($warnings, $updater->update($name)); - } catch (UpdaterErrorException $e) { - $errors[] = $e->getMessage(); - if ($name == 'core') { - $coreError = true; - break; - } elseif (\Piwik\Plugin\Manager::getInstance()->isPluginActivated($name)) { - \Piwik\Plugin\Manager::getInstance()->deactivatePlugin($name); - $deactivatedPlugins[] = $name; - } - } - } - - if (!$hasSuperUserAccess) { - $currentAccess->setSuperUserAccess(false); - } - } - - Filesystem::deleteAllCacheOnUpdate(); - - $result = array( - 'warnings' => $warnings, - 'errors' => $errors, - 'coreError' => $coreError, - 'deactivatedPlugins' => $deactivatedPlugins - ); - - /** - * Triggered after Piwik has been updated. - */ - Piwik::postEvent('CoreUpdater.update.end'); - - return $result; + return $updater->updateComponents($componentsWithUpdateFile); } + /** + * @deprecated + */ public static function getComponentUpdates(Updater $updater) { - $updater->addComponentToCheck('core', Version::VERSION); - $manager = \Piwik\Plugin\Manager::getInstance(); - $plugins = $manager->getLoadedPlugins(); - foreach ($plugins as $pluginName => $plugin) { - if ($manager->isPluginInstalled($pluginName)) { - $updater->addComponentToCheck($pluginName, $plugin->getVersion()); - } - } - - $columnsVersions = ColumnsUpdater::getAllVersions(); - foreach ($columnsVersions as $component => $version) { - $updater->addComponentToCheck($component, $version); - } - - $componentsWithUpdateFile = $updater->getComponentsWithUpdateFile(); - - if (count($componentsWithUpdateFile) == 0) { - ColumnsUpdater::onNoUpdateAvailable($columnsVersions); - - if (!$updater->hasNewVersion('core')) { - return null; - } - } - - return $componentsWithUpdateFile; + return $updater->getComponentUpdates(); } public function dispatch() @@ -127,12 +57,11 @@ class CoreUpdater extends \Piwik\Plugin $action = Common::getRequestVar('action', '', 'string'); $updater = new Updater(); - $updater->addComponentToCheck('core', Version::VERSION); - $updates = $updater->getComponentsWithNewVersion(); + $updates = $updater->getComponentsWithNewVersion(array('core' => Version::VERSION)); if (!empty($updates)) { Filesystem::deleteAllCacheOnUpdate(); } - if (self::getComponentUpdates($updater) !== null + if ($updater->getComponentUpdates() !== null && $module != 'CoreUpdater' // Proxy module is used to redirect users to piwik.org, should still work when Piwik must be updated && $module != 'Proxy' diff --git a/plugins/CoreUpdater/templates/runUpdaterAndExit_done_cli.twig b/plugins/CoreUpdater/templates/runUpdaterAndExit_done_cli.twig deleted file mode 100644 index bee8fadcbf12f8b9ce1a53d92f9b230e9dc91b2b..0000000000000000000000000000000000000000 --- a/plugins/CoreUpdater/templates/runUpdaterAndExit_done_cli.twig +++ /dev/null @@ -1,51 +0,0 @@ -{% autoescape false %} -{% set helpMessage %}{{- 'CoreUpdater_HelpMessageContent'|translate('[',']',"\n\n *") }}{% endset %} -{% if coreError %} - [X] {{ 'CoreUpdater_CriticalErrorDuringTheUpgradeProcess'|translate }} - - {% for message in errorMessages %} - * {{ message }} - {% endfor %} - - {{ 'CoreUpdater_HelpMessageIntroductionWhenError'|translate }} - - * {{ helpMessage }} - - {{ 'CoreUpdater_ErrorDIYHelp'|translate }} - * {{ 'CoreUpdater_ErrorDIYHelp_1'|translate }} - * {{ 'CoreUpdater_ErrorDIYHelp_2'|translate }} - * {{ 'CoreUpdater_ErrorDIYHelp_3'|translate }} - * {{ 'CoreUpdater_ErrorDIYHelp_4'|translate }} - * {{ 'CoreUpdater_ErrorDIYHelp_5'|translate }} - -{% else %} -{% if warningMessages|length > 0 %} - [!] {{ 'CoreUpdater_WarningMessages'|translate }} - - {% for message in warningMessages -%} - * {{ message }} - {%- endfor %} -{%- endif %} -{% if errorMessages|length > 0 -%} - - [X] {{ 'CoreUpdater_ErrorDuringPluginsUpdates'|translate }} - - {% for message in errorMessages %} - * {{ message }} - {% endfor %} - - {% if deactivatedPlugins|length > 0 -%} - {% set listOfDeactivatedPlugins %}{{ deactivatedPlugins|implode(', ') }}{% endset %} - - [!] {{ 'CoreUpdater_WeAutomaticallyDeactivatedTheFollowingPlugins'|translate(listOfDeactivatedPlugins) }} - {% endif %} -{% endif %} -{% if errorMessages|length > 0 or warningMessages|length > 0 %} - {{ 'CoreUpdater_HelpMessageIntroductionWhenWarning'|translate }} - - * {{ helpMessage }} -{% else %} - Done! -{% endif %} -{% endif %} -{% endautoescape %} diff --git a/plugins/CoreUpdater/templates/runUpdaterAndExit_welcome_cli.twig b/plugins/CoreUpdater/templates/runUpdaterAndExit_welcome_cli.twig deleted file mode 100644 index 33c21a15e32fc7bfd9f6e349994b2bd2666cd309..0000000000000000000000000000000000000000 --- a/plugins/CoreUpdater/templates/runUpdaterAndExit_welcome_cli.twig +++ /dev/null @@ -1,53 +0,0 @@ -{% autoescape false %} -{% set helpMessage %} -{{- 'CoreUpdater_HelpMessageContent'|translate('[',']','\n\n *') }} -{% endset %} - -*** {{ 'CoreUpdater_UpdateTitle'|translate }} *** -{% if coreError %} - - [X] {{ 'CoreUpdater_CriticalErrorDuringTheUpgradeProcess'|translate }} - - {% for message in errorMessages %} - {{- message }} - {% endfor %} - - {{ 'CoreUpdater_HelpMessageIntroductionWhenError'|translate }} - - * {{ helpMessage }} - -{% elseif coreToUpdate or pluginNamesToUpdate|length > 0 or dimensionsToUpdate|length > 0 %} - - {{ 'CoreUpdater_DatabaseUpgradeRequired'|translate }} - - {{ 'CoreUpdater_YourDatabaseIsOutOfDate'|translate }} - -{% if coreToUpdate %} - {{ 'CoreUpdater_PiwikWillBeUpgradedFromVersionXToVersionY'|translate(current_piwik_version, new_piwik_version) }} -{% endif %} - -{%- if pluginNamesToUpdate|length > 0 %} - {%- set listOfPlugins %}{{ pluginNamesToUpdate|implode(', ') }}{% endset %} - {{ 'CoreUpdater_TheFollowingPluginsWillBeUpgradedX'|translate( listOfPlugins) }} -{% endif %} - -{%- if dimensionsToUpdate|length > 0 %} - {%- set listOfDimensions %}{{ dimensionsToUpdate|implode(', ') }}{% endset %} - {{ 'CoreUpdater_TheFollowingDimensionsWillBeUpgradedX'|translate( listOfDimensions) }} -{% endif %} - -{# dry run #} -{% if queries is defined and queries is not empty %} -*** Note: this is a Dry Run *** - - {% for query in queries %}{{ query|trim }} - {% endfor %} - -*** End of Dry Run *** -{% else %} - {{ 'CoreUpdater_TheUpgradeProcessMayTakeAWhilePleaseBePatient'|translate }} -{% endif %} - -{% endif %} -{% endautoescape %} - diff --git a/plugins/CoreUpdater/tests/Integration/Commands/UpdateTest.php b/plugins/CoreUpdater/tests/Integration/Commands/UpdateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a3c97d86d8a8f6bbed7922fd7065a4ea50712a2f --- /dev/null +++ b/plugins/CoreUpdater/tests/Integration/Commands/UpdateTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Plugins\CoreUpdater\tests\Integration\Commands; + +use Piwik\Common; +use Piwik\Config; +use Piwik\DataAccess\ArchiveTableCreator; +use Piwik\Date; +use Piwik\Db; +use Piwik\DbHelper; +use Piwik\Option; +use Piwik\Tests\Framework\TestCase\ConsoleCommandTestCase; +use Piwik\Updates\Updates_2_10_0_b5; +use Piwik\Version; +use Symfony\Component\Console\Helper\QuestionHelper; + +require_once PIWIK_INCLUDE_PATH . '/core/Updates/2.10.0-b5.php'; + +/** + * @group CoreUpdater + * @group CoreUpdater_Integration + */ +class UpdateTest extends ConsoleCommandTestCase +{ + const VERSION_TO_UPDATE_FROM = '2.9.0'; + const EXPECTED_SQL_FROM_2_10 = "UPDATE report SET reports = REPLACE(reports, 'UserSettings_getBrowserVersion', 'DevicesDetection_getBrowserVersions');"; + + private $oldScriptName = null; + + public function setUp() + { + parent::setUp(); + + Config::getInstance()->setTestEnvironment(); + Option::set('version_core', self::VERSION_TO_UPDATE_FROM); + + $this->oldScriptName = $_SERVER['SCRIPT_NAME']; + $_SERVER['SCRIPT_NAME'] = $_SERVER['SCRIPT_NAME'] . " console"; // update won't execute w/o this, see Common::isRunningConsoleCommand() + + ArchiveTableCreator::clear(); + DbHelper::getTablesInstalled($forceReload = true); // force reload so internal cache in Mysql.php is refreshed + Updates_2_10_0_b5::$archiveBlobTables = null; + } + + public function tearDown() + { + $_SERVER['SCRIPT_NAME'] = $this->oldScriptName; + + parent::tearDown(); + } + + public function test_UpdateCommand_SuccessfullyExecutesUpdate() + { + $result = $this->applicationTester->run(array( + 'command' => 'core:update', + '--yes' => true + )); + + $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage()); + + $this->assertDryRunExecuted($this->applicationTester->getDisplay()); + + // make sure update went through + $this->assertEquals(Version::VERSION, Option::get('version_core')); + } + + public function test_UpdateCommand_DoesntExecuteSql_WhenUserSaysNo() + { + /** @var QuestionHelper $dialog */ + $dialog = $this->application->getHelperSet()->get('question'); + $dialog->setInputStream($this->getInputStream("N\n")); + + $result = $this->applicationTester->run(array( + 'command' => 'core:update' + )); + + $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage()); + + $this->assertDryRunExecuted($this->applicationTester->getDisplay()); + + // make sure update did not go through + $this->assertEquals(self::VERSION_TO_UPDATE_FROM, Option::get('version_core')); + } + + public function test_UpdateCommand_DoesNotExecuteUpdate_IfPiwikUpToDate() + { + Option::set('version_core', Version::VERSION); + + $result = $this->applicationTester->run(array( + 'command' => 'core:update', + '--yes' => true + )); + + $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage()); + + // check no update occurred + $this->assertContains("Everything is already up to date.", $this->applicationTester->getDisplay()); + $this->assertEquals(Version::VERSION, Option::get('version_core')); + } + + public function test_UpdateCommand_ReturnsCorrectExitCode_WhenErrorOccurs() + { + // create a blob table, then drop it manually so update 2.10.0-b10 will fail + $tableName = ArchiveTableCreator::getBlobTable(Date::factory('2015-01-01')); + Db::exec("DROP TABLE $tableName"); + + $result = $this->applicationTester->run(array( + 'command' => 'core:update', + '--yes' => true + )); + + $this->assertEquals(1, $result, $this->getCommandDisplayOutputErrorMessage()); + $this->assertContains("Piwik could not be updated! See above for more information.", $this->applicationTester->getDisplay()); + } + + private function assertDryRunExecuted($output) + { + $this->assertContains("Note: this is a Dry Run", $output); + $this->assertContains(self::EXPECTED_SQL_FROM_2_10, $output); + } +} \ No newline at end of file diff --git a/plugins/Installation/Controller.php b/plugins/Installation/Controller.php index 7641624cb38014904cea89e6dde288a603ad8ba2..9976f4ad58ae64e9ef678cc9569766ddbde51242 100644 --- a/plugins/Installation/Controller.php +++ b/plugins/Installation/Controller.php @@ -720,12 +720,12 @@ class Controller extends \Piwik\Plugin\ControllerAdmin return Access::doAsSuperUser(function () { $updater = new Updater(); - $componentsWithUpdateFile = CoreUpdater::getComponentUpdates($updater); + $componentsWithUpdateFile = $updater->getComponentUpdates(); if (empty($componentsWithUpdateFile)) { return false; } - $result = CoreUpdater::updateComponents($updater, $componentsWithUpdateFile); + $result = $updater->updateComponents($componentsWithUpdateFile); return $result; }); } diff --git a/tests/PHPUnit/Framework/Fixture.php b/tests/PHPUnit/Framework/Fixture.php index 302d1192e2cc84a9b8955370e0370f3753e74775..9d2fb8a2d7db19c6e61b0d5527c06779583a6e55 100644 --- a/tests/PHPUnit/Framework/Fixture.php +++ b/tests/PHPUnit/Framework/Fixture.php @@ -867,12 +867,12 @@ class Fixture extends \PHPUnit_Framework_Assert } $updater = new Updater(); - $componentsWithUpdateFile = CoreUpdater::getComponentUpdates($updater); + $componentsWithUpdateFile = $updater->getComponentUpdates(); if (empty($componentsWithUpdateFile)) { return false; } - $result = CoreUpdater::updateComponents($updater, $componentsWithUpdateFile); + $result = $updater->updateComponents($componentsWithUpdateFile); if (!empty($result['coreError']) || !empty($result['warnings']) || !empty($result['errors']) diff --git a/tests/PHPUnit/Framework/TestCase/ConsoleCommandTestCase.php b/tests/PHPUnit/Framework/TestCase/ConsoleCommandTestCase.php index 3a8d33c7a5453582ba4db58a4d31c64c3f5578bd..2da07556e1eb3ba8ecf8aa1565f351bec8a9cef1 100644 --- a/tests/PHPUnit/Framework/TestCase/ConsoleCommandTestCase.php +++ b/tests/PHPUnit/Framework/TestCase/ConsoleCommandTestCase.php @@ -61,4 +61,13 @@ class ConsoleCommandTestCase extends SystemTestCase { return "Command did not behave as expected. Command output: " . $this->applicationTester->getDisplay(); } + + protected function getInputStream($input) + { + $stream = fopen('php://memory', 'r+', false); + fputs($stream, $input); + rewind($stream); + + return $stream; + } } \ No newline at end of file diff --git a/tests/PHPUnit/Integration/Columns/UpdaterTest.php b/tests/PHPUnit/Integration/Columns/UpdaterTest.php new file mode 100644 index 0000000000000000000000000000000000000000..fe2a4caa898c486f0a304268e052591eb57ef1ce --- /dev/null +++ b/tests/PHPUnit/Integration/Columns/UpdaterTest.php @@ -0,0 +1,285 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Plugins\Test\Columns; + +use Piwik\Columns\Updater as ColumnsUpdater; +use Piwik\Common; +use Piwik\Db; +use Piwik\DbHelper; +use Piwik\Plugin\Dimension\ActionDimension; +use Piwik\Plugin\Dimension\ConversionDimension; +use Piwik\Plugin\Dimension\VisitDimension; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; +use Piwik\Updater; + +// NOTE: we can't use PHPUnit mock framework since we have to set columnName/columnType. reflection will set it, but +// for some reason, methods of base type don't see the set value. +class MockVisitDimension extends VisitDimension +{ + public function __construct($columnName, $columnType) + { + $this->columnName = $columnName; + $this->columnType = $columnType; + } +} + +class MockActionDimension extends ActionDimension +{ + public function __construct($columnName, $columnType) + { + $this->columnName = $columnName; + $this->columnType = $columnType; + } +} + +class MockConversionDimension extends ConversionDimension +{ + public function __construct($columnName, $columnType) + { + $this->columnName = $columnName; + $this->columnType = $columnType; + } +} + +/** + * @group Core + */ +class UpdaterTest extends IntegrationTestCase +{ + private $tableColumnsCache = array(); // for test performance + + /** + * @var ColumnsUpdater + */ + private $columnsUpdater; + + public function setUp() + { + parent::setUp(); + + // recreate log_visit/log_link_visit_action/log_conversion tables w/o any dimensions + $tablesToRecreate = array('log_visit', 'log_link_visit_action', 'log_conversion'); + foreach ($tablesToRecreate as $table) { + Db::exec("DROP TABLE `" . Common::prefixTable($table) . "`"); + + $tableCreateSql = DbHelper::getTableCreateSql($table); + Db::exec($tableCreateSql); + } + + $visitDimensions = array( + $this->getMockVisitDimension("test_visit_col_1", "INTEGER(10) UNSIGNED NOT NULL"), + $this->getMockVisitDimension("test_visit_col_2", "VARCHAR(32) NOT NULL") + ); + + $actionDimensions = array( + $this->getMockActionDimension("test_action_col_1", "VARCHAR(32) NOT NULL"), + $this->getMockActionDimension("test_action_col_2", "INTEGER(10) UNSIGNED DEFAULT NULL") + ); + + $conversionDimensions = array( + $this->getMockConversionDimension("test_conv_col_1", "FLOAT DEFAULT NULL"), + $this->getMockConversionDimension("test_conv_col_2", "VARCHAR(32) NOT NULL") + ); + + $this->columnsUpdater = new ColumnsUpdater($visitDimensions, $actionDimensions, $conversionDimensions); + + $this->tableColumnsCache = array(); + } + + public function test_getMigrationQueries_ReturnsCorrectQueries_IfDimensionIsNotInTable() + { + $updater = $this->getMockUpdater(); + $actualMigrationQueries = $this->columnsUpdater->getMigrationQueries($updater); + + $expectedMigrationQueries = array( + 'ALTER TABLE `log_visit` ADD COLUMN `test_visit_col_1` INTEGER(10) UNSIGNED NOT NULL, ADD COLUMN `test_visit_col_2` VARCHAR(32) NOT NULL' => array('1091', '1060'), + 'ALTER TABLE `log_link_visit_action` ADD COLUMN `test_action_col_1` VARCHAR(32) NOT NULL, ADD COLUMN `test_action_col_2` INTEGER(10) UNSIGNED DEFAULT NULL' => array('1091', '1060'), + 'ALTER TABLE `log_conversion` ADD COLUMN `test_conv_col_1` FLOAT DEFAULT NULL, ADD COLUMN `test_conv_col_2` VARCHAR(32) NOT NULL' => array('1091', '1060'), + ); + $this->assertEquals($expectedMigrationQueries, $actualMigrationQueries); + } + + public function test_getMigrationQueries_ReturnsCorrectQueries_IfDimensionIsInTable_ButHasNewVersion() + { + $this->addDimensionsToTables(); + + $updater = $this->getMockUpdater(); + $actualMigrationQueries = $this->columnsUpdater->getMigrationQueries($updater); + + $expectedMigrationQueries = array( + 'ALTER TABLE `log_visit` MODIFY COLUMN `test_visit_col_1` INTEGER(10) UNSIGNED NOT NULL, MODIFY COLUMN `test_visit_col_2` VARCHAR(32) NOT NULL' => array('1091', '1060'), + 'ALTER TABLE `log_link_visit_action` MODIFY COLUMN `test_action_col_1` VARCHAR(32) NOT NULL, MODIFY COLUMN `test_action_col_2` INTEGER(10) UNSIGNED DEFAULT NULL' => array('1091', '1060'), + 'ALTER TABLE `log_conversion` MODIFY COLUMN `test_conv_col_1` FLOAT DEFAULT NULL, MODIFY COLUMN `test_conv_col_2` VARCHAR(32) NOT NULL' => array('1091', '1060') + ); + $this->assertEquals($expectedMigrationQueries, $actualMigrationQueries); + } + + public function test_getMigrationQueries_ReturnsNoQueries_IfDimensionsAreInTable_ButHaveNoNewVersions() + { + $this->addDimensionsToTables(); + + $updater = $this->getMockUpdater($hasNewVersion = false); + $actualMigrationQueries = $this->columnsUpdater->getMigrationQueries($updater); + + $this->assertEquals(array(), $actualMigrationQueries); + } + + public function test_doUpdate_AddsDimensions_WhenDimensionsNotInTables() + { + $updater = $this->getMockUpdater(); + $this->columnsUpdater->doUpdate($updater); + + $this->assertDimensionsAddedToTables(); + } + + public function test_doUpdate_DoesNotError_WhenDimensionsAlreadyInTables() + { + $this->addDimensionsToTables(); + + $updater = $this->getMockUpdater(); + $this->columnsUpdater->doUpdate($updater); + + $this->assertDimensionsAddedToTables(); + } + + public function test_getAllVersions_ReturnsFileVersionsOfAllDimensions() + { + $updater = $this->getMockUpdater(); + $actualVersions = $this->columnsUpdater->getAllVersions($updater); + + $expectedVersions = array( + 'log_visit.test_visit_col_1' => 'INTEGER(10) UNSIGNED NOT NULL', + 'log_visit.test_visit_col_2' => 'VARCHAR(32) NOT NULL', + 'log_link_visit_action.test_action_col_1' => 'VARCHAR(32) NOT NULL', + 'log_link_visit_action.test_action_col_2' => 'INTEGER(10) UNSIGNED DEFAULT NULL', + 'log_conversion.test_conv_col_1' => 'FLOAT DEFAULT NULL', + 'log_conversion.test_conv_col_2' => 'VARCHAR(32) NOT NULL' + ); + $this->assertEquals($actualVersions, $expectedVersions); + } + + /** + * @dataProvider getCoreDimensionsForGetAllVersionsTest + */ + public function test_getAllVersions_ReturnsNoVersions_ForCoreDimensions_ThatWereRefactored_AndHaveNoDbVersion($table, $columnName, $columnType) + { + $this->addDimensionsToTables(); + $this->addDimensionToTable($table, $columnName, $columnType); + + $updater = $this->getMockUpdater(); + $actualVersions = $this->columnsUpdater->getAllVersions($updater); + + $expectedVersions = array( + 'log_visit.test_visit_col_1' => 'INTEGER(10) UNSIGNED NOT NULL', + 'log_visit.test_visit_col_2' => 'VARCHAR(32) NOT NULL', + 'log_link_visit_action.test_action_col_1' => 'VARCHAR(32) NOT NULL', + 'log_link_visit_action.test_action_col_2' => 'INTEGER(10) UNSIGNED DEFAULT NULL', + 'log_conversion.test_conv_col_1' => 'FLOAT DEFAULT NULL', + 'log_conversion.test_conv_col_2' => 'VARCHAR(32) NOT NULL' + ); + $this->assertEquals($actualVersions, $expectedVersions); + } + + public function getCoreDimensionsForGetAllVersionsTest() + { + // only one test per table. otherwise test will be too slow (~2 mins for all). + return array( + array('log_visit', 'user_id', 'VARCHAR(200) NULL'), + array('log_link_visit_action', 'idaction_event_category', 'INTEGER(10) UNSIGNED DEFAULT NULL'), + array('log_conversion', 'revenue_tax', 'float default NULL') + ); + } + + private function getMockVisitDimension($columnName, $columnType) + { + return new MockVisitDimension($columnName, $columnType); + } + + private function getMockActionDimension($columnName, $columnType) + { + return new MockActionDimension($columnName, $columnType); + } + + private function getMockConversionDimension($columnName, $columnType) + { + return new MockConversionDimension($columnName, $columnType); + } + + private function getMockUpdater($hasNewVersion = true) + { + $result = $this->getMock("Piwik\\Updater", array('hasNewVersion')); + + $result->expects($this->any())->method('hasNewVersion')->will($this->returnCallback(function () use ($hasNewVersion) { + return $hasNewVersion; + })); + + return $result; + } + + private function assertDimensionsAddedToTables() + { + $this->assertTableHasColumn('log_visit', 'test_visit_col_1', 'int(10) unsigned', $allowNull = false); + $this->assertTableHasColumn('log_visit', 'test_visit_col_2', 'varchar(32)', $allowNull = false); + + $this->assertTableHasColumn('log_link_visit_action', 'test_action_col_1', 'varchar(32)', $allowNull = false); + $this->assertTableHasColumn('log_link_visit_action', 'test_action_col_2', 'int(10) unsigned', $allowNull = true); + + $this->assertTableHasColumn('log_conversion', 'test_conv_col_1', 'float', $allowNull = true); + $this->assertTableHasColumn('log_conversion', 'test_conv_col_2', 'varchar(32)', $allowNull = false); + } + + private function assertTableHasColumn($table, $columnName, $columnType, $allowNull) + { + $column = $this->getTableColumnInfo($table, $columnName); + + $this->assertNotNull($column, "Column '$columnName' does not exist in '$table'."); + + $this->assertEquals(strtolower($columnType), strtolower($column['Type'])); + if ($allowNull) { + $this->assertEquals("yes", strtolower($column['Null'])); + } else { + $this->assertEquals("no", strtolower($column['Null'])); + } + } + + private function getTableColumns($table) + { + if (empty($this->tableColumnsCache[$table])) { + $this->tableColumnsCache[$table] = Db::fetchAll("SHOW COLUMNS IN `" . Common::prefixTable($table) . "`"); + } + return $this->tableColumnsCache[$table]; + } + + private function getTableColumnInfo($table, $columnName) + { + $columns = $this->getTableColumns($table); + foreach ($columns as $row) { + if ($row['Field'] == $columnName) { + return $row; + } + } + return null; + } + + private function addDimensionsToTables() + { + $this->addDimensionToTable('log_visit', 'test_visit_col_1', "INTEGER UNSIGNED NOT NULL"); + $this->addDimensionToTable('log_visit', 'test_visit_col_2', "VARCHAR(32) NOT NULL"); + + $this->addDimensionToTable('log_link_visit_action', 'test_action_col_1', "VARCHAR(32) NOT NULL"); + $this->addDimensionToTable('log_link_visit_action', 'test_action_col_2', "INTEGER(10) UNSIGNED DEFAULT NULL"); + + $this->addDimensionToTable('log_conversion', 'test_conv_col_1', "FLOAT DEFAULT NULL"); + $this->addDimensionToTable('log_conversion', 'test_conv_col_2', "VARCHAR(32) NOT NULL"); + } + + private function addDimensionToTable($table, $column, $type) + { + Db::exec("ALTER TABLE `" . Common::prefixTable($table) . "` ADD COLUMN $column $type"); + } +} \ No newline at end of file diff --git a/tests/PHPUnit/Integration/UpdaterTest.php b/tests/PHPUnit/Integration/UpdaterTest.php index c62210bde68f2642aba0ad9bfac86fd202f9e154..e49a54b08e20746ea8e15803121adb88035065bd 100644 --- a/tests/PHPUnit/Integration/UpdaterTest.php +++ b/tests/PHPUnit/Integration/UpdaterTest.php @@ -19,21 +19,17 @@ class UpdaterTest extends IntegrationTestCase { public function testUpdaterChecksCoreVersionAndDetectsUpdateFile() { - $updater = new Updater(); - $updater->pathUpdateFileCore = PIWIK_INCLUDE_PATH . '/tests/resources/Updater/core/'; - $updater->recordComponentSuccessfullyUpdated('core', '0.1'); - $updater->addComponentToCheck('core', '0.3'); - $componentsWithUpdateFile = $updater->getComponentsWithUpdateFile(); + $updater = new Updater(PIWIK_INCLUDE_PATH . '/tests/resources/Updater/core/'); + $updater->markComponentSuccessfullyUpdated('core', '0.1'); + $componentsWithUpdateFile = $updater->getComponentsWithUpdateFile(array('core' => '0.3')); $this->assertEquals(1, count($componentsWithUpdateFile)); } public function testUpdaterChecksGivenPluginVersionAndDetectsMultipleUpdateFileInOrder() { - $updater = new Updater(); - $updater->pathUpdateFilePlugins = PIWIK_INCLUDE_PATH . '/tests/resources/Updater/%s/'; - $updater->recordComponentSuccessfullyUpdated('testpluginUpdates', '0.1beta'); - $updater->addComponentToCheck('testpluginUpdates', '0.1'); - $componentsWithUpdateFile = $updater->getComponentsWithUpdateFile(); + $updater = new Updater($pathToCoreUpdates = null, PIWIK_INCLUDE_PATH . '/tests/resources/Updater/%s/'); + $updater->markComponentSuccessfullyUpdated('testpluginUpdates', '0.1beta'); + $componentsWithUpdateFile = $updater->getComponentsWithUpdateFile(array('testpluginUpdates' => '0.1')); $this->assertEquals(1, count($componentsWithUpdateFile)); $updateFiles = $componentsWithUpdateFile['testpluginUpdates']; @@ -49,17 +45,18 @@ class UpdaterTest extends IntegrationTestCase public function testUpdaterChecksCoreAndPluginCheckThatCoreIsRanFirst() { - $updater = new Updater(); - $updater->pathUpdateFilePlugins = PIWIK_INCLUDE_PATH . '/tests/resources/Updater/%s/'; - $updater->pathUpdateFileCore = PIWIK_INCLUDE_PATH . '/tests/resources/Updater/core/'; - - $updater->recordComponentSuccessfullyUpdated('testpluginUpdates', '0.1beta'); - $updater->addComponentToCheck('testpluginUpdates', '0.1'); + $updater = new Updater( + PIWIK_INCLUDE_PATH . '/tests/resources/Updater/core/', + PIWIK_INCLUDE_PATH . '/tests/resources/Updater/%s/' + ); - $updater->recordComponentSuccessfullyUpdated('core', '0.1'); - $updater->addComponentToCheck('core', '0.3'); + $updater->markComponentSuccessfullyUpdated('testpluginUpdates', '0.1beta'); + $updater->markComponentSuccessfullyUpdated('core', '0.1'); - $componentsWithUpdateFile = $updater->getComponentsWithUpdateFile(); + $componentsWithUpdateFile = $updater->getComponentsWithUpdateFile(array( + 'testpluginUpdates' => '0.1', + 'core' => '0.3' + )); $this->assertEquals(2, count($componentsWithUpdateFile)); reset($componentsWithUpdateFile); $this->assertEquals('core', key($componentsWithUpdateFile));