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