335 lines
12 KiB
PHP
335 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Piwik - Open source web analytics
|
|
*
|
|
* @link http://piwik.org
|
|
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
|
*
|
|
*/
|
|
namespace Piwik;
|
|
|
|
/**
|
|
* Load and execute all relevant, incremental update scripts for Piwik core and plugins, and bump the component version numbers for completed updates.
|
|
*
|
|
*/
|
|
class Updater
|
|
{
|
|
const INDEX_CURRENT_VERSION = 0;
|
|
const INDEX_NEW_VERSION = 1;
|
|
|
|
public $pathUpdateFileCore;
|
|
public $pathUpdateFilePlugins;
|
|
private $componentsToCheck = array();
|
|
private $hasMajorDbUpdate = false;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->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) {
|
|
self::executeMigrationQuery($update, $ignoreError, $file);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes a database update query.
|
|
*
|
|
* @param string $updateSql Update SQL query.
|
|
* @param int|false $errorToIgnore A MySQL error code to ignore.
|
|
* @param string $file The Update file that's calling this method.
|
|
*/
|
|
public static function executeMigrationQuery($updateSql, $errorToIgnore, $file)
|
|
{
|
|
try {
|
|
Db::exec($updateSql);
|
|
} catch (\Exception $e) {
|
|
self::handleQueryError($e, $updateSql, $errorToIgnore, $file);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle an error that is thrown from a database query.
|
|
*
|
|
* @param \Exception $e the exception thrown.
|
|
* @param string $updateSql Update SQL query.
|
|
* @param int|false $errorToIgnore A MySQL error code to ignore.
|
|
* @param string $file The Update file that's calling this method.
|
|
*/
|
|
public static function handleQueryError($e, $updateSql, $errorToIgnore, $file)
|
|
{
|
|
if (($errorToIgnore === false)
|
|
|| !Db::get()->isErrNo($e, $errorToIgnore)
|
|
) {
|
|
$message = $file . ":\nError trying to execute the query '" . $updateSql . "'.\nThe error was: " . $e->getMessage();
|
|
throw new UpdaterErrorException($message);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exception thrown by updater if a non-recoverable error occurs
|
|
*
|
|
*/
|
|
class UpdaterErrorException extends \Exception
|
|
{
|
|
}
|