[ 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', 'authmysql', 'authpdo',
 130                              'authpgsql', 'authplain', 'acl', 'info', 'extension',
 131                              'revert', 'popularity', 'config', 'safefnrecode', 'styling',
 132                              'testing', '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/'.($this->isTemplate() ? 'template' : 'plugin').':'.$this->getBase();
 310      }
 311  
 312      /**
 313       * Get the installed version of the extension
 314       *
 315       * @return string|bool The version, usually in the form yyyy-mm-dd if there is any
 316       */
 317      public function getInstalledVersion()
 318      {
 319          if (!empty($this->localInfo['date'])) return $this->localInfo['date'];
 320          if ($this->isInstalled()) return $this->getLang('unknownversion');
 321          return false;
 322      }
 323  
 324      /**
 325       * Get the install date of the current version
 326       *
 327       * @return string|bool The date of the last update or false if not available
 328       */
 329      public function getUpdateDate()
 330      {
 331          if (!empty($this->managerData['updated'])) return $this->managerData['updated'];
 332          return $this->getInstallDate();
 333      }
 334  
 335      /**
 336       * Get the date of the installation of the plugin
 337       *
 338       * @return string|bool The date of the installation or false if not available
 339       */
 340      public function getInstallDate()
 341      {
 342          if (!empty($this->managerData['installed'])) return $this->managerData['installed'];
 343          return false;
 344      }
 345  
 346      /**
 347       * Get the names of the dependencies of this extension
 348       *
 349       * @return array The base names of the dependencies
 350       */
 351      public function getDependencies()
 352      {
 353          if (!empty($this->remoteInfo['dependencies'])) return $this->remoteInfo['dependencies'];
 354          return array();
 355      }
 356  
 357      /**
 358       * Get the names of the missing dependencies
 359       *
 360       * @return array The base names of the missing dependencies
 361       */
 362      public function getMissingDependencies()
 363      {
 364          /* @var PluginController $plugin_controller */
 365          global $plugin_controller;
 366          $dependencies = $this->getDependencies();
 367          $missing_dependencies = array();
 368          foreach ($dependencies as $dependency) {
 369              if ($plugin_controller->isdisabled($dependency)) {
 370                  $missing_dependencies[] = $dependency;
 371              }
 372          }
 373          return $missing_dependencies;
 374      }
 375  
 376      /**
 377       * Get the names of all conflicting extensions
 378       *
 379       * @return array The names of the conflicting extensions
 380       */
 381      public function getConflicts()
 382      {
 383          if (!empty($this->remoteInfo['conflicts'])) return $this->remoteInfo['conflicts'];
 384          return array();
 385      }
 386  
 387      /**
 388       * Get the names of similar extensions
 389       *
 390       * @return array The names of similar extensions
 391       */
 392      public function getSimilarExtensions()
 393      {
 394          if (!empty($this->remoteInfo['similar'])) return $this->remoteInfo['similar'];
 395          return array();
 396      }
 397  
 398      /**
 399       * Get the names of the tags of the extension
 400       *
 401       * @return array The names of the tags of the extension
 402       */
 403      public function getTags()
 404      {
 405          if (!empty($this->remoteInfo['tags'])) return $this->remoteInfo['tags'];
 406          return array();
 407      }
 408  
 409      /**
 410       * Get the popularity information as floating point number [0,1]
 411       *
 412       * @return float|bool The popularity information or false if it isn't available
 413       */
 414      public function getPopularity()
 415      {
 416          if (!empty($this->remoteInfo['popularity'])) return $this->remoteInfo['popularity'];
 417          return false;
 418      }
 419  
 420  
 421      /**
 422       * Get the text of the security warning if there is any
 423       *
 424       * @return string|bool The security warning if there is any, false otherwise
 425       */
 426      public function getSecurityWarning()
 427      {
 428          if (!empty($this->remoteInfo['securitywarning'])) return $this->remoteInfo['securitywarning'];
 429          return false;
 430      }
 431  
 432      /**
 433       * Get the text of the security issue if there is any
 434       *
 435       * @return string|bool The security issue if there is any, false otherwise
 436       */
 437      public function getSecurityIssue()
 438      {
 439          if (!empty($this->remoteInfo['securityissue'])) return $this->remoteInfo['securityissue'];
 440          return false;
 441      }
 442  
 443      /**
 444       * Get the URL of the screenshot of the extension if there is any
 445       *
 446       * @return string|bool The screenshot URL if there is any, false otherwise
 447       */
 448      public function getScreenshotURL()
 449      {
 450          if (!empty($this->remoteInfo['screenshoturl'])) return $this->remoteInfo['screenshoturl'];
 451          return false;
 452      }
 453  
 454      /**
 455       * Get the URL of the thumbnail of the extension if there is any
 456       *
 457       * @return string|bool The thumbnail URL if there is any, false otherwise
 458       */
 459      public function getThumbnailURL()
 460      {
 461          if (!empty($this->remoteInfo['thumbnailurl'])) return $this->remoteInfo['thumbnailurl'];
 462          return false;
 463      }
 464      /**
 465       * Get the last used download URL of the extension if there is any
 466       *
 467       * @return string|bool The previously used download URL, false if the extension has been installed manually
 468       */
 469      public function getLastDownloadURL()
 470      {
 471          if (!empty($this->managerData['downloadurl'])) return $this->managerData['downloadurl'];
 472          return false;
 473      }
 474  
 475      /**
 476       * Get the download URL of the extension if there is any
 477       *
 478       * @return string|bool The download URL if there is any, false otherwise
 479       */
 480      public function getDownloadURL()
 481      {
 482          if (!empty($this->remoteInfo['downloadurl'])) return $this->remoteInfo['downloadurl'];
 483          return false;
 484      }
 485  
 486      /**
 487       * If the download URL has changed since the last download
 488       *
 489       * @return bool If the download URL has changed
 490       */
 491      public function hasDownloadURLChanged()
 492      {
 493          $lasturl = $this->getLastDownloadURL();
 494          $currenturl = $this->getDownloadURL();
 495          return ($lasturl && $currenturl && $lasturl != $currenturl);
 496      }
 497  
 498      /**
 499       * Get the bug tracker URL of the extension if there is any
 500       *
 501       * @return string|bool The bug tracker URL if there is any, false otherwise
 502       */
 503      public function getBugtrackerURL()
 504      {
 505          if (!empty($this->remoteInfo['bugtracker'])) return $this->remoteInfo['bugtracker'];
 506          return false;
 507      }
 508  
 509      /**
 510       * Get the URL of the source repository if there is any
 511       *
 512       * @return string|bool The URL of the source repository if there is any, false otherwise
 513       */
 514      public function getSourcerepoURL()
 515      {
 516          if (!empty($this->remoteInfo['sourcerepo'])) return $this->remoteInfo['sourcerepo'];
 517          return false;
 518      }
 519  
 520      /**
 521       * Get the donation URL of the extension if there is any
 522       *
 523       * @return string|bool The donation URL if there is any, false otherwise
 524       */
 525      public function getDonationURL()
 526      {
 527          if (!empty($this->remoteInfo['donationurl'])) return $this->remoteInfo['donationurl'];
 528          return false;
 529      }
 530  
 531      /**
 532       * Get the extension type(s)
 533       *
 534       * @return array The type(s) as array of strings
 535       */
 536      public function getTypes()
 537      {
 538          if (!empty($this->remoteInfo['types'])) return $this->remoteInfo['types'];
 539          if ($this->isTemplate()) return array(32 => 'template');
 540          return array();
 541      }
 542  
 543      /**
 544       * Get a list of all DokuWiki versions this extension is compatible with
 545       *
 546       * @return array The versions in the form yyyy-mm-dd => ('label' => label, 'implicit' => implicit)
 547       */
 548      public function getCompatibleVersions()
 549      {
 550          if (!empty($this->remoteInfo['compatible'])) return $this->remoteInfo['compatible'];
 551          return array();
 552      }
 553  
 554      /**
 555       * Get the date of the last available update
 556       *
 557       * @return string|bool The last available update in the form yyyy-mm-dd if there is any, false otherwise
 558       */
 559      public function getLastUpdate()
 560      {
 561          if (!empty($this->remoteInfo['lastupdate'])) return $this->remoteInfo['lastupdate'];
 562          return false;
 563      }
 564  
 565      /**
 566       * Get the base path of the extension
 567       *
 568       * @return string The base path of the extension
 569       */
 570      public function getInstallDir()
 571      {
 572          if ($this->isTemplate()) {
 573              return $this->tpllib.$this->base;
 574          } else {
 575              return DOKU_PLUGIN.$this->base;
 576          }
 577      }
 578  
 579      /**
 580       * The type of extension installation
 581       *
 582       * @return string One of "none", "manual", "git" or "automatic"
 583       */
 584      public function getInstallType()
 585      {
 586          if (!$this->isInstalled()) return 'none';
 587          if (!empty($this->managerData)) return 'automatic';
 588          if (is_dir($this->getInstallDir().'/.git')) return 'git';
 589          return 'manual';
 590      }
 591  
 592      /**
 593       * If the extension can probably be installed/updated or uninstalled
 594       *
 595       * @return bool|string True or error string
 596       */
 597      public function canModify()
 598      {
 599          if ($this->isInstalled()) {
 600              if (!is_writable($this->getInstallDir())) {
 601                  return 'noperms';
 602              }
 603          }
 604  
 605          if ($this->isTemplate() && !is_writable($this->tpllib)) {
 606              return 'notplperms';
 607          } elseif (!is_writable(DOKU_PLUGIN)) {
 608              return 'nopluginperms';
 609          }
 610          return true;
 611      }
 612  
 613      /**
 614       * Install an extension from a user upload
 615       *
 616       * @param string $field name of the upload file
 617       * @throws Exception when something goes wrong
 618       * @return array The list of installed extensions
 619       */
 620      public function installFromUpload($field)
 621      {
 622          if ($_FILES[$field]['error']) {
 623              throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')');
 624          }
 625  
 626          $tmp = $this->mkTmpDir();
 627          if (!$tmp) throw new Exception($this->getLang('error_dircreate'));
 628  
 629          // filename may contain the plugin name for old style plugins...
 630          $basename = basename($_FILES[$field]['name']);
 631          $basename = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $basename);
 632          $basename = preg_replace('/[\W]+/', '', $basename);
 633  
 634          if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) {
 635              throw new Exception($this->getLang('msg_upload_failed'));
 636          }
 637  
 638          try {
 639              $installed = $this->installArchive("$tmp/upload.archive", true, $basename);
 640              $this->updateManagerData('', $installed);
 641              $this->removeDeletedfiles($installed);
 642              // purge cache
 643              $this->purgeCache();
 644          } catch (Exception $e) {
 645              throw $e;
 646          }
 647          return $installed;
 648      }
 649  
 650      /**
 651       * Install an extension from a remote URL
 652       *
 653       * @param string $url
 654       * @throws Exception when something goes wrong
 655       * @return array The list of installed extensions
 656       */
 657      public function installFromURL($url)
 658      {
 659          try {
 660              $path      = $this->download($url);
 661              $installed = $this->installArchive($path, true);
 662              $this->updateManagerData($url, $installed);
 663              $this->removeDeletedfiles($installed);
 664  
 665              // purge cache
 666              $this->purgeCache();
 667          } catch (Exception $e) {
 668              throw $e;
 669          }
 670          return $installed;
 671      }
 672  
 673      /**
 674       * Install or update the extension
 675       *
 676       * @throws \Exception when something goes wrong
 677       * @return array The list of installed extensions
 678       */
 679      public function installOrUpdate()
 680      {
 681          $url       = $this->getDownloadURL();
 682          $path      = $this->download($url);
 683          $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase());
 684          $this->updateManagerData($url, $installed);
 685  
 686          // refresh extension information
 687          if (!isset($installed[$this->getID()])) {
 688              throw new Exception('Error, the requested extension hasn\'t been installed or updated');
 689          }
 690          $this->removeDeletedfiles($installed);
 691          $this->setExtension($this->getID());
 692          $this->purgeCache();
 693          return $installed;
 694      }
 695  
 696      /**
 697       * Uninstall the extension
 698       *
 699       * @return bool If the plugin was sucessfully uninstalled
 700       */
 701      public function uninstall()
 702      {
 703          $this->purgeCache();
 704          return io_rmdir($this->getInstallDir(), true);
 705      }
 706  
 707      /**
 708       * Enable the extension
 709       *
 710       * @return bool|string True or an error message
 711       */
 712      public function enable()
 713      {
 714          if ($this->isTemplate()) return $this->getLang('notimplemented');
 715          if (!$this->isInstalled()) return $this->getLang('notinstalled');
 716          if ($this->isEnabled()) return $this->getLang('alreadyenabled');
 717  
 718          /* @var PluginController $plugin_controller */
 719          global $plugin_controller;
 720          if ($plugin_controller->enable($this->base)) {
 721              $this->purgeCache();
 722              return true;
 723          } else {
 724              return $this->getLang('pluginlistsaveerror');
 725          }
 726      }
 727  
 728      /**
 729       * Disable the extension
 730       *
 731       * @return bool|string True or an error message
 732       */
 733      public function disable()
 734      {
 735          if ($this->isTemplate()) return $this->getLang('notimplemented');
 736  
 737          /* @var PluginController $plugin_controller */
 738          global $plugin_controller;
 739          if (!$this->isInstalled()) return $this->getLang('notinstalled');
 740          if (!$this->isEnabled()) return $this->getLang('alreadydisabled');
 741          if ($plugin_controller->disable($this->base)) {
 742              $this->purgeCache();
 743              return true;
 744          } else {
 745              return $this->getLang('pluginlistsaveerror');
 746          }
 747      }
 748  
 749      /**
 750       * Purge the cache by touching the main configuration file
 751       */
 752      protected function purgeCache()
 753      {
 754          global $config_cascade;
 755  
 756          // expire dokuwiki caches
 757          // touching local.php expires wiki page, JS and CSS caches
 758          @touch(reset($config_cascade['main']['local']));
 759      }
 760  
 761      /**
 762       * Read local extension data either from info.txt or getInfo()
 763       */
 764      protected function readLocalData()
 765      {
 766          if ($this->isTemplate()) {
 767              $infopath = $this->getInstallDir().'/template.info.txt';
 768          } else {
 769              $infopath = $this->getInstallDir().'/plugin.info.txt';
 770          }
 771  
 772          if (is_readable($infopath)) {
 773              $this->localInfo = confToHash($infopath);
 774          } elseif (!$this->isTemplate() && $this->isEnabled()) {
 775              $path       = $this->getInstallDir().'/';
 776              $plugin     = null;
 777  
 778              foreach (PluginController::PLUGIN_TYPES as $type) {
 779                  if (file_exists($path.$type.'.php')) {
 780                      $plugin = plugin_load($type, $this->base);
 781                      if ($plugin) break;
 782                  }
 783  
 784                  if ($dh = @opendir($path.$type.'/')) {
 785                      while (false !== ($cp = readdir($dh))) {
 786                          if ($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
 787  
 788                          $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
 789                          if ($plugin) break;
 790                      }
 791                      if ($plugin) break;
 792                      closedir($dh);
 793                  }
 794              }
 795  
 796              if ($plugin) {
 797                  /* @var DokuWiki_Plugin $plugin */
 798                  $this->localInfo = $plugin->getInfo();
 799              }
 800          }
 801      }
 802  
 803      /**
 804       * Save the given URL and current datetime in the manager.dat file of all installed extensions
 805       *
 806       * @param string $url       Where the extension was downloaded from. (empty for manual installs via upload)
 807       * @param array  $installed Optional list of installed plugins
 808       */
 809      protected function updateManagerData($url = '', $installed = null)
 810      {
 811          $origID = $this->getID();
 812  
 813          if (is_null($installed)) {
 814              $installed = array($origID);
 815          }
 816  
 817          foreach ($installed as $ext => $info) {
 818              if ($this->getID() != $ext) $this->setExtension($ext);
 819              if ($url) {
 820                  $this->managerData['downloadurl'] = $url;
 821              } elseif (isset($this->managerData['downloadurl'])) {
 822                  unset($this->managerData['downloadurl']);
 823              }
 824              if (isset($this->managerData['installed'])) {
 825                  $this->managerData['updated'] = date('r');
 826              } else {
 827                  $this->managerData['installed'] = date('r');
 828              }
 829              $this->writeManagerData();
 830          }
 831  
 832          if ($this->getID() != $origID) $this->setExtension($origID);
 833      }
 834  
 835      /**
 836       * Read the manager.dat file
 837       */
 838      protected function readManagerData()
 839      {
 840          $managerpath = $this->getInstallDir().'/manager.dat';
 841          if (is_readable($managerpath)) {
 842              $file = @file($managerpath);
 843              if (!empty($file)) {
 844                  foreach ($file as $line) {
 845                      list($key, $value) = explode('=', trim($line, DOKU_LF), 2);
 846                      $key = trim($key);
 847                      $value = trim($value);
 848                      // backwards compatible with old plugin manager
 849                      if ($key == 'url') $key = 'downloadurl';
 850                      $this->managerData[$key] = $value;
 851                  }
 852              }
 853          }
 854      }
 855  
 856      /**
 857       * Write the manager.data file
 858       */
 859      protected function writeManagerData()
 860      {
 861          $managerpath = $this->getInstallDir().'/manager.dat';
 862          $data = '';
 863          foreach ($this->managerData as $k => $v) {
 864              $data .= $k.'='.$v.DOKU_LF;
 865          }
 866          io_saveFile($managerpath, $data);
 867      }
 868  
 869      /**
 870       * Returns a temporary directory
 871       *
 872       * The directory is registered for cleanup when the class is destroyed
 873       *
 874       * @return false|string
 875       */
 876      protected function mkTmpDir()
 877      {
 878          $dir = io_mktmpdir();
 879          if (!$dir) return false;
 880          $this->temporary[] = $dir;
 881          return $dir;
 882      }
 883  
 884      /**
 885       * downloads a file from the net and saves it
 886       *
 887       * - $file is the directory where the file should be saved
 888       * - if successful will return the name used for the saved file, false otherwise
 889       *
 890       * @author Andreas Gohr <andi@splitbrain.org>
 891       * @author Chris Smith <chris@jalakai.co.uk>
 892       *
 893       * @param string $url           url to download
 894       * @param string $file          path to file or directory where to save
 895       * @param string $defaultName   fallback for name of download
 896       * @return bool|string          if failed false, otherwise true or the name of the file in the given dir
 897       */
 898      protected function downloadToFile($url,$file,$defaultName=''){
 899          global $conf;
 900          $http = new DokuHTTPClient();
 901          $http->max_bodysize = 0;
 902          $http->timeout = 25; //max. 25 sec
 903          $http->keep_alive = false; // we do single ops here, no need for keep-alive
 904          $http->agent = 'DokuWiki HTTP Client (Extension Manager)';
 905  
 906          $data = $http->get($url);
 907          if ($data === false) return false;
 908  
 909          $name = '';
 910          if (isset($http->resp_headers['content-disposition'])) {
 911              $content_disposition = $http->resp_headers['content-disposition'];
 912              $match=array();
 913              if (is_string($content_disposition) &&
 914                      preg_match('/attachment;\s*filename\s*=\s*"([^"]*)"/i', $content_disposition, $match)) {
 915  
 916                  $name = \dokuwiki\Utf8\PhpString::basename($match[1]);
 917              }
 918  
 919          }
 920  
 921          if (!$name) {
 922              if (!$defaultName) return false;
 923              $name = $defaultName;
 924          }
 925  
 926          $file = $file.$name;
 927  
 928          $fileexists = file_exists($file);
 929          $fp = @fopen($file,"w");
 930          if(!$fp) return false;
 931          fwrite($fp,$data);
 932          fclose($fp);
 933          if(!$fileexists and $conf['fperm']) chmod($file, $conf['fperm']);
 934          return $name;
 935      }
 936  
 937      /**
 938       * Download an archive to a protected path
 939       *
 940       * @param string $url  The url to get the archive from
 941       * @throws Exception   when something goes wrong
 942       * @return string The path where the archive was saved
 943       */
 944      public function download($url)
 945      {
 946          // check the url
 947          if (!preg_match('/https?:\/\//i', $url)) {
 948              throw new Exception($this->getLang('error_badurl'));
 949          }
 950  
 951          // try to get the file from the path (used as plugin name fallback)
 952          $file = parse_url($url, PHP_URL_PATH);
 953          if (is_null($file)) {
 954              $file = md5($url);
 955          } else {
 956              $file = \dokuwiki\Utf8\PhpString::basename($file);
 957          }
 958  
 959          // create tmp directory for download
 960          if (!($tmp = $this->mkTmpDir())) {
 961              throw new Exception($this->getLang('error_dircreate'));
 962          }
 963  
 964          // download
 965          if (!$file = $this->downloadToFile($url, $tmp.'/', $file)) {
 966              io_rmdir($tmp, true);
 967              throw new Exception(sprintf($this->getLang('error_download'), '<bdi>'.hsc($url).'</bdi>'));
 968          }
 969  
 970          return $tmp.'/'.$file;
 971      }
 972  
 973      /**
 974       * @param string $file      The path to the archive that shall be installed
 975       * @param bool   $overwrite If an already installed plugin should be overwritten
 976       * @param string $base      The basename of the plugin if it's known
 977       * @throws Exception        when something went wrong
 978       * @return array            list of installed extensions
 979       */
 980      public function installArchive($file, $overwrite = false, $base = '')
 981      {
 982          $installed_extensions = array();
 983  
 984          // create tmp directory for decompression
 985          if (!($tmp = $this->mkTmpDir())) {
 986              throw new Exception($this->getLang('error_dircreate'));
 987          }
 988  
 989          // add default base folder if specified to handle case where zip doesn't contain this
 990          if ($base && !@mkdir($tmp.'/'.$base)) {
 991              throw new Exception($this->getLang('error_dircreate'));
 992          }
 993  
 994          // decompress
 995          $this->decompress($file, "$tmp/".$base);
 996  
 997          // search $tmp/$base for the folder(s) that has been created
 998          // move the folder(s) to lib/..
 999          $result = array('old'=>array(), 'new'=>array());
1000          $default = ($this->isTemplate() ? 'template' : 'plugin');
1001          if (!$this->findFolders($result, $tmp.'/'.$base, $default)) {
1002              throw new Exception($this->getLang('error_findfolder'));
1003          }
1004  
1005          // choose correct result array
1006          if (count($result['new'])) {
1007              $install = $result['new'];
1008          } else {
1009              $install = $result['old'];
1010          }
1011  
1012          if (!count($install)) {
1013              throw new Exception($this->getLang('error_findfolder'));
1014          }
1015  
1016          // now install all found items
1017          foreach ($install as $item) {
1018              // where to install?
1019              if ($item['type'] == 'template') {
1020                  $target_base_dir = $this->tpllib;
1021              } else {
1022                  $target_base_dir = DOKU_PLUGIN;
1023              }
1024  
1025              if (!empty($item['base'])) {
1026                  // use base set in info.txt
1027              } elseif ($base && count($install) == 1) {
1028                  $item['base'] = $base;
1029              } else {
1030                  // default - use directory as found in zip
1031                  // plugins from github/master without *.info.txt will install in wrong folder
1032                  // but using $info->id will make 'code3' fail (which should install in lib/code/..)
1033                  $item['base'] = basename($item['tmp']);
1034              }
1035  
1036              // check to make sure we aren't overwriting anything
1037              $target = $target_base_dir.$item['base'];
1038              if (!$overwrite && file_exists($target)) {
1039                  // TODO remember our settings, ask the user to confirm overwrite
1040                  continue;
1041              }
1042  
1043              $action = file_exists($target) ? 'update' : 'install';
1044  
1045              // copy action
1046              if ($this->dircopy($item['tmp'], $target)) {
1047                  // return info
1048                  $id = $item['base'];
1049                  if ($item['type'] == 'template') {
1050                      $id = 'template:'.$id;
1051                  }
1052                  $installed_extensions[$id] = array(
1053                      'base' => $item['base'],
1054                      'type' => $item['type'],
1055                      'action' => $action
1056                  );
1057              } else {
1058                  throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF, '<bdi>'.$item['base'].'</bdi>'));
1059              }
1060          }
1061  
1062          // cleanup
1063          if ($tmp) io_rmdir($tmp, true);
1064  
1065          return $installed_extensions;
1066      }
1067  
1068      /**
1069       * Find out what was in the extracted directory
1070       *
1071       * Correct folders are searched recursively using the "*.info.txt" configs
1072       * as indicator for a root folder. When such a file is found, it's base
1073       * setting is used (when set). All folders found by this method are stored
1074       * in the 'new' key of the $result array.
1075       *
1076       * For backwards compatibility all found top level folders are stored as
1077       * in the 'old' key of the $result array.
1078       *
1079       * When no items are found in 'new' the copy mechanism should fall back
1080       * the 'old' list.
1081       *
1082       * @author Andreas Gohr <andi@splitbrain.org>
1083       * @param array $result - results are stored here
1084       * @param string $directory - the temp directory where the package was unpacked to
1085       * @param string $default_type - type used if no info.txt available
1086       * @param string $subdir - a subdirectory. do not set. used by recursion
1087       * @return bool - false on error
1088       */
1089      protected function findFolders(&$result, $directory, $default_type = 'plugin', $subdir = '')
1090      {
1091          $this_dir = "$directory$subdir";
1092          $dh       = @opendir($this_dir);
1093          if (!$dh) return false;
1094  
1095          $found_dirs           = array();
1096          $found_files          = 0;
1097          $found_template_parts = 0;
1098          while (false !== ($f = readdir($dh))) {
1099              if ($f == '.' || $f == '..') continue;
1100  
1101              if (is_dir("$this_dir/$f")) {
1102                  $found_dirs[] = "$subdir/$f";
1103              } else {
1104                  // it's a file -> check for config
1105                  $found_files++;
1106                  switch ($f) {
1107                      case 'plugin.info.txt':
1108                      case 'template.info.txt':
1109                          // we have  found a clear marker, save and return
1110                          $info = array();
1111                          $type = explode('.', $f, 2);
1112                          $info['type'] = $type[0];
1113                          $info['tmp']  = $this_dir;
1114                          $conf = confToHash("$this_dir/$f");
1115                          $info['base'] = basename($conf['base']);
1116                          $result['new'][] = $info;
1117                          return true;
1118  
1119                      case 'main.php':
1120                      case 'details.php':
1121                      case 'mediamanager.php':
1122                      case 'style.ini':
1123                          $found_template_parts++;
1124                          break;
1125                  }
1126              }
1127          }
1128          closedir($dh);
1129  
1130          // files where found but no info.txt - use old method
1131          if ($found_files) {
1132              $info            = array();
1133              $info['tmp']     = $this_dir;
1134              // does this look like a template or should we use the default type?
1135              if ($found_template_parts >= 2) {
1136                  $info['type']    = 'template';
1137              } else {
1138                  $info['type']    = $default_type;
1139              }
1140  
1141              $result['old'][] = $info;
1142              return true;
1143          }
1144  
1145          // we have no files yet -> recurse
1146          foreach ($found_dirs as $found_dir) {
1147              $this->findFolders($result, $directory, $default_type, "$found_dir");
1148          }
1149          return true;
1150      }
1151  
1152      /**
1153       * Decompress a given file to the given target directory
1154       *
1155       * Determines the compression type from the file extension
1156       *
1157       * @param string $file   archive to extract
1158       * @param string $target directory to extract to
1159       * @throws Exception
1160       * @return bool
1161       */
1162      private function decompress($file, $target)
1163      {
1164          // decompression library doesn't like target folders ending in "/"
1165          if (substr($target, -1) == "/") $target = substr($target, 0, -1);
1166  
1167          $ext = $this->guessArchiveType($file);
1168          if (in_array($ext, array('tar', 'bz', 'gz'))) {
1169              try {
1170                  $tar = new \splitbrain\PHPArchive\Tar();
1171                  $tar->open($file);
1172                  $tar->extract($target);
1173              } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1174                  throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1175              }
1176  
1177              return true;
1178          } elseif ($ext == 'zip') {
1179              try {
1180                  $zip = new \splitbrain\PHPArchive\Zip();
1181                  $zip->open($file);
1182                  $zip->extract($target);
1183              } catch (\splitbrain\PHPArchive\ArchiveIOException $e) {
1184                  throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
1185              }
1186  
1187              return true;
1188          }
1189  
1190          // the only case when we don't get one of the recognized archive types is when the archive file can't be read
1191          throw new Exception($this->getLang('error_decompress').' Couldn\'t read archive file');
1192      }
1193  
1194      /**
1195       * Determine the archive type of the given file
1196       *
1197       * Reads the first magic bytes of the given file for content type guessing,
1198       * if neither bz, gz or zip are recognized, tar is assumed.
1199       *
1200       * @author Andreas Gohr <andi@splitbrain.org>
1201       * @param string $file The file to analyze
1202       * @return string|false false if the file can't be read, otherwise an "extension"
1203       */
1204      private function guessArchiveType($file)
1205      {
1206          $fh = fopen($file, 'rb');
1207          if (!$fh) return false;
1208          $magic = fread($fh, 5);
1209          fclose($fh);
1210  
1211          if (strpos($magic, "\x42\x5a") === 0) return 'bz';
1212          if (strpos($magic, "\x1f\x8b") === 0) return 'gz';
1213          if (strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip';
1214          return 'tar';
1215      }
1216  
1217      /**
1218       * Copy with recursive sub-directory support
1219       *
1220       * @param string $src filename path to file
1221       * @param string $dst filename path to file
1222       * @return bool|int|string
1223       */
1224      private function dircopy($src, $dst)
1225      {
1226          global $conf;
1227  
1228          if (is_dir($src)) {
1229              if (!$dh = @opendir($src)) return false;
1230  
1231              if ($ok = io_mkdir_p($dst)) {
1232                  while ($ok && (false !== ($f = readdir($dh)))) {
1233                      if ($f == '..' || $f == '.') continue;
1234                      $ok = $this->dircopy("$src/$f", "$dst/$f");
1235                  }
1236              }
1237  
1238              closedir($dh);
1239              return $ok;
1240          } else {
1241              $exists = file_exists($dst);
1242  
1243              if (!@copy($src, $dst)) return false;
1244              if (!$exists && !empty($conf['fperm'])) chmod($dst, $conf['fperm']);
1245              @touch($dst, filemtime($src));
1246          }
1247  
1248          return true;
1249      }
1250  
1251      /**
1252       * Delete outdated files from updated plugins
1253       *
1254       * @param array $installed
1255       */
1256      private function removeDeletedfiles($installed)
1257      {
1258          foreach ($installed as $id => $extension) {
1259              // only on update
1260              if ($extension['action'] == 'install') continue;
1261  
1262              // get definition file
1263              if ($extension['type'] == 'template') {
1264                  $extensiondir = $this->tpllib;
1265              } else {
1266                  $extensiondir = DOKU_PLUGIN;
1267              }
1268              $extensiondir = $extensiondir . $extension['base'] .'/';
1269              $definitionfile = $extensiondir . 'deleted.files';
1270              if (!file_exists($definitionfile)) continue;
1271  
1272              // delete the old files
1273              $list = file($definitionfile);
1274  
1275              foreach ($list as $line) {
1276                  $line = trim(preg_replace('/#.*$/', '', $line));
1277                  if (!$line) continue;
1278                  $file = $extensiondir . $line;
1279                  if (!file_exists($file)) continue;
1280  
1281                  io_rmdir($file, true);
1282              }
1283          }
1284      }
1285  }
1286  
1287  // vim:ts=4:sw=4:et: