Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Updater.php 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. use Piwik\Columns\Updater as ColumnUpdater;
  11. /**
  12. * Load and execute all relevant, incremental update scripts for Piwik core and plugins, and bump the component version numbers for completed updates.
  13. *
  14. */
  15. class Updater
  16. {
  17. const INDEX_CURRENT_VERSION = 0;
  18. const INDEX_NEW_VERSION = 1;
  19. public $pathUpdateFileCore;
  20. public $pathUpdateFilePlugins;
  21. private $componentsToCheck = array();
  22. private $hasMajorDbUpdate = false;
  23. private $updatedClasses = array();
  24. /**
  25. * Constructor
  26. */
  27. public function __construct()
  28. {
  29. $this->pathUpdateFileCore = PIWIK_INCLUDE_PATH . '/core/Updates/';
  30. $this->pathUpdateFilePlugins = PIWIK_INCLUDE_PATH . '/plugins/%s/Updates/';
  31. ColumnUpdater::setUpdater($this);
  32. }
  33. /**
  34. * Add component to check
  35. *
  36. * @param string $name
  37. * @param string $version
  38. */
  39. public function addComponentToCheck($name, $version)
  40. {
  41. $this->componentsToCheck[$name] = $version;
  42. }
  43. /**
  44. * Record version of successfully completed component update
  45. *
  46. * @param string $name
  47. * @param string $version
  48. */
  49. public static function recordComponentSuccessfullyUpdated($name, $version)
  50. {
  51. try {
  52. Option::set(self::getNameInOptionTable($name), $version, $autoLoad = 1);
  53. } catch (\Exception $e) {
  54. // case when the option table is not yet created (before 0.2.10)
  55. }
  56. }
  57. /**
  58. * Retrieve the current version of a recorded component
  59. * @param string $name
  60. * @return false|string
  61. * @throws \Exception
  62. */
  63. public static function getCurrentRecordedComponentVersion($name)
  64. {
  65. try {
  66. $currentVersion = Option::get(self::getNameInOptionTable($name));
  67. } catch (\Exception $e) {
  68. // mysql error 1146: table doesn't exist
  69. if (Db::get()->isErrNo($e, '1146')) {
  70. // case when the option table is not yet created (before 0.2.10)
  71. $currentVersion = false;
  72. } else {
  73. // failed for some other reason
  74. throw $e;
  75. }
  76. }
  77. return $currentVersion;
  78. }
  79. /**
  80. * Returns the flag name to use in the option table to record current schema version
  81. * @param string $name
  82. * @return string
  83. */
  84. private static function getNameInOptionTable($name)
  85. {
  86. return 'version_' . $name;
  87. }
  88. /**
  89. * Returns a list of components (core | plugin) that need to run through the upgrade process.
  90. *
  91. * @return array( componentName => array( file1 => version1, [...]), [...])
  92. */
  93. public function getComponentsWithUpdateFile()
  94. {
  95. $this->componentsWithNewVersion = $this->getComponentsWithNewVersion();
  96. $this->componentsWithUpdateFile = $this->loadComponentsWithUpdateFile();
  97. return $this->componentsWithUpdateFile;
  98. }
  99. /**
  100. * Component has a new version?
  101. *
  102. * @param string $componentName
  103. * @return bool TRUE if compoment is to be updated; FALSE if not
  104. */
  105. public function hasNewVersion($componentName)
  106. {
  107. return isset($this->componentsWithNewVersion) &&
  108. isset($this->componentsWithNewVersion[$componentName]);
  109. }
  110. /**
  111. * Does one of the new versions involve a major database update?
  112. * Note: getSqlQueriesToExecute() must be called before this method!
  113. *
  114. * @return bool
  115. */
  116. public function hasMajorDbUpdate()
  117. {
  118. return $this->hasMajorDbUpdate;
  119. }
  120. /**
  121. * Returns the list of SQL queries that would be executed during the update
  122. *
  123. * @return array of SQL queries
  124. * @throws \Exception
  125. */
  126. public function getSqlQueriesToExecute()
  127. {
  128. $queries = array();
  129. $classNames = array();
  130. foreach ($this->componentsWithUpdateFile as $componentName => $componentUpdateInfo) {
  131. foreach ($componentUpdateInfo as $file => $fileVersion) {
  132. require_once $file; // prefixed by PIWIK_INCLUDE_PATH
  133. $className = $this->getUpdateClassName($componentName, $fileVersion);
  134. if (!class_exists($className, false)) {
  135. throw new \Exception("The class $className was not found in $file");
  136. }
  137. if (in_array($className, $classNames)) {
  138. continue; // prevent from getting updates from Piwik\Columns\Updater multiple times
  139. }
  140. $classNames[] = $className;
  141. $queriesForComponent = call_user_func(array($className, 'getSql'));
  142. foreach ($queriesForComponent as $query => $error) {
  143. $queries[] = $query . ';';
  144. }
  145. $this->hasMajorDbUpdate = $this->hasMajorDbUpdate || call_user_func(array($className, 'isMajorUpdate'));
  146. }
  147. // unfortunately had to extract this query from the Option class
  148. $queries[] = 'UPDATE `' . Common::prefixTable('option') . '` '.
  149. 'SET option_value = \'' . $fileVersion . '\' '.
  150. 'WHERE option_name = \'' . self::getNameInOptionTable($componentName) . '\';';
  151. }
  152. return $queries;
  153. }
  154. public function getUpdateClassName($componentName, $fileVersion)
  155. {
  156. $suffix = strtolower(str_replace(array('-', '.'), '_', $fileVersion));
  157. $className = 'Updates_' . $suffix;
  158. if ($componentName == 'core') {
  159. return '\\Piwik\\Updates\\' . $className;
  160. }
  161. if (ColumnUpdater::isDimensionComponent($componentName)) {
  162. return '\\Piwik\\Columns\\Updater';
  163. }
  164. return '\\Piwik\\Plugins\\' . $componentName . '\\' . $className;
  165. }
  166. /**
  167. * Update the named component
  168. *
  169. * @param string $componentName 'core', or plugin name
  170. * @throws \Exception|UpdaterErrorException
  171. * @return array of warning strings if applicable
  172. */
  173. public function update($componentName)
  174. {
  175. $warningMessages = array();
  176. foreach ($this->componentsWithUpdateFile[$componentName] as $file => $fileVersion) {
  177. try {
  178. require_once $file; // prefixed by PIWIK_INCLUDE_PATH
  179. $className = $this->getUpdateClassName($componentName, $fileVersion);
  180. if (!in_array($className, $this->updatedClasses) && class_exists($className, false)) {
  181. // update()
  182. call_user_func(array($className, 'update'));
  183. // makes sure to call Piwik\Columns\Updater only once as one call updates all dimensions at the same
  184. // time for better performance
  185. $this->updatedClasses[] = $className;
  186. }
  187. self::recordComponentSuccessfullyUpdated($componentName, $fileVersion);
  188. } catch (UpdaterErrorException $e) {
  189. throw $e;
  190. } catch (\Exception $e) {
  191. $warningMessages[] = $e->getMessage();
  192. }
  193. }
  194. // to debug, create core/Updates/X.php, update the core/Version.php, throw an Exception in the try, and comment the following line
  195. self::recordComponentSuccessfullyUpdated($componentName, $this->componentsWithNewVersion[$componentName][self::INDEX_NEW_VERSION]);
  196. return $warningMessages;
  197. }
  198. /**
  199. * Construct list of update files for the outdated components
  200. *
  201. * @return array( componentName => array( file1 => version1, [...]), [...])
  202. */
  203. private function loadComponentsWithUpdateFile()
  204. {
  205. $componentsWithUpdateFile = array();
  206. foreach ($this->componentsWithNewVersion as $name => $versions) {
  207. $currentVersion = $versions[self::INDEX_CURRENT_VERSION];
  208. $newVersion = $versions[self::INDEX_NEW_VERSION];
  209. if ($name == 'core') {
  210. $pathToUpdates = $this->pathUpdateFileCore . '*.php';
  211. } elseif (ColumnUpdater::isDimensionComponent($name)) {
  212. $componentsWithUpdateFile[$name][PIWIK_INCLUDE_PATH . '/core/Columns/Updater.php'] = $newVersion;
  213. } else {
  214. $pathToUpdates = sprintf($this->pathUpdateFilePlugins, $name) . '*.php';
  215. }
  216. if (!empty($pathToUpdates)) {
  217. $files = _glob($pathToUpdates);
  218. if ($files == false) {
  219. $files = array();
  220. }
  221. foreach ($files as $file) {
  222. $fileVersion = basename($file, '.php');
  223. if ( // if the update is from a newer version
  224. version_compare($currentVersion, $fileVersion) == -1
  225. // but we don't execute updates from non existing future releases
  226. && version_compare($fileVersion, $newVersion) <= 0
  227. ) {
  228. $componentsWithUpdateFile[$name][$file] = $fileVersion;
  229. }
  230. }
  231. }
  232. if (isset($componentsWithUpdateFile[$name])) {
  233. // order the update files by version asc
  234. uasort($componentsWithUpdateFile[$name], "version_compare");
  235. } else {
  236. // there are no update file => nothing to do, update to the new version is successful
  237. self::recordComponentSuccessfullyUpdated($name, $newVersion);
  238. }
  239. }
  240. return $componentsWithUpdateFile;
  241. }
  242. /**
  243. * Construct list of outdated components
  244. *
  245. * @throws \Exception
  246. * @return array array( componentName => array( oldVersion, newVersion), [...])
  247. */
  248. public function getComponentsWithNewVersion()
  249. {
  250. $componentsToUpdate = array();
  251. // we make sure core updates are processed before any plugin updates
  252. if (isset($this->componentsToCheck['core'])) {
  253. $coreVersions = $this->componentsToCheck['core'];
  254. unset($this->componentsToCheck['core']);
  255. $this->componentsToCheck = array_merge(array('core' => $coreVersions), $this->componentsToCheck);
  256. }
  257. $recordedCoreVersion = self::getCurrentRecordedComponentVersion('core');
  258. if ($recordedCoreVersion === false) {
  259. // This should not happen
  260. $recordedCoreVersion = Version::VERSION;
  261. self::recordComponentSuccessfullyUpdated('core', $recordedCoreVersion);
  262. }
  263. foreach ($this->componentsToCheck as $name => $version) {
  264. $currentVersion = self::getCurrentRecordedComponentVersion($name);
  265. if (ColumnUpdater::isDimensionComponent($name)) {
  266. $isComponentOutdated = $currentVersion !== $version;
  267. } else {
  268. // note: when versionCompare == 1, the version in the DB is newer, we choose to ignore
  269. $isComponentOutdated = version_compare($currentVersion, $version) == -1;
  270. }
  271. if ($isComponentOutdated || $currentVersion === false) {
  272. $componentsToUpdate[$name] = array(
  273. self::INDEX_CURRENT_VERSION => $currentVersion,
  274. self::INDEX_NEW_VERSION => $version
  275. );
  276. }
  277. }
  278. return $componentsToUpdate;
  279. }
  280. /**
  281. * Performs database update(s)
  282. *
  283. * @param string $file Update script filename
  284. * @param array $sqlarray An array of SQL queries to be executed
  285. * @throws UpdaterErrorException
  286. */
  287. static function updateDatabase($file, $sqlarray)
  288. {
  289. foreach ($sqlarray as $update => $ignoreError) {
  290. self::executeMigrationQuery($update, $ignoreError, $file);
  291. }
  292. }
  293. /**
  294. * Executes a database update query.
  295. *
  296. * @param string $updateSql Update SQL query.
  297. * @param int|false $errorToIgnore A MySQL error code to ignore.
  298. * @param string $file The Update file that's calling this method.
  299. */
  300. public static function executeMigrationQuery($updateSql, $errorToIgnore, $file)
  301. {
  302. try {
  303. Db::exec($updateSql);
  304. } catch (\Exception $e) {
  305. self::handleQueryError($e, $updateSql, $errorToIgnore, $file);
  306. }
  307. }
  308. /**
  309. * Handle an error that is thrown from a database query.
  310. *
  311. * @param \Exception $e the exception thrown.
  312. * @param string $updateSql Update SQL query.
  313. * @param int|false $errorToIgnore A MySQL error code to ignore.
  314. * @param string $file The Update file that's calling this method.
  315. */
  316. public static function handleQueryError($e, $updateSql, $errorToIgnore, $file)
  317. {
  318. if (($errorToIgnore === false)
  319. || !self::isDbErrorOneOf($e, $errorToIgnore)
  320. ) {
  321. $message = $file . ":\nError trying to execute the query '" . $updateSql . "'.\nThe error was: " . $e->getMessage();
  322. throw new UpdaterErrorException($message);
  323. }
  324. }
  325. /**
  326. * Returns whether an exception is a DB error with a code in the $errorCodesToIgnore list.
  327. *
  328. * @param int $error
  329. * @param int|int[] $errorCodesToIgnore
  330. */
  331. public static function isDbErrorOneOf($error, $errorCodesToIgnore)
  332. {
  333. $errorCodesToIgnore = is_array($errorCodesToIgnore) ? $errorCodesToIgnore : array($errorCodesToIgnore);
  334. foreach ($errorCodesToIgnore as $code) {
  335. if (Db::get()->isErrNo($error, $code)) {
  336. return true;
  337. }
  338. }
  339. return false;
  340. }
  341. }
  342. /**
  343. * Exception thrown by updater if a non-recoverable error occurs
  344. *
  345. */
  346. class UpdaterErrorException extends \Exception
  347. {
  348. }