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'; 14 15class admin_plugin_upgrade extends DokuWiki_Admin_Plugin { 16 private $tgzurl; 17 private $tgzfile; 18 private $tgzdir; 19 20 public function __construct() { 21 global $conf; 22 23 $branch = 'stable'; 24 25 $this->tgzurl = "https://github.com/splitbrain/dokuwiki/archive/$branch.tar.gz"; 26 $this->tgzfile = $conf['tmpdir'].'/dokuwiki-upgrade.tgz'; 27 $this->tgzdir = $conf['tmpdir'].'/dokuwiki-upgrade/'; 28 } 29 30 public function getMenuSort() { 31 return 555; 32 } 33 34 public function handle() { 35 if($_REQUEST['step'] && !checkSecurityToken()) { 36 unset($_REQUEST['step']); 37 } 38 } 39 40 public function html() { 41 $abrt = false; 42 $next = false; 43 44 echo '<h1>'.$this->getLang('menu').'</h1>'; 45 46 $this->_say('<div id="plugin__upgrade">'); 47 // enable auto scroll 48 ?> 49 <script language="javascript" type="text/javascript"> 50 var plugin_upgrade = window.setInterval(function () { 51 var obj = document.getElementById('plugin__upgrade'); 52 if (obj) obj.scrollTop = obj.scrollHeight; 53 }, 25); 54 </script> 55 <?php 56 57 // handle current step 58 $this->_stepit($abrt, $next); 59 60 // disable auto scroll 61 ?> 62 <script language="javascript" type="text/javascript"> 63 window.setTimeout(function () { 64 window.clearInterval(plugin_upgrade); 65 }, 50); 66 </script> 67 <?php 68 $this->_say('</div>'); 69 70 echo '<form action="" method="get" id="plugin__upgrade_form">'; 71 echo '<input type="hidden" name="do" value="admin" />'; 72 echo '<input type="hidden" name="page" value="upgrade" />'; 73 echo '<input type="hidden" name="sectok" value="'.getSecurityToken().'" />'; 74 if($next) echo '<input type="submit" name="step['.$next.']" value="Continue" class="button continue" />'; 75 if($abrt) echo '<input type="submit" name="step[cancel]" value="Abort" class="button abort" />'; 76 echo '</form>'; 77 } 78 79 /** 80 * Decides the current step and executes it 81 * 82 * @param bool $abrt 83 * @param bool $next 84 */ 85 private function _stepit(&$abrt, &$next) { 86 global $conf; 87 if($conf['safemodehack']) { 88 $abrt = false; 89 $next = false; 90 echo $this->locale_xhtml('safemode'); 91 } 92 93 if(isset($_REQUEST['step']) && is_array($_REQUEST['step'])) { 94 $step = array_shift(array_keys($_REQUEST['step'])); 95 } else { 96 $step = ''; 97 } 98 99 if($step == 'cancel') { 100 # cleanup 101 @unlink($this->tgzfile); 102 $this->_rdel($this->tgzdir); 103 $step = ''; 104 } 105 106 if($step) { 107 $abrt = true; 108 $next = false; 109 if(!file_exists($this->tgzfile)) { 110 if($this->_step_download()) $next = 'unpack'; 111 } elseif(!is_dir($this->tgzdir)) { 112 if($this->_step_unpack()) $next = 'check'; 113 } elseif($step != 'upgrade') { 114 if($this->_step_check()) $next = 'upgrade'; 115 } elseif($step == 'upgrade') { 116 if($this->_step_copy()) $next = 'cancel'; 117 } else { 118 echo 'uhm. what happened? where am I? This should not happen'; 119 } 120 } else { 121 # first time run, show intro 122 echo $this->locale_xhtml('step0'); 123 $abrt = false; 124 $next = 'download'; 125 } 126 } 127 128 /** 129 * Output the given arguments using vsprintf and flush buffers 130 */ 131 private function _say() { 132 $args = func_get_args(); 133 echo vsprintf(array_shift($args)."<br />\n", $args); 134 flush(); 135 ob_flush(); 136 } 137 138 /** 139 * Recursive delete 140 * 141 * @author Jon Hassall 142 * @link http://de.php.net/manual/en/function.unlink.php#87045 143 */ 144 private function _rdel($dir) { 145 if(!$dh = @opendir($dir)) { 146 return false; 147 } 148 while(false !== ($obj = readdir($dh))) { 149 if($obj == '.' || $obj == '..') continue; 150 151 if(!@unlink($dir.'/'.$obj)) { 152 $this->_rdel($dir.'/'.$obj); 153 } 154 } 155 closedir($dh); 156 return @rmdir($dir); 157 } 158 159 /** 160 * Download the tarball 161 * 162 * @return bool 163 */ 164 private function _step_download() { 165 $this->_say($this->getLang('dl_from'), $this->tgzurl); 166 167 @set_time_limit(120); 168 @ignore_user_abort(); 169 170 $http = new DokuHTTPClient(); 171 $http->timeout = 120; 172 $data = $http->get($this->tgzurl); 173 174 if(!$data) { 175 $this->_say($http->error); 176 $this->_say($this->getLang('dl_fail')); 177 return false; 178 } 179 180 if(!io_saveFile($this->tgzfile, $data)) { 181 $this->_say($this->getLang('dl_fail')); 182 return false; 183 } 184 185 $this->_say($this->getLang('dl_done'), filesize_h(strlen($data))); 186 187 return true; 188 } 189 190 /** 191 * Unpack the tarball 192 * 193 * @return bool 194 */ 195 private function _step_unpack() { 196 global $conf; 197 $this->_say('<b>'.$this->getLang('pk_extract').'</b>'); 198 199 @set_time_limit(120); 200 @ignore_user_abort(); 201 202 $tar = new VerboseTarLib($this->tgzfile); 203 if($tar->_initerror < 0) { 204 $this->_say($tar->TarErrorStr($tar->_initerror)); 205 $this->_say($this->getLang('pk_fail')); 206 return false; 207 } 208 209 $ok = $tar->Extract(VerboseTarLib::FULL_ARCHIVE, $this->tgzdir, 1, $conf['fmode'], '/^(_cs|_test|\.gitignore)/'); 210 if($ok < 1) { 211 $this->_say($tar->TarErrorStr($ok)); 212 $this->_say($this->getLang('pk_fail')); 213 return false; 214 } 215 216 $this->_say($this->getLang('pk_done')); 217 218 $this->_say( 219 $this->getLang('pk_version'), 220 hsc(file_get_contents($this->tgzdir.'/VERSION')), 221 getVersion() 222 ); 223 return true; 224 } 225 226 /** 227 * Check permissions of files to change 228 * 229 * @return bool 230 */ 231 private function _step_check() { 232 $this->_say($this->getLang('ck_start')); 233 $ok = $this->_traverse('', true); 234 if($ok) { 235 $this->_say('<b>'.$this->getLang('ck_done').'</b>'); 236 } else { 237 $this->_say('<b>'.$this->getLang('ck_fail').'</b>'); 238 } 239 return $ok; 240 } 241 242 /** 243 * Copy over new files 244 * 245 * @return bool 246 */ 247 private function _step_copy() { 248 $this->_say($this->getLang('cp_start')); 249 $ok = $this->_traverse('', false); 250 if($ok) { 251 $this->_say('<b>'.$this->getLang('cp_done').'</b>'); 252 $this->_rmold(); 253 $this->_say('<b>'.$this->getLang('finish').'</b>'); 254 } else { 255 $this->_say('<b>'.$this->getLang('cp_fail').'</b>'); 256 } 257 return $ok; 258 } 259 260 /** 261 * Delete outdated files 262 */ 263 private function _rmold() { 264 $list = file($this->tgzdir.'data/deleted.files'); 265 foreach($list as $line) { 266 $line = trim(preg_replace('/#.*$/', '', $line)); 267 if(!$line) continue; 268 $file = DOKU_INC.$line; 269 if(!file_exists($file)) continue; 270 if((is_dir($file) && $this->_rdel($file)) || 271 @unlink($file) 272 ) { 273 $this->_say($this->getLang('rm_done'), hsc($line)); 274 } else { 275 $this->_say($this->getLang('rm_fail'), hsc($line)); 276 } 277 } 278 } 279 280 /** 281 * Traverse over the given dir and compare it to the DokuWiki dir 282 * 283 * Checks what files need an update, tests for writability and copies 284 * 285 * @param string $dir 286 * @param bool $dryrun do not copy but only check permissions 287 * @return bool 288 */ 289 private function _traverse($dir, $dryrun) { 290 $base = $this->tgzdir; 291 $ok = true; 292 293 $dh = @opendir($base.'/'.$dir); 294 if(!$dh) return false; 295 while(($file = readdir($dh)) !== false) { 296 if($file == '.' || $file == '..') continue; 297 $from = "$base/$dir/$file"; 298 $to = DOKU_INC."$dir/$file"; 299 300 if(is_dir($from)) { 301 if($dryrun) { 302 // just check for writability 303 if(!is_dir($to)) { 304 if(is_dir(dirname($to)) && !is_writable(dirname($to))) { 305 $this->_say('<b>'.$this->getLang('tv_noperm').'</b>', hsc("$dir/$file")); 306 $ok = false; 307 } 308 } 309 } 310 311 // recursion 312 if(!$this->_traverse("$dir/$file", $dryrun)) { 313 $ok = false; 314 } 315 } else { 316 $fmd5 = md5(@file_get_contents($from)); 317 $tmd5 = md5(@file_get_contents($to)); 318 if($fmd5 != $tmd5 || !file_exists($to)) { 319 if($dryrun) { 320 // just check for writability 321 if((file_exists($to) && !is_writable($to)) || 322 (!file_exists($to) && is_dir(dirname($to)) && !is_writable(dirname($to))) 323 ) { 324 325 $this->_say('<b>'.$this->getLang('tv_noperm').'</b>', hsc("$dir/$file")); 326 $ok = false; 327 } else { 328 $this->_say($this->getLang('tv_upd'), hsc("$dir/$file")); 329 } 330 } else { 331 // check dir 332 if(io_mkdir_p(dirname($to))) { 333 // copy 334 if(!copy($from, $to)) { 335 $this->_say('<b>'.$this->getLang('tv_nocopy').'</b>', hsc("$dir/$file")); 336 $ok = false; 337 } else { 338 $this->_say($this->getLang('tv_done'), hsc("$dir/$file")); 339 } 340 } else { 341 $this->_say('<b>'.$this->getLang('tv_nodir').'</b>', hsc("$dir")); 342 $ok = false; 343 } 344 } 345 } 346 } 347 } 348 closedir($dh); 349 return $ok; 350 } 351} 352 353// vim:ts=4:sw=4:et:enc=utf-8: 354