updateController.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. <?php
  2. declare(strict_types=1);
  3. class FreshRSS_update_Controller extends FreshRSS_ActionController {
  4. private const LASTUPDATEFILE = 'last_update.txt';
  5. public static function isGit(): bool {
  6. return is_dir(FRESHRSS_PATH . '/.git/');
  7. }
  8. /**
  9. * Automatic change to the new name of edge branch since FreshRSS 1.18.0.
  10. */
  11. public static function migrateToGitEdge(): bool {
  12. $errorMessage = 'Error during git checkout to edge branch. Please change branch manually!';
  13. if (!is_writable(FRESHRSS_PATH . '/.git/config')) {
  14. throw new Exception($errorMessage);
  15. }
  16. //Note `git branch --show-current` requires git 2.22+
  17. exec('git symbolic-ref --short HEAD', $output, $return);
  18. if ($return != 0) {
  19. throw new Exception($errorMessage);
  20. }
  21. $line = implode('', $output);
  22. if ($line !== 'master' && $line !== 'dev') {
  23. return true; // not on master or dev, nothing to do
  24. }
  25. Minz_Log::warning('Automatic migration to git edge branch');
  26. unset($output);
  27. exec('git checkout edge --guess -f', $output, $return);
  28. if ($return != 0) {
  29. throw new Exception($errorMessage);
  30. }
  31. unset($output);
  32. exec('git reset --hard FETCH_HEAD', $output, $return);
  33. if ($return != 0) {
  34. throw new Exception($errorMessage);
  35. }
  36. return true;
  37. }
  38. public static function getCurrentGitBranch(): string {
  39. $output = [];
  40. exec('git branch --show-current', $output, $return);
  41. if ($return === 0) {
  42. return 'git branch: ' . $output[0];
  43. } else {
  44. return 'git';
  45. }
  46. }
  47. public static function hasGitUpdate(): bool {
  48. $cwd = getcwd();
  49. if ($cwd === false) {
  50. Minz_Log::warning('getcwd() failed');
  51. return false;
  52. }
  53. chdir(FRESHRSS_PATH);
  54. $output = [];
  55. try {
  56. exec('git fetch --prune', $output, $return);
  57. if ($return == 0) {
  58. $output = [];
  59. exec('git status -sb --porcelain remote', $output, $return);
  60. } else {
  61. $line = implode('; ', $output);
  62. Minz_Log::warning('git fetch warning: ' . $line);
  63. }
  64. } catch (Exception $e) {
  65. Minz_Log::warning('git fetch error: ' . $e->getMessage());
  66. }
  67. chdir($cwd);
  68. $line = implode('; ', $output);
  69. return $line == '' ||
  70. strpos($line, '[behind') !== false || strpos($line, '[ahead') !== false || strpos($line, '[gone') !== false;
  71. }
  72. /** @return string|true */
  73. public static function gitPull() {
  74. Minz_Log::notice(_t('admin.update.viaGit'));
  75. $cwd = getcwd();
  76. if ($cwd === false) {
  77. Minz_Log::warning('getcwd() failed');
  78. return 'getcwd() failed';
  79. }
  80. chdir(FRESHRSS_PATH);
  81. $output = [];
  82. $return = 1;
  83. try {
  84. exec('git fetch --prune', $output, $return);
  85. if ($return == 0) {
  86. $output = [];
  87. exec('git reset --hard FETCH_HEAD', $output, $return);
  88. }
  89. $output = [];
  90. self::migrateToGitEdge();
  91. } catch (Exception $e) {
  92. Minz_Log::warning('Git error: ' . $e->getMessage());
  93. if (empty($output)) {
  94. $output = $e->getMessage();
  95. }
  96. $return = 1;
  97. }
  98. chdir($cwd);
  99. $line = is_array($output) ? implode('; ', $output) : $output;
  100. return $return == 0 ? true : 'Git error: ' . $line;
  101. }
  102. public function firstAction(): void {
  103. if (!FreshRSS_Auth::hasAccess('admin')) {
  104. Minz_Error::error(403);
  105. }
  106. include_once(LIB_PATH . '/lib_install.php');
  107. invalidateHttpCache();
  108. $this->view->is_release_channel_stable = $this->is_release_channel_stable(FRESHRSS_VERSION);
  109. $this->view->update_to_apply = false;
  110. $this->view->last_update_time = 'unknown';
  111. $timestamp = @filemtime(join_path(DATA_PATH, self::LASTUPDATEFILE));
  112. if ($timestamp !== false) {
  113. $this->view->last_update_time = timestamptodate($timestamp);
  114. }
  115. }
  116. public function indexAction(): void {
  117. FreshRSS_View::prependTitle(_t('admin.update.title') . ' · ');
  118. if (file_exists(UPDATE_FILENAME)) {
  119. // There is an update file to apply!
  120. $version = @file_get_contents(join_path(DATA_PATH, self::LASTUPDATEFILE));
  121. if ($version == '') {
  122. $version = 'unknown';
  123. }
  124. if (touch(FRESHRSS_PATH . '/index.html')) {
  125. $this->view->update_to_apply = true;
  126. $this->view->message = [
  127. 'status' => 'good',
  128. 'title' => _t('gen.short.ok'),
  129. 'body' => _t('feedback.update.can_apply', $version),
  130. ];
  131. } else {
  132. $this->view->message = [
  133. 'status' => 'bad',
  134. 'title' => _t('gen.short.damn'),
  135. 'body' => _t('feedback.update.file_is_nok', $version, FRESHRSS_PATH),
  136. ];
  137. }
  138. }
  139. }
  140. private function is_release_channel_stable(string $currentVersion): bool {
  141. return strpos($currentVersion, 'dev') === false &&
  142. strpos($currentVersion, 'edge') === false;
  143. }
  144. /* Check installation if there is a newer version.
  145. via Git, if available.
  146. Else via system configuration auto_update_url
  147. */
  148. public function checkAction(): void {
  149. FreshRSS_View::prependTitle(_t('admin.update.title') . ' · ');
  150. $this->view->_path('update/index.phtml');
  151. if (file_exists(UPDATE_FILENAME)) {
  152. // There is already an update file to apply: we don’t need to check
  153. // the webserver!
  154. // Or if already check during the last hour, do nothing.
  155. Minz_Request::forward(['c' => 'update'], true);
  156. return;
  157. }
  158. $script = '';
  159. if (self::isGit()) {
  160. if (self::hasGitUpdate()) {
  161. $version = self::getCurrentGitBranch();
  162. } else {
  163. $this->view->message = [
  164. 'status' => 'latest',
  165. 'body' => _t('feedback.update.none'),
  166. ];
  167. @touch(join_path(DATA_PATH, self::LASTUPDATEFILE));
  168. return;
  169. }
  170. } else {
  171. $auto_update_url = FreshRSS_Context::systemConf()->auto_update_url . '/?v=' . FRESHRSS_VERSION;
  172. Minz_Log::debug('HTTP GET ' . $auto_update_url);
  173. $curlResource = curl_init($auto_update_url);
  174. if ($curlResource === false) {
  175. Minz_Log::warning('curl_init() failed');
  176. $this->view->message = [
  177. 'status' => 'bad',
  178. 'title' => _t('gen.short.damn'),
  179. 'body' => _t('feedback.update.server_not_found', $auto_update_url)
  180. ];
  181. return;
  182. }
  183. curl_setopt($curlResource, CURLOPT_RETURNTRANSFER, true);
  184. curl_setopt($curlResource, CURLOPT_SSL_VERIFYPEER, true);
  185. curl_setopt($curlResource, CURLOPT_SSL_VERIFYHOST, 2);
  186. $result = curl_exec($curlResource);
  187. $curlGetinfo = curl_getinfo($curlResource, CURLINFO_HTTP_CODE);
  188. $curlError = curl_error($curlResource);
  189. curl_close($curlResource);
  190. if ($curlGetinfo !== 200) {
  191. Minz_Log::warning(
  192. 'Error during update (HTTP code ' . $curlGetinfo . '): ' . $curlError
  193. );
  194. $this->view->message = [
  195. 'status' => 'bad',
  196. 'body' => _t('feedback.update.server_not_found', $auto_update_url),
  197. ];
  198. return;
  199. }
  200. $res_array = explode("\n", (string)$result, 2);
  201. $status = $res_array[0];
  202. if (strpos($status, 'UPDATE') !== 0) {
  203. $this->view->message = [
  204. 'status' => 'latest',
  205. 'body' => _t('feedback.update.none'),
  206. ];
  207. @touch(join_path(DATA_PATH, self::LASTUPDATEFILE));
  208. return;
  209. }
  210. $script = $res_array[1];
  211. $version = explode(' ', $status, 2);
  212. $version = $version[1];
  213. Minz_Log::notice(_t('admin.update.copiedFromURL', $auto_update_url));
  214. }
  215. if (file_put_contents(UPDATE_FILENAME, $script) !== false) {
  216. @file_put_contents(join_path(DATA_PATH, self::LASTUPDATEFILE), $version);
  217. Minz_Request::forward(['c' => 'update'], true);
  218. } else {
  219. $this->view->message = [
  220. 'status' => 'bad',
  221. 'body' => _t('feedback.update.error', 'Cannot save the update script'),
  222. ];
  223. }
  224. }
  225. public function applyAction(): void {
  226. if (FreshRSS_Context::systemConf()->disable_update || !file_exists(UPDATE_FILENAME) || !touch(FRESHRSS_PATH . '/index.html')) {
  227. Minz_Request::forward(['c' => 'update'], true);
  228. }
  229. if (Minz_Request::paramBoolean('post_conf')) {
  230. if (self::isGit()) {
  231. $res = !self::hasGitUpdate();
  232. } else {
  233. require(UPDATE_FILENAME);
  234. // @phpstan-ignore-next-line
  235. $res = do_post_update();
  236. }
  237. Minz_ExtensionManager::callHookVoid('post_update');
  238. if ($res === true) {
  239. @unlink(UPDATE_FILENAME);
  240. @file_put_contents(join_path(DATA_PATH, self::LASTUPDATEFILE), '');
  241. Minz_Log::notice(_t('feedback.update.finished'));
  242. Minz_Request::good(_t('feedback.update.finished'));
  243. } else {
  244. Minz_Log::error(_t('feedback.update.error', $res));
  245. Minz_Request::bad(_t('feedback.update.error', $res), [ 'c' => 'update', 'a' => 'index' ]);
  246. }
  247. } else {
  248. $res = false;
  249. if (self::isGit()) {
  250. $res = self::gitPull();
  251. } else {
  252. require(UPDATE_FILENAME);
  253. if (Minz_Request::isPost()) {
  254. // @phpstan-ignore-next-line
  255. save_info_update();
  256. }
  257. // @phpstan-ignore-next-line
  258. if (!need_info_update()) {
  259. // @phpstan-ignore-next-line
  260. $res = apply_update();
  261. } else {
  262. return;
  263. }
  264. }
  265. if (function_exists('opcache_reset')) {
  266. opcache_reset();
  267. }
  268. if ($res === true) {
  269. Minz_Request::forward([
  270. 'c' => 'update',
  271. 'a' => 'apply',
  272. 'params' => ['post_conf' => '1'],
  273. ], true);
  274. } else {
  275. Minz_Log::error(_t('feedback.update.error', $res));
  276. Minz_Request::bad(_t('feedback.update.error', $res), [ 'c' => 'update', 'a' => 'index' ]);
  277. }
  278. }
  279. }
  280. /**
  281. * This action displays information about installation.
  282. */
  283. public function checkInstallAction(): void {
  284. FreshRSS_View::prependTitle(_t('admin.check_install.title') . ' · ');
  285. $this->view->status_php = check_install_php();
  286. $this->view->status_files = check_install_files();
  287. $this->view->status_database = check_install_database();
  288. }
  289. }