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