PageRenderTime 48ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/include/class.upgrader.php

https://gitlab.com/billyprice1/osTicket
PHP | 433 lines | 354 code | 37 blank | 42 comment | 20 complexity | a66ef3f4e77debf5c7257bc778b948b0 MD5 | raw file
  1. <?php
  2. /*********************************************************************
  3. class.upgrader.php
  4. osTicket Upgrader
  5. Peter Rotich <peter@osticket.com>
  6. Copyright (c) 2006-2013 osTicket
  7. http://www.osticket.com
  8. Released under the GNU General Public License WITHOUT ANY WARRANTY.
  9. See LICENSE.TXT for details.
  10. vim: expandtab sw=4 ts=4 sts=4:
  11. **********************************************************************/
  12. require_once INCLUDE_DIR.'class.setup.php';
  13. require_once INCLUDE_DIR.'class.migrater.php';
  14. class Upgrader {
  15. function __construct($prefix, $basedir) {
  16. global $ost;
  17. $this->streams = array();
  18. foreach (DatabaseMigrater::getUpgradeStreams($basedir) as $stream=>$hash) {
  19. $signature = $ost->getConfig()->getSchemaSignature($stream);
  20. $this->streams[$stream] = new StreamUpgrader($signature, $hash, $stream,
  21. $prefix, $basedir.$stream.'/', $this);
  22. }
  23. //Init persistent state of upgrade.
  24. $this->state = &$_SESSION['ost_upgrader']['state'];
  25. $this->mode = &$_SESSION['ost_upgrader']['mode'];
  26. $this->current = &$_SESSION['ost_upgrader']['stream'];
  27. if (!$this->current || $this->getCurrentStream()->isFinished()) {
  28. $streams = array_keys($this->streams);
  29. do {
  30. $this->current = array_shift($streams);
  31. } while ($this->current && $this->getCurrentStream()->isFinished());
  32. }
  33. }
  34. function getCurrentStream() {
  35. return $this->streams[$this->current];
  36. }
  37. function isUpgradable() {
  38. if ($this->isAborted())
  39. return false;
  40. foreach ($this->streams as $s)
  41. if (!$s->isUpgradable())
  42. return false;
  43. return true;
  44. }
  45. function isAborted() {
  46. return !strcasecmp($this->getState(), 'aborted');
  47. }
  48. function abort($msg, $debug=false) {
  49. if ($this->getCurrentStream())
  50. $this->getCurrentStream()->abort($msg, $debug);
  51. }
  52. function getState() {
  53. return $this->state;
  54. }
  55. function setState($state) {
  56. $this->state = $state;
  57. if ($state == 'done') {
  58. ModelMeta::flushModelCache();
  59. $this->createUpgradedTicket();
  60. }
  61. }
  62. function createUpgradedTicket() {
  63. global $cfg;
  64. $i18n = new Internationalization();
  65. $vars = $i18n->getTemplate('templates/ticket/upgraded.yaml')->getData();
  66. $vars['deptId'] = $cfg->getDefaultDeptId();
  67. //Create a ticket to make the system warm and happy.
  68. $errors = array();
  69. Ticket::create($vars, $errors, 'api', false, false);
  70. }
  71. function getMode() {
  72. return $this->mode;
  73. }
  74. function setMode($mode) {
  75. $this->mode = $mode;
  76. }
  77. function upgrade() {
  78. if (!$this->current)
  79. return true;
  80. return $this->getCurrentStream()->upgrade();
  81. }
  82. function __call($what, $args) {
  83. if ($this->getCurrentStream()) {
  84. $callable = array($this->getCurrentStream(), $what);
  85. if (!is_callable($callable))
  86. throw new Exception('InternalError: Upgrader method not callable: '
  87. . $what);
  88. return call_user_func_array($callable, $args);
  89. }
  90. }
  91. }
  92. /**
  93. * Updates a single database stream. In the classical sense, osTicket only
  94. * maintained a single database update stream. In that model, this
  95. * represents upgrading that single stream. In multi-stream mode,
  96. * customizations and plugins are supported to have their own respective
  97. * database update streams. The Upgrader class is used to coordinate updates
  98. * for all the streams, whereas the work to upgrade each stream is done in
  99. * this class
  100. */
  101. class StreamUpgrader extends SetupWizard {
  102. var $prefix;
  103. var $sqldir;
  104. var $signature;
  105. var $state;
  106. var $mode;
  107. var $phash;
  108. /**
  109. * Parameters:
  110. * schema_signature - (string<hash-hex>) Current database-reflected (via
  111. * config table) version of the stream
  112. * target - (stream<hash-hex>) Current stream tip, as reflected by
  113. * streams/<stream>.sig
  114. * stream - (string) Name of the stream (folder)
  115. * prefix - (string) Database table prefix
  116. * sqldir - (string<path>) Path of sql patches
  117. * upgrader - (Upgrader) Parent coordinator of parallel stream updates
  118. */
  119. function __construct($schema_signature, $target, $stream, $prefix, $sqldir, $upgrader) {
  120. $this->signature = $schema_signature;
  121. $this->target = $target;
  122. $this->prefix = $prefix;
  123. $this->sqldir = $sqldir;
  124. $this->errors = array();
  125. $this->mode = 'ajax'; //
  126. $this->upgrader = $upgrader;
  127. $this->name = $stream;
  128. //Disable time limit if - safe mode is set.
  129. if(!ini_get('safe_mode'))
  130. set_time_limit(0);
  131. //Init the task Manager.
  132. if(!isset($_SESSION['ost_upgrader'][$this->getShash()]))
  133. $_SESSION['ost_upgrader']['task'] = array();
  134. //Tasks to perform - saved on the session.
  135. $this->phash = &$_SESSION['ost_upgrader']['phash'];
  136. //Database migrater
  137. $this->migrater = null;
  138. }
  139. function check_prereq() {
  140. return (parent::check_prereq() && $this->check_mysql_version());
  141. }
  142. function onError($error) {
  143. global $ost, $thisstaff;
  144. $subject = '['.$this->name.']: '._S('Upgrader Error');
  145. $ost->logError($subject, $error);
  146. $this->setError($error);
  147. $this->upgrader->setState('aborted');
  148. //Alert staff upgrading the system - if the email is not same as admin's
  149. // admin gets alerted on error log (above)
  150. if(!$thisstaff || !strcasecmp($thisstaff->getEmail(), $ost->getConfig()->getAdminEmail()))
  151. return;
  152. $email=null;
  153. if(!($email=$ost->getConfig()->getAlertEmail()))
  154. $email=$ost->getConfig()->getDefaultEmail(); //will take the default email.
  155. if($email) {
  156. $email->sendAlert($thisstaff->getEmail(), $subject, $error);
  157. } else {//no luck - try the system mail.
  158. Mailer::sendmail($thisstaff->getEmail(), $subject, $error,
  159. '"'._S('osTicket Alerts')."\" <{$thisstaff->getEmail()}>");
  160. }
  161. }
  162. function isUpgradable() {
  163. return $this->getNextPatch();
  164. }
  165. function getSchemaSignature() {
  166. return $this->signature;
  167. }
  168. function getShash() {
  169. return substr($this->getSchemaSignature(), 0, 8);
  170. }
  171. function getTablePrefix() {
  172. return $this->prefix;
  173. }
  174. function getSQLDir() {
  175. return $this->sqldir;
  176. }
  177. function getMigrater() {
  178. if(!$this->migrater)
  179. $this->migrater = new DatabaseMigrater($this->signature, $this->target, $this->sqldir);
  180. return $this->migrater;
  181. }
  182. function getPatches() {
  183. $patches = array();
  184. if($this->getMigrater())
  185. $patches = $this->getMigrater()->getPatches();
  186. return $patches;
  187. }
  188. function getNextPatch() {
  189. return (($p=$this->getPatches()) && count($p)) ? $p[0] : false;
  190. }
  191. function getNextVersion() {
  192. if(!$patch=$this->getNextPatch())
  193. return __('(Latest)');
  194. $info = $this->readPatchInfo($patch);
  195. return $info['version'];
  196. }
  197. function isFinished() {
  198. # TODO: 1. Check if current and target hashes match,
  199. # 2. Any pending tasks
  200. return !($this->getNextPatch() || $this->getPendingTask());
  201. }
  202. function readPatchInfo($patch) {
  203. $info = $matches = $matches2 = array();
  204. if (preg_match(':/\*\*(.*)\*/:s', file_get_contents($patch), $matches)) {
  205. if (preg_match_all('/@([\w\d_-]+)\s+(.*)$/m', $matches[0],
  206. $matches2, PREG_SET_ORDER))
  207. foreach ($matches2 as $match)
  208. $info[$match[1]] = $match[2];
  209. }
  210. if (!isset($info['version']))
  211. $info['version'] = substr(basename($patch), 9, 8);
  212. return $info;
  213. }
  214. function getUpgradeSummary() {
  215. $summary = '';
  216. foreach ($this->getPatches() as $p) {
  217. $info = $this->readPatchInfo($p);
  218. $summary .= '<div class="patch">' . $info['version'];
  219. if (isset($info['title']))
  220. $summary .= ': <span class="patch-title">'.$info['title']
  221. .'</span>';
  222. $summary .= '</div>';
  223. }
  224. return $summary;
  225. }
  226. function getNextAction() {
  227. $action=sprintf(__('Upgrade osTicket to %s'), $this->getVersion());
  228. if($task=$this->getTask()) {
  229. $action = $task->getDescription() .' ('.$task->getStatus().')';
  230. } elseif($this->isUpgradable() && ($nextversion = $this->getNextVersion())) {
  231. $action = sprintf(__("Upgrade to %s"),$nextversion);
  232. }
  233. return '['.$this->name.'] '.$action;
  234. }
  235. function getPendingTask() {
  236. $pending=array();
  237. if (($task=$this->getTask()) && ($task instanceof MigrationTask))
  238. return ($task->isFinished()) ? 1 : 0;
  239. return false;
  240. }
  241. function getTask() {
  242. global $ost;
  243. $task_file = $this->getSQLDir() . "{$this->phash}.task.php";
  244. if (!file_exists($task_file))
  245. return null;
  246. if (!isset($this->task)) {
  247. $class = (include $task_file);
  248. if (!is_string($class) || !class_exists($class))
  249. return $ost->logError("Bogus migration task",
  250. "{$this->phash}:{$class}"); //FIXME: This can cause crash
  251. $this->task = new $class();
  252. if (isset($_SESSION['ost_upgrader']['task'][$this->phash]))
  253. $this->task->wakeup($_SESSION['ost_upgrader']['task'][$this->phash]);
  254. }
  255. return $this->task;
  256. }
  257. function doTask() {
  258. if(!($task = $this->getTask()))
  259. return false; //Nothing to do.
  260. $this->log(
  261. sprintf(_S('Upgrader - %s (task pending).'), $this->getShash()),
  262. sprintf(_S('The %s task reports there is work to do'),
  263. get_class($task))
  264. );
  265. if(!($max_time = ini_get('max_execution_time')))
  266. $max_time = 30; //Default to 30 sec batches.
  267. // Drop any model meta cache to ensure model changes do not cause
  268. // crashes
  269. ModelMeta::flushModelCache();
  270. $task->run($max_time);
  271. if (!$task->isFinished()) {
  272. $_SESSION['ost_upgrader']['task'][$this->phash] = $task->sleep();
  273. return true;
  274. }
  275. // Run the cleanup script, if any, and destroy the task's session
  276. // data
  277. $this->cleanup();
  278. unset($_SESSION['ost_upgrader']['task'][$this->phash]);
  279. $this->phash = null;
  280. unset($this->task);
  281. return false;
  282. }
  283. function upgrade() {
  284. global $ost;
  285. if($this->getPendingTask() || !($patches=$this->getPatches()))
  286. return false;
  287. $start_time = Misc::micro_time();
  288. if(!($max_time = ini_get('max_execution_time')))
  289. $max_time = 300; //Apache/IIS defaults.
  290. // Apply up to five patches at a time
  291. foreach (array_slice($patches, 0, 5) as $patch) {
  292. //TODO: check time used vs. max execution - break if need be
  293. if (!$this->load_sql_file($patch, $this->getTablePrefix()))
  294. return false;
  295. //clear previous patch info -
  296. unset($_SESSION['ost_upgrader'][$this->getShash()]);
  297. $phash = substr(basename($patch), 0, 17);
  298. $shash = substr($phash, 9, 8);
  299. //Log the patch info
  300. $logMsg = sprintf(_S("Patch %s applied successfully"), $phash);
  301. if(($info = $this->readPatchInfo($patch)) && $info['version'])
  302. $logMsg.= ' ('.$info['version'].') ';
  303. $this->log(sprintf(_S("Upgrader - %s applied"), $shash), $logMsg);
  304. $this->signature = $shash; //Update signature to the *new* HEAD
  305. $this->phash = $phash;
  306. //Break IF elapsed time is greater than 80% max time allowed.
  307. if (!($task=$this->getTask())) {
  308. $this->cleanup();
  309. if (($elapsedtime=(Misc::micro_time()-$start_time))
  310. && $max_time && $elapsedtime>($max_time*0.80))
  311. break;
  312. else
  313. // Apply the next patch
  314. continue;
  315. }
  316. //We have work to do... set the tasks and break.
  317. $_SESSION['ost_upgrader'][$shash]['state'] = 'upgrade';
  318. break;
  319. }
  320. //Reset the migrater
  321. $this->migrater = null;
  322. return true;
  323. }
  324. function log($title, $message, $level=LOG_DEBUG) {
  325. global $ost;
  326. // Never alert the admin, and force the write to the database
  327. $ost->log($level, $title, $message, false, true);
  328. }
  329. /************* TASKS **********************/
  330. function cleanup() {
  331. $file = $this->getSQLDir().$this->phash.'.cleanup.sql';
  332. if(!file_exists($file)) //No cleanup script.
  333. return 0;
  334. //We have a cleanup script ::XXX: Don't abort on error?
  335. if($this->load_sql_file($file, $this->getTablePrefix(), false, true)) {
  336. $this->log(sprintf(_S("Upgrader - %s cleanup"), $this->phash),
  337. sprintf(_S("Applied cleanup script %s"), $file));
  338. return 0;
  339. }
  340. $this->log(_S('Upgrader'), sprintf(_S("%s: Unable to process cleanup file"),
  341. $this->phash));
  342. return 0;
  343. }
  344. }
  345. ?>