UpgradeService.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. <?php
  2. namespace Elgg;
  3. /**
  4. * Upgrade service for Elgg
  5. *
  6. * This is a straight port of the procedural code used for upgrading before
  7. * Elgg 1.9.
  8. *
  9. * @access private
  10. *
  11. * @package Elgg.Core
  12. * @subpackage Upgrade
  13. */
  14. class UpgradeService {
  15. /**
  16. * Global Elgg configuration
  17. *
  18. * @var \stdClass
  19. */
  20. private $CONFIG;
  21. /**
  22. * Constructor
  23. */
  24. public function __construct() {
  25. global $CONFIG;
  26. $this->CONFIG = $CONFIG;
  27. }
  28. /**
  29. * Run the upgrade process
  30. *
  31. * @return array
  32. */
  33. public function run() {
  34. $result = array(
  35. 'failure' => false,
  36. 'reason' => '',
  37. );
  38. // prevent someone from running the upgrade script in parallel (see #4643)
  39. if (!$this->getUpgradeMutex()) {
  40. $result['failure'] = true;
  41. $result['reason'] = _elgg_services()->translator->translate('upgrade:locked');
  42. return $result;
  43. }
  44. // disable the system log for upgrades to avoid exceptions when the schema changes.
  45. _elgg_services()->events->unregisterHandler('log', 'systemlog', 'system_log_default_logger');
  46. _elgg_services()->events->unregisterHandler('all', 'all', 'system_log_listener');
  47. // turn off time limit
  48. set_time_limit(0);
  49. if ($this->getUnprocessedUpgrades()) {
  50. $this->processUpgrades();
  51. }
  52. _elgg_services()->events->trigger('upgrade', 'system', null);
  53. elgg_flush_caches();
  54. $this->releaseUpgradeMutex();
  55. return $result;
  56. }
  57. /**
  58. * Run any php upgrade scripts which are required
  59. *
  60. * @param int $version Version upgrading from.
  61. * @param bool $quiet Suppress errors. Don't use this.
  62. *
  63. * @return bool
  64. */
  65. protected function upgradeCode($version, $quiet = false) {
  66. $version = (int) $version;
  67. $upgrade_path = _elgg_services()->config->get('path') . 'engine/lib/upgrades/';
  68. $processed_upgrades = $this->getProcessedUpgrades();
  69. // upgrading from 1.7 to 1.8. Need to bootstrap.
  70. if (!$processed_upgrades) {
  71. $this->bootstrap17to18();
  72. // grab accurate processed upgrades
  73. $processed_upgrades = $this->getProcessedUpgrades();
  74. }
  75. $upgrade_files = $this->getUpgradeFiles($upgrade_path);
  76. if ($upgrade_files === false) {
  77. return false;
  78. }
  79. $upgrades = $this->getUnprocessedUpgrades($upgrade_files, $processed_upgrades);
  80. // Sort and execute
  81. sort($upgrades);
  82. foreach ($upgrades as $upgrade) {
  83. $upgrade_version = $this->getUpgradeFileVersion($upgrade);
  84. $success = true;
  85. if ($upgrade_version <= $version) {
  86. // skip upgrade files from before the installation version of Elgg
  87. // because the upgrade files from before the installation version aren't
  88. // added to the database.
  89. continue;
  90. }
  91. // hide all errors.
  92. if ($quiet) {
  93. // hide include errors as well as any exceptions that might happen
  94. try {
  95. if (!@self::includeCode("$upgrade_path/$upgrade")) {
  96. $success = false;
  97. error_log("Could not include $upgrade_path/$upgrade");
  98. }
  99. } catch (\Exception $e) {
  100. $success = false;
  101. error_log($e->getMessage());
  102. }
  103. } else {
  104. if (!self::includeCode("$upgrade_path/$upgrade")) {
  105. $success = false;
  106. error_log("Could not include $upgrade_path/$upgrade");
  107. }
  108. }
  109. if ($success) {
  110. // don't set the version to a lower number in instances where an upgrade
  111. // has been merged from a lower version of Elgg
  112. if ($upgrade_version > $version) {
  113. _elgg_services()->datalist->set('version', $upgrade_version);
  114. }
  115. // incrementally set upgrade so we know where to start if something fails.
  116. $this->setProcessedUpgrade($upgrade);
  117. } else {
  118. return false;
  119. }
  120. }
  121. return true;
  122. }
  123. /**
  124. * PHP include a file with a very limited scope
  125. *
  126. * @param string $file File path to include
  127. * @return mixed
  128. */
  129. protected static function includeCode($file) {
  130. // do not remove - some upgrade scripts depend on this
  131. global $CONFIG;
  132. return include $file;
  133. }
  134. /**
  135. * Saves a processed upgrade to a dataset.
  136. *
  137. * @param string $upgrade Filename of the processed upgrade
  138. * (not the path, just the file)
  139. * @return bool
  140. */
  141. protected function setProcessedUpgrade($upgrade) {
  142. $processed_upgrades = $this->getProcessedUpgrades();
  143. $processed_upgrades[] = $upgrade;
  144. $processed_upgrades = array_unique($processed_upgrades);
  145. return _elgg_services()->datalist->set('processed_upgrades', serialize($processed_upgrades));
  146. }
  147. /**
  148. * Gets a list of processes upgrades
  149. *
  150. * @return mixed Array of processed upgrade filenames or false
  151. */
  152. protected function getProcessedUpgrades() {
  153. $upgrades = _elgg_services()->datalist->get('processed_upgrades');
  154. $unserialized = unserialize($upgrades);
  155. return $unserialized;
  156. }
  157. /**
  158. * Returns the version of the upgrade filename.
  159. *
  160. * @param string $filename The upgrade filename. No full path.
  161. * @return int|false
  162. * @since 1.8.0
  163. */
  164. protected function getUpgradeFileVersion($filename) {
  165. preg_match('/^([0-9]{10})([\.a-z0-9-_]+)?\.(php)$/i', $filename, $matches);
  166. if (isset($matches[1])) {
  167. return (int) $matches[1];
  168. }
  169. return false;
  170. }
  171. /**
  172. * Returns a list of upgrade files relative to the $upgrade_path dir.
  173. *
  174. * @param string $upgrade_path The up
  175. * @return array|false
  176. */
  177. protected function getUpgradeFiles($upgrade_path = null) {
  178. if (!$upgrade_path) {
  179. $upgrade_path = _elgg_services()->config->get('path') . 'engine/lib/upgrades/';
  180. }
  181. $upgrade_path = sanitise_filepath($upgrade_path);
  182. $handle = opendir($upgrade_path);
  183. if (!$handle) {
  184. return false;
  185. }
  186. $upgrade_files = array();
  187. while ($upgrade_file = readdir($handle)) {
  188. // make sure this is a wellformed upgrade.
  189. if (is_dir($upgrade_path . '$upgrade_file')) {
  190. continue;
  191. }
  192. $upgrade_version = $this->getUpgradeFileVersion($upgrade_file);
  193. if (!$upgrade_version) {
  194. continue;
  195. }
  196. $upgrade_files[] = $upgrade_file;
  197. }
  198. sort($upgrade_files);
  199. return $upgrade_files;
  200. }
  201. /**
  202. * Checks if any upgrades need to be run.
  203. *
  204. * @param null|array $upgrade_files Optional upgrade files
  205. * @param null|array $processed_upgrades Optional processed upgrades
  206. *
  207. * @return array
  208. */
  209. protected function getUnprocessedUpgrades($upgrade_files = null, $processed_upgrades = null) {
  210. if ($upgrade_files === null) {
  211. $upgrade_files = $this->getUpgradeFiles();
  212. }
  213. if ($processed_upgrades === null) {
  214. $processed_upgrades = unserialize(_elgg_services()->datalist->get('processed_upgrades'));
  215. if (!is_array($processed_upgrades)) {
  216. $processed_upgrades = array();
  217. }
  218. }
  219. $unprocessed = array_diff($upgrade_files, $processed_upgrades);
  220. return $unprocessed;
  221. }
  222. /**
  223. * Upgrades Elgg Database and code
  224. *
  225. * @return bool
  226. */
  227. protected function processUpgrades() {
  228. $dbversion = (int) _elgg_services()->datalist->get('version');
  229. // No version number? Oh snap...this is an upgrade from a clean installation < 1.7.
  230. // Run all upgrades without error reporting and hope for the best.
  231. // See https://github.com/elgg/elgg/issues/1432 for more.
  232. $quiet = !$dbversion;
  233. // Note: Database upgrades are deprecated as of 1.8. Use code upgrades. See #1433
  234. if ($this->dbUpgrade($dbversion, '', $quiet)) {
  235. system_message(_elgg_services()->translator->translate('upgrade:db'));
  236. }
  237. if ($this->upgradeCode($dbversion, $quiet)) {
  238. system_message(_elgg_services()->translator->translate('upgrade:core'));
  239. // Now we trigger an event to give the option for plugins to do something
  240. $upgrade_details = new \stdClass;
  241. $upgrade_details->from = $dbversion;
  242. $upgrade_details->to = elgg_get_version();
  243. _elgg_services()->events->trigger('upgrade', 'upgrade', $upgrade_details);
  244. return true;
  245. }
  246. return false;
  247. }
  248. /**
  249. * Boot straps into 1.8 upgrade system from 1.7
  250. *
  251. * This runs all the 1.7 upgrades, then sets the processed_upgrades to all existing 1.7 upgrades.
  252. * Control is then passed back to the main upgrade function which detects and runs the
  253. * 1.8 upgrades, regardless of filename convention.
  254. *
  255. * @return bool
  256. */
  257. protected function bootstrap17to18() {
  258. $db_version = (int) _elgg_services()->datalist->get('version');
  259. // the 1.8 upgrades before the upgrade system change that are interspersed with 1.7 upgrades.
  260. $upgrades_18 = array(
  261. '2010111501.php',
  262. '2010121601.php',
  263. '2010121602.php',
  264. '2010121701.php',
  265. '2010123101.php',
  266. '2011010101.php',
  267. );
  268. $upgrade_files = $this->getUpgradeFiles();
  269. $processed_upgrades = array();
  270. foreach ($upgrade_files as $upgrade_file) {
  271. // ignore if not in 1.7 format or if it's a 1.8 upgrade
  272. if (in_array($upgrade_file, $upgrades_18) || !preg_match("/[0-9]{10}\.php/", $upgrade_file)) {
  273. continue;
  274. }
  275. $upgrade_version = $this->getUpgradeFileVersion($upgrade_file);
  276. // this has already been run in a previous 1.7.X -> 1.7.X upgrade
  277. if ($upgrade_version < $db_version) {
  278. $this->setProcessedUpgrade($upgrade_file);
  279. }
  280. }
  281. }
  282. /**
  283. * Creates a table {prefix}upgrade_lock that is used as a mutex for upgrades.
  284. *
  285. * @return bool
  286. */
  287. protected function getUpgradeMutex() {
  288. if (!$this->isUpgradeLocked()) {
  289. // lock it
  290. _elgg_services()->db->insertData("create table {$this->CONFIG->dbprefix}upgrade_lock (id INT)");
  291. _elgg_services()->logger->notice('Locked for upgrade.');
  292. return true;
  293. }
  294. _elgg_services()->logger->warn('Cannot lock for upgrade: already locked');
  295. return false;
  296. }
  297. /**
  298. * Unlocks upgrade.
  299. *
  300. * @return void
  301. */
  302. public function releaseUpgradeMutex() {
  303. _elgg_services()->db->deleteData("drop table {$this->CONFIG->dbprefix}upgrade_lock");
  304. _elgg_services()->logger->notice('Upgrade unlocked.');
  305. }
  306. /**
  307. * Checks if upgrade is locked
  308. *
  309. * @return bool
  310. */
  311. public function isUpgradeLocked() {
  312. $is_locked = count(_elgg_services()->db->getData("SHOW TABLES LIKE '{$this->CONFIG->dbprefix}upgrade_lock'"));
  313. return (bool)$is_locked;
  314. }
  315. /**
  316. * ***************************************************************************
  317. * NOTE: If this is ever removed from Elgg, sites lose the ability to upgrade
  318. * from 1.7.x and earlier to the latest version of Elgg without upgrading to
  319. * 1.8 first.
  320. * ***************************************************************************
  321. *
  322. * Upgrade the database schema in an ordered sequence.
  323. *
  324. * Executes all upgrade files in elgg/engine/schema/upgrades/ in sequential order.
  325. * Upgrade files must be in the standard Elgg release format of YYYYMMDDII.sql
  326. * where II is an incrementor starting from 01.
  327. *
  328. * Files that are < $version will be ignored.
  329. *
  330. * @param int $version The version you are upgrading from in the format YYYYMMDDII.
  331. * @param string $fromdir Optional directory to load upgrades from. default: engine/schema/upgrades/
  332. * @param bool $quiet If true, suppress all error messages. Only use for the upgrade from <=1.6.
  333. *
  334. * @return int The number of upgrades run.
  335. * @deprecated 1.8 Use PHP upgrades for sql changes.
  336. */
  337. protected function dbUpgrade($version, $fromdir = "", $quiet = false) {
  338. $version = (int) $version;
  339. if (!$fromdir) {
  340. $fromdir = $this->CONFIG->path . 'engine/schema/upgrades/';
  341. }
  342. $i = 0;
  343. if ($handle = opendir($fromdir)) {
  344. $sqlupgrades = array();
  345. while ($sqlfile = readdir($handle)) {
  346. if (!is_dir($fromdir . $sqlfile)) {
  347. if (preg_match('/^([0-9]{10})\.(sql)$/', $sqlfile, $matches)) {
  348. $sql_version = (int) $matches[1];
  349. if ($sql_version > $version) {
  350. $sqlupgrades[] = $sqlfile;
  351. }
  352. }
  353. }
  354. }
  355. asort($sqlupgrades);
  356. if (sizeof($sqlupgrades) > 0) {
  357. foreach ($sqlupgrades as $sqlfile) {
  358. // hide all errors.
  359. if ($quiet) {
  360. try {
  361. _elgg_services()->db->runSqlScript($fromdir . $sqlfile);
  362. } catch (\DatabaseException $e) {
  363. error_log($e->getmessage());
  364. }
  365. } else {
  366. _elgg_services()->db->runSqlScript($fromdir . $sqlfile);
  367. }
  368. $i++;
  369. }
  370. }
  371. }
  372. return $i;
  373. }
  374. }