[ 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', '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) = explode('=', 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: