*/ use dokuwiki\plugin\upgrade\HTTP\DokuHTTPClient; use splitbrain\PHPArchive\FileInfo; use splitbrain\PHPArchive\Tar; class helper_plugin_upgrade extends DokuWiki_Plugin { /** @var string download URL for the new DokuWiki release */ public $tgzurl; /** @var string full path to where the file will be downloaded to */ public $tgzfile; /** @var string full path to where the file will be extracted to */ public $tgzdir; /** @var string URL to the VERSION file of the new DokuWiki release */ public $tgzversion; /** @var string URL to the plugin.info.txt file of the upgrade plugin */ public $pluginversion; /** @var admin_plugin_upgrade|cli_plugin_upgrade */ protected $logger; public function __construct() { global $conf; $branch = 'stable'; $this->tgzurl = "https://github.com/splitbrain/dokuwiki/archive/$branch.tar.gz"; $this->tgzfile = $conf['tmpdir'] . '/dokuwiki-upgrade.tgz'; $this->tgzdir = $conf['tmpdir'] . '/dokuwiki-upgrade/'; $this->tgzversion = "https://raw.githubusercontent.com/splitbrain/dokuwiki/$branch/VERSION"; $this->pluginversion = "https://raw.githubusercontent.com/splitbrain/dokuwiki-plugin-upgrade/master/plugin.info.txt"; } /** * @param admin_plugin_upgrade|cli_plugin_upgrade $logger Logger object * @return void */ public function setLogger($logger) { $this->logger = $logger; } // region Steps /** * Check various versions * * @return bool */ public function checkVersions() { $ok = true; // we need SSL - only newer HTTPClients check that themselves if (!in_array('ssl', stream_get_transports())) { $this->log('error', $this->getLang('vs_ssl')); $ok = false; } // get the available version $http = new DokuHTTPClient(); $tgzversion = trim($http->get($this->tgzversion)); if (!$tgzversion) { $this->log('error', $this->getLang('vs_tgzno') . ' ' . hsc($http->error)); $ok = false; } $tgzversionnum = $this->dateFromVersion($tgzversion); if ($tgzversionnum === 0) { $this->log('error', $this->getLang('vs_tgzno')); $ok = false; } else { $this->log('notice', $this->getLang('vs_tgz'), $tgzversion); } // get the current version $versiondata = getVersionData(); $version = trim($versiondata['date']); $versionnum = $this->dateFromVersion($version); $this->log('notice', $this->getLang('vs_local'), $version); // compare versions if (!$versionnum) { $this->log('warning', $this->getLang('vs_localno')); $ok = false; } else if ($tgzversionnum) { if ($tgzversionnum < $versionnum) { $this->log('warning', $this->getLang('vs_newer')); $ok = false; } elseif ($tgzversionnum == $versionnum && $tgzversion == $version) { $this->log('warning', $this->getLang('vs_same')); $ok = false; } } // check plugin version $pluginversion = $http->get($this->pluginversion); if ($pluginversion) { $plugininfo = linesToHash(explode("\n", $pluginversion)); $myinfo = $this->getInfo(); if ($plugininfo['date'] > $myinfo['date']) { $this->log('warning', $this->getLang('vs_plugin'), $plugininfo['date']); $ok = false; } } // check if PHP is up to date $minphp = '7.2'; // FIXME get this from the composer file upstream if (version_compare(phpversion(), $minphp, '<')) { $this->log('error', $this->getLang('vs_php'), $minphp, phpversion()); $ok = false; } return $ok; } /** * Download the tarball * * @return bool */ public function downloadTarball() { $this->log('notice', $this->getLang('dl_from'), $this->tgzurl); @set_time_limit(300); @ignore_user_abort(); $http = new DokuHTTPClient(); $http->timeout = 300; $data = $http->get($this->tgzurl); if (!$data) { $this->log('error', $http->error); $this->log('error', $this->getLang('dl_fail')); return false; } io_mkdir_p(dirname($this->tgzfile)); if (!file_put_contents($this->tgzfile, $data)) { $this->log('error', $this->getLang('dl_fail')); return false; } $this->log('success', $this->getLang('dl_done'), filesize_h(strlen($data))); return true; } /** * Unpack the tarball * * @return bool */ public function extractTarball() { $this->log('notice', '' . $this->getLang('pk_extract') . ''); @set_time_limit(300); @ignore_user_abort(); try { $tar = new Tar(); $tar->setCallback(function ($file) { /** @var FileInfo $file */ $this->log('info', $file->getPath()); }); $tar->open($this->tgzfile); $tar->extract($this->tgzdir, 1); $tar->close(); } catch (Exception $e) { $this->log('error', $e->getMessage()); $this->log('error', $this->getLang('pk_fail')); return false; } $this->log('success', $this->getLang('pk_done')); $this->log( 'notice', $this->getLang('pk_version'), hsc(file_get_contents($this->tgzdir . '/VERSION')), getVersion() ); return true; } /** * Check permissions of files to change * * @return bool */ public function checkPermissions() { $this->log('notice', $this->getLang('ck_start')); $ok = $this->traverseCheckAndCopy('', true); if ($ok) { $this->log('success', '' . $this->getLang('ck_done') . ''); } else { $this->log('error', '' . $this->getLang('ck_fail') . ''); } return $ok; } /** * Copy over new files * * @return bool */ public function copyFiles() { $this->log('notice', $this->getLang('cp_start')); $ok = $this->traverseCheckAndCopy('', false); if ($ok) { $this->log('success', '' . $this->getLang('cp_done') . ''); } else { $this->log('error', '' . $this->getLang('cp_fail') . ''); } return $ok; } /** * Delete outdated files */ public function deleteObsoleteFiles() { global $conf; $list = file($this->tgzdir . 'data/deleted.files'); foreach ($list as $line) { $line = trim(preg_replace('/#.*$/', '', $line)); if (!$line) continue; $file = DOKU_INC . $line; if (!file_exists($file)) continue; // check that the given file is a case sensitive match if (basename(realpath($file)) != basename($file)) { $this->log('info', $this->getLang('rm_mismatch'), hsc($line)); continue; } if ((is_dir($file) && $this->recursiveDelete($file)) || @unlink($file) ) { $this->log('info', $this->getLang('rm_done'), hsc($line)); } else { $this->log('error', $this->getLang('rm_fail'), hsc($line)); } } // delete install @unlink(DOKU_INC . 'install.php'); // make sure update message will be gone @touch(DOKU_INC . 'doku.php'); @unlink($conf['cachedir'] . '/messages.txt'); // clear opcache if (function_exists('opcache_reset')) { opcache_reset(); } $this->log('success', '' . $this->getLang('finish') . ''); return true; } /** * Remove the downloaded and extracted files * * @return bool */ public function cleanUp() { @unlink($this->tgzfile); $this->recursiveDelete($this->tgzdir); return true; } // endregion /** * Traverse over the given dir and compare it to the DokuWiki dir * * Checks what files need an update, tests for writability and copies * * @param string $dir * @param bool $dryrun do not copy but only check permissions * @return bool */ private function traverseCheckAndCopy($dir, $dryrun) { $base = $this->tgzdir; $ok = true; $dh = @opendir($base . '/' . $dir); if (!$dh) return false; while (($file = readdir($dh)) !== false) { if ($file == '.' || $file == '..') continue; $from = "$base/$dir/$file"; $to = DOKU_INC . "$dir/$file"; if (is_dir($from)) { if ($dryrun) { // just check for writability if (!is_dir($to)) { if (is_dir(dirname($to)) && !is_writable(dirname($to))) { $this->log('error', '' . $this->getLang('tv_noperm') . '', hsc("$dir/$file")); $ok = false; } } } // recursion if (!$this->traverseCheckAndCopy("$dir/$file", $dryrun)) { $ok = false; } } else { $fmd5 = md5(@file_get_contents($from)); $tmd5 = md5(@file_get_contents($to)); if ($fmd5 != $tmd5 || !file_exists($to)) { if ($dryrun) { // just check for writability if ((file_exists($to) && !is_writable($to)) || (!file_exists($to) && is_dir(dirname($to)) && !is_writable(dirname($to))) ) { $this->log('error', '' . $this->getLang('tv_noperm') . '', hsc("$dir/$file")); $ok = false; } else { $this->log('info', $this->getLang('tv_upd'), hsc("$dir/$file")); } } else { // check dir if (io_mkdir_p(dirname($to))) { // remove existing (avoid case sensitivity problems) if (file_exists($to) && !@unlink($to)) { $this->log('error', '' . $this->getLang('tv_nodel') . '', hsc("$dir/$file")); $ok = false; } // copy if (!copy($from, $to)) { $this->log('error', '' . $this->getLang('tv_nocopy') . '', hsc("$dir/$file")); $ok = false; } else { $this->log('info', $this->getLang('tv_done'), hsc("$dir/$file")); } } else { $this->log('error', '' . $this->getLang('tv_nodir') . '', hsc("$dir")); $ok = false; } } } } } closedir($dh); return $ok; } // region utilities /** * Figure out the release date from the version string * * @param $version * @return int|string returns 0 if the version can't be read */ protected function dateFromVersion($version) { if (preg_match('/(^|\D)(\d\d\d\d-\d\d-\d\d)(\D|$)/i', $version, $m)) { return $m[2]; } return 0; } /** * Recursive delete * * @author Jon Hassall * @link http://de.php.net/manual/en/function.unlink.php#87045 */ protected function recursiveDelete($dir) { if (!$dh = @opendir($dir)) { return false; } while (false !== ($obj = readdir($dh))) { if ($obj == '.' || $obj == '..') continue; if (!@unlink($dir . '/' . $obj)) { $this->recursiveDelete($dir . '/' . $obj); } } closedir($dh); return @rmdir($dir); } /** * Log a message * * @param string ...$level, $msg */ protected function log() { $args = func_get_args(); $level = array_shift($args); $msg = array_shift($args); $msg = vsprintf($msg, $args); if ($this->logger) $this->logger->log($level, $msg); } // endregion }