helper.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. <?php
  2. /**
  3. * DokuWiki Plugin upgrade (Helper Component)
  4. *
  5. * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
  6. * @author Andreas Gohr <andi@splitbrain.org>
  7. */
  8. use dokuwiki\plugin\upgrade\HTTP\DokuHTTPClient;
  9. use splitbrain\PHPArchive\FileInfo;
  10. use splitbrain\PHPArchive\Tar;
  11. class helper_plugin_upgrade extends DokuWiki_Plugin
  12. {
  13. /** @var string download URL for the new DokuWiki release */
  14. public $tgzurl;
  15. /** @var string full path to where the file will be downloaded to */
  16. public $tgzfile;
  17. /** @var string full path to where the file will be extracted to */
  18. public $tgzdir;
  19. /** @var string URL to the VERSION file of the new DokuWiki release */
  20. public $tgzversion;
  21. /** @var string URL to the plugin.info.txt file of the upgrade plugin */
  22. public $pluginversion;
  23. /** @var admin_plugin_upgrade|cli_plugin_upgrade */
  24. protected $logger;
  25. public function __construct()
  26. {
  27. global $conf;
  28. $branch = 'stable';
  29. $this->tgzurl = "https://github.com/splitbrain/dokuwiki/archive/$branch.tar.gz";
  30. $this->tgzfile = $conf['tmpdir'] . '/dokuwiki-upgrade.tgz';
  31. $this->tgzdir = $conf['tmpdir'] . '/dokuwiki-upgrade/';
  32. $this->tgzversion = "https://raw.githubusercontent.com/splitbrain/dokuwiki/$branch/VERSION";
  33. $this->pluginversion = "https://raw.githubusercontent.com/splitbrain/dokuwiki-plugin-upgrade/master/plugin.info.txt";
  34. }
  35. /**
  36. * @param admin_plugin_upgrade|cli_plugin_upgrade $logger Logger object
  37. * @return void
  38. */
  39. public function setLogger($logger)
  40. {
  41. $this->logger = $logger;
  42. }
  43. // region Steps
  44. /**
  45. * Check various versions
  46. *
  47. * @return bool
  48. */
  49. public function checkVersions()
  50. {
  51. $ok = true;
  52. // we need SSL - only newer HTTPClients check that themselves
  53. if (!in_array('ssl', stream_get_transports())) {
  54. $this->log('error', $this->getLang('vs_ssl'));
  55. $ok = false;
  56. }
  57. // get the available version
  58. $http = new DokuHTTPClient();
  59. $tgzversion = trim($http->get($this->tgzversion));
  60. if (!$tgzversion) {
  61. $this->log('error', $this->getLang('vs_tgzno') . ' ' . hsc($http->error));
  62. $ok = false;
  63. }
  64. $tgzversionnum = $this->dateFromVersion($tgzversion);
  65. if ($tgzversionnum === 0) {
  66. $this->log('error', $this->getLang('vs_tgzno'));
  67. $ok = false;
  68. } else {
  69. $this->log('notice', $this->getLang('vs_tgz'), $tgzversion);
  70. }
  71. // get the current version
  72. $versiondata = getVersionData();
  73. $version = trim($versiondata['date']);
  74. $versionnum = $this->dateFromVersion($version);
  75. $this->log('notice', $this->getLang('vs_local'), $version);
  76. // compare versions
  77. if (!$versionnum) {
  78. $this->log('warning', $this->getLang('vs_localno'));
  79. $ok = false;
  80. } else if ($tgzversionnum) {
  81. if ($tgzversionnum < $versionnum) {
  82. $this->log('warning', $this->getLang('vs_newer'));
  83. $ok = false;
  84. } elseif ($tgzversionnum == $versionnum && $tgzversion == $version) {
  85. $this->log('warning', $this->getLang('vs_same'));
  86. $ok = false;
  87. }
  88. }
  89. // check plugin version
  90. $pluginversion = $http->get($this->pluginversion);
  91. if ($pluginversion) {
  92. $plugininfo = linesToHash(explode("\n", $pluginversion));
  93. $myinfo = $this->getInfo();
  94. if ($plugininfo['date'] > $myinfo['date']) {
  95. $this->log('warning', $this->getLang('vs_plugin'), $plugininfo['date']);
  96. $ok = false;
  97. }
  98. }
  99. // check if PHP is up to date
  100. $minphp = '7.2'; // FIXME get this from the composer file upstream
  101. if (version_compare(phpversion(), $minphp, '<')) {
  102. $this->log('error', $this->getLang('vs_php'), $minphp, phpversion());
  103. $ok = false;
  104. }
  105. return $ok;
  106. }
  107. /**
  108. * Download the tarball
  109. *
  110. * @return bool
  111. */
  112. public function downloadTarball()
  113. {
  114. $this->log('notice', $this->getLang('dl_from'), $this->tgzurl);
  115. @set_time_limit(300);
  116. @ignore_user_abort();
  117. $http = new DokuHTTPClient();
  118. $http->timeout = 300;
  119. $data = $http->get($this->tgzurl);
  120. if (!$data) {
  121. $this->log('error', $http->error);
  122. $this->log('error', $this->getLang('dl_fail'));
  123. return false;
  124. }
  125. io_mkdir_p(dirname($this->tgzfile));
  126. if (!file_put_contents($this->tgzfile, $data)) {
  127. $this->log('error', $this->getLang('dl_fail'));
  128. return false;
  129. }
  130. $this->log('success', $this->getLang('dl_done'), filesize_h(strlen($data)));
  131. return true;
  132. }
  133. /**
  134. * Unpack the tarball
  135. *
  136. * @return bool
  137. */
  138. public function extractTarball()
  139. {
  140. $this->log('notice', '<b>' . $this->getLang('pk_extract') . '</b>');
  141. @set_time_limit(300);
  142. @ignore_user_abort();
  143. try {
  144. $tar = new Tar();
  145. $tar->setCallback(function ($file) {
  146. /** @var FileInfo $file */
  147. $this->log('info', $file->getPath());
  148. });
  149. $tar->open($this->tgzfile);
  150. $tar->extract($this->tgzdir, 1);
  151. $tar->close();
  152. } catch (Exception $e) {
  153. $this->log('error', $e->getMessage());
  154. $this->log('error', $this->getLang('pk_fail'));
  155. return false;
  156. }
  157. $this->log('success', $this->getLang('pk_done'));
  158. $this->log(
  159. 'notice',
  160. $this->getLang('pk_version'),
  161. hsc(file_get_contents($this->tgzdir . '/VERSION')),
  162. getVersion()
  163. );
  164. return true;
  165. }
  166. /**
  167. * Check permissions of files to change
  168. *
  169. * @return bool
  170. */
  171. public function checkPermissions()
  172. {
  173. $this->log('notice', $this->getLang('ck_start'));
  174. $ok = $this->traverseCheckAndCopy('', true);
  175. if ($ok) {
  176. $this->log('success', '<b>' . $this->getLang('ck_done') . '</b>');
  177. } else {
  178. $this->log('error', '<b>' . $this->getLang('ck_fail') . '</b>');
  179. }
  180. return $ok;
  181. }
  182. /**
  183. * Copy over new files
  184. *
  185. * @return bool
  186. */
  187. public function copyFiles()
  188. {
  189. $this->log('notice', $this->getLang('cp_start'));
  190. $ok = $this->traverseCheckAndCopy('', false);
  191. if ($ok) {
  192. $this->log('success', '<b>' . $this->getLang('cp_done') . '</b>');
  193. } else {
  194. $this->log('error', '<b>' . $this->getLang('cp_fail') . '</b>');
  195. }
  196. return $ok;
  197. }
  198. /**
  199. * Delete outdated files
  200. */
  201. public function deleteObsoleteFiles()
  202. {
  203. global $conf;
  204. $list = file($this->tgzdir . 'data/deleted.files');
  205. foreach ($list as $line) {
  206. $line = trim(preg_replace('/#.*$/', '', $line));
  207. if (!$line) continue;
  208. $file = DOKU_INC . $line;
  209. if (!file_exists($file)) continue;
  210. // check that the given file is a case sensitive match
  211. if (basename(realpath($file)) != basename($file)) {
  212. $this->log('info', $this->getLang('rm_mismatch'), hsc($line));
  213. continue;
  214. }
  215. if ((is_dir($file) && $this->recursiveDelete($file)) ||
  216. @unlink($file)
  217. ) {
  218. $this->log('info', $this->getLang('rm_done'), hsc($line));
  219. } else {
  220. $this->log('error', $this->getLang('rm_fail'), hsc($line));
  221. }
  222. }
  223. // delete install
  224. @unlink(DOKU_INC . 'install.php');
  225. // make sure update message will be gone
  226. @touch(DOKU_INC . 'doku.php');
  227. @unlink($conf['cachedir'] . '/messages.txt');
  228. // clear opcache
  229. if (function_exists('opcache_reset')) {
  230. opcache_reset();
  231. }
  232. $this->log('success', '<b>' . $this->getLang('finish') . '</b>');
  233. return true;
  234. }
  235. /**
  236. * Remove the downloaded and extracted files
  237. *
  238. * @return bool
  239. */
  240. public function cleanUp()
  241. {
  242. @unlink($this->tgzfile);
  243. $this->recursiveDelete($this->tgzdir);
  244. return true;
  245. }
  246. // endregion
  247. /**
  248. * Traverse over the given dir and compare it to the DokuWiki dir
  249. *
  250. * Checks what files need an update, tests for writability and copies
  251. *
  252. * @param string $dir
  253. * @param bool $dryrun do not copy but only check permissions
  254. * @return bool
  255. */
  256. private function traverseCheckAndCopy($dir, $dryrun)
  257. {
  258. $base = $this->tgzdir;
  259. $ok = true;
  260. $dh = @opendir($base . '/' . $dir);
  261. if (!$dh) return false;
  262. while (($file = readdir($dh)) !== false) {
  263. if ($file == '.' || $file == '..') continue;
  264. $from = "$base/$dir/$file";
  265. $to = DOKU_INC . "$dir/$file";
  266. if (is_dir($from)) {
  267. if ($dryrun) {
  268. // just check for writability
  269. if (!is_dir($to)) {
  270. if (is_dir(dirname($to)) && !is_writable(dirname($to))) {
  271. $this->log('error', '<b>' . $this->getLang('tv_noperm') . '</b>', hsc("$dir/$file"));
  272. $ok = false;
  273. }
  274. }
  275. }
  276. // recursion
  277. if (!$this->traverseCheckAndCopy("$dir/$file", $dryrun)) {
  278. $ok = false;
  279. }
  280. } else {
  281. $fmd5 = md5(@file_get_contents($from));
  282. $tmd5 = md5(@file_get_contents($to));
  283. if ($fmd5 != $tmd5 || !file_exists($to)) {
  284. if ($dryrun) {
  285. // just check for writability
  286. if ((file_exists($to) && !is_writable($to)) ||
  287. (!file_exists($to) && is_dir(dirname($to)) && !is_writable(dirname($to)))
  288. ) {
  289. $this->log('error', '<b>' . $this->getLang('tv_noperm') . '</b>', hsc("$dir/$file"));
  290. $ok = false;
  291. } else {
  292. $this->log('info', $this->getLang('tv_upd'), hsc("$dir/$file"));
  293. }
  294. } else {
  295. // check dir
  296. if (io_mkdir_p(dirname($to))) {
  297. // remove existing (avoid case sensitivity problems)
  298. if (file_exists($to) && !@unlink($to)) {
  299. $this->log('error', '<b>' . $this->getLang('tv_nodel') . '</b>', hsc("$dir/$file"));
  300. $ok = false;
  301. }
  302. // copy
  303. if (!copy($from, $to)) {
  304. $this->log('error', '<b>' . $this->getLang('tv_nocopy') . '</b>', hsc("$dir/$file"));
  305. $ok = false;
  306. } else {
  307. $this->log('info', $this->getLang('tv_done'), hsc("$dir/$file"));
  308. }
  309. } else {
  310. $this->log('error', '<b>' . $this->getLang('tv_nodir') . '</b>', hsc("$dir"));
  311. $ok = false;
  312. }
  313. }
  314. }
  315. }
  316. }
  317. closedir($dh);
  318. return $ok;
  319. }
  320. // region utilities
  321. /**
  322. * Figure out the release date from the version string
  323. *
  324. * @param $version
  325. * @return int|string returns 0 if the version can't be read
  326. */
  327. protected function dateFromVersion($version)
  328. {
  329. if (preg_match('/(^|\D)(\d\d\d\d-\d\d-\d\d)(\D|$)/i', $version, $m)) {
  330. return $m[2];
  331. }
  332. return 0;
  333. }
  334. /**
  335. * Recursive delete
  336. *
  337. * @author Jon Hassall
  338. * @link http://de.php.net/manual/en/function.unlink.php#87045
  339. */
  340. protected function recursiveDelete($dir)
  341. {
  342. if (!$dh = @opendir($dir)) {
  343. return false;
  344. }
  345. while (false !== ($obj = readdir($dh))) {
  346. if ($obj == '.' || $obj == '..') continue;
  347. if (!@unlink($dir . '/' . $obj)) {
  348. $this->recursiveDelete($dir . '/' . $obj);
  349. }
  350. }
  351. closedir($dh);
  352. return @rmdir($dir);
  353. }
  354. /**
  355. * Log a message
  356. *
  357. * @param string ...$level, $msg
  358. */
  359. protected function log()
  360. {
  361. $args = func_get_args();
  362. $level = array_shift($args);
  363. $msg = array_shift($args);
  364. $msg = vsprintf($msg, $args);
  365. if ($this->logger) $this->logger->log($level, $msg);
  366. }
  367. // endregion
  368. }