Update.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. <?php
  2. /**
  3. * This file is part of the ForkBB <https://github.com/forkbb>.
  4. *
  5. * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
  6. * @license The MIT License (MIT)
  7. */
  8. declare(strict_types=1);
  9. namespace ForkBB\Models\Pages\Admin;
  10. use ForkBB\Core\Config as CoreConfig;
  11. use ForkBB\Core\Container;
  12. use ForkBB\Core\Validator;
  13. use ForkBB\Models\Page;
  14. use ForkBB\Models\Pages\Admin;
  15. use PDO;
  16. use PDOException;
  17. use RuntimeException;
  18. use ForkBB\Core\Exceptions\ForkException;
  19. use function \ForkBB\__;
  20. class Update extends Admin
  21. {
  22. const PHP_MIN = '7.3.0';
  23. const REV_MIN_FOR_UPDATE = 42;
  24. const LATEST_REV_WITH_DB_CHANGES = 43;
  25. const LOCK_NAME = 'lock_update';
  26. const LOCk_TTL = 1800;
  27. const JSON_OPTIONS = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR;
  28. const CONFIG_FILE = 'main.php';
  29. protected $configFile;
  30. public function __construct(Container $container)
  31. {
  32. parent::__construct($container);
  33. $container->Lang->load('validator');
  34. $container->Lang->load('admin_update');
  35. $this->aIndex = 'update';
  36. $this->httpStatus = 503;
  37. $this->onlinePos = null;
  38. $this->nameTpl = 'admin/form';
  39. $this->titleForm = 'Update ForkBB';
  40. $this->classForm = ['updateforkbb'];
  41. $this->configFile = $container->DIR_APP . '/config/' . self::CONFIG_FILE;
  42. $this->header('Retry-After', '3600');
  43. }
  44. /**
  45. * Подготовка страницы к отображению
  46. */
  47. public function prepare(): void
  48. {
  49. $this->aNavigation = $this->aNavigation();
  50. $this->crumbs = $this->crumbs(...$this->aCrumbs);
  51. }
  52. /**
  53. * Возвращает массив ссылок с описанием для построения навигации админки
  54. */
  55. protected function aNavigation(): array
  56. {
  57. return [
  58. 'update' => [
  59. $this->c->Router->link('AdminUpdate'),
  60. __('Update ForkBB'),
  61. ],
  62. ];
  63. }
  64. /**
  65. * Возвращает страницу обслуживания с доп.сообщением
  66. */
  67. protected function returnMaintenance(bool $isStage = true): Page
  68. {
  69. $maintenance = $this->c->Maintenance;
  70. $maintenance->fIswev = ['w', 'Update script is running'];
  71. if ($isStage) {
  72. $maintenance->fIswev = ['e', 'Script runs error'];
  73. }
  74. return $maintenance;
  75. }
  76. /**
  77. * Проверяет наличие блокировки скрипта обновления
  78. */
  79. protected function hasLock(string $uid = null): bool
  80. {
  81. $lock = $this->c->Cache->get(self::LOCK_NAME);
  82. if (null === $uid) {
  83. return ! empty($lock);
  84. } else {
  85. return empty($lock) || ! \hash_equals($uid, (string) $lock);
  86. }
  87. }
  88. protected function setLock(string $uid = null): ?string
  89. {
  90. if (true === $this->hasLock($uid)) {
  91. return null;
  92. }
  93. if (null === $uid) {
  94. $uid = $this->c->Secury->randomHash(33);
  95. }
  96. $this->c->Cache->set(self::LOCK_NAME, $uid, self::LOCk_TTL);
  97. if (true === $this->hasLock($uid)) {
  98. return null;
  99. }
  100. return $uid;
  101. }
  102. /**
  103. * Подготавливает данные для страницы обновления форума
  104. */
  105. public function view(array $args, string $method): Page
  106. {
  107. if (true === $this->hasLock()) {
  108. return $this->returnMaintenance(false);
  109. }
  110. if (
  111. 'POST' === $method
  112. && empty($this->fIswev)
  113. ) {
  114. $v = $this->c->Validator->reset()
  115. ->addValidators([
  116. 'check_pass' => [$this, 'vCheckPass'],
  117. ])->addRules([
  118. 'token' => 'token:AdminUpdate',
  119. 'dbpass' => 'required|string:trim|check_pass',
  120. 'o_maintenance_message' => 'required|string:trim|max:65000 bytes|html',
  121. ])->addAliases([
  122. 'dbpass' => 'Database password',
  123. 'o_maintenance_message' => 'Maintenance message',
  124. ])->addMessages([
  125. ]);
  126. if ($v->validation($_POST)) {
  127. $e = null;
  128. // версия PHP
  129. if (
  130. null === $e
  131. && \version_compare(\PHP_VERSION, self::PHP_MIN, '<')
  132. ) {
  133. $e = __(['You are running error', 'PHP', \PHP_VERSION, $this->c->FORK_REVISION, self::PHP_MIN]);
  134. }
  135. // база не от ForkBB или старая ревизия
  136. if (
  137. null === $e
  138. && $this->c->config->i_fork_revision < self::REV_MIN_FOR_UPDATE
  139. ) {
  140. $e = 'Version mismatch error';
  141. }
  142. // загрузка и проверка конфига
  143. if (null === $e) {
  144. try {
  145. $coreConfig = new CoreConfig($this->configFile);
  146. } catch (ForkException $excp) {
  147. $e = $excp->getMessage();
  148. }
  149. }
  150. // проверка доступности базы данных на изменения
  151. if (
  152. null === $e
  153. && $this->c->config->i_fork_revision < self::LATEST_REV_WITH_DB_CHANGES
  154. ) {
  155. $test_table = 'test_tb_for_update';
  156. if (
  157. null === $e
  158. && true === $this->c->DB->tableExists($test_table)
  159. ) {
  160. $e = ['The %s table already exists. Delete it.', $test_table];
  161. }
  162. $schema = [
  163. 'FIELDS' => [
  164. 'id' => ['SERIAL', false],
  165. ],
  166. 'PRIMARY KEY' => ['id'],
  167. ];
  168. if (
  169. null === $e
  170. && false === $this->c->DB->createTable($test_table, $schema)
  171. ) {
  172. $e = ['Unable to create %s table', $test_table];
  173. }
  174. if (
  175. null === $e
  176. && false === $this->c->DB->addField($test_table, 'test_field', 'VARCHAR(80)', false, '')
  177. ) {
  178. $e = ['Unable to add test_field field to %s table', $test_table];
  179. }
  180. $sql = "INSERT INTO ::{$test_table} (test_field) VALUES ('TEST_VALUE')";
  181. if (
  182. null === $e
  183. && false === $this->c->DB->exec($sql)
  184. ) {
  185. $e = ['Unable to insert line to %s table', $test_table];
  186. }
  187. if (
  188. null === $e
  189. && false === $this->c->DB->dropField($test_table, 'test_field')
  190. ) {
  191. $e = ['Unable to drop test_field field from %s table', $test_table];
  192. }
  193. if (
  194. null === $e
  195. && false === $this->c->DB->dropTable($test_table)
  196. ) {
  197. $e = ['Unable to drop %s table', $test_table];
  198. }
  199. }
  200. if (null !== $e) {
  201. return $this->c->Message->message($e, true, 503);
  202. }
  203. $uid = $this->setLock();
  204. if (null === $uid) {
  205. $this->fIswev = ['e', 'Unable to write update lock'];
  206. } else {
  207. $this->c->config->o_maintenance_message = $v->o_maintenance_message;
  208. $this->c->config->save();
  209. return $this->c->Redirect->page('AdminUpdateStage', ['uid' => $uid, 'stage' => 1]);
  210. }
  211. } else {
  212. $this->fIswev = $v->getErrors();
  213. }
  214. } else {
  215. $v = null;
  216. }
  217. $this->form = $this->form($v);
  218. return $this;
  219. }
  220. /**
  221. * Проверяет пароль базы
  222. */
  223. public function vCheckPass(Validator $v, $dbpass)
  224. {
  225. if (\substr($this->c->DB_DSN, 0, 6) === 'sqlite') {
  226. if (! \hash_equals($this->c->DB_DSN, "sqlite:{$dbpass}")) { // ????
  227. $v->addError(['Invalid file error', self::CONFIG_FILE]);
  228. }
  229. } else {
  230. if (! \hash_equals($this->c->DB_PASSWORD, $dbpass)) {
  231. $v->addError(['Invalid password error', self::CONFIG_FILE]);
  232. }
  233. }
  234. return $dbpass;
  235. }
  236. /**
  237. * Формирует массив для формы
  238. */
  239. protected function form(?Validator $v): array
  240. {
  241. return [
  242. 'action' => $this->c->Router->link('AdminUpdate'),
  243. 'hidden' => [
  244. 'token' => $this->c->Csrf->create('AdminUpdate'),
  245. ],
  246. 'sets' => [
  247. 'update-info' => [
  248. 'info' => [
  249. [
  250. 'value' => __('Update message'),
  251. ],
  252. ],
  253. ],
  254. 'update' => [
  255. 'legend' => 'Update ForkBB',
  256. 'fields' => [
  257. 'dbpass' => [
  258. 'type' => 'password',
  259. 'value' => '',
  260. 'caption' => 'Database password',
  261. 'help' => 'Database password note',
  262. 'required' => true,
  263. ],
  264. 'o_maintenance_message' => [
  265. 'type' => 'textarea',
  266. 'value' => $v ? $v->o_maintenance_message : $this->c->config->o_maintenance_message,
  267. 'caption' => 'Maintenance message',
  268. 'help' => 'Maintenance message info',
  269. 'required' => true,
  270. ],
  271. ],
  272. ],
  273. 'member-info' => [
  274. 'info' => [
  275. [
  276. 'value' => __('Members message'),
  277. ],
  278. ],
  279. ],
  280. ],
  281. 'btns' => [
  282. 'start' => [
  283. 'type' => 'submit',
  284. 'value' => __('Start update'),
  285. ],
  286. ],
  287. ];
  288. }
  289. /**
  290. * Обновляет форум
  291. */
  292. public function stage(array $args, string $method): Page
  293. {
  294. try {
  295. $uid = $this->setLock($args['uid']);
  296. if (null === $uid) {
  297. return $this->returnMaintenance();
  298. }
  299. $stage = \max($args['stage'], $this->c->config->i_fork_revision);
  300. do {
  301. if (\method_exists($this, 'stageNumber' . $stage)) {
  302. $start = $this->{'stageNumber' . $stage}($args);
  303. if (null === $start) {
  304. ++$stage;
  305. }
  306. return $this->c->Redirect->page(
  307. 'AdminUpdateStage',
  308. ['uid' => $uid, 'stage' => $stage, 'start' => $start]
  309. )->message(['Stage %1$s (%2$s)', $stage, (int) $start]);
  310. }
  311. ++$stage;
  312. } while ($stage < $this->c->FORK_REVISION);
  313. $this->c->config->i_fork_revision = $this->c->FORK_REVISION;
  314. $this->c->config->save();
  315. if (true !== $this->c->Cache->clear()) {
  316. throw new RuntimeException('Unable to clear cache');
  317. }
  318. return $this->c->Redirect->page('Index')->message('Successfully updated');
  319. } catch (ForkException $excp) {
  320. return $this->c->Message->message($excp->getMessage(), true, 503);
  321. }
  322. }
  323. # /**
  324. # * Выполняет определенный шаг обновления
  325. # *
  326. # * Возвращает null, если шаг выпонен
  327. # * Возвращает положительный int, если требуется продолжить выполнение шага
  328. # */
  329. # protected function stageNumber1(array $args): ?int
  330. # {
  331. # $coreConfig = new CoreConfig($this->configFile);
  332. #
  333. # $coreConfig->add(
  334. # 'multiple=>AdminUsersRecalculate',
  335. # '\\ForkBB\\Models\\Pages\\Admin\\Users\\Recalculate::class',
  336. # 'AdminUsersNew'
  337. # );
  338. #
  339. # $coreConfig->save();
  340. #
  341. # return null;
  342. # }
  343. /**
  344. * rev.42 to rev.43
  345. */
  346. protected function stageNumber42(array $args): ?int
  347. {
  348. $query = 'DELETE FROM ::users WHERE id=1';
  349. $this->c->DB->exec($query);
  350. $query = 'UPDATE ::forums SET last_poster_id=0 WHERE last_poster_id=1';
  351. $this->c->DB->exec($query);
  352. $query = 'UPDATE ::online SET user_id=0 WHERE user_id=1';
  353. $this->c->DB->exec($query);
  354. $query = 'UPDATE ::pm_posts SET poster_id=0 WHERE poster_id=1';
  355. $this->c->DB->exec($query);
  356. $query = 'UPDATE ::pm_topics SET poster_id=0 WHERE poster_id=1';
  357. $this->c->DB->exec($query);
  358. $query = 'UPDATE ::pm_topics SET target_id=0 WHERE target_id=1';
  359. $this->c->DB->exec($query);
  360. $query = 'UPDATE ::posts SET poster_id=0 WHERE poster_id=1';
  361. $this->c->DB->exec($query);
  362. $query = 'UPDATE ::posts SET editor_id=0 WHERE editor_id=1';
  363. $this->c->DB->exec($query);
  364. $query = 'UPDATE ::reports SET reported_by=0 WHERE reported_by=1';
  365. $this->c->DB->exec($query);
  366. $query = 'UPDATE ::reports SET zapped_by=0 WHERE zapped_by=1';
  367. $this->c->DB->exec($query);
  368. $query = 'UPDATE ::topics SET poster_id=0 WHERE poster_id=1';
  369. $this->c->DB->exec($query);
  370. $query = 'UPDATE ::topics SET last_poster_id=0 WHERE last_poster_id=1';
  371. $this->c->DB->exec($query);
  372. $query = 'UPDATE ::warnings SET poster_id=0 WHERE poster_id=1';
  373. $this->c->DB->exec($query);
  374. return null;
  375. }
  376. }