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 if (!io_saveFile($this->tgzfile, $data)) { 147 $this->log('error', $this->getLang('dl_fail')); 148 return false; 149 } 150 151 $this->log('success', $this->getLang('dl_done'), filesize_h(strlen($data))); 152 return true; 153 } 154 155 /** 156 * Unpack the tarball 157 * 158 * @return bool 159 */ 160 public function extractTarball() 161 { 162 $this->log('notice', '<b>' . $this->getLang('pk_extract') . '</b>'); 163 164 @set_time_limit(300); 165 @ignore_user_abort(); 166 167 try { 168 $tar = new Tar(); 169 $tar->setCallback(function ($file) { 170 /** @var FileInfo $file */ 171 $this->log('info', $file->getPath()); 172 }); 173 $tar->open($this->tgzfile); 174 $tar->extract($this->tgzdir, 1); 175 $tar->close(); 176 } catch (Exception $e) { 177 $this->log('error', $e->getMessage()); 178 $this->log('error', $this->getLang('pk_fail')); 179 return false; 180 } 181 182 $this->log('success', $this->getLang('pk_done')); 183 184 $this->log( 185 'notice', 186 $this->getLang('pk_version'), 187 hsc(file_get_contents($this->tgzdir . '/VERSION')), 188 getVersion() 189 ); 190 return true; 191 } 192 193 /** 194 * Check permissions of files to change 195 * 196 * @return bool 197 */ 198 public function checkPermissions() 199 { 200 $this->log('notice', $this->getLang('ck_start')); 201 $ok = $this->traverseCheckAndCopy('', true); 202 if ($ok) { 203 $this->log('success', '<b>' . $this->getLang('ck_done') . '</b>'); 204 } else { 205 $this->log('error', '<b>' . $this->getLang('ck_fail') . '</b>'); 206 } 207 return $ok; 208 } 209 210 /** 211 * Copy over new files 212 * 213 * @return bool 214 */ 215 public function copyFiles() 216 { 217 $this->log('notice', $this->getLang('cp_start')); 218 $ok = $this->traverseCheckAndCopy('', false); 219 if ($ok) { 220 $this->log('success', '<b>' . $this->getLang('cp_done') . '</b>'); 221 } else { 222 $this->log('error', '<b>' . $this->getLang('cp_fail') . '</b>'); 223 } 224 return $ok; 225 } 226 227 /** 228 * Delete outdated files 229 */ 230 public function deleteObsoleteFiles() 231 { 232 global $conf; 233 234 $list = file($this->tgzdir . 'data/deleted.files'); 235 foreach ($list as $line) { 236 $line = trim(preg_replace('/#.*$/', '', $line)); 237 if (!$line) continue; 238 $file = DOKU_INC . $line; 239 if (!file_exists($file)) continue; 240 241 // check that the given file is a case sensitive match 242 if (basename(realpath($file)) != basename($file)) { 243 $this->log('info', $this->getLang('rm_mismatch'), hsc($line)); 244 continue; 245 } 246 247 if ((is_dir($file) && $this->recursiveDelete($file)) || 248 @unlink($file) 249 ) { 250 $this->log('info', $this->getLang('rm_done'), hsc($line)); 251 } else { 252 $this->log('error', $this->getLang('rm_fail'), hsc($line)); 253 } 254 } 255 // delete install 256 @unlink(DOKU_INC . 'install.php'); 257 258 // make sure update message will be gone 259 @touch(DOKU_INC . 'doku.php'); 260 @unlink($conf['cachedir'] . '/messages.txt'); 261 262 // clear opcache 263 if (function_exists('opcache_reset')) { 264 opcache_reset(); 265 } 266 267 $this->log('success', '<b>' . $this->getLang('finish') . '</b>'); 268 return true; 269 } 270 271 /** 272 * Remove the downloaded and extracted files 273 * 274 * @return bool 275 */ 276 public function cleanUp() 277 { 278 @unlink($this->tgzfile); 279 $this->recursiveDelete($this->tgzdir); 280 return true; 281 } 282 283 // endregion 284 285 /** 286 * Traverse over the given dir and compare it to the DokuWiki dir 287 * 288 * Checks what files need an update, tests for writability and copies 289 * 290 * @param string $dir 291 * @param bool $dryrun do not copy but only check permissions 292 * @return bool 293 */ 294 private function traverseCheckAndCopy($dir, $dryrun) 295 { 296 $base = $this->tgzdir; 297 $ok = true; 298 299 $dh = @opendir($base . '/' . $dir); 300 if (!$dh) return false; 301 while (($file = readdir($dh)) !== false) { 302 if ($file == '.' || $file == '..') continue; 303 $from = "$base/$dir/$file"; 304 $to = DOKU_INC . "$dir/$file"; 305 306 if (is_dir($from)) { 307 if ($dryrun) { 308 // just check for writability 309 if (!is_dir($to)) { 310 if (is_dir(dirname($to)) && !is_writable(dirname($to))) { 311 $this->log('error', '<b>' . $this->getLang('tv_noperm') . '</b>', hsc("$dir/$file")); 312 $ok = false; 313 } 314 } 315 } 316 317 // recursion 318 if (!$this->traverseCheckAndCopy("$dir/$file", $dryrun)) { 319 $ok = false; 320 } 321 } else { 322 $fmd5 = md5(@file_get_contents($from)); 323 $tmd5 = md5(@file_get_contents($to)); 324 if ($fmd5 != $tmd5 || !file_exists($to)) { 325 if ($dryrun) { 326 // just check for writability 327 if ((file_exists($to) && !is_writable($to)) || 328 (!file_exists($to) && is_dir(dirname($to)) && !is_writable(dirname($to))) 329 ) { 330 331 $this->log('error', '<b>' . $this->getLang('tv_noperm') . '</b>', hsc("$dir/$file")); 332 $ok = false; 333 } else { 334 $this->log('info', $this->getLang('tv_upd'), hsc("$dir/$file")); 335 } 336 } else { 337 // check dir 338 if (io_mkdir_p(dirname($to))) { 339 // remove existing (avoid case sensitivity problems) 340 if (file_exists($to) && !@unlink($to)) { 341 $this->log('error', '<b>' . $this->getLang('tv_nodel') . '</b>', hsc("$dir/$file")); 342 $ok = false; 343 } 344 // copy 345 if (!copy($from, $to)) { 346 $this->log('error', '<b>' . $this->getLang('tv_nocopy') . '</b>', hsc("$dir/$file")); 347 $ok = false; 348 } else { 349 $this->log('info', $this->getLang('tv_done'), hsc("$dir/$file")); 350 } 351 } else { 352 $this->log('error', '<b>' . $this->getLang('tv_nodir') . '</b>', hsc("$dir")); 353 $ok = false; 354 } 355 } 356 } 357 } 358 } 359 closedir($dh); 360 return $ok; 361 } 362 363 // region utilities 364 365 /** 366 * Figure out the release date from the version string 367 * 368 * @param $version 369 * @return int|string returns 0 if the version can't be read 370 */ 371 protected function dateFromVersion($version) 372 { 373 if (preg_match('/(^|\D)(\d\d\d\d-\d\d-\d\d)(\D|$)/i', $version, $m)) { 374 return $m[2]; 375 } 376 return 0; 377 } 378 379 /** 380 * Recursive delete 381 * 382 * @author Jon Hassall 383 * @link http://de.php.net/manual/en/function.unlink.php#87045 384 */ 385 protected function recursiveDelete($dir) 386 { 387 if (!$dh = @opendir($dir)) { 388 return false; 389 } 390 while (false !== ($obj = readdir($dh))) { 391 if ($obj == '.' || $obj == '..') continue; 392 393 if (!@unlink($dir . '/' . $obj)) { 394 $this->recursiveDelete($dir . '/' . $obj); 395 } 396 } 397 closedir($dh); 398 return @rmdir($dir); 399 } 400 401 /** 402 * Log a message 403 * 404 * @param string ...$level, $msg 405 */ 406 protected function log() 407 { 408 $args = func_get_args(); 409 $level = array_shift($args); 410 $msg = array_shift($args); 411 $msg = vsprintf($msg, $args); 412 if ($this->logger) $this->logger->log($level, $msg); 413 } 414 415 // endregion 416} 417