[ Index ] |
PHP Cross Reference of DokuWiki |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * DokuWiki Plugin extension (Helper Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Michael Hamann <michael@content-space.de> 7 */ 8 9 use dokuwiki\HTTP\DokuHTTPClient; 10 use dokuwiki\Extension\PluginController; 11 12 /** 13 * Class helper_plugin_extension_extension represents a single extension (plugin or template) 14 */ 15 class helper_plugin_extension_extension extends DokuWiki_Plugin 16 { 17 private $id; 18 private $base; 19 private $is_template = false; 20 private $localInfo; 21 private $remoteInfo; 22 private $managerData; 23 /** @var helper_plugin_extension_repository $repository */ 24 private $repository = null; 25 26 /** @var array list of temporary directories */ 27 private $temporary = array(); 28 29 /** @var string where templates are installed to */ 30 private $tpllib = ''; 31 32 /** 33 * helper_plugin_extension_extension constructor. 34 */ 35 public function __construct() 36 { 37 $this->tpllib = dirname(tpl_incdir()).'/'; 38 } 39 40 /** 41 * Destructor 42 * 43 * deletes any dangling temporary directories 44 */ 45 public function __destruct() 46 { 47 foreach ($this->temporary as $dir) { 48 io_rmdir($dir, true); 49 } 50 } 51 52 /** 53 * @return bool false, this component is not a singleton 54 */ 55 public function isSingleton() 56 { 57 return false; 58 } 59 60 /** 61 * Set the name of the extension this instance shall represents, triggers loading the local and remote data 62 * 63 * @param string $id The id of the extension (prefixed with template: for templates) 64 * @return bool If some (local or remote) data was found 65 */ 66 public function setExtension($id) 67 { 68 $id = cleanID($id); 69 $this->id = $id; 70 $this->base = $id; 71 72 if (substr($id, 0, 9) == 'template:') { 73 $this->base = substr($id, 9); 74 $this->is_template = true; 75 } else { 76 $this->is_template = false; 77 } 78 79 $this->localInfo = array(); 80 $this->managerData = array(); 81 $this->remoteInfo = array(); 82 83 if ($this->isInstalled()) { 84 $this->readLocalData(); 85 $this->readManagerData(); 86 } 87 88 if ($this->repository == null) { 89 $this->repository = $this->loadHelper('extension_repository'); 90 } 91 92 $this->remoteInfo = $this->repository->getData($this->getID()); 93 94 return ($this->localInfo || $this->remoteInfo); 95 } 96 97 /** 98 * If the extension is installed locally 99 * 100 * @return bool If the extension is installed locally 101 */ 102 public function isInstalled() 103 { 104 return is_dir($this->getInstallDir()); 105 } 106 107 /** 108 * If the extension is under git control 109 * 110 * @return bool 111 */ 112 public function isGitControlled() 113 { 114 if (!$this->isInstalled()) return false; 115 return file_exists($this->getInstallDir().'/.git'); 116 } 117 118 /** 119 * If the extension is bundled 120 * 121 * @return bool If the extension is bundled 122 */ 123 public function isBundled() 124 { 125 if (!empty($this->remoteInfo['bundled'])) return $this->remoteInfo['bundled']; 126 return in_array( 127 $this->id, 128 array( 129 'authad', 'authldap', 'authpdo', 'authplain', 130 'acl', 'config', 'extension', 'info', 'popularity', 'revert', 131 'safefnrecode', 'styling', 'testing', 'usermanager', 'logviewer', 132 'template:dokuwiki', 133 ) 134 ); 135 } 136 137 /** 138 * If the extension is protected against any modification (disable/uninstall) 139 * 140 * @return bool if the extension is protected 141 */ 142 public function isProtected() 143 { 144 // never allow deinstalling the current auth plugin: 145 global $conf; 146 if ($this->id == $conf['authtype']) return true; 147 148 /** @var PluginController $plugin_controller */ 149 global $plugin_controller; 150 $cascade = $plugin_controller->getCascade(); 151 return (isset($cascade['protected'][$this->id]) && $cascade['protected'][$this->id]); 152 } 153 154 /** 155 * If the extension is installed in the correct directory 156 * 157 * @return bool If the extension is installed in the correct directory 158 */ 159 public function isInWrongFolder() 160 { 161 return $this->base != $this->getBase(); 162 } 163 164 /** 165 * If the extension is enabled 166 * 167 * @return bool If the extension is enabled 168 */ 169 public function isEnabled() 170 { 171 global $conf; 172 if ($this->isTemplate()) { 173 return ($conf['template'] == $this->getBase()); 174 } 175 176 /* @var PluginController $plugin_controller */ 177 global $plugin_controller; 178 return $plugin_controller->isEnabled($this->base); 179 } 180 181 /** 182 * If the extension should be updated, i.e. if an updated version is available 183 * 184 * @return bool If an update is available 185 */ 186 public function updateAvailable() 187 { 188 if (!$this->isInstalled()) return false; 189 if ($this->isBundled()) return false; 190 $lastupdate = $this->getLastUpdate(); 191 if ($lastupdate === false) return false; 192 $installed = $this->getInstalledVersion(); 193 if ($installed === false || $installed === $this->getLang('unknownversion')) return true; 194 return $this->getInstalledVersion() < $this->getLastUpdate(); 195 } 196 197 /** 198 * If the extension is a template 199 * 200 * @return bool If this extension is a template 201 */ 202 public function isTemplate() 203 { 204 return $this->is_template; 205 } 206 207 /** 208 * Get the ID of the extension 209 * 210 * This is the same as getName() for plugins, for templates it's getName() prefixed with 'template:' 211 * 212 * @return string 213 */ 214 public function getID() 215 { 216 return $this->id; 217 } 218 219 /** 220 * Get the name of the installation directory 221 * 222 * @return string The name of the installation directory 223 */ 224 public function getInstallName() 225 { 226 return $this->base; 227 } 228 229 // Data from plugin.info.txt/template.info.txt or the repo when not available locally 230 /** 231 * Get the basename of the extension 232 * 233 * @return string The basename 234 */ 235 public function getBase() 236 { 237 if (!empty($this->localInfo['base'])) return $this->localInfo['base']; 238 return $this->base; 239 } 240 241 /** 242 * Get the display name of the extension 243 * 244 * @return string The display name 245 */ 246 public function getDisplayName() 247 { 248 if (!empty($this->localInfo['name'])) return $this->localInfo['name']; 249 if (!empty($this->remoteInfo['name'])) return $this->remoteInfo['name']; 250 return $this->base; 251 } 252 253 /** 254 * Get the author name of the extension 255 * 256 * @return string|bool The name of the author or false if there is none 257 */ 258 public function getAuthor() 259 { 260 if (!empty($this->localInfo['author'])) return $this->localInfo['author']; 261 if (!empty($this->remoteInfo['author'])) return $this->remoteInfo['author']; 262 return false; 263 } 264 265 /** 266 * Get the email of the author of the extension if there is any 267 * 268 * @return string|bool The email address or false if there is none 269 */ 270 public function getEmail() 271 { 272 // email is only in the local data 273 if (!empty($this->localInfo['email'])) return $this->localInfo['email']; 274 return false; 275 } 276 277 /** 278 * Get the email id, i.e. the md5sum of the email 279 * 280 * @return string|bool The md5sum of the email if there is any, false otherwise 281 */ 282 public function getEmailID() 283 { 284 if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid']; 285 if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']); 286 return false; 287 } 288 289 /** 290 * Get the description of the extension 291 * 292 * @return string The description 293 */ 294 public function getDescription() 295 { 296 if (!empty($this->localInfo['desc'])) return $this->localInfo['desc']; 297 if (!empty($this->remoteInfo['description'])) return $this->remoteInfo['description']; 298 return ''; 299 } 300 301 /** 302 * Get the URL of the extension, usually a page on dokuwiki.org 303 * 304 * @return string The URL 305 */ 306 public function getURL() 307 { 308 if (!empty($this->localInfo['url'])) return $this->localInfo['url']; 309 return 'https://www.dokuwiki.org/'. 310 ($this->isTemplate() ? 'template' : 'plugin').':'.$this->getBase(); 311 } 312 313 /** 314 * Get the installed version of the extension 315 * 316 * @return string|bool The version, usually in the form yyyy-mm-dd if there is any 317 */ 318 public function getInstalledVersion() 319 { 320 if (!empty($this->localInfo['date'])) return $this->localInfo['date']; 321 if ($this->isInstalled()) return $this->getLang('unknownversion'); 322 return false; 323 } 324 325 /** 326 * Get the install date of the current version 327 * 328 * @return string|bool The date of the last update or false if not available 329 */ 330 public function getUpdateDate() 331 { 332 if (!empty($this->managerData['updated'])) return $this->managerData['updated']; 333 return $this->getInstallDate(); 334 } 335 336 /** 337 * Get the date of the installation of the plugin 338 * 339 * @return string|bool The date of the installation or false if not available 340 */ 341 public function getInstallDate() 342 { 343 if (!empty($this->managerData['installed'])) return $this->managerData['installed']; 344 return false; 345 } 346 347 /** 348 * Get the names of the dependencies of this extension 349 * 350 * @return array The base names of the dependencies 351 */ 352 public function getDependencies() 353 { 354 if (!empty($this->remoteInfo['dependencies'])) return $this->remoteInfo['dependencies']; 355 return array(); 356 } 357 358 /** 359 * Get the names of the missing dependencies 360 * 361 * @return array The base names of the missing dependencies 362 */ 363 public function getMissingDependencies() 364 { 365 /* @var PluginController $plugin_controller */ 366 global $plugin_controller; 367 $dependencies = $this->getDependencies(); 368 $missing_dependencies = array(); 369 foreach ($dependencies as $dependency) { 370 if (!$plugin_controller->isEnabled($dependency)) { 371 $missing_dependencies[] = $dependency; 372 } 373 } 374 return $missing_dependencies; 375 } 376 377 /** 378 * Get the names of all conflicting extensions 379 * 380 * @return array The names of the conflicting extensions 381 */ 382 public function getConflicts() 383 { 384 if (!empty($this->remoteInfo['conflicts'])) return $this->remoteInfo['conflicts']; 385 return array(); 386 } 387 388 /** 389 * Get the names of similar extensions 390 * 391 * @return array The names of similar extensions 392 */ 393 public function getSimilarExtensions() 394 { 395 if (!empty($this->remoteInfo['similar'])) return $this->remoteInfo['similar']; 396 return array(); 397 } 398 399 /** 400 * Get the names of the tags of the extension 401 * 402 * @return array The names of the tags of the extension 403 */ 404 public function getTags() 405 { 406 if (!empty($this->remoteInfo['tags'])) return $this->remoteInfo['tags']; 407 return array(); 408 } 409 410 /** 411 * Get the popularity information as floating point number [0,1] 412 * 413 * @return float|bool The popularity information or false if it isn't available 414 */ 415 public function getPopularity() 416 { 417 if (!empty($this->remoteInfo['popularity'])) return $this->remoteInfo['popularity']; 418 return false; 419 } 420 421 422 /** 423 * Get the text of the security warning if there is any 424 * 425 * @return string|bool The security warning if there is any, false otherwise 426 */ 427 public function getSecurityWarning() 428 { 429 if (!empty($this->remoteInfo['securitywarning'])) return $this->remoteInfo['securitywarning']; 430 return false; 431 } 432 433 /** 434 * Get the text of the security issue if there is any 435 * 436 * @return string|bool The security issue if there is any, false otherwise 437 */ 438 public function getSecurityIssue() 439 { 440 if (!empty($this->remoteInfo['securityissue'])) return $this->remoteInfo['securityissue']; 441 return false; 442 } 443 444 /** 445 * Get the URL of the screenshot of the extension if there is any 446 * 447 * @return string|bool The screenshot URL if there is any, false otherwise 448 */ 449 public function getScreenshotURL() 450 { 451 if (!empty($this->remoteInfo['screenshoturl'])) return $this->remoteInfo['screenshoturl']; 452 return false; 453 } 454 455 /** 456 * Get the URL of the thumbnail of the extension if there is any 457 * 458 * @return string|bool The thumbnail URL if there is any, false otherwise 459 */ 460 public function getThumbnailURL() 461 { 462 if (!empty($this->remoteInfo['thumbnailurl'])) return $this->remoteInfo['thumbnailurl']; 463 return false; 464 } 465 /** 466 * Get the last used download URL of the extension if there is any 467 * 468 * @return string|bool The previously used download URL, false if the extension has been installed manually 469 */ 470 public function getLastDownloadURL() 471 { 472 if (!empty($this->managerData['downloadurl'])) return $this->managerData['downloadurl']; 473 return false; 474 } 475 476 /** 477 * Get the download URL of the extension if there is any 478 * 479 * @return string|bool The download URL if there is any, false otherwise 480 */ 481 public function getDownloadURL() 482 { 483 if (!empty($this->remoteInfo['downloadurl'])) return $this->remoteInfo['downloadurl']; 484 return false; 485 } 486 487 /** 488 * If the download URL has changed since the last download 489 * 490 * @return bool If the download URL has changed 491 */ 492 public function hasDownloadURLChanged() 493 { 494 $lasturl = $this->getLastDownloadURL(); 495 $currenturl = $this->getDownloadURL(); 496 return ($lasturl && $currenturl && $lasturl != $currenturl); 497 } 498 499 /** 500 * Get the bug tracker URL of the extension if there is any 501 * 502 * @return string|bool The bug tracker URL if there is any, false otherwise 503 */ 504 public function getBugtrackerURL() 505 { 506 if (!empty($this->remoteInfo['bugtracker'])) return $this->remoteInfo['bugtracker']; 507 return false; 508 } 509 510 /** 511 * Get the URL of the source repository if there is any 512 * 513 * @return string|bool The URL of the source repository if there is any, false otherwise 514 */ 515 public function getSourcerepoURL() 516 { 517 if (!empty($this->remoteInfo['sourcerepo'])) return $this->remoteInfo['sourcerepo']; 518 return false; 519 } 520 521 /** 522 * Get the donation URL of the extension if there is any 523 * 524 * @return string|bool The donation URL if there is any, false otherwise 525 */ 526 public function getDonationURL() 527 { 528 if (!empty($this->remoteInfo['donationurl'])) return $this->remoteInfo['donationurl']; 529 return false; 530 } 531 532 /** 533 * Get the extension type(s) 534 * 535 * @return array The type(s) as array of strings 536 */ 537 public function getTypes() 538 { 539 if (!empty($this->remoteInfo['types'])) return $this->remoteInfo['types']; 540 if ($this->isTemplate()) return array(32 => 'template'); 541 return array(); 542 } 543 544 /** 545 * Get a list of all DokuWiki versions this extension is compatible with 546 * 547 * @return array The versions in the form yyyy-mm-dd => ('label' => label, 'implicit' => implicit) 548 */ 549 public function getCompatibleVersions() 550 { 551 if (!empty($this->remoteInfo['compatible'])) return $this->remoteInfo['compatible']; 552 return array(); 553 } 554 555 /** 556 * Get the date of the last available update 557 * 558 * @return string|bool The last available update in the form yyyy-mm-dd if there is any, false otherwise 559 */ 560 public function getLastUpdate() 561 { 562 if (!empty($this->remoteInfo['lastupdate'])) return $this->remoteInfo['lastupdate']; 563 return false; 564 } 565 566 /** 567 * Get the base path of the extension 568 * 569 * @return string The base path of the extension 570 */ 571 public function getInstallDir() 572 { 573 if ($this->isTemplate()) { 574 return $this->tpllib.$this->base; 575 } else { 576 return DOKU_PLUGIN.$this->base; 577 } 578 } 579 580 /** 581 * The type of extension installation 582 * 583 * @return string One of "none", "manual", "git" or "automatic" 584 */ 585 public function getInstallType() 586 { 587 if (!$this->isInstalled()) return 'none'; 588 if (!empty($this->managerData)) return 'automatic'; 589 if (is_dir($this->getInstallDir().'/.git')) return 'git'; 590 return 'manual'; 591 } 592 593 /** 594 * If the extension can probably be installed/updated or uninstalled 595 * 596 * @return bool|string True or error string 597 */ 598 public function canModify() 599 { 600 if ($this->isInstalled()) { 601 if (!is_writable($this->getInstallDir())) { 602 return 'noperms'; 603 } 604 } 605 606 if ($this->isTemplate() && !is_writable($this->tpllib)) { 607 return 'notplperms'; 608 } elseif (!is_writable(DOKU_PLUGIN)) { 609 return 'nopluginperms'; 610 } 611 return true; 612 } 613 614 /** 615 * Install an extension from a user upload 616 * 617 * @param string $field name of the upload file 618 * @param boolean $overwrite overwrite folder if the extension name is the same 619 * @throws Exception when something goes wrong 620 * @return array The list of installed extensions 621 */ 622 public function installFromUpload($field, $overwrite = true) 623 { 624 if ($_FILES[$field]['error']) { 625 throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')'); 626 } 627 628 $tmp = $this->mkTmpDir(); 629 if (!$tmp) throw new Exception($this->getLang('error_dircreate')); 630 631 // filename may contain the plugin name for old style plugins... 632 $basename = basename($_FILES[$field]['name']); 633 $basename = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $basename); 634 $basename = preg_replace('/[\W]+/', '', $basename); 635 636 if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) { 637 throw new Exception($this->getLang('msg_upload_failed')); 638 } 639 640 try { 641 $installed = $this->installArchive("$tmp/upload.archive", $overwrite, $basename); 642 $this->updateManagerData('', $installed); 643 $this->removeDeletedfiles($installed); 644 // purge cache 645 $this->purgeCache(); 646 } catch (Exception $e) { 647 throw $e; 648 } 649 return $installed; 650 } 651 652 /** 653 * Install an extension from a remote URL 654 * 655 * @param string $url 656 * @param boolean $overwrite overwrite folder if the extension name is the same 657 * @throws Exception when something goes wrong 658 * @return array The list of installed extensions 659 */ 660 public function installFromURL($url, $overwrite = true) 661 { 662 try { 663 $path = $this->download($url); 664 $installed = $this->installArchive($path, $overwrite); 665 $this->updateManagerData($url, $installed); 666 $this->removeDeletedfiles($installed); 667 668 // purge cache 669 $this->purgeCache(); 670 } catch (Exception $e) { 671 throw $e; 672 } 673 return $installed; 674 } 675 676 /** 677 * Install or update the extension 678 * 679 * @throws \Exception when something goes wrong 680 * @return array The list of installed extensions 681 */ 682 public function installOrUpdate() 683 { 684 $url = $this->getDownloadURL(); 685 $path = $this->download($url); 686 $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase()); 687 $this->updateManagerData($url, $installed); 688 689 // refresh extension information 690 if (!isset($installed[$this->getID()])) { 691 throw new Exception('Error, the requested extension hasn\'t been installed or updated'); 692 } 693 $this->removeDeletedfiles($installed); 694 $this->setExtension($this->getID()); 695 $this->purgeCache(); 696 return $installed; 697 } 698 699 /** 700 * Uninstall the extension 701 * 702 * @return bool If the plugin was sucessfully uninstalled 703 */ 704 public function uninstall() 705 { 706 $this->purgeCache(); 707 return io_rmdir($this->getInstallDir(), true); 708 } 709 710 /** 711 * Enable the extension 712 * 713 * @return bool|string True or an error message 714 */ 715 public function enable() 716 { 717 if ($this->isTemplate()) return $this->getLang('notimplemented'); 718 if (!$this->isInstalled()) return $this->getLang('notinstalled'); 719 if ($this->isEnabled()) return $this->getLang('alreadyenabled'); 720 721 /* @var PluginController $plugin_controller */ 722 global $plugin_controller; 723 if ($plugin_controller->enable($this->base)) { 724 $this->purgeCache(); 725 return true; 726 } else { 727 return $this->getLang('pluginlistsaveerror'); 728 } 729 } 730 731 /** 732 * Disable the extension 733 * 734 * @return bool|string True or an error message 735 */ 736 public function disable() 737 { 738 if ($this->isTemplate()) return $this->getLang('notimplemented'); 739 740 /* @var PluginController $plugin_controller */ 741 global $plugin_controller; 742 if (!$this->isInstalled()) return $this->getLang('notinstalled'); 743 if (!$this->isEnabled()) return $this->getLang('alreadydisabled'); 744 if ($plugin_controller->disable($this->base)) { 745 $this->purgeCache(); 746 return true; 747 } else { 748 return $this->getLang('pluginlistsaveerror'); 749 } 750 } 751 752 /** 753 * Purge the cache by touching the main configuration file 754 */ 755 protected function purgeCache() 756 { 757 global $config_cascade; 758 759 // expire dokuwiki caches 760 // touching local.php expires wiki page, JS and CSS caches 761 @touch(reset($config_cascade['main']['local'])); 762 } 763 764 /** 765 * Read local extension data either from info.txt or getInfo() 766 */ 767 protected function readLocalData() 768 { 769 if ($this->isTemplate()) { 770 $infopath = $this->getInstallDir().'/template.info.txt'; 771 } else { 772 $infopath = $this->getInstallDir().'/plugin.info.txt'; 773 } 774 775 if (is_readable($infopath)) { 776 $this->localInfo = confToHash($infopath); 777 } elseif (!$this->isTemplate() && $this->isEnabled()) { 778 $path = $this->getInstallDir().'/'; 779 $plugin = null; 780 781 foreach (PluginController::PLUGIN_TYPES as $type) { 782 if (file_exists($path.$type.'.php')) { 783 $plugin = plugin_load($type, $this->base); 784 if ($plugin) break; 785 } 786 787 if ($dh = @opendir($path.$type.'/')) { 788 while (false !== ($cp = readdir($dh))) { 789 if ($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue; 790 791 $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4)); 792 if ($plugin) break; 793 } 794 if ($plugin) break; 795 closedir($dh); 796 } 797 } 798 799 if ($plugin) { 800 /* @var DokuWiki_Plugin $plugin */ 801 $this->localInfo = $plugin->getInfo(); 802 } 803 } 804 } 805 806 /** 807 * Save the given URL and current datetime in the manager.dat file of all installed extensions 808 * 809 * @param string $url Where the extension was downloaded from. (empty for manual installs via upload) 810 * @param array $installed Optional list of installed plugins 811 */ 812 protected function updateManagerData($url = '', $installed = null) 813 { 814 $origID = $this->getID(); 815 816 if (is_null($installed)) { 817 $installed = array($origID); 818 } 819 820 foreach ($installed as $ext => $info) { 821 if ($this->getID() != $ext) $this->setExtension($ext); 822 if ($url) { 823 $this->managerData['downloadurl'] = $url; 824 } elseif (isset($this->managerData['downloadurl'])) { 825 unset($this->managerData['downloadurl']); 826 } 827 if (isset($this->managerData['installed'])) { 828 $this->managerData['updated'] = date('r'); 829 } else { 830 $this->managerData['installed'] = date('r'); 831 } 832 $this->writeManagerData(); 833 } 834 835 if ($this->getID() != $origID) $this->setExtension($origID); 836 } 837 838 /** 839 * Read the manager.dat file 840 */ 841 protected function readManagerData() 842 { 843 $managerpath = $this->getInstallDir().'/manager.dat'; 844 if (is_readable($managerpath)) { 845 $file = @file($managerpath); 846 if (!empty($file)) { 847 foreach ($file as $line) { 848 list($key, $value) = sexplode('=', trim($line, DOKU_LF), 2, ''); 849 $key = trim($key); 850 $value = trim($value); 851 // backwards compatible with old plugin manager 852 if ($key == 'url') $key = 'downloadurl'; 853 $this->managerData[$key] = $value; 854 } 855 } 856 } 857 } 858 859 /** 860 * Write the manager.data file 861 */ 862 protected function writeManagerData() 863 { 864 $managerpath = $this->getInstallDir().'/manager.dat'; 865 $data = ''; 866 foreach ($this->managerData as $k => $v) { 867 $data .= $k.'='.$v.DOKU_LF; 868 } 869 io_saveFile($managerpath, $data); 870 } 871 872 /** 873 * Returns a temporary directory 874 * 875 * The directory is registered for cleanup when the class is destroyed 876 * 877 * @return false|string 878 */ 879 protected function mkTmpDir() 880 { 881 $dir = io_mktmpdir(); 882 if (!$dir) return false; 883 $this->temporary[] = $dir; 884 return $dir; 885 } 886 887 /** 888 * downloads a file from the net and saves it 889 * 890 * - $file is the directory where the file should be saved 891 * - if successful will return the name used for the saved file, false otherwise 892 * 893 * @author Andreas Gohr <andi@splitbrain.org> 894 * @author Chris Smith <chris@jalakai.co.uk> 895 * 896 * @param string $url url to download 897 * @param string $file path to file or directory where to save 898 * @param string $defaultName fallback for name of download 899 * @return bool|string if failed false, otherwise true or the name of the file in the given dir 900 */ 901 protected function downloadToFile($url, $file, $defaultName = '') 902 { 903 global $conf; 904 $http = new DokuHTTPClient(); 905 $http->max_bodysize = 0; 906 $http->timeout = 25; //max. 25 sec 907 $http->keep_alive = false; // we do single ops here, no need for keep-alive 908 $http->agent = 'DokuWiki HTTP Client (Extension Manager)'; 909 910 $data = $http->get($url); 911 if ($data === false) return false; 912 913 $name = ''; 914 if (isset($http->resp_headers['content-disposition'])) { 915 $content_disposition = $http->resp_headers['content-disposition']; 916 $match = array(); 917 if (is_string($content_disposition) && 918 preg_match('/attachment;\s*filename\s*=\s*"([^"]*)"/i', $content_disposition, $match) 919 ) { 920 $name = \dokuwiki\Utf8\PhpString::basename($match[1]); 921 } 922 923 } 924 925 if (!$name) { 926 if (!$defaultName) return false; 927 $name = $defaultName; 928 } 929 930 $file = $file.$name; 931 932 $fileexists = file_exists($file); 933 $fp = @fopen($file,"w"); 934 if (!$fp) return false; 935 fwrite($fp, $data); 936 fclose($fp); 937 if (!$fileexists and $conf['fperm']) chmod($file, $conf['fperm']); 938 return $name; 939 } 940 941 /** 942 * Download an archive to a protected path 943 * 944 * @param string $url The url to get the archive from 945 * @throws Exception when something goes wrong 946 * @return string The path where the archive was saved 947 */ 948 public function download($url) 949 { 950 // check the url 951 if (!preg_match('/https?:\/\//i', $url)) { 952 throw new Exception($this->getLang('error_badurl')); 953 } 954 955 // try to get the file from the path (used as plugin name fallback) 956 $file = parse_url($url, PHP_URL_PATH); 957 if (is_null($file)) { 958 $file = md5($url); 959 } else { 960 $file = \dokuwiki\Utf8\PhpString::basename($file); 961 } 962 963 // create tmp directory for download 964 if (!($tmp = $this->mkTmpDir())) { 965 throw new Exception($this->getLang('error_dircreate')); 966 } 967 968 // download 969 if (!$file = $this->downloadToFile($url, $tmp.'/', $file)) { 970 io_rmdir($tmp, true); 971 throw new Exception(sprintf($this->getLang('error_download'), 972 '<bdi>'.hsc($url).'</bdi>') 973 ); 974 } 975 976 return $tmp.'/'.$file; 977 } 978 979 /** 980 * @param string $file The path to the archive that shall be installed 981 * @param bool $overwrite If an already installed plugin should be overwritten 982 * @param string $base The basename of the plugin if it's known 983 * @throws Exception when something went wrong 984 * @return array list of installed extensions 985 */ 986 public function installArchive($file, $overwrite = false, $base = '') 987 { 988 $installed_extensions = array(); 989 990 // create tmp directory for decompression 991 if (!($tmp = $this->mkTmpDir())) { 992 throw new Exception($this->getLang('error_dircreate')); 993 } 994 995 // add default base folder if specified to handle case where zip doesn't contain this 996 if ($base && !@mkdir($tmp.'/'.$base)) { 997 throw new Exception($this->getLang('error_dircreate')); 998 } 999 1000 // decompress 1001 $this->decompress($file, "$tmp/".$base); 1002 1003 // search $tmp/$base for the folder(s) that has been created 1004 // move the folder(s) to lib/.. 1005 $result = array('old'=>array(), 'new'=>array()); 1006 $default = ($this->isTemplate() ? 'template' : 'plugin'); 1007 if (!$this->findFolders($result, $tmp.'/'.$base, $default)) { 1008 throw new Exception($this->getLang('error_findfolder')); 1009 } 1010 1011 // choose correct result array 1012 if (count($result['new'])) { 1013 $install = $result['new']; 1014 } else { 1015 $install = $result['old']; 1016 } 1017 1018 if (!count($install)) { 1019 throw new Exception($this->getLang('error_findfolder')); 1020 } 1021 1022 // now install all found items 1023 foreach ($install as $item) { 1024 // where to install? 1025 if ($item['type'] == 'template') { 1026 $target_base_dir = $this->tpllib; 1027 } else { 1028 $target_base_dir = DOKU_PLUGIN; 1029 } 1030 1031 if (!empty($item['base'])) { 1032 // use base set in info.txt 1033 } elseif ($base && count($install) == 1) { 1034 $item['base'] = $base; 1035 } else { 1036 // default - use directory as found in zip 1037 // plugins from github/master without *.info.txt will install in wrong folder 1038 // but using $info->id will make 'code3' fail (which should install in lib/code/..) 1039 $item['base'] = basename($item['tmp']); 1040 } 1041 1042 // check to make sure we aren't overwriting anything 1043 $target = $target_base_dir.$item['base']; 1044 if (!$overwrite && file_exists($target)) { 1045 // this info message is not being exposed via exception, 1046 // so that it's not interrupting the installation 1047 msg(sprintf($this->getLang('msg_nooverwrite'), $item['base'])); 1048 continue; 1049 } 1050 1051 $action = file_exists($target) ? 'update' : 'install'; 1052 1053 // copy action 1054 if ($this->dircopy($item['tmp'], $target)) { 1055 // return info 1056 $id = $item['base']; 1057 if ($item['type'] == 'template') { 1058 $id = 'template:'.$id; 1059 } 1060 $installed_extensions[$id] = array( 1061 'base' => $item['base'], 1062 'type' => $item['type'], 1063 'action' => $action 1064 ); 1065 } else { 1066 throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF, 1067 '<bdi>'.$item['base'].'</bdi>') 1068 ); 1069 } 1070 } 1071 1072 // cleanup 1073 if ($tmp) io_rmdir($tmp, true); 1074 1075 return $installed_extensions; 1076 } 1077 1078 /** 1079 * Find out what was in the extracted directory 1080 * 1081 * Correct folders are searched recursively using the "*.info.txt" configs 1082 * as indicator for a root folder. When such a file is found, it's base 1083 * setting is used (when set). All folders found by this method are stored 1084 * in the 'new' key of the $result array. 1085 * 1086 * For backwards compatibility all found top level folders are stored as 1087 * in the 'old' key of the $result array. 1088 * 1089 * When no items are found in 'new' the copy mechanism should fall back 1090 * the 'old' list. 1091 * 1092 * @author Andreas Gohr <andi@splitbrain.org> 1093 * @param array $result - results are stored here 1094 * @param string $directory - the temp directory where the package was unpacked to 1095 * @param string $default_type - type used if no info.txt available 1096 * @param string $subdir - a subdirectory. do not set. used by recursion 1097 * @return bool - false on error 1098 */ 1099 protected function findFolders(&$result, $directory, $default_type = 'plugin', $subdir = '') 1100 { 1101 $this_dir = "$directory$subdir"; 1102 $dh = @opendir($this_dir); 1103 if (!$dh) return false; 1104 1105 $found_dirs = array(); 1106 $found_files = 0; 1107 $found_template_parts = 0; 1108 while (false !== ($f = readdir($dh))) { 1109 if ($f == '.' || $f == '..') continue; 1110 1111 if (is_dir("$this_dir/$f")) { 1112 $found_dirs[] = "$subdir/$f"; 1113 } else { 1114 // it's a file -> check for config 1115 $found_files++; 1116 switch ($f) { 1117 case 'plugin.info.txt': 1118 case 'template.info.txt': 1119 // we have found a clear marker, save and return 1120 $info = array(); 1121 $type = explode('.', $f, 2); 1122 $info['type'] = $type[0]; 1123 $info['tmp'] = $this_dir; 1124 $conf = confToHash("$this_dir/$f"); 1125 $info['base'] = basename($conf['base']); 1126 $result['new'][] = $info; 1127 return true; 1128 1129 case 'main.php': 1130 case 'details.php': 1131 case 'mediamanager.php': 1132 case 'style.ini': 1133 $found_template_parts++; 1134 break; 1135 } 1136 } 1137 } 1138 closedir($dh); 1139 1140 // files where found but no info.txt - use old method 1141 if ($found_files) { 1142 $info = array(); 1143 $info['tmp'] = $this_dir; 1144 // does this look like a template or should we use the default type? 1145 if ($found_template_parts >= 2) { 1146 $info['type'] = 'template'; 1147 } else { 1148 $info['type'] = $default_type; 1149 } 1150 1151 $result['old'][] = $info; 1152 return true; 1153 } 1154 1155 // we have no files yet -> recurse 1156 foreach ($found_dirs as $found_dir) { 1157 $this->findFolders($result, $directory, $default_type, "$found_dir"); 1158 } 1159 return true; 1160 } 1161 1162 /** 1163 * Decompress a given file to the given target directory 1164 * 1165 * Determines the compression type from the file extension 1166 * 1167 * @param string $file archive to extract 1168 * @param string $target directory to extract to 1169 * @throws Exception 1170 * @return bool 1171 */ 1172 private function decompress($file, $target) 1173 { 1174 // decompression library doesn't like target folders ending in "/" 1175 if (substr($target, -1) == "/") $target = substr($target, 0, -1); 1176 1177 $ext = $this->guessArchiveType($file); 1178 if (in_array($ext, array('tar', 'bz', 'gz'))) { 1179 try { 1180 $tar = new \splitbrain\PHPArchive\Tar(); 1181 $tar->open($file); 1182 $tar->extract($target); 1183 } catch (\splitbrain\PHPArchive\ArchiveIOException $e) { 1184 throw new Exception($this->getLang('error_decompress').' '.$e->getMessage()); 1185 } 1186 1187 return true; 1188 } elseif ($ext == 'zip') { 1189 try { 1190 $zip = new \splitbrain\PHPArchive\Zip(); 1191 $zip->open($file); 1192 $zip->extract($target); 1193 } catch (\splitbrain\PHPArchive\ArchiveIOException $e) { 1194 throw new Exception($this->getLang('error_decompress').' '.$e->getMessage()); 1195 } 1196 1197 return true; 1198 } 1199 1200 // the only case when we don't get one of the recognized archive types is 1201 // when the archive file can't be read 1202 throw new Exception($this->getLang('error_decompress').' Couldn\'t read archive file'); 1203 } 1204 1205 /** 1206 * Determine the archive type of the given file 1207 * 1208 * Reads the first magic bytes of the given file for content type guessing, 1209 * if neither bz, gz or zip are recognized, tar is assumed. 1210 * 1211 * @author Andreas Gohr <andi@splitbrain.org> 1212 * @param string $file The file to analyze 1213 * @return string|false false if the file can't be read, otherwise an "extension" 1214 */ 1215 private function guessArchiveType($file) 1216 { 1217 $fh = fopen($file, 'rb'); 1218 if (!$fh) return false; 1219 $magic = fread($fh, 5); 1220 fclose($fh); 1221 1222 if (strpos($magic, "\x42\x5a") === 0) return 'bz'; 1223 if (strpos($magic, "\x1f\x8b") === 0) return 'gz'; 1224 if (strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip'; 1225 return 'tar'; 1226 } 1227 1228 /** 1229 * Copy with recursive sub-directory support 1230 * 1231 * @param string $src filename path to file 1232 * @param string $dst filename path to file 1233 * @return bool|int|string 1234 */ 1235 private function dircopy($src, $dst) 1236 { 1237 global $conf; 1238 1239 if (is_dir($src)) { 1240 if (!$dh = @opendir($src)) return false; 1241 1242 if ($ok = io_mkdir_p($dst)) { 1243 while ($ok && (false !== ($f = readdir($dh)))) { 1244 if ($f == '..' || $f == '.') continue; 1245 $ok = $this->dircopy("$src/$f", "$dst/$f"); 1246 } 1247 } 1248 1249 closedir($dh); 1250 return $ok; 1251 } else { 1252 $existed = file_exists($dst); 1253 1254 if (!@copy($src, $dst)) return false; 1255 if (!$existed && $conf['fperm']) chmod($dst, $conf['fperm']); 1256 @touch($dst, filemtime($src)); 1257 } 1258 1259 return true; 1260 } 1261 1262 /** 1263 * Delete outdated files from updated plugins 1264 * 1265 * @param array $installed 1266 */ 1267 private function removeDeletedfiles($installed) 1268 { 1269 foreach ($installed as $id => $extension) { 1270 // only on update 1271 if ($extension['action'] == 'install') continue; 1272 1273 // get definition file 1274 if ($extension['type'] == 'template') { 1275 $extensiondir = $this->tpllib; 1276 } else { 1277 $extensiondir = DOKU_PLUGIN; 1278 } 1279 $extensiondir = $extensiondir . $extension['base'] .'/'; 1280 $definitionfile = $extensiondir . 'deleted.files'; 1281 if (!file_exists($definitionfile)) continue; 1282 1283 // delete the old files 1284 $list = file($definitionfile); 1285 1286 foreach ($list as $line) { 1287 $line = trim(preg_replace('/#.*$/', '', $line)); 1288 if (!$line) continue; 1289 $file = $extensiondir . $line; 1290 if (!file_exists($file)) continue; 1291 1292 io_rmdir($file, true); 1293 } 1294 } 1295 } 1296 } 1297 1298 // vim:ts=4:sw=4:et:
title
Description
Body
title
Description
Body
title
Description
Body
title
Body