replaced deprecated utf8 functions
[dokuwiki.git] / lib / plugins / extension / helper / extension.php
blob0186620d06d2905b170c0822f0a4e0ce333e192b
1 <?php
2 /**
3 * DokuWiki Plugin extension (Helper Component)
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author Michael Hamann <michael@content-space.de>
7 */
9 use dokuwiki\HTTP\DokuHTTPClient;
10 use dokuwiki\Extension\PluginController;
12 /**
13 * Class helper_plugin_extension_extension represents a single extension (plugin or template)
15 class helper_plugin_extension_extension extends DokuWiki_Plugin
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;
26 /** @var array list of temporary directories */
27 private $temporary = array();
29 /** @var string where templates are installed to */
30 private $tpllib = '';
32 /**
33 * helper_plugin_extension_extension constructor.
35 public function __construct()
37 $this->tpllib = dirname(tpl_incdir()).'/';
40 /**
41 * Destructor
43 * deletes any dangling temporary directories
45 public function __destruct()
47 foreach ($this->temporary as $dir) {
48 io_rmdir($dir, true);
52 /**
53 * @return bool false, this component is not a singleton
55 public function isSingleton()
57 return false;
60 /**
61 * Set the name of the extension this instance shall represents, triggers loading the local and remote data
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
66 public function setExtension($id)
68 $id = cleanID($id);
69 $this->id = $id;
70 $this->base = $id;
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;
79 $this->localInfo = array();
80 $this->managerData = array();
81 $this->remoteInfo = array();
83 if ($this->isInstalled()) {
84 $this->readLocalData();
85 $this->readManagerData();
88 if ($this->repository == null) {
89 $this->repository = $this->loadHelper('extension_repository');
92 $this->remoteInfo = $this->repository->getData($this->getID());
94 return ($this->localInfo || $this->remoteInfo);
97 /**
98 * If the extension is installed locally
100 * @return bool If the extension is installed locally
102 public function isInstalled()
104 return is_dir($this->getInstallDir());
108 * If the extension is under git control
110 * @return bool
112 public function isGitControlled()
114 if (!$this->isInstalled()) return false;
115 return is_dir($this->getInstallDir().'/.git');
119 * If the extension is bundled
121 * @return bool If the extension is bundled
123 public function isBundled()
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'
138 * If the extension is protected against any modification (disable/uninstall)
140 * @return bool if the extension is protected
142 public function isProtected()
144 // never allow deinstalling the current auth plugin:
145 global $conf;
146 if ($this->id == $conf['authtype']) return true;
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]);
155 * If the extension is installed in the correct directory
157 * @return bool If the extension is installed in the correct directory
159 public function isInWrongFolder()
161 return $this->base != $this->getBase();
165 * If the extension is enabled
167 * @return bool If the extension is enabled
169 public function isEnabled()
171 global $conf;
172 if ($this->isTemplate()) {
173 return ($conf['template'] == $this->getBase());
176 /* @var PluginController $plugin_controller */
177 global $plugin_controller;
178 return !$plugin_controller->isdisabled($this->base);
182 * If the extension should be updated, i.e. if an updated version is available
184 * @return bool If an update is available
186 public function updateAvailable()
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();
198 * If the extension is a template
200 * @return bool If this extension is a template
202 public function isTemplate()
204 return $this->is_template;
208 * Get the ID of the extension
210 * This is the same as getName() for plugins, for templates it's getName() prefixed with 'template:'
212 * @return string
214 public function getID()
216 return $this->id;
220 * Get the name of the installation directory
222 * @return string The name of the installation directory
224 public function getInstallName()
226 return $this->base;
229 // Data from plugin.info.txt/template.info.txt or the repo when not available locally
231 * Get the basename of the extension
233 * @return string The basename
235 public function getBase()
237 if (!empty($this->localInfo['base'])) return $this->localInfo['base'];
238 return $this->base;
242 * Get the display name of the extension
244 * @return string The display name
246 public function getDisplayName()
248 if (!empty($this->localInfo['name'])) return $this->localInfo['name'];
249 if (!empty($this->remoteInfo['name'])) return $this->remoteInfo['name'];
250 return $this->base;
254 * Get the author name of the extension
256 * @return string|bool The name of the author or false if there is none
258 public function getAuthor()
260 if (!empty($this->localInfo['author'])) return $this->localInfo['author'];
261 if (!empty($this->remoteInfo['author'])) return $this->remoteInfo['author'];
262 return false;
266 * Get the email of the author of the extension if there is any
268 * @return string|bool The email address or false if there is none
270 public function getEmail()
272 // email is only in the local data
273 if (!empty($this->localInfo['email'])) return $this->localInfo['email'];
274 return false;
278 * Get the email id, i.e. the md5sum of the email
280 * @return string|bool The md5sum of the email if there is any, false otherwise
282 public function getEmailID()
284 if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid'];
285 if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']);
286 return false;
290 * Get the description of the extension
292 * @return string The description
294 public function getDescription()
296 if (!empty($this->localInfo['desc'])) return $this->localInfo['desc'];
297 if (!empty($this->remoteInfo['description'])) return $this->remoteInfo['description'];
298 return '';
302 * Get the URL of the extension, usually a page on dokuwiki.org
304 * @return string The URL
306 public function getURL()
308 if (!empty($this->localInfo['url'])) return $this->localInfo['url'];
309 return 'https://www.dokuwiki.org/'.($this->isTemplate() ? 'template' : 'plugin').':'.$this->getBase();
313 * Get the installed version of the extension
315 * @return string|bool The version, usually in the form yyyy-mm-dd if there is any
317 public function getInstalledVersion()
319 if (!empty($this->localInfo['date'])) return $this->localInfo['date'];
320 if ($this->isInstalled()) return $this->getLang('unknownversion');
321 return false;
325 * Get the install date of the current version
327 * @return string|bool The date of the last update or false if not available
329 public function getUpdateDate()
331 if (!empty($this->managerData['updated'])) return $this->managerData['updated'];
332 return $this->getInstallDate();
336 * Get the date of the installation of the plugin
338 * @return string|bool The date of the installation or false if not available
340 public function getInstallDate()
342 if (!empty($this->managerData['installed'])) return $this->managerData['installed'];
343 return false;
347 * Get the names of the dependencies of this extension
349 * @return array The base names of the dependencies
351 public function getDependencies()
353 if (!empty($this->remoteInfo['dependencies'])) return $this->remoteInfo['dependencies'];
354 return array();
358 * Get the names of the missing dependencies
360 * @return array The base names of the missing dependencies
362 public function getMissingDependencies()
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;
373 return $missing_dependencies;
377 * Get the names of all conflicting extensions
379 * @return array The names of the conflicting extensions
381 public function getConflicts()
383 if (!empty($this->remoteInfo['conflicts'])) return $this->remoteInfo['conflicts'];
384 return array();
388 * Get the names of similar extensions
390 * @return array The names of similar extensions
392 public function getSimilarExtensions()
394 if (!empty($this->remoteInfo['similar'])) return $this->remoteInfo['similar'];
395 return array();
399 * Get the names of the tags of the extension
401 * @return array The names of the tags of the extension
403 public function getTags()
405 if (!empty($this->remoteInfo['tags'])) return $this->remoteInfo['tags'];
406 return array();
410 * Get the popularity information as floating point number [0,1]
412 * @return float|bool The popularity information or false if it isn't available
414 public function getPopularity()
416 if (!empty($this->remoteInfo['popularity'])) return $this->remoteInfo['popularity'];
417 return false;
422 * Get the text of the security warning if there is any
424 * @return string|bool The security warning if there is any, false otherwise
426 public function getSecurityWarning()
428 if (!empty($this->remoteInfo['securitywarning'])) return $this->remoteInfo['securitywarning'];
429 return false;
433 * Get the text of the security issue if there is any
435 * @return string|bool The security issue if there is any, false otherwise
437 public function getSecurityIssue()
439 if (!empty($this->remoteInfo['securityissue'])) return $this->remoteInfo['securityissue'];
440 return false;
444 * Get the URL of the screenshot of the extension if there is any
446 * @return string|bool The screenshot URL if there is any, false otherwise
448 public function getScreenshotURL()
450 if (!empty($this->remoteInfo['screenshoturl'])) return $this->remoteInfo['screenshoturl'];
451 return false;
455 * Get the URL of the thumbnail of the extension if there is any
457 * @return string|bool The thumbnail URL if there is any, false otherwise
459 public function getThumbnailURL()
461 if (!empty($this->remoteInfo['thumbnailurl'])) return $this->remoteInfo['thumbnailurl'];
462 return false;
465 * Get the last used download URL of the extension if there is any
467 * @return string|bool The previously used download URL, false if the extension has been installed manually
469 public function getLastDownloadURL()
471 if (!empty($this->managerData['downloadurl'])) return $this->managerData['downloadurl'];
472 return false;
476 * Get the download URL of the extension if there is any
478 * @return string|bool The download URL if there is any, false otherwise
480 public function getDownloadURL()
482 if (!empty($this->remoteInfo['downloadurl'])) return $this->remoteInfo['downloadurl'];
483 return false;
487 * If the download URL has changed since the last download
489 * @return bool If the download URL has changed
491 public function hasDownloadURLChanged()
493 $lasturl = $this->getLastDownloadURL();
494 $currenturl = $this->getDownloadURL();
495 return ($lasturl && $currenturl && $lasturl != $currenturl);
499 * Get the bug tracker URL of the extension if there is any
501 * @return string|bool The bug tracker URL if there is any, false otherwise
503 public function getBugtrackerURL()
505 if (!empty($this->remoteInfo['bugtracker'])) return $this->remoteInfo['bugtracker'];
506 return false;
510 * Get the URL of the source repository if there is any
512 * @return string|bool The URL of the source repository if there is any, false otherwise
514 public function getSourcerepoURL()
516 if (!empty($this->remoteInfo['sourcerepo'])) return $this->remoteInfo['sourcerepo'];
517 return false;
521 * Get the donation URL of the extension if there is any
523 * @return string|bool The donation URL if there is any, false otherwise
525 public function getDonationURL()
527 if (!empty($this->remoteInfo['donationurl'])) return $this->remoteInfo['donationurl'];
528 return false;
532 * Get the extension type(s)
534 * @return array The type(s) as array of strings
536 public function getTypes()
538 if (!empty($this->remoteInfo['types'])) return $this->remoteInfo['types'];
539 if ($this->isTemplate()) return array(32 => 'template');
540 return array();
544 * Get a list of all DokuWiki versions this extension is compatible with
546 * @return array The versions in the form yyyy-mm-dd => ('label' => label, 'implicit' => implicit)
548 public function getCompatibleVersions()
550 if (!empty($this->remoteInfo['compatible'])) return $this->remoteInfo['compatible'];
551 return array();
555 * Get the date of the last available update
557 * @return string|bool The last available update in the form yyyy-mm-dd if there is any, false otherwise
559 public function getLastUpdate()
561 if (!empty($this->remoteInfo['lastupdate'])) return $this->remoteInfo['lastupdate'];
562 return false;
566 * Get the base path of the extension
568 * @return string The base path of the extension
570 public function getInstallDir()
572 if ($this->isTemplate()) {
573 return $this->tpllib.$this->base;
574 } else {
575 return DOKU_PLUGIN.$this->base;
580 * The type of extension installation
582 * @return string One of "none", "manual", "git" or "automatic"
584 public function getInstallType()
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';
593 * If the extension can probably be installed/updated or uninstalled
595 * @return bool|string True or error string
597 public function canModify()
599 if ($this->isInstalled()) {
600 if (!is_writable($this->getInstallDir())) {
601 return 'noperms';
605 if ($this->isTemplate() && !is_writable($this->tpllib)) {
606 return 'notplperms';
607 } elseif (!is_writable(DOKU_PLUGIN)) {
608 return 'nopluginperms';
610 return true;
614 * Install an extension from a user upload
616 * @param string $field name of the upload file
617 * @throws Exception when something goes wrong
618 * @return array The list of installed extensions
620 public function installFromUpload($field)
622 if ($_FILES[$field]['error']) {
623 throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')');
626 $tmp = $this->mkTmpDir();
627 if (!$tmp) throw new Exception($this->getLang('error_dircreate'));
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);
634 if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) {
635 throw new Exception($this->getLang('msg_upload_failed'));
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;
647 return $installed;
651 * Install an extension from a remote URL
653 * @param string $url
654 * @throws Exception when something goes wrong
655 * @return array The list of installed extensions
657 public function installFromURL($url)
659 try {
660 $path = $this->download($url);
661 $installed = $this->installArchive($path, true);
662 $this->updateManagerData($url, $installed);
663 $this->removeDeletedfiles($installed);
665 // purge cache
666 $this->purgeCache();
667 } catch (Exception $e) {
668 throw $e;
670 return $installed;
674 * Install or update the extension
676 * @throws \Exception when something goes wrong
677 * @return array The list of installed extensions
679 public function installOrUpdate()
681 $url = $this->getDownloadURL();
682 $path = $this->download($url);
683 $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase());
684 $this->updateManagerData($url, $installed);
686 // refresh extension information
687 if (!isset($installed[$this->getID()])) {
688 throw new Exception('Error, the requested extension hasn\'t been installed or updated');
690 $this->removeDeletedfiles($installed);
691 $this->setExtension($this->getID());
692 $this->purgeCache();
693 return $installed;
697 * Uninstall the extension
699 * @return bool If the plugin was sucessfully uninstalled
701 public function uninstall()
703 $this->purgeCache();
704 return io_rmdir($this->getInstallDir(), true);
708 * Enable the extension
710 * @return bool|string True or an error message
712 public function enable()
714 if ($this->isTemplate()) return $this->getLang('notimplemented');
715 if (!$this->isInstalled()) return $this->getLang('notinstalled');
716 if ($this->isEnabled()) return $this->getLang('alreadyenabled');
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');
729 * Disable the extension
731 * @return bool|string True or an error message
733 public function disable()
735 if ($this->isTemplate()) return $this->getLang('notimplemented');
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');
750 * Purge the cache by touching the main configuration file
752 protected function purgeCache()
754 global $config_cascade;
756 // expire dokuwiki caches
757 // touching local.php expires wiki page, JS and CSS caches
758 @touch(reset($config_cascade['main']['local']));
762 * Read local extension data either from info.txt or getInfo()
764 protected function readLocalData()
766 if ($this->isTemplate()) {
767 $infopath = $this->getInstallDir().'/template.info.txt';
768 } else {
769 $infopath = $this->getInstallDir().'/plugin.info.txt';
772 if (is_readable($infopath)) {
773 $this->localInfo = confToHash($infopath);
774 } elseif (!$this->isTemplate() && $this->isEnabled()) {
775 $path = $this->getInstallDir().'/';
776 $plugin = null;
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;
784 if ($dh = @opendir($path.$type.'/')) {
785 while (false !== ($cp = readdir($dh))) {
786 if ($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
788 $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
789 if ($plugin) break;
791 if ($plugin) break;
792 closedir($dh);
796 if ($plugin) {
797 /* @var DokuWiki_Plugin $plugin */
798 $this->localInfo = $plugin->getInfo();
804 * Save the given URL and current datetime in the manager.dat file of all installed extensions
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
809 protected function updateManagerData($url = '', $installed = null)
811 $origID = $this->getID();
813 if (is_null($installed)) {
814 $installed = array($origID);
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']);
824 if (isset($this->managerData['installed'])) {
825 $this->managerData['updated'] = date('r');
826 } else {
827 $this->managerData['installed'] = date('r');
829 $this->writeManagerData();
832 if ($this->getID() != $origID) $this->setExtension($origID);
836 * Read the manager.dat file
838 protected function readManagerData()
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;
857 * Write the manager.data file
859 protected function writeManagerData()
861 $managerpath = $this->getInstallDir().'/manager.dat';
862 $data = '';
863 foreach ($this->managerData as $k => $v) {
864 $data .= $k.'='.$v.DOKU_LF;
866 io_saveFile($managerpath, $data);
870 * Returns a temporary directory
872 * The directory is registered for cleanup when the class is destroyed
874 * @return false|string
876 protected function mkTmpDir()
878 $dir = io_mktmpdir();
879 if (!$dir) return false;
880 $this->temporary[] = $dir;
881 return $dir;
885 * downloads a file from the net and saves it
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
890 * @author Andreas Gohr <andi@splitbrain.org>
891 * @author Chris Smith <chris@jalakai.co.uk>
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
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)';
906 $data = $http->get($url);
907 if ($data === false) return false;
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)) {
916 $name = \dokuwiki\Utf8\PhpString::basename($match[1]);
921 if (!$name) {
922 if (!$defaultName) return false;
923 $name = $defaultName;
926 $file = $file.$name;
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;
938 * Download an archive to a protected path
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
944 public function download($url)
946 // check the url
947 if (!preg_match('/https?:\/\//i', $url)) {
948 throw new Exception($this->getLang('error_badurl'));
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);
959 // create tmp directory for download
960 if (!($tmp = $this->mkTmpDir())) {
961 throw new Exception($this->getLang('error_dircreate'));
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>'));
970 return $tmp.'/'.$file;
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
980 public function installArchive($file, $overwrite = false, $base = '')
982 $installed_extensions = array();
984 // create tmp directory for decompression
985 if (!($tmp = $this->mkTmpDir())) {
986 throw new Exception($this->getLang('error_dircreate'));
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'));
994 // decompress
995 $this->decompress($file, "$tmp/".$base);
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'));
1005 // choose correct result array
1006 if (count($result['new'])) {
1007 $install = $result['new'];
1008 } else {
1009 $install = $result['old'];
1012 if (!count($install)) {
1013 throw new Exception($this->getLang('error_findfolder'));
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;
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']);
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;
1043 $action = file_exists($target) ? 'update' : 'install';
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;
1052 $installed_extensions[$id] = array(
1053 'base' => $item['base'],
1054 'type' => $item['type'],
1055 'action' => $action
1057 } else {
1058 throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF, '<bdi>'.$item['base'].'</bdi>'));
1062 // cleanup
1063 if ($tmp) io_rmdir($tmp, true);
1065 return $installed_extensions;
1069 * Find out what was in the extracted directory
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.
1076 * For backwards compatibility all found top level folders are stored as
1077 * in the 'old' key of the $result array.
1079 * When no items are found in 'new' the copy mechanism should fall back
1080 * the 'old' list.
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
1089 protected function findFolders(&$result, $directory, $default_type = 'plugin', $subdir = '')
1091 $this_dir = "$directory$subdir";
1092 $dh = @opendir($this_dir);
1093 if (!$dh) return false;
1095 $found_dirs = array();
1096 $found_files = 0;
1097 $found_template_parts = 0;
1098 while (false !== ($f = readdir($dh))) {
1099 if ($f == '.' || $f == '..') continue;
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;
1119 case 'main.php':
1120 case 'details.php':
1121 case 'mediamanager.php':
1122 case 'style.ini':
1123 $found_template_parts++;
1124 break;
1128 closedir($dh);
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;
1141 $result['old'][] = $info;
1142 return true;
1145 // we have no files yet -> recurse
1146 foreach ($found_dirs as $found_dir) {
1147 $this->findFolders($result, $directory, $default_type, "$found_dir");
1149 return true;
1153 * Decompress a given file to the given target directory
1155 * Determines the compression type from the file extension
1157 * @param string $file archive to extract
1158 * @param string $target directory to extract to
1159 * @throws Exception
1160 * @return bool
1162 private function decompress($file, $target)
1164 // decompression library doesn't like target folders ending in "/"
1165 if (substr($target, -1) == "/") $target = substr($target, 0, -1);
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());
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());
1187 return true;
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');
1195 * Determine the archive type of the given file
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.
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"
1204 private function guessArchiveType($file)
1206 $fh = fopen($file, 'rb');
1207 if (!$fh) return false;
1208 $magic = fread($fh, 5);
1209 fclose($fh);
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';
1218 * Copy with recursive sub-directory support
1220 * @param string $src filename path to file
1221 * @param string $dst filename path to file
1222 * @return bool|int|string
1224 private function dircopy($src, $dst)
1226 global $conf;
1228 if (is_dir($src)) {
1229 if (!$dh = @opendir($src)) return false;
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");
1238 closedir($dh);
1239 return $ok;
1240 } else {
1241 $exists = file_exists($dst);
1243 if (!@copy($src, $dst)) return false;
1244 if (!$exists && !empty($conf['fperm'])) chmod($dst, $conf['fperm']);
1245 @touch($dst, filemtime($src));
1248 return true;
1252 * Delete outdated files from updated plugins
1254 * @param array $installed
1256 private function removeDeletedfiles($installed)
1258 foreach ($installed as $id => $extension) {
1259 // only on update
1260 if ($extension['action'] == 'install') continue;
1262 // get definition file
1263 if ($extension['type'] == 'template') {
1264 $extensiondir = $this->tpllib;
1265 } else {
1266 $extensiondir = DOKU_PLUGIN;
1268 $extensiondir = $extensiondir . $extension['base'] .'/';
1269 $definitionfile = $extensiondir . 'deleted.files';
1270 if (!file_exists($definitionfile)) continue;
1272 // delete the old files
1273 $list = file($definitionfile);
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;
1281 io_rmdir($file, true);
1287 // vim:ts=4:sw=4:et: