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