1<?php 2/** 3 * DokuWiki Plugin upgrade (Admin Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) die(); 11if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/'); 12require_once DOKU_PLUGIN.'admin.php'; 13require_once DOKU_PLUGIN.'upgrade/VerboseTarLib.class.php'; 14require_once DOKU_PLUGIN.'upgrade/HTTPClient.php'; 15 16use dokuwiki\plugin\upgrade\DokuHTTPClient; 17 18class admin_plugin_upgrade extends DokuWiki_Admin_Plugin { 19 private $tgzurl; 20 private $tgzfile; 21 private $tgzdir; 22 private $tgzversion; 23 private $pluginversion; 24 25 protected $haderrors = false; 26 27 public function __construct() { 28 global $conf; 29 30 $branch = 'stable'; 31 32 $this->tgzurl = "https://github.com/splitbrain/dokuwiki/archive/$branch.tar.gz"; 33 $this->tgzfile = $conf['tmpdir'].'/dokuwiki-upgrade.tgz'; 34 $this->tgzdir = $conf['tmpdir'].'/dokuwiki-upgrade/'; 35 $this->tgzversion = "https://raw.githubusercontent.com/splitbrain/dokuwiki/$branch/VERSION"; 36 $this->pluginversion = "https://raw.githubusercontent.com/splitbrain/dokuwiki-plugin-upgrade/master/plugin.info.txt"; 37 } 38 39 public function getMenuSort() { 40 return 555; 41 } 42 43 public function handle() { 44 if($_REQUEST['step'] && !checkSecurityToken()) { 45 unset($_REQUEST['step']); 46 } 47 } 48 49 public function html() { 50 $abrt = false; 51 $next = false; 52 53 echo '<h1>'.$this->getLang('menu').'</h1>'; 54 55 global $conf; 56 if($conf['safemodehack']) { 57 $abrt = false; 58 $next = false; 59 echo $this->locale_xhtml('safemode'); 60 return; 61 } 62 63 $this->_say('<div id="plugin__upgrade">'); 64 // enable auto scroll 65 ?> 66 <script language="javascript" type="text/javascript"> 67 var plugin_upgrade = window.setInterval(function () { 68 var obj = document.getElementById('plugin__upgrade'); 69 if (obj) obj.scrollTop = obj.scrollHeight; 70 }, 25); 71 </script> 72 <?php 73 74 // handle current step 75 $this->_stepit($abrt, $next); 76 77 // disable auto scroll 78 ?> 79 <script language="javascript" type="text/javascript"> 80 window.setTimeout(function () { 81 window.clearInterval(plugin_upgrade); 82 }, 50); 83 </script> 84 <?php 85 $this->_say('</div>'); 86 87 $careful = ''; 88 if($this->haderrors) { 89 echo '<div id="plugin__upgrade_careful">'.$this->getLang('careful').'</div>'; 90 $careful = 'careful'; 91 } 92 93 $action = script(); 94 echo '<form action="' . $action . '" method="post" id="plugin__upgrade_form">'; 95 echo '<input type="hidden" name="do" value="admin" />'; 96 echo '<input type="hidden" name="page" value="upgrade" />'; 97 echo '<input type="hidden" name="sectok" value="' . getSecurityToken() . '" />'; 98 if($next) echo '<button type="submit" name="step[' . $next . ']" value="1" class="button continue '.$careful.'">' . $this->getLang('btn_continue') . ' ➡</button>'; 99 if($abrt) echo '<button type="submit" name="step[cancel]" value="1" class="button abort">✖ ' . $this->getLang('btn_abort') . '</button>'; 100 echo '</form>'; 101 102 $this->_progress($next); 103 } 104 105 /** 106 * Display a progress bar of all steps 107 * 108 * @param string $next the next step 109 */ 110 private function _progress($next) { 111 $steps = array('version', 'download', 'unpack', 'check', 'upgrade'); 112 $active = true; 113 $count = 0; 114 115 echo '<div id="plugin__upgrade_meter"><ol>'; 116 foreach($steps as $step) { 117 $count++; 118 if($step == $next) $active = false; 119 if($active) { 120 echo '<li class="active">'; 121 echo '<span class="step">✔</span>'; 122 } else { 123 echo '<li>'; 124 echo '<span class="step">'.$count.'</span>'; 125 } 126 127 echo '<span class="stage">'.$this->getLang('step_'.$step).'</span>'; 128 echo '</li>'; 129 } 130 echo '</ol></div>'; 131 } 132 133 /** 134 * Decides the current step and executes it 135 * 136 * @param bool $abrt 137 * @param bool $next 138 */ 139 private function _stepit(&$abrt, &$next) { 140 141 if(isset($_REQUEST['step']) && is_array($_REQUEST['step'])) { 142 $step = array_shift(array_keys($_REQUEST['step'])); 143 } else { 144 $step = ''; 145 } 146 147 if($step == 'cancel' || $step == 'done') { 148 # cleanup 149 @unlink($this->tgzfile); 150 $this->_rdel($this->tgzdir); 151 if($step == 'cancel') $step = ''; 152 } 153 154 if($step) { 155 $abrt = true; 156 $next = false; 157 if($step == 'version') { 158 $this->_step_version(); 159 $next = 'download'; 160 } elseif ($step == 'done') { 161 $this->_step_done(); 162 $next = ''; 163 $abrt = ''; 164 } elseif(!file_exists($this->tgzfile)) { 165 if($this->_step_download()) $next = 'unpack'; 166 } elseif(!is_dir($this->tgzdir)) { 167 if($this->_step_unpack()) $next = 'check'; 168 } elseif($step != 'upgrade') { 169 if($this->_step_check()) $next = 'upgrade'; 170 } elseif($step == 'upgrade') { 171 if($this->_step_copy()) { 172 $next = 'done'; 173 $abrt = ''; 174 } 175 } else { 176 echo 'uhm. what happened? where am I? This should not happen'; 177 } 178 } else { 179 # first time run, show intro 180 echo $this->locale_xhtml('step0'); 181 $abrt = false; 182 $next = 'version'; 183 } 184 } 185 186 /** 187 * Output the given arguments using vsprintf and flush buffers 188 */ 189 public function _say() { 190 $args = func_get_args(); 191 echo '<img src="'.DOKU_BASE.'lib/images/blank.gif" width="16" height="16" alt="" /> '; 192 echo vsprintf(array_shift($args)."<br />\n", $args); 193 flush(); 194 ob_flush(); 195 } 196 197 /** 198 * Print a warning using the given arguments with vsprintf and flush buffers 199 */ 200 public function _warn() { 201 $this->haderrors = true; 202 203 $args = func_get_args(); 204 echo '<img src="'.DOKU_BASE.'lib/images/error.png" width="16" height="16" alt="!" /> '; 205 echo vsprintf(array_shift($args)."<br />\n", $args); 206 flush(); 207 ob_flush(); 208 } 209 210 /** 211 * Recursive delete 212 * 213 * @author Jon Hassall 214 * @link http://de.php.net/manual/en/function.unlink.php#87045 215 */ 216 private function _rdel($dir) { 217 if(!$dh = @opendir($dir)) { 218 return false; 219 } 220 while(false !== ($obj = readdir($dh))) { 221 if($obj == '.' || $obj == '..') continue; 222 223 if(!@unlink($dir.'/'.$obj)) { 224 $this->_rdel($dir.'/'.$obj); 225 } 226 } 227 closedir($dh); 228 return @rmdir($dir); 229 } 230 231 /** 232 * Check various versions 233 * 234 * @return bool 235 */ 236 private function _step_version() { 237 $ok = true; 238 239 // we need SSL - only newer HTTPClients check that themselves 240 if(!in_array('ssl', stream_get_transports())) { 241 $this->_warn($this->getLang('vs_ssl')); 242 $ok = false; 243 } 244 245 // get the available version 246 $http = new DokuHTTPClient(); 247 $tgzversion = $http->get($this->tgzversion); 248 if(!$tgzversion) { 249 $this->_warn($this->getLang('vs_tgzno').' '.hsc($http->error)); 250 $ok = false; 251 } 252 if(!preg_match('/(^| )(\d\d\d\d-\d\d-\d\d[a-z]*)( |$)/i', $tgzversion, $m)) { 253 $this->_warn($this->getLang('vs_tgzno')); 254 $ok = false; 255 $tgzversionnum = 0; 256 } else { 257 $tgzversionnum = $m[2]; 258 $this->_say($this->getLang('vs_tgz'), $tgzversion); 259 } 260 261 // get the current version 262 $version = getVersion(); 263 if(!preg_match('/(^| )(\d\d\d\d-\d\d-\d\d[a-z]*)( |$)/i', $version, $m)) { 264 $versionnum = 0; 265 } else { 266 $versionnum = $m[2]; 267 } 268 $this->_say($this->getLang('vs_local'), $version); 269 270 // compare versions 271 if(!$versionnum) { 272 $this->_warn($this->getLang('vs_localno')); 273 $ok = false; 274 } else if($tgzversionnum) { 275 if($tgzversionnum < $versionnum) { 276 $this->_warn($this->getLang('vs_newer')); 277 $ok = false; 278 } elseif($tgzversionnum == $versionnum) { 279 $this->_warn($this->getLang('vs_same')); 280 $ok = false; 281 } 282 } 283 284 // check plugin version 285 $pluginversion = $http->get($this->pluginversion); 286 if($pluginversion) { 287 $plugininfo = linesToHash(explode("\n", $pluginversion)); 288 $myinfo = $this->getInfo(); 289 if($plugininfo['date'] > $myinfo['date']) { 290 $this->_warn($this->getLang('vs_plugin'), $plugininfo['date']); 291 $ok = false; 292 } 293 } 294 295 // check if PHP is up to date 296 $minphp = '5.6'; 297 if(version_compare(phpversion(), $minphp, '<')) { 298 $this->_warn($this->getLang('vs_php'), $minphp, phpversion()); 299 $ok = false; 300 } 301 302 return $ok; 303 } 304 305 /** 306 * Redirect to the start page 307 */ 308 private function _step_done() { 309 echo $this->getLang('finish'); 310 echo "<script type='text/javascript'>location.href='".DOKU_URL."';</script>"; 311 } 312 313 /** 314 * Download the tarball 315 * 316 * @return bool 317 */ 318 private function _step_download() { 319 $this->_say($this->getLang('dl_from'), $this->tgzurl); 320 321 @set_time_limit(300); 322 @ignore_user_abort(); 323 324 $http = new DokuHTTPClient(); 325 $http->timeout = 300; 326 $data = $http->get($this->tgzurl); 327 328 if(!$data) { 329 $this->_warn($http->error); 330 $this->_warn($this->getLang('dl_fail')); 331 return false; 332 } 333 334 if(!io_saveFile($this->tgzfile, $data)) { 335 $this->_warn($this->getLang('dl_fail')); 336 return false; 337 } 338 339 $this->_say($this->getLang('dl_done'), filesize_h(strlen($data))); 340 341 return true; 342 } 343 344 /** 345 * Unpack the tarball 346 * 347 * @return bool 348 */ 349 private function _step_unpack() { 350 $this->_say('<b>'.$this->getLang('pk_extract').'</b>'); 351 352 @set_time_limit(300); 353 @ignore_user_abort(); 354 355 try { 356 $tar = new VerboseTar(); 357 $tar->open($this->tgzfile); 358 $tar->extract($this->tgzdir, 1); 359 $tar->close(); 360 } catch (Exception $e) { 361 $this->_warn($e->getMessage()); 362 $this->_warn($this->getLang('pk_fail')); 363 return false; 364 } 365 366 $this->_say($this->getLang('pk_done')); 367 368 $this->_say( 369 $this->getLang('pk_version'), 370 hsc(file_get_contents($this->tgzdir.'/VERSION')), 371 getVersion() 372 ); 373 return true; 374 } 375 376 /** 377 * Check permissions of files to change 378 * 379 * @return bool 380 */ 381 private function _step_check() { 382 $this->_say($this->getLang('ck_start')); 383 $ok = $this->_traverse('', true); 384 if($ok) { 385 $this->_say('<b>'.$this->getLang('ck_done').'</b>'); 386 } else { 387 $this->_warn('<b>'.$this->getLang('ck_fail').'</b>'); 388 } 389 return $ok; 390 } 391 392 /** 393 * Copy over new files 394 * 395 * @return bool 396 */ 397 private function _step_copy() { 398 $this->_say($this->getLang('cp_start')); 399 $ok = $this->_traverse('', false); 400 if($ok) { 401 $this->_say('<b>'.$this->getLang('cp_done').'</b>'); 402 $this->_rmold(); 403 $this->_say('<b>'.$this->getLang('finish').'</b>'); 404 } else { 405 $this->_warn('<b>'.$this->getLang('cp_fail').'</b>'); 406 } 407 return $ok; 408 } 409 410 /** 411 * Delete outdated files 412 */ 413 private function _rmold() { 414 global $conf; 415 416 $list = file($this->tgzdir.'data/deleted.files'); 417 foreach($list as $line) { 418 $line = trim(preg_replace('/#.*$/', '', $line)); 419 if(!$line) continue; 420 $file = DOKU_INC.$line; 421 if(!file_exists($file)) continue; 422 423 // check that the given file is an case sensitive match 424 if(basename(realpath($file)) != basename($file)) { 425 $this->_warn($this->getLang('rm_mismatch'), hsc($line)); 426 } 427 428 if((is_dir($file) && $this->_rdel($file)) || 429 @unlink($file) 430 ) { 431 $this->_say($this->getLang('rm_done'), hsc($line)); 432 } else { 433 $this->_warn($this->getLang('rm_fail'), hsc($line)); 434 } 435 } 436 // delete install 437 @unlink(DOKU_INC.'install.php'); 438 439 // make sure update message will be gone 440 @touch(DOKU_INC.'doku.php'); 441 @unlink($conf['cachedir'].'/messages.txt'); 442 } 443 444 /** 445 * Traverse over the given dir and compare it to the DokuWiki dir 446 * 447 * Checks what files need an update, tests for writability and copies 448 * 449 * @param string $dir 450 * @param bool $dryrun do not copy but only check permissions 451 * @return bool 452 */ 453 private function _traverse($dir, $dryrun) { 454 $base = $this->tgzdir; 455 $ok = true; 456 457 $dh = @opendir($base.'/'.$dir); 458 if(!$dh) return false; 459 while(($file = readdir($dh)) !== false) { 460 if($file == '.' || $file == '..') continue; 461 $from = "$base/$dir/$file"; 462 $to = DOKU_INC."$dir/$file"; 463 464 if(is_dir($from)) { 465 if($dryrun) { 466 // just check for writability 467 if(!is_dir($to)) { 468 if(is_dir(dirname($to)) && !is_writable(dirname($to))) { 469 $this->_warn('<b>'.$this->getLang('tv_noperm').'</b>', hsc("$dir/$file")); 470 $ok = false; 471 } 472 } 473 } 474 475 // recursion 476 if(!$this->_traverse("$dir/$file", $dryrun)) { 477 $ok = false; 478 } 479 } else { 480 $fmd5 = md5(@file_get_contents($from)); 481 $tmd5 = md5(@file_get_contents($to)); 482 if($fmd5 != $tmd5 || !file_exists($to)) { 483 if($dryrun) { 484 // just check for writability 485 if((file_exists($to) && !is_writable($to)) || 486 (!file_exists($to) && is_dir(dirname($to)) && !is_writable(dirname($to))) 487 ) { 488 489 $this->_warn('<b>'.$this->getLang('tv_noperm').'</b>', hsc("$dir/$file")); 490 $ok = false; 491 } else { 492 $this->_say($this->getLang('tv_upd'), hsc("$dir/$file")); 493 } 494 } else { 495 // check dir 496 if(io_mkdir_p(dirname($to))) { 497 // remove existing (avoid case sensitivity problems) 498 if(file_exists($to) && !@unlink($to)) { 499 $this->_warn('<b>'.$this->getLang('tv_nodel').'</b>', hsc("$dir/$file")); 500 $ok = false; 501 } 502 // copy 503 if(!copy($from, $to)) { 504 $this->_warn('<b>'.$this->getLang('tv_nocopy').'</b>', hsc("$dir/$file")); 505 $ok = false; 506 } else { 507 $this->_say($this->getLang('tv_done'), hsc("$dir/$file")); 508 } 509 } else { 510 $this->_warn('<b>'.$this->getLang('tv_nodir').'</b>', hsc("$dir")); 511 $ok = false; 512 } 513 } 514 } 515 } 516 } 517 closedir($dh); 518 return $ok; 519 } 520} 521 522// vim:ts=4:sw=4:et:enc=utf-8: 523