[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/lib/plugins/extension/helper/ -> extension.php (source)

   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 is_dir($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',
 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->isdisabled($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->isdisabled($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       * @throws Exception when something goes wrong
 619       * @return array The list of installed extensions
 620       */
 621      public function installFromUpload($field)
 622      {
 623          if ($_FILES[$field]['error']) {
 624              throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')');
 625          }
 626  
 627          $tmp = $this->mkTmpDir();
 628          if (!$tmp) throw new Exception($this->getLang('error_dircreate'));
 629  
 630          // filename may contain the plugin name for old style plugins...
 631          $basename = basename($_FILES[$field]['name']);
 632          $basename = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $basename);
 633          $basename = preg_replace('/[\W]+/', '', $basename);
 634  
 635          if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) {
 636              throw new Exception($this->getLang('msg_upload_failed'));
 637          }
 638  
 639          try {
 640              $installed = $this->installArchive("$tmp/upload.archive", true, $basename);
 641              $this->updateManagerData('', $installed);
 642              $this->removeDeletedfiles($installed);
 643              // purge cache
 644              $this->purgeCache();
 645          } catch (Exception $e) {
 646              throw $e;
 647          }
 648          return $installed;
 649      }
 650  
 651      /**
 652       * Install an extension from a remote URL
 653       *
 654       * @param string $url
 655       * @throws Exception when something goes wrong
 656       * @return array The list of installed extensions
 657       */
 658      public function installFromURL($url)
 659      {
 660          try {
 661              $path      = $this->download($url);
 662              $installed = $this->installArchive($path, true);
 663              $this->updateManagerData($url, $installed);
 664              $this->removeDeletedfiles($installed);
 665  
 666              // purge cache
 667              $this->purgeCache();
 668          } catch (Exception $e) {
 669              throw $e;
 670          }
 671          return $installed;
 672      }
 673  
 674      /**
 675       * Install or update the extension
 676       *
 677       * @throws \Exception when something goes wrong
 678       * @return array The list of installed extensions
 679       */
 680      public function installOrUpdate()
 681      {
 682          $url       = $this->getDownloadURL();
 683          $path      = $this->download($url);
 684          $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase());
 685          $this->updateManagerData($url, $installed);
 686  
 687          // refresh extension information
 688          if (!isset($installed[$this->getID()])) {
 689              throw new Exception('Error, the requested extension hasn\'t been installed or updated');
 690          }
 691          $this->removeDeletedfiles($installed);
 692          $this->setExtension($this->getID());
 693          $this->purgeCache();
 694          return $installed;
 695      }
 696  
 697      /**
 698       * Uninstall the extension
 699       *
 700       * @return bool If the plugin was sucessfully uninstalled
 701       */
 702      public function uninstall()
 703      {
 704          $this->purgeCache();
 705          return io_rmdir($this->getInstallDir(), true);
 706      }
 707  
 708      /**
 709       * Enable the extension
 710       *
 711       * @return bool|string True or an error message
 712       */
 713      public function enable()
 714      {
 715          if ($this->isTemplate()) return $this->getLang('notimplemented');
 716          if (!$this->isInstalled()) return $this->getLang('notinstalled');
 717          if ($this->isEnabled()) return $this->getLang('alreadyenabled');
 718  
 719          /* @var PluginController $plugin_controller */
 720          global $plugin_controller;
 721          if ($plugin_controller->enable($this->base)) {
 722              $this->purgeCache();
 723              return true;
 724          } else {
 725              return $this->getLang('pluginlistsaveerror');
 726          }
 727      }
 728  
 729      /**
 730       * Disable the extension
 731       *
 732       * @return bool|string True or an error message
 733       */
 734      public function disable()
 735      {
 736          if ($this->isTemplate()) return $this->getLang('notimplemented');
 737  
 738          /* @var PluginController $plugin_controller */
 739          global $plugin_controller;
 740          if (!$this->isInstalled()) return $this->getLang('notinstalled');
 741          if (!$this->isEnabled()) return $this->getLang('alreadydisabled');
 742          if ($plugin_controller->disable($this->base)) {
 743              $this->purgeCache();
 744              return true;
 745          } else {
 746              return $this->getLang('pluginlistsaveerror');
 747          }
 748      }
 749  
 750      /**
 751       * Purge the cache by touching the main configuration file
 752       */
 753      protected function purgeCache()
 754      {
 755          global $config_cascade;
 756  
 757          // expire dokuwiki caches
 758          // touching local.php expires wiki page, JS and CSS caches
 759          @touch(reset($config_cascade['main']['local']));
 760      }
 761  
 762      /**
 763       * Read local extension data either from info.txt or getInfo()
 764       */
 765      protected function readLocalData()
 766      {
 767          if ($this->isTemplate()) {
 768              $infopath = $this->getInstallDir().'/template.info.txt';
 769          } else {
 770              $infopath = $this->getInstallDir().'/plugin.info.txt';
 771          }
 772  
 773          if (is_readable($infopath)) {
 774              $this->localInfo = confToHash($infopath);
 775          } elseif (!$this->isTemplate() && $this->isEnabled()) {
 776              $path   = $this->getInstallDir().'/';
 777              $plugin = null;
 778  
 779              foreach (PluginController::PLUGIN_TYPES as $type) {
 780                  if (file_exists($path.$type.'.php')) {
 781                      $plugin = plugin_load($type, $this->base);
 782                      if ($plugin) break;
 783                  }
 784  
 785                  if ($dh = @opendir($path.$type.'/')) {
 786                      while (false !== ($cp = readdir($dh))) {
 787                          if ($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
 788  
 789                          $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
 790                          if ($plugin) break;
 791                      }
 792                      if ($plugin) break;
 793                      closedir($dh);
 794                  }
 795              }
 796  
 797              if ($plugin) {
 798                  /* @var DokuWiki_Plugin $plugin */
 799                  $this->localInfo = $plugin->getInfo();
 800              }
 801          }
 802      }
 803  
 804      /**
 805       * Save the given URL and current datetime in the manager.dat file of all installed extensions
 806       *
 807       * @param string $url       Where the extension was downloaded from. (empty for manual installs via upload)
 808       * @param array  $installed Optional list of installed plugins
 809       */
 810      protected function updateManagerData($url = '', $installed = null)
 811      {
 812          $origID = $this->getID();
 813  
 814          if (is_null($installed)) {
 815              $installed = array($origID);
 816          }
 817  
 818          foreach ($installed as $ext => $info) {
 819              if ($this->getID() != $ext) $this->setExtension($ext);
 820              if ($url) {
 821                  $this->managerData['downloadurl'] = $url;
 822              } elseif (isset($this->managerData['downloadurl'])) {
 823                  unset($this->managerData['downloadurl']);
 824              }
 825              if (isset($this->managerData['installed'])) {
 826                  $this->managerData['updated'] = date('r');
 827              } else {
 828                  $this->managerData['installed'] = date('r');
 829              }
 830              $this->writeManagerData();
 831          }
 832  
 833          if ($this->getID() != $origID) $this->setExtension($origID);
 834      }
 835  
 836      /**
 837       * Read the manager.dat file
 838       */
 839      protected function readManagerData()
 840      {
 841          $managerpath = $this->getInstallDir().'/manager.dat';
 842          if (is_readable($managerpath)) {
 843              $file = @file($managerpath);
 844              if (!empty($file)) {
 845                  foreach ($file as $line) {
 846                      list($key, $value) = explode('=', trim($line, DOKU_LF), 2);
 847                      $key = trim($key);
 848                      $value = trim($value);
 849                      // backwards compatible with old plugin manager
 850                      if ($key == 'url') $key = 'downloadurl';
 851                      $this->managerData[$key] = $value;
 852                  }
 853              }
 854          }
 855      }
 856  
 857      /**
 858       * Write the manager.data file
 859       */
 860      protected function writeManagerData()
 861      {
 862          $managerpath = $this->getInstallDir().'/manager.dat';
 863          $data = '';
 864          foreach ($this->managerData as $k => $v) {
 865              $data .= $k.'='.$v.DOKU_LF;
 866          }
 867          io_saveFile($managerpath, $data);
 868      }
 869  
 870      /**
 871       * Returns a temporary directory
 872       *
 873       * The directory is registered for cleanup when the class is destroyed
 874       *
 875       * @return false|string
 876       */
 877      protected function mkTmpDir()
 878      {
 879          $dir = io_mktmpdir();
 880          if (!$dir) return false;
 881          $this->temporary[] = $dir;
 882          return $dir;
 883      }
 884  
 885      /**
 886       * downloads a file from the net and saves it
 887       *
 888       * - $file is the directory where the file should be saved
 889       * - if successful will return the name used for the saved file, false otherwise
 890       *
 891       * @author Andreas Gohr <andi@splitbrain.org>
 892       * @author Chris Smith <chris@jalakai.co.uk>
 893       *
 894       * @param string $url           url to download
 895       * @param string $file          path to file or directory where to save
 896       * @param string $defaultName   fallback for name of download
 897       * @return bool|string          if failed false, otherwise true or the name of the file in the given dir
 898       */
 899      protected function downloadToFile($url, $file, $defaultName = '')
 900      {
 901          global $conf;
 902          $http = new DokuHTTPClient();
 903          $http->max_bodysize = 0;
 904          $http->timeout = 25; //max. 25 sec
 905          $http->keep_alive = false; // we do single ops here, no need for keep-alive
 906          $http->agent = 'DokuWiki HTTP Client (Extension Manager)';
 907  
 908          $data = $http->get($url);
 909          if ($data === false) return false;
 910  
 911          $name = '';
 912          if (isset($http->resp_headers['content-disposition'])) {
 913              $content_disposition = $http->resp_headers['content-disposition'];
 914              $match = array();
 915              if (is_string($content_disposition) &&
 916                  preg_match('/attachment;\s*filename\s*=\s*"([^"]*)"/i', $content_disposition, $match)
 917              ) {
 918                  $name = \dokuwiki\Utf8\PhpString::basename($match[1]);
 919              }
 920  
 921          }
 922  
 923          if (!$name) {
 924              if (!$defaultName) return false;
 925              $name = $defaultName;
 926          }
 927  
 928          $file = $file.$name;
 929  
 930          $fileexists = file_exists($file);
 931          $fp = @fopen($file,"w");
 932          if (!$fp) return false;
 933          fwrite($fp, $data);
 934          fclose($fp);
 935          if (!$fileexists and $conf['fperm']) chmod($file, $conf['fperm']);
 936          return $name;
 937      }
 938  
 939      /**
 940       * Download an archive to a protected path
 941       *
 942       * @param string $url  The url to get the archive from
 943       * @throws Exception   when something goes wrong
 944       * @return string The path where the archive was saved
 945       */
 946      public function download($url)
 947      {
 948          // check the url
 949          if (!preg_match('/https?:\/\//i', $url)) {
 950              throw new Exception($this->getLang('error_badurl'));
 951          }
 952  
 953          // try to get the file from the path (used as plugin name fallback)
 954          $file = parse_url($url, PHP_URL_PATH);
 955          if (is_null($file)) {
 956              $file = md5($url);
 957          } else {
 958              $file = \dokuwiki\Utf8\PhpString::basename($file);
 959          }
 960  
 961          // create tmp directory for download
 962          if (!($tmp = $this->mkTmpDir())) {
 963              throw new Exception($this->getLang('error_dircreate'));
 964          }
 965  
 966          // download
 967          if (!$file = $this->downloadToFile($url, $tmp.'/', $file)) {
 968              io_rmdir($tmp, true);
 969              throw new Exception(sprintf($this->getLang('error_download'),
 970                  '<bdi>'.hsc($url).'</bdi>')
 971              );
 972          }
 973  
 974          return $tmp.'/'.$file;
 975      }
 976  
 977      /**
 978       * @param string $file      The path to the archive that shall be installed
 979       * @param bool   $overwrite If an already installed plugin should be overwritten
 980       * @param string $base      The basename of the plugin if it's known
 981       * @throws Exception        when something went wrong
 982       * @return array            list of installed extensions
 983       */
 984      public function installArchive($file, $overwrite = false, $base = '')
 985      {
 986          $installed_extensions = array();
 987  
 988          // create tmp directory for decompression
 989          if (!($tmp = $this->mkTmpDir())) {
 990              throw new Exception($this->getLang('error_dircreate'));
 991          }
 992  
 993          // add default base folder if specified to handle case where zip doesn't contain this
 994          if ($base && !@mkdir($tmp.'/'.$base)) {
 995              throw new Exception($this->getLang('error_dircreate'));
 996          }
 997  
 998          // decompress
 999          $this->decompress($file, "$tmp/".$base);
1000  
1001          // search $tmp/$base for the folder(s) that has been created
1002          // move the folder(s) to lib/..
1003          $result = array('old'=>array(), 'new'=>array());
1004          $default = ($this->isTemplate() ? 'template' : 'plugin');
1005          if (!$this->findFolders($result, $tmp.'/'.$base, $default)) {
1006              throw new Exception($this->getLang('error_findfolder'));
1007          }
1008  
1009          // choose correct result array
1010          if (count($result['new'])) {
1011              $install = $result['new'];
1012          } else {
1013              $install = $result['old'];
1014          }
1015  
1016          if (!count($install)) {
1017              throw new Exception($this->getLang('error_findfolder'));
1018          }
1019  
1020          // now install all found items
1021          foreach ($install as $item) {
1022              // where to install?
1023              if ($item['type'] == 'template') {
1024                  $target_base_dir = $this->tpllib;
1025              } else {
1026                  $target_base_dir = DOKU_PLUGIN;
1027              }
1028  
1029              if (!empty($item['base'])) {
1030                  // use base set in info.txt
1031              } elseif ($base && count($install) == 1) {
1032                  $item['base'] = $base;
1033              } else {
1034                  // default - use directory as found in zip
1035                  // plugins from github/master without *.info.txt will install in wrong folder
1036                  // but using $info->id will make 'code3' fail (which should install in lib/code/..)
1037                  $item['base'] = basename($item['tmp']);
1038              }
1039  
1040              // check to make sure we aren't overwriting anything
1041              $target = $target_base_dir.$item['base'];
1042              if (!$overwrite && file_exists($target)) {
1043                  // TODO remember our settings, ask the user to confirm overwrite
1044                  continue;
1045              }
1046  
1047              $action = file_exists($target) ? 'update' : 'install';
1048  
1049              // copy action
1050              if ($this->dircopy($item['tmp'], $target)) {
1051                  // return info
1052                  $id = $item['base'];
1053                  if ($item['type'] == 'template') {
1054                      $id = 'template:'.$id;
1055                  }
1056                  $installed_extensions[$id] = array(
1057                      'base' => $item['base'],
1058                      'type' => $item['type'],
1059                      'action' => $action
1060                  );
1061              } else {
1062                  throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF,
1063                      '<bdi>'.$item['base'].'</bdi>')
1064                  );
1065              }
1066          }
1067  
1068          // cleanup
1069          if ($tmp) io_rmdir($tmp, true);
1070  
1071          return $installed_extensions;
1072      }
1073  
1074      /**
1075       * Find out what was in the extracted directory
1076       *
1077       * Correct folders are searched recursively using the "*.info.txt" configs
1078       * as indicator for a root folder. When such a file is found, it's base
1079       * setting is used (when set). All folders found by this method are stored
1080       * in the 'new' key of the $result array.
1081       *
1082       * For backwards compatibility all found top level folders are stored as
1083       * in the 'old' key of the $result array.
1084       *
1085       * When no items are found in 'new' the copy mechanism should fall back
1086       * the 'old' list.
1087       *
1088       * @author Andreas Gohr <andi@splitbrain.org>
1089       * @param array $result - results are stored here
1090       * @param string $directory - the temp directory where the package was unpacked to
1091       * @param string $default_type - type used if no info.txt available
1092       * @param string $subdir - a subdirectory. do not set. used by recursion
1093       * @return bool - false on error
1094       */
1095      protected function findFolders(&$result, $directory, $default_type = 'plugin', $subdir = '')
1096      {
1097          $this_dir = "$directory$subdir";
1098          $dh       = @opendir($this_dir);
1099          if (!$dh) return false;
1100  
1101          $found_dirs           = array();
1102          $found_files          = 0;
1103          $found_template_parts = 0;
1104          while (false !== ($f = readdir($dh))) {
1105              if ($f == '.' || $f == '..') continue;
1106  
1107              if (is_dir("$this_dir/$f")) {
1108                  $found_dirs[] = "$subdir/$f";
1109              } else {
1110                  // it's a file -> check for config
1111                  $found_files++;
1112                  switch ($f) {
1113                      case 'plugin.info.txt':
1114                      case 'template.info.txt':
1115                          // we have  found a clear marker, save and return
1116                          $info = array();
1117                          $type = explode('.', $f, 2);
1118                          $info['type'] = $type[0];
1119                          $info['tmp']  = $this_dir;
1120                          $conf = confToHash("$this_dir/$f");
1121                          $info['base'] = basename($conf['base']);
1122                          $result['new'][] = $info;
1123                          return true;
1124  
1125                      case 'main.php':
1126                      case 'details.php':
1127                      case 'mediamanager.php':
1128                      case 'style.ini':
1129                          $found_template_parts++;
1130                          break;
1131                  }
1132              }
1133          }
1134          closedir($dh);
1135  
1136          // files where found but no info.txt - use old method
1137          if ($found_files) {
1138              $info        = array();
1139              $info['tmp'] = $this_dir;
1140              // does this look like a template or should we use the default type?
1141              if ($found_template_parts >= 2) {
1142                  $info['type'] = 'template';
1143              } else {
1144                  $info['type'] = $default_type;
1145              }
1146  
1147              $result['old'][] = $info;
1148              return true;
1149          }
1150  
1151          // we have no files yet -> recurse
1152          foreach ($found_dirs as $found_dir) {
1153              $this->findFolders($result, $directory, $default_type, "$found_dir");
1154          }
1155          return true;
1156      }
1157  
1158      /**
1159       * Decompress a given file to the given target directory
1160       *
1161       * Determines the compression type from the file extension
1162       *
1163       * @param string $file   archive to extract
1164       * @param string $target directory to extract to
1165       * @throws Exception
1166       * @return bool
1167       */
1168      private function decompress($file, $target)
1169      {
1170          // decompression library doesn't like target folders ending in "/"
1171          if (substr($target, -1) == "/") $target = substr($target, 0, -1);
1172  
1173          $ext = $this->guessArchiveType($file);
1174          if (in_array($ext, array('tar', 'bz', 'gz'))) {
1175              try {
1176                  $tar = new \splitbrain\PHPArchive\Tar();
1177                  $tar->open($file);
1178                  $tar->extract($target);
1179              } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1180                  throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1181              }
1182  
1183              return true;
1184          } elseif ($ext == 'zip') {
1185              try {
1186                  $zip = new \splitbrain\PHPArchive\Zip();
1187                  $zip->open($file);
1188                  $zip->extract($target);
1189              } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1190                  throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1191              }
1192  
1193              return true;
1194          }
1195  
1196          // the only case when we don't get one of the recognized archive types is
1197          // when the archive file can't be read
1198          throw new Exception($this->getLang('error_decompress').' Couldn\'t read archive file');
1199      }
1200  
1201      /**
1202       * Determine the archive type of the given file
1203       *
1204       * Reads the first magic bytes of the given file for content type guessing,
1205       * if neither bz, gz or zip are recognized, tar is assumed.
1206       *
1207       * @author Andreas Gohr <andi@splitbrain.org>
1208       * @param string $file The file to analyze
1209       * @return string|false false if the file can't be read, otherwise an "extension"
1210       */
1211      private function guessArchiveType($file)
1212      {
1213          $fh = fopen($file, 'rb');
1214          if (!$fh) return false;
1215          $magic = fread($fh, 5);
1216          fclose($fh);
1217  
1218          if (strpos($magic, "\x42\x5a") === 0) return 'bz';
1219          if (strpos($magic, "\x1f\x8b") === 0) return 'gz';
1220          if (strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip';
1221          return 'tar';
1222      }
1223  
1224      /**
1225       * Copy with recursive sub-directory support
1226       *
1227       * @param string $src filename path to file
1228       * @param string $dst filename path to file
1229       * @return bool|int|string
1230       */
1231      private function dircopy($src, $dst)
1232      {
1233          global $conf;
1234  
1235          if (is_dir($src)) {
1236              if (!$dh = @opendir($src)) return false;
1237  
1238              if ($ok = io_mkdir_p($dst)) {
1239                  while ($ok && (false !== ($f = readdir($dh)))) {
1240                      if ($f == '..' || $f == '.') continue;
1241                      $ok = $this->dircopy("$src/$f", "$dst/$f");
1242                  }
1243              }
1244  
1245              closedir($dh);
1246              return $ok;
1247          } else {
1248              $existed = file_exists($dst);
1249  
1250              if (!@copy($src, $dst)) return false;
1251              if (!$existed && $conf['fperm']) chmod($dst, $conf['fperm']);
1252              @touch($dst, filemtime($src));
1253          }
1254  
1255          return true;
1256      }
1257  
1258      /**
1259       * Delete outdated files from updated plugins
1260       *
1261       * @param array $installed
1262       */
1263      private function removeDeletedfiles($installed)
1264      {
1265          foreach ($installed as $id => $extension) {
1266              // only on update
1267              if ($extension['action'] == 'install') continue;
1268  
1269              // get definition file
1270              if ($extension['type'] == 'template') {
1271                  $extensiondir = $this->tpllib;
1272              } else {
1273                  $extensiondir = DOKU_PLUGIN;
1274              }
1275              $extensiondir = $extensiondir . $extension['base'] .'/';
1276              $definitionfile = $extensiondir . 'deleted.files';
1277              if (!file_exists($definitionfile)) continue;
1278  
1279              // delete the old files
1280              $list = file($definitionfile);
1281  
1282              foreach ($list as $line) {
1283                  $line = trim(preg_replace('/#.*$/', '', $line));
1284                  if (!$line) continue;
1285                  $file = $extensiondir . $line;
1286                  if (!file_exists($file)) continue;
1287  
1288                  io_rmdir($file, true);
1289              }
1290          }
1291      }
1292  }
1293  
1294  // vim:ts=4:sw=4:et: