pathUpdateFileCore = PIWIK_INCLUDE_PATH . '/core/Updates/'; $this->pathUpdateFilePlugins = PIWIK_INCLUDE_PATH . '/plugins/%s/Updates/'; } /** * Add component to check * * @param string $name * @param string $version */ public function addComponentToCheck($name, $version) { $this->componentsToCheck[$name] = $version; } /** * Record version of successfully completed component update * * @param string $name * @param string $version */ public static function recordComponentSuccessfullyUpdated($name, $version) { try { Option::set(self::getNameInOptionTable($name), $version, $autoLoad = 1); } catch (\Exception $e) { // case when the option table is not yet created (before 0.2.10) } } /** * 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; } /** * Returns a list of components (core | plugin) that need to run through the upgrade process. * * @return array( componentName => array( file1 => version1, [...]), [...]) */ public function getComponentsWithUpdateFile() { $this->componentsWithNewVersion = $this->getComponentsWithNewVersion(); $this->componentsWithUpdateFile = $this->loadComponentsWithUpdateFile(); return $this->componentsWithUpdateFile; } /** * Component has a new version? * * @param string $componentName * @return bool TRUE if compoment is to be updated; FALSE if not */ public function hasNewVersion($componentName) { return isset($this->componentsWithNewVersion) && isset($this->componentsWithNewVersion[$componentName]); } /** * Does one of the new versions involve a major database update? * Note: getSqlQueriesToExecute() must be called before this method! * * @return bool */ public function hasMajorDbUpdate() { return $this->hasMajorDbUpdate; } /** * Returns the list of SQL queries that would be executed during the update * * @return array of SQL queries * @throws \Exception */ public function getSqlQueriesToExecute() { $queries = array(); foreach ($this->componentsWithUpdateFile as $componentName => $componentUpdateInfo) { foreach ($componentUpdateInfo as $file => $fileVersion) { require_once $file; // prefixed by PIWIK_INCLUDE_PATH $className = $this->getUpdateClassName($componentName, $fileVersion); if (!class_exists($className, false)) { throw new \Exception("The class $className was not found in $file"); } $queriesForComponent = call_user_func(array($className, 'getSql')); foreach ($queriesForComponent as $query => $error) { $queries[] = $query . ';'; } $this->hasMajorDbUpdate = $this->hasMajorDbUpdate || call_user_func(array($className, 'isMajorUpdate')); } // unfortunately had to extract this query from the Option class $queries[] = 'UPDATE `' . Common::prefixTable('option') . '` SET option_value = \'' . $fileVersion . '\' WHERE option_name = \'' . self::getNameInOptionTable($componentName) . '\';'; } return $queries; } private function getUpdateClassName($componentName, $fileVersion) { $suffix = strtolower(str_replace(array('-', '.'), '_', $fileVersion)); $className = 'Updates_' . $suffix; if ($componentName == 'core') { return '\\Piwik\\Updates\\' . $className; } return '\\Piwik\\Plugins\\' . $componentName . '\\' . $className; } /** * Update the named component * * @param string $componentName 'core', or plugin name * @throws \Exception|UpdaterErrorException * @return array of warning strings if applicable */ public function update($componentName) { $warningMessages = array(); foreach ($this->componentsWithUpdateFile[$componentName] as $file => $fileVersion) { try { require_once $file; // prefixed by PIWIK_INCLUDE_PATH $className = $this->getUpdateClassName($componentName, $fileVersion); if (class_exists($className, false)) { // update() call_user_func(array($className, 'update')); } self::recordComponentSuccessfullyUpdated($componentName, $fileVersion); } catch (UpdaterErrorException $e) { throw $e; } catch (\Exception $e) { $warningMessages[] = $e->getMessage(); } } // 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]); return $warningMessages; } /** * Construct list of update files for the outdated components * * @return array( componentName => array( file1 => version1, [...]), [...]) */ private function loadComponentsWithUpdateFile() { $componentsWithUpdateFile = array(); foreach ($this->componentsWithNewVersion as $name => $versions) { $currentVersion = $versions[self::INDEX_CURRENT_VERSION]; $newVersion = $versions[self::INDEX_NEW_VERSION]; if ($name == 'core') { $pathToUpdates = $this->pathUpdateFileCore . '*.php'; } else { $pathToUpdates = sprintf($this->pathUpdateFilePlugins, $name) . '*.php'; } $files = _glob($pathToUpdates); if ($files == false) { $files = array(); } foreach ($files as $file) { $fileVersion = basename($file, '.php'); if ( // if the update is from a newer version version_compare($currentVersion, $fileVersion) == -1 // but we don't execute updates from non existing future releases && version_compare($fileVersion, $newVersion) <= 0 ) { $componentsWithUpdateFile[$name][$file] = $fileVersion; } } if (isset($componentsWithUpdateFile[$name])) { // order the update files by version asc 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); } } return $componentsWithUpdateFile; } /** * Construct list of outdated components * * @throws \Exception * @return array array( componentName => array( oldVersion, newVersion), [...]) */ public function getComponentsWithNewVersion() { $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); } foreach ($this->componentsToCheck as $name => $version) { try { $currentVersion = Option::get(self::getNameInOptionTable($name)); } catch (\Exception $e) { // mysql error 1146: table doesn't exist if (Db::get()->isErrNo($e, '1146')) { // case when the option table is not yet created (before 0.2.10) $currentVersion = false; } else { // failed for some other reason throw $e; } } if ($currentVersion === false) { if ($name === 'core') { // This should not happen $currentVersion = Version::VERSION; } else { // When plugins have been installed since Piwik 2.0 this should not happen // We "fix" the data for any plugin that may have been ported from Piwik 1.x $currentVersion = $version; } self::recordComponentSuccessfullyUpdated($name, $currentVersion); } $versionCompare = version_compare($currentVersion, $version); if ($versionCompare == -1) { $componentsToUpdate[$name] = array( self::INDEX_CURRENT_VERSION => $currentVersion, self::INDEX_NEW_VERSION => $version ); } else if ($versionCompare == 1) { // the version in the DB is newest.. we choose to ignore } } return $componentsToUpdate; } /** * Performs database update(s) * * @param string $file Update script filename * @param array $sqlarray An array of SQL queries to be executed * @throws UpdaterErrorException */ static function updateDatabase($file, $sqlarray) { foreach ($sqlarray as $update => $ignoreError) { try { Db::exec($update); } catch (\Exception $e) { if (($ignoreError === false) || !Db::get()->isErrNo($e, $ignoreError) ) { $message = $file . ":\nError trying to execute the query '" . $update . "'.\nThe error was: " . $e->getMessage(); throw new UpdaterErrorException($message); } } } } } /** * Exception thrown by updater if a non-recoverable error occurs * */ class UpdaterErrorException extends \Exception { }