diff options
author | Andreas Gohr <andi@splitbrain.org> | 2013-08-04 09:57:50 +0200 |
---|---|---|
committer | Andreas Gohr <andi@splitbrain.org> | 2013-08-04 09:57:50 +0200 |
commit | 3ab981498716c293a96d63c2e1c94b9694b4c581 (patch) | |
tree | 5353c1d4598353cca5ce0784befee2653af81d83 | |
parent | 08d5d52a34fa7f972f3a7da214f796ad72a9d944 (diff) | |
parent | 02779b18797b2ee1304613de684d54988815dacb (diff) | |
download | rpg-3ab981498716c293a96d63c2e1c94b9694b4c581.tar.gz rpg-3ab981498716c293a96d63c2e1c94b9694b4c581.tar.bz2 |
Merge branch 'extension_manager' of github.com:splitbrain/dokuwiki into extension_manager
* 'extension_manager' of github.com:splitbrain/dokuwiki:
Extension manager: Implement extension table
Extension manager: implement uninstall, canModify and install/update
Extension manager: Improve update check
Extension manager: Use getInfo() when no info.txt is available
Extension manager: fix install dir for templates
Extension manager: add language file and simple admin component
Extension manager: implemented more extension info and basic repository access
Extension manager: First draft of the extension class
18 files changed, 2221 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore index e83fed24f..64816ba38 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ !/lib/plugins/authplain !/lib/plugins/acl !/lib/plugins/config +!/lib/plugins/extension !/lib/plugins/info !/lib/plugins/plugin !/lib/plugins/popularity diff --git a/lib/plugins/extension/README b/lib/plugins/extension/README new file mode 100644 index 000000000..5eefe924d --- /dev/null +++ b/lib/plugins/extension/README @@ -0,0 +1,27 @@ +extension Plugin for DokuWiki + +Extension manager + +All documentation for this plugin can be found at +https://www.dokuwiki.org/plugin:extension + +If you install this plugin manually, make sure it is installed in +lib/plugins/extension/ - if the folder is called different it +will not work! + +Please refer to http://www.dokuwiki.org/plugins for additional info +on how to install plugins in DokuWiki. + +---- +Copyright (C) Michael Hamann <michael@content-space.de> + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; version 2 of the License + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +See the COPYING file in your DokuWiki folder for details diff --git a/lib/plugins/extension/admin.php b/lib/plugins/extension/admin.php new file mode 100644 index 000000000..373f90183 --- /dev/null +++ b/lib/plugins/extension/admin.php @@ -0,0 +1,124 @@ +<?php +/** + * DokuWiki Plugin extension (Admin Component) + * + * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html + * @author Michael Hamann <michael@content-space.de> + */ + +// must be run within Dokuwiki +if(!defined('DOKU_INC')) die(); + +/** + * Admin part of the extension manager + */ +class admin_plugin_extension extends DokuWiki_Admin_Plugin { + protected $infoFor = null; + + /** + * @return int sort number in admin menu + */ + public function getMenuSort() { + return 0; + } + + /** + * @return bool true if only access for superuser, false is for superusers and moderators + */ + public function forAdminOnly() { + return true; + } + + /** + * Execute the requested action(s) and initialize the plugin repository + */ + public function handle() { + global $INPUT; + // initialize the remote repository + /* @var helper_plugin_extension_repository $repository */ + $repository = $this->loadHelper('extension_repository'); + $repository->init(); + + /* @var helper_plugin_extension_extension $extension */ + $extension = $this->loadHelper('extension_extension'); + + if ($INPUT->post->has('fn')) { + $actions = $INPUT->post->arr('fn'); + foreach ($actions as $action => $extensions) { + foreach ($extensions as $extname => $label) { + switch ($action) { + case 'info': + $this->infoFor = $extname; + break; + case 'install': + msg('Not implemented'); + break; + case 'reinstall': + case 'update': + $extension->setExtension($extname, false); + $status = $extension->installOrUpdate(); + if ($status !== true) { + msg($status, -1); + } else { + msg(sprintf($this->getLang('msg_update_success'), hsc($extension->getName())), 1); + } + break; + case 'uninstall': + $extension->setExtension($extname, false); + $status = $extension->uninstall(); + if ($status !== true) { + msg($status, -1); + } else { + msg(sprintf($this->getLang('msg_delete_success'), hsc($extension->getName())), 1); + } + break; + case 'enable'; + $extension->setExtension($extname, false); + $status = $extension->enable(); + if ($status !== true) { + msg($status, -1); + } else { + msg(sprintf($this->getLang('msg_enabled'), hsc($extension->getName())), 1); + } + break; + case 'disable'; + $extension->setExtension($extname, false); + $status = $extension->disable(); + if ($status !== true) { + msg($status, -1); + } else { + msg(sprintf($this->getLang('msg_disabled'), hsc($extension->getName())), 1); + } + break; + } + } + } + } + } + + /** + * Render HTML output + */ + public function html() { + /* @var Doku_Plugin_Controller $plugin_controller */ + global $plugin_controller; + ptln('<h1>'.$this->getLang('menu').'</h1>'); + ptln('<div id="extension__manager">'); + + $pluginlist = $plugin_controller->getList('', true); + /* @var helper_plugin_extension_extension $extension */ + $extension = $this->loadHelper('extension_extension'); + /* @var helper_plugin_extension_list $list */ + $list = $this->loadHelper('extension_list'); + $list->start_form(); + foreach ($pluginlist as $name) { + $extension->setExtension($name, false); + $list->add_row($extension, $name == $this->infoFor); + } + $list->end_form(); + $list->render(); + ptln('</div>'); + } +} + +// vim:ts=4:sw=4:et:
\ No newline at end of file diff --git a/lib/plugins/extension/helper/extension.php b/lib/plugins/extension/helper/extension.php new file mode 100644 index 000000000..093ee7828 --- /dev/null +++ b/lib/plugins/extension/helper/extension.php @@ -0,0 +1,980 @@ +<?php +/** + * DokuWiki Plugin extension (Helper Component) + * + * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html + * @author Michael Hamann <michael@content-space.de> + */ + +// must be run within Dokuwiki +if(!defined('DOKU_INC')) die(); +if(!defined('DOKU_TPLLIB')) define('DOKU_TPLLIB', DOKU_INC.'lib/tpl/'); + +/** + * Class helper_plugin_extension_extension represents a single extension (plugin or template) + */ +class helper_plugin_extension_extension extends DokuWiki_Plugin { + private $name; + private $is_template; + private $localInfo; + private $remoteInfo; + private $managerData; + /** @var helper_plugin_extension_repository $repository */ + private $repository = null; + + /** + * @return bool false, this component is not a singleton + */ + public function isSingleton() { + return false; + } + + /** + * Set the name of the extension this instance shall represents, triggers loading the local and remote data + * + * @param string $name The base name of the extension + * @param bool $is_template If the extension is a template + * @return bool If some (local or remote) data was found + */ + public function setExtension($name, $is_template) { + $this->name = $name; + $this->is_template = $is_template; + $this->localInfo = array(); + $this->managerData = array(); + $this->remoteInfo = array(); + + if ($this->isInstalled()) { + $this->readLocalData(); + $this->readManagerData(); + } + + if ($this->repository == null) { + $this->repository = $this->loadHelper('extension_repository'); + } + + $this->remoteInfo = $this->repository->getData(($this->isTemplate() ? 'template:' : '').$this->getBase()); + } + + /** + * If the extension is installed locally + * + * @return bool If the extension is installed locally + */ + public function isInstalled() { + return is_dir($this->getInstallDir()); + } + + /** + * If the extension is bundled + * + * @return bool If the extension is bundled + */ + public function isBundled() { + if (!empty($this->remoteInfo['bundled'])) return $this->remoteInfo['bundled']; + return in_array($this->name, + array('acl', 'info', 'extension', 'test', 'revert', 'popularity', 'config', 'plugin', 'safefnrecode', 'authplain')); + } + + /** + * If the extension is protected + * + * @return bool if the extension is protected + */ + public function isProtected() { + return in_array($this->name, array('acl', 'config', 'info', 'plugin', 'revert', 'usermanager')); + } + + /** + * If the extension is installed in the correct directory + * + * @return bool If the extension is installed in the correct directory + */ + public function isInWrongFolder() { + return $this->name != $this->getBase(); + } + + /** + * If the extension is enabled + * + * @return bool If the extension is enabled + */ + public function isEnabled() { + /* @var Doku_Plugin_Controller $plugin_controller */ + global $plugin_controller; + return !$plugin_controller->isdisabled($this->name); + } + + /** + * If the extension should be updated, i.e. if an updated version is available + * + * @return bool If an update is available + */ + public function updateAvailable() { + $lastupdate = $this->getLastUpdate(); + if ($lastupdate === false) return false; + $installed = $this->getInstalledVersion(); + if ($installed === false || $installed === $this->getLang('unknownversion')) return true; + return $this->getInstalledVersion() < $this->getLastUpdate(); + } + + /** + * If the extension is a template + * + * @return bool If this extension is a template + */ + public function isTemplate() { + return $this->is_template; + } + + /** + * Get the name of the installation directory + * + * @return string The name of the installation directory + */ + public function getInstallName() { + return $this->name; + } + + // Data from plugin.info.txt/template.info.txt or the repo when not available locally + /** + * Get the basename of the extension + * + * @return string The basename + */ + public function getBase() { + if (!empty($this->localInfo['base'])) return $this->localInfo['base']; + return $this->name; + } + + /** + * Get the display name of the extension + * + * @return string The display name + */ + public function getName() { + if (!empty($this->localInfo['name'])) return $this->localInfo['name']; + if (!empty($this->remoteInfo['name'])) return $this->remoteInfo['name']; + return $this->name; + } + + /** + * Get the author name of the extension + * + * @return string|bool The name of the author or false if there is none + */ + public function getAuthor() { + if (!empty($this->localInfo['author'])) return $this->localInfo['author']; + if (!empty($this->remoteInfo['author'])) return $this->remoteInfo['author']; + return false; + } + + /** + * Get the email of the author of the extension if there is any + * + * @return string|bool The email address or false if there is none + */ + public function getEmail() { + // email is only in the local data + if (!empty($this->localInfo['email'])) return $this->localInfo['email']; + return false; + } + + /** + * Get the email id, i.e. the md5sum of the email + * + * @return string|bool The md5sum of the email if there is any, false otherwise + */ + public function getEmailID() { + if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid']; + if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']); + return false; + } + + /** + * Get the description of the extension + * + * @return string The description + */ + public function getDescription() { + if (!empty($this->localInfo['desc'])) return $this->localInfo['desc']; + if (!empty($this->remoteInfo['description'])) return $this->remoteInfo['description']; + return ''; + } + + /** + * Get the URL of the extension, usually a page on dokuwiki.org + * + * @return string The URL + */ + public function getURL() { + if (!empty($this->localInfo['url'])) return $this->localInfo['url']; + return 'https://www.dokuwiki.org/'.($this->isTemplate() ? 'template' : 'plugin').':'.$this->getBase(); + } + + /** + * Get the installed version of the extension + * + * @return string|bool The version, usually in the form yyyy-mm-dd if there is any + */ + public function getInstalledVersion() { + if (!empty($this->localInfo['date'])) return $this->localInfo['date']; + if ($this->isInstalled()) return $this->getLang('unknownversion'); + return false; + } + + /** + * Get the install date of the current version + * + * @return string|bool The date of the last update or false if not available + */ + public function getUpdateDate() { + if (!empty($this->managerData['updated'])) return $this->managerData['updated']; + return false; + } + + /** + * Get the date of the installation of the plugin + * + * @return string|bool The date of the installation or false if not available + */ + public function getInstallDate() { + if (!empty($this->managerData['installed'])) return $this->managerData['installed']; + return false; + } + + /** + * Get the names of the dependencies of this extension + * + * @return array The base names of the dependencies + */ + public function getDependencies() { + if (!empty($this->remoteInfo['dependencies'])) return $this->remoteInfo['dependencies']; + return array(); + } + + /** + * Get the names of the missing dependencies + * + * @return array The base names of the missing dependencies + */ + public function getMissingDependencies() { + /* @var Doku_Plugin_Controller $plugin_controller */ + global $plugin_controller; + $dependencies = $this->getDependencies(); + $missing_dependencies = array(); + foreach ($dependencies as $dependency) { + if ($plugin_controller->isdisabled($dependency)) { + $missing_dependencies[] = $dependency; + } + } + return $missing_dependencies; + } + + /** + * Get the names of all conflicting extensions + * + * @return array The names of the conflicting extensions + */ + public function getConflicts() { + if (!empty($this->remoteInfo['conflicts'])) return $this->remoteInfo['dependencies']; + return array(); + } + + /** + * Get the names of similar extensions + * + * @return array The names of similar extensions + */ + public function getSimilarExtensions() { + if (!empty($this->remoteInfo['similar'])) return $this->remoteInfo['similar']; + return array(); + } + + /** + * Get the names of the tags of the extension + * + * @return array The names of the tags of the extension + */ + public function getTags() { + if (!empty($this->remoteInfo['tags'])) return $this->remoteInfo['tags']; + return array(); + } + + /** + * Get the popularity information as floating point number [0,1] + * + * @return float|bool The popularity information or false if it isn't available + */ + public function getPopularity() { + if (!empty($this->remoteInfo['popularity'])) return $this->remoteInfo['popularity']/200.0; + return false; + } + + + /** + * Get the text of the security warning if there is any + * + * @return string|bool The security warning if there is any, false otherwise + */ + public function getSecurityWarning() { + if (!empty($this->remoteInfo['securitywarning'])) return $this->remoteInfo['securitywarning']; + return false; + } + + /** + * Get the text of the security issue if there is any + * + * @return string|bool The security issue if there is any, false otherwise + */ + public function getSecurityIssue() { + if (!empty($this->remoteInfo['securityissue'])) return $this->remoteInfo['securityissue']; + return false; + } + + /** + * Get the URL of the screenshot of the extension if there is any + * + * @return string|bool The screenshot URL if there is any, false otherwise + */ + public function getScreenshotURL() { + if (!empty($this->remoteInfo['screenshoturl'])) return $this->remoteInfo['screenshoturl']; + return false; + } + + /** + * Get the URL of the thumbnail of the extension if there is any + * + * @return string|bool The thumbnail URL if there is any, false otherwise + */ + public function getThumbnailURL() { + if (!empty($this->remoteInfo['thumbnailurl'])) return $this->remoteInfo['thumbnailurl']; + return false; + } + /** + * Get the last used download URL of the extension if there is any + * + * @return string|bool The previously used download URL, false if the extension has been installed manually + */ + public function getLastDownloadURL() { + if (!empty($this->managerData['downloadurl'])) return $this->managerData['downloadurl']; + return false; + } + + /** + * Get the download URL of the extension if there is any + * + * @return string|bool The download URL if there is any, false otherwise + */ + public function getDownloadURL() { + if (!empty($this->remoteInfo['downloadurl'])) return $this->remoteInfo['downloadurl']; + return false; + } + + /** + * If the download URL has changed since the last download + * + * @return bool If the download URL has changed + */ + public function hasDownloadURLChanged() { + $lasturl = $this->getLastDownloadURL(); + $currenturl = $this->getDownloadURL(); + return ($lasturl && $currenturl && $lasturl != $currenturl); + } + + /** + * Get the bug tracker URL of the extension if there is any + * + * @return string|bool The bug tracker URL if there is any, false otherwise + */ + public function getBugtrackerURL() { + if (!empty($this->remoteInfo['bugtracker'])) return $this->remoteInfo['bugtracker']; + return false; + } + + /** + * Get the URL of the source repository if there is any + * + * @return string|bool The URL of the source repository if there is any, false otherwise + */ + public function getSourcerepoURL() { + if (!empty($this->remoteInfo['sourcerepo'])) return $this->remoteInfo['sourcerepo']; + return false; + } + + /** + * Get the donation URL of the extension if there is any + * + * @return string|bool The donation URL if there is any, false otherwise + */ + public function getDonationURL() { + if (!empty($this->remoteInfo['donationurl'])) return $this->remoteInfo['donationurl']; + return false; + } + + /** + * Get the extension type(s) + * + * @return array The type(s) as array of strings + */ + public function getTypes() { + if (!empty($this->remoteInfo['types'])) return $this->remoteInfo['types']; + if ($this->isTemplate()) return array(32 => 'template'); + return array(); + } + + /** + * Get a list of all DokuWiki versions this extension is compatible with + * + * @return array The versions in the form yyyy-mm-dd => ('label' => label, 'implicit' => implicit) + */ + public function getCompatibleVersions() { + if (!empty($this->remoteInfo['compatible'])) return $this->remoteInfo['compatible']; + return array(); + } + + /** + * Get the date of the last available update + * + * @return string|bool The last available update in the form yyyy-mm-dd if there is any, false otherwise + */ + public function getLastUpdate() { + if (!empty($this->remoteInfo['lastupdate'])) return $this->remoteInfo['lastupdate']; + return false; + } + + /** + * Get the base path of the extension + * + * @return string The base path of the extension + */ + public function getInstallDir() { + if ($this->isTemplate()) { + return DOKU_TPLLIB.$this->name; + } else { + return DOKU_PLUGIN.$this->name; + } + } + + /** + * The type of extension installation + * + * @return string One of "none", "manual", "git" or "automatic" + */ + public function getInstallType() { + if (!$this->isInstalled()) return 'none'; + if (!empty($this->managerData)) return 'automatic'; + if (is_dir($this->getInstallDir().'/.git')) return 'git'; + return 'manual'; + } + + /** + * If the extension can probably be installed/updated or uninstalled + * + * @return bool|string True or one of "nourl", "noparentperms" (template/plugin install path not writable), "noperms" (extension itself not writable) + */ + public function canModify() { + if ($this->isInstalled()) { + if (!is_writable($this->getInstallDir())) { + return 'noperms'; + } + } + $parent_path = ($this->isTemplate() ? DOKU_TPLLIB : DOKU_PLUGIN); + if (!is_writable($parent_path)) { + return 'noparentperms'; + } + + if (!$this->getDownloadURL()) return 'nourl'; + return true; + } + + /** + * Install or update the extension + * + * @return bool|string True or an error message + */ + public function installOrUpdate() { + if (($status = $this->download($this->getDownloadURL(), $path)) === true) { + if (($status = $this->installArchive($path, $installed_extensions, $this->isInstalled(), $this->getBase())) == true) { + // refresh extension information + if (!isset($installed_extensions[$this->getBase()])) { + $status = 'Error, the requested extension hasn\'t been installed or updated'; + } + $this->setExtension($this->name, $this->isTemplate()); + $this->purgeCache(); + } + $this->dir_delete(dirname($path)); + } + return $status; + } + + /** + * Uninstall the extension + * + * @return bool If the plugin was sucessfully installed + */ + public function uninstall() { + $this->purgeCache(); + return $this->dir_delete($this->getInstallDir()); + } + + /** + * Enable the extension + * + * @return bool|string True or an error message + */ + public function enable() { + if ($this->isTemplate()) return $this->getLang('notimplemented'); + /* @var Doku_Plugin_Controller $plugin_controller */ + global $plugin_controller; + if (!$this->isInstalled()) return $this->getLang('notinstalled'); + if ($this->isEnabled()) return $this->getLang('alreadyenabled'); + if ($plugin_controller->enable($this->name)) { + $this->purgeCache(); + return true; + } else { + return $this->getLang('pluginlistsaveerror'); + } + } + + /** + * Disable the extension + * + * @return bool|string True or an error message + */ + public function disable() { + if ($this->isTemplate()) return $this->getLang('notimplemented'); + + /* @var Doku_Plugin_Controller $plugin_controller */ + global $plugin_controller; + if (!$this->isInstalled()) return $this->getLang('notinstalled'); + if (!$this->isEnabled()) return $this->getLang('alreadydisabled'); + if ($plugin_controller->disable($this->name)) { + $this->purgeCache(); + return true; + } else { + return $this->getLang('pluginlistsaveerror'); + } + } + + /** + * Purge the cache by touching the main configuration file + */ + protected function purgeCache() { + global $config_cascade; + + // expire dokuwiki caches + // touching local.php expires wiki page, JS and CSS caches + @touch(reset($config_cascade['main']['local'])); + } + + /** + * Read local extension data either from info.txt or getInfo() + */ + protected function readLocalData() { + if ($this->isTemplate()) { + $infopath = $this->getInstallDir().'/template.info.txt'; + } else { + $infopath = $this->getInstallDir().'/plugin.info.txt'; + } + + if (is_readable($infopath)) { + $this->localInfo = confToHash($infopath); + } elseif (!$this->isTemplate() && $this->isEnabled()) { + global $plugin_types; + $path = $this->getInstallDir().'/'; + $plugin = null; + + foreach($plugin_types as $type) { + if(@file_exists($path.$type.'.php')) { + $plugin = plugin_load($type, $this->name); + if ($plugin) break; + } + + if($dh = @opendir($path.$type.'/')) { + while(false !== ($cp = readdir($dh))) { + if($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue; + + $plugin = plugin_load($type, $this->name.'_'.substr($cp, 0, -4)); + if ($plugin) break; + } + if ($plugin) break; + closedir($dh); + } + } + + if ($plugin) { + /* @var DokuWiki_Plugin $plugin */ + $this->localInfo = $plugin->getInfo(); + } + } + } + + /** + * Read the manager.dat file + */ + protected function readManagerData() { + $managerpath = $this->getInstallDir().'/manager.dat'; + if (is_readable($managerpath)) { + $file = @file($managerpath); + if(!empty($file)) { + foreach($file as $line) { + list($key, $value) = explode('=', trim($line, PHP_EOL), 2); + $key = trim($key); + $value = trim($value); + // backwards compatible with old plugin manager + if($key == 'url') $key = 'downloadurl'; + $this->managerData[$key] = $value; + } + } + } + } + + /** + * Write the manager.data file + */ + protected function writeManagerData() { + $managerpath = $this->getInstallDir().'/manager.dat'; + $data = ''; + foreach ($this->managerData as $k => $v) { + $data .= $k.'='.$v.DOKU_LF; + } + io_saveFile($managerpath, $data); + } + + /** + * delete, with recursive sub-directory support + * + * @param string $path The path that shall be deleted + * @return bool If the directory has been successfully deleted + */ + protected function dir_delete($path) { + if(!is_string($path) || $path == "") return false; + + if(is_dir($path) && !is_link($path)) { + if(!$dh = @opendir($path)) return false; + + while ($f = readdir($dh)) { + if($f == '..' || $f == '.') continue; + $this->dir_delete("$path/$f"); + } + + closedir($dh); + return @rmdir($path); + } else { + return @unlink($path); + } + } + + /** + * Download an archive to a protected path + * @param string $url The url to get the archive from + * @param string $path The path where the archive was saved (output parameter) + * @return bool|string True on success, an error message on failure + */ + public function download($url, &$path) { + // check the url + $matches = array(); + if(!preg_match("/[^\/]*$/", $url, $matches) || !$matches[0]) { + return $this->getLang('baddownloadurl'); + } + $file = $matches[0]; + + // create tmp directory for download + if(!($tmp = io_mktmpdir())) { + return $this->getLang('error_dircreate'); + } + + // download + if(!$file = io_download($url, $tmp.'/', true, $file)) { + $this->dir_delete($tmp); + return sprintf($this->getLang('error_download'), $url); + } + + $path = $tmp.'/'.$file; + + return true; + } + + /** + * @param string $file The path to the archive that shall be installed + * @param bool $overwrite If an already installed plugin should be overwritten + * @param array $installed_extensions Array of all installed extensions in the form $base => ('type' => $type, 'action' => 'update'|'install') + * @param string $base The basename of the plugin if it's known + * @return bool|string True on success, an error message on failure + */ + public function installArchive($file, &$installed_extensions, $overwrite=false, $base = '') { + $error = false; + + // create tmp directory for decompression + if(!($tmp = io_mktmpdir())) { + return $this->getLang('error_dircreate'); + } + + // add default base folder if specified to handle case where zip doesn't contain this + if($base && !@mkdir($tmp.'/'.$base)) { + $error = $this->getLang('error_dircreate'); + } + + if(!$error && !$this->decompress("$tmp/$file", "$tmp/".$base)) { + $error = sprintf($this->getLang('error_decompress'), $file); + } + + // search $tmp/$base for the folder(s) that has been created + // move the folder(s) to lib/.. + if(!$error) { + $result = array('old'=>array(), 'new'=>array()); + + if(!$this->find_folders($result, $tmp.'/'.$base, ($this->isTemplate() ? 'template' : 'plugin'))) { + $error = $this->getLang('error_findfolder'); + + } else { + // choose correct result array + if(count($result['new'])) { + $install = $result['new']; + }else{ + $install = $result['old']; + } + + // now install all found items + foreach($install as $item) { + // where to install? + if($item['type'] == 'template') { + $target_base_dir = DOKU_TPLLIB; + }else{ + $target_base_dir = DOKU_PLUGIN; + } + + if(!empty($item['base'])) { + // use base set in info.txt + } elseif($base && count($install) == 1) { + $item['base'] = $base; + } else { + // default - use directory as found in zip + // plugins from github/master without *.info.txt will install in wrong folder + // but using $info->id will make 'code3' fail (which should install in lib/code/..) + $item['base'] = basename($item['tmp']); + } + + // check to make sure we aren't overwriting anything + $target = $target_base_dir.$item['base']; + if(!$overwrite && @file_exists($target)) { + // TODO remember our settings, ask the user to confirm overwrite + continue; + } + + $action = @file_exists($target) ? 'update' : 'install'; + + // copy action + if($this->dircopy($item['tmp'], $target)) { + // TODO: write manager.dat! + $installed_extensions[$item['base']] = array('type' => $item['type'], 'action' => $action); + } else { + $error = sprintf($this->getLang('error_copy').DOKU_LF, $item['base']); + break; + } + } + } + } + + // cleanup + if($tmp) $this->dir_delete($tmp); + + if($error) { + return $error; + } + + return true; + } + + /** + * Find out what was in the extracted directory + * + * Correct folders are searched recursively using the "*.info.txt" configs + * as indicator for a root folder. When such a file is found, it's base + * setting is used (when set). All folders found by this method are stored + * in the 'new' key of the $result array. + * + * For backwards compatibility all found top level folders are stored as + * in the 'old' key of the $result array. + * + * When no items are found in 'new' the copy mechanism should fall back + * the 'old' list. + * + * @author Andreas Gohr <andi@splitbrain.org> + * @param array $result - results are stored here + * @param string $base - the temp directory where the package was unpacked to + * @param string $default_type - type used if no info.txt available + * @param string $dir - a subdirectory. do not set. used by recursion + * @return bool - false on error + */ + private function find_folders(&$result, $base, $default_type, $dir='') { + $this_dir = "$base$dir"; + $dh = @opendir($this_dir); + if(!$dh) return false; + + $found_dirs = array(); + $found_files = 0; + $found_template_parts = 0; + $found_info_txt = false; + while (false !== ($f = readdir($dh))) { + if($f == '.' || $f == '..') continue; + + if(is_dir("$this_dir/$f")) { + $found_dirs[] = "$dir/$f"; + + } else { + // it's a file -> check for config + $found_files++; + switch ($f) { + case 'plugin.info.txt': + case 'template.info.txt': + $found_info_txt = true; + $info = array(); + $type = explode('.', $f, 2); + $info['type'] = $type[0]; + $info['tmp'] = $this_dir; + $conf = confToHash("$this_dir/$f"); + $info['base'] = basename($conf['base']); + $result['new'][] = $info; + break; + + case 'main.php': + case 'details.php': + case 'mediamanager.php': + case 'style.ini': + $found_template_parts++; + break; + } + } + } + closedir($dh); + + // URL downloads default to 'plugin', try extra hard to indentify templates + if(!$default_type && $found_template_parts > 2 && !$found_info_txt) { + $info = array(); + $info['type'] = 'template'; + $info['tmp'] = $this_dir; + $result['new'][] = $info; + } + + // files in top level but no info.txt, assume this is zip missing a base directory + // works for all downloads unless direct URL where $base will be the tmp directory ($info->id was empty) + if(!$dir && $found_files > 0 && !$found_info_txt && $default_type) { + $info = array(); + $info['type'] = $default_type; + $info['tmp'] = $base; + $result['old'][] = $info; + return true; + } + + foreach ($found_dirs as $found_dir) { + // if top level add to dir list for old method, then recurse + if(!$dir) { + $info = array(); + $info['type'] = ($default_type ? $default_type : 'plugin'); + $info['tmp'] = "$base$found_dir"; + $result['old'][] = $info; + } + $this->find_folders($result, $base, $default_type, "$found_dir"); + } + return true; + } + + + /** + * Decompress a given file to the given target directory + * + * Determines the compression type from the file extension + */ + private function decompress($file, $target) { + // decompression library doesn't like target folders ending in "/" + if(substr($target, -1) == "/") $target = substr($target, 0, -1); + + $ext = $this->guess_archive($file); + if(in_array($ext, array('tar', 'bz', 'gz'))) { + switch($ext) { + case 'bz': + $compress_type = Tar::COMPRESS_BZIP; + break; + case 'gz': + $compress_type = Tar::COMPRESS_GZIP; + break; + default: + $compress_type = Tar::COMPRESS_NONE; + } + + $tar = new Tar(); + try { + $tar->open($file, $compress_type); + $tar->extract($target); + } catch (Exception $e) { + return $e->getMessage(); + } + + return true; + } elseif($ext == 'zip') { + + $zip = new ZipLib(); + $ok = $zip->Extract($file, $target); + + return ($ok==-1 ? 'Error extracting the zip archive' : true); + } + + // the only case when we don't get one of the recognized archive types is when the archive file can't be read + return 'Couldn\'t read archive file'; + } + + /** + * Determine the archive type of the given file + * + * Reads the first magic bytes of the given file for content type guessing, + * if neither bz, gz or zip are recognized, tar is assumed. + * + * @author Andreas Gohr <andi@splitbrain.org> + * @param string $file The file to analyze + * @return string|bool false if the file can't be read, otherwise an "extension" + */ + private function guess_archive($file) { + $fh = fopen($file, 'rb'); + if(!$fh) return false; + $magic = fread($fh, 5); + fclose($fh); + + if(strpos($magic, "\x42\x5a") === 0) return 'bz'; + if(strpos($magic, "\x1f\x8b") === 0) return 'gz'; + if(strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip'; + return 'tar'; + } + + /** + * Copy with recursive sub-directory support + */ + private function dircopy($src, $dst) { + global $conf; + + if(is_dir($src)) { + if(!$dh = @opendir($src)) return false; + + if($ok = io_mkdir_p($dst)) { + while ($ok && (false !== ($f = readdir($dh)))) { + if($f == '..' || $f == '.') continue; + $ok = $this->dircopy("$src/$f", "$dst/$f"); + } + } + + closedir($dh); + return $ok; + + } else { + $exists = @file_exists($dst); + + if(!@copy($src, $dst)) return false; + if(!$exists && !empty($conf['fperm'])) chmod($dst, $conf['fperm']); + @touch($dst, filemtime($src)); + } + + return true; + } +} + +// vim:ts=4:sw=4:et: diff --git a/lib/plugins/extension/helper/list.php b/lib/plugins/extension/helper/list.php new file mode 100644 index 000000000..ffb88114d --- /dev/null +++ b/lib/plugins/extension/helper/list.php @@ -0,0 +1,477 @@ +<?php +/** + * DokuWiki Plugin extension (Helper Component) + * + * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html + * @author Michael Hamann <michael@content-space.de> + */ + +// must be run within Dokuwiki +if(!defined('DOKU_INC')) die(); + +/** + * Class helper_plugin_extension_extension represents a single extension (plugin or template) + */ +class helper_plugin_extension_list extends DokuWiki_Plugin { + protected $form = ''; + + function start_form() { + $this->form .= '<form id="extension__list" accept-charset="utf-8" method="post" action="">'; + $hidden = array( + 'do'=>'admin', + 'page'=>'extension', + 'sectok'=>getSecurityToken() + ); + $this->add_hidden($hidden); + $this->form .= '<ul class="extensionList">'; + } + /** + * Build single row of extension table + * @param helper_plugin_extension_extension $extension The extension that shall be added + * @param bool $showinfo Show the info area + */ + function add_row(helper_plugin_extension_extension $extension, $showinfo = false) { + $this->start_row($extension); + $this->populate_column('legend', $this->make_legend($extension, $showinfo)); + $this->populate_column('actions', $this->make_actions($extension)); + $this->end_row(); + } + + /** + * Adds a header to the form + * + * @param string $id The id of the header + * @param string $header The content of the header + * @param int $level The level of the header + */ + function add_header($id, $header, $level = 2) { + $this->form .='<h'.$level.' id="'.$id.'">'.hsc($header).'</h'.$level.'>'; + } + + /** + * Adds a paragraph to the form + * + * @param string $data The content + */ + function add_p($data) { + $this->form .= '<p>'.hsc($data).'</p>'; + } + + /** + * Add hidden fields to the form with the given data + * @param array $array + */ + function add_hidden(array $array) { + $this->form .= '<div class="no">'; + foreach ($array as $key => $value) { + $this->form .= '<input type="hidden" name="'.hsc($key).'" value="'.hsc($value).'" />'; + } + $this->form .= '</div>'; + } + + /** + * Add closing tags + */ + function end_form() { + $this->form .= '</ul>'; + $this->form .= '</form>'; + } + + /** + * Print the form + */ + function render() { + echo $this->form; + } + + /** + * Start the HTML for the row for the extension + * + * @param helper_plugin_extension_extension $extension The extension + */ + private function start_row(helper_plugin_extension_extension $extension) { + $this->form .= '<li id="extensionplugin__'.hsc($extension->getInstallName()).'" class="'.$this->make_class($extension).'">'; + } + + /** + * Add a column with the given class and content + * @param string $class The class name + * @param string $html The content + */ + private function populate_column($class, $html) { + $this->form .= '<div class="'.$class.' col">'.$html.'</div>'; + } + + /** + * End the row + */ + private function end_row() { + $this->form .= '</li>'.DOKU_LF; + } + + /** + * Generate the link to the plugin homepage + * + * @param helper_plugin_extension_extension $extension The extension + * @return string The HTML code + */ + function make_homepagelink(helper_plugin_extension_extension $extension) { + $text = $this->getLang('homepage_link'); + $url = hsc($extension->getURL()); + return '<a href="'.$url.'" title="'.$url.'" class ="urlextern">'.$text.'</a> '; + } + + /** + * Generate the class name for the row of the extensio + * + * @param helper_plugin_extension_extension $extension The extension object + * @return string The class name + */ + function make_class(helper_plugin_extension_extension $extension) { + $class = ($extension->isTemplate()) ? 'template' : 'plugin'; + if($extension->isInstalled()) { + $class.=' installed'; + $class.= ($extension->isEnabled()) ? ' enabled':' disabled'; + } + if(!$extension->canModify()) $class.= ' notselect'; + if($extension->isProtected()) $class.= ' protected'; + //if($this->showinfo) $class.= ' showinfo'; + return $class; + } + + /** + * Generate a link to the author of the extension + * + * @param helper_plugin_extension_extension $extension The extension object + * @return string The HTML code of the link + */ + function make_author(helper_plugin_extension_extension $extension) { + global $ID; + + if($extension->getAuthor()) { + + $params = array( + 'do'=>'admin', + 'page'=>'extension', + 'tab'=>'search', + 'q'=>'author:'.$extension->getAuthor() + ); + $url = wl($ID, $params); + return '<a href="'.$url.'" title="'.$this->getLang('author_hint').'" >'.hsc($extension->getAuthor()).'</a>'; + } + return "<em>".$this->getLang('unknown_author')."</em>"; + } + + /** + * Get the link and image tag for the screenshot/thumbnail + * + * @param helper_plugin_extension_extension $extension The extension object + * @return string The HTML code + */ + function make_screenshot(helper_plugin_extension_extension $extension) { + if($extension->getScreenshotURL()) { + $img = '<a title="'.hsc($extension->getName()).'" href="'.hsc($extension->getScreenshotURL()).'" target="_blank">'. + '<img alt="'.hsc($extension->getName()).'" width="120" height="70" src="'.hsc($extension->getThumbnailURL()).'" />'. + '</a>'; + } elseif($extension->isTemplate()) { + $img = '<img alt="template" width="120" height="70" src="'.DOKU_BASE.'lib/plugins/extension/images/template.png" />'; + + } else { + $img = '<img alt="plugin" width="120" height="70" src="'.DOKU_BASE.'lib/plugins/extension/images/plugin.png" />'; + } + return '<div class="screenshot" >'.$img.'<span></span></div>'; + } + + /** + * Extension main description + * + * @param helper_plugin_extension_extension $extension The extension object + * @param bool $showinfo Show the info section + * @return string The HTML code + */ + function make_legend(helper_plugin_extension_extension $extension, $showinfo = false) { + $return = '<div>'; + $return .= '<h2>'; + $return .= sprintf($this->getLang('extensionby'), hsc($extension->getName()), $this->make_author($extension)); + $return .= '</h2>'; + + $return .= $this->make_screenshot($extension); + + $popularity = $extension->getPopularity(); + if ($popularity !== false && !$extension->isBundled()) { + $popularityText = sprintf($this->getLang('popularity'), $popularity); + $return .= '<div class="popularity" title="'.$popularityText.'"><div style="width: '.($popularity * 100).'%;"><span></span></div></div>'; + } + + $return .= '<p>'; + if($extension->getDescription()) { + $return .= hsc($extension->getDescription()).' '; + } + $return .= '</p>'; + + $return .= $this->make_linkbar($extension); + $return .= $this->make_action('info', $extension, $showinfo); + if ($showinfo) { + $return .= $this->make_info($extension); + } + $return .= $this->make_noticearea($extension); + $return .= '</div>'; + return $return; + } + + /** + * Generate the link bar HTML code + * + * @param helper_plugin_extension_extension $extension The extension instance + * @return string The HTML code + */ + function make_linkbar(helper_plugin_extension_extension $extension) { + $return = '<span class="linkbar">'; + $return .= $this->make_homepagelink($extension); + if ($extension->getBugtrackerURL()) { + $return .= ' <a href="'.hsc($extension->getBugtrackerURL()).'" title="'.hsc($extension->getBugtrackerURL()).'" class ="interwiki iw_dokubug">'.$this->getLang('bugs_features').'</a> '; + } + foreach ($extension->getTags() as $tag) { + $return .= hsc($tag).' '; //$this->manager->handler->html_taglink($tag); + } + $return .= '</span>'; + return $return; + } + + /** + * Notice area + * + * @param helper_plugin_extension_extension $extension The extension + * @return string The HTML code + */ + function make_noticearea(helper_plugin_extension_extension $extension) { + $return = ''; + $missing_dependencies = $extension->getMissingDependencies(); + if(!empty($missing_dependencies)) { + $return .= '<div class="msg error">'. + sprintf($this->getLang('missing_dependency'), implode(', ', /*array_map(array($this->helper, 'make_extensionsearchlink'),*/ $missing_dependencies)). + '</div>'; + } + if($extension->isInWrongFolder()) { + $return .= '<div class="msg error">'. + sprintf($this->getLang('wrong_folder'), hsc($extension->getInstallName()), hsc($extension->getBase())). + '</div>'; + } + if(($securityissue = $extension->getSecurityIssue()) !== false) { + $return .= '<div class="msg error">'. + sprintf($this->getLang('security_issue'), hsc($securityissue )). + '</div>'; + } + if(($securitywarning = $extension->getSecurityWarning()) !== false) { + $return .= '<div class="msg notify">'. + sprintf($this->getLang('security_warning'), hsc($securitywarning)). + '</div>'; + } + if($extension->updateAvailable()) { + $return .= '<div class="msg notify">'. + sprintf($this->getLang('update_available'), hsc($extension->getLastUpdate())). + '</div>'; + } + if($extension->hasDownloadURLChanged()) { + $return .= '<div class="msg notify">'. + sprintf($this->getLang('url_change'), hsc($extension->getDownloadURL()), hsc($extension->getLastDownloadURL())). + '</div>'; + } + return $return; + } + + /** + * Create a link from the given URL + * + * Shortens the URL for display + * + * @param string $url + * + * @return string HTML link + */ + function shortlink($url){ + $link = parse_url($url); + + $base = $link['host']; + if($link['port']) $base .= $base.':'.$link['port']; + $long = $link['path']; + if($link['query']) $long .= $link['query']; + + $name = shorten($base, $long, 55); + + return '<a href="'.hsc($url).'" class="urlextern">'.hsc($name).'</a>'; + } + + /** + * Plugin/template details + * + * @param helper_plugin_extension_extension $extension The extension + * @return string The HTML code + */ + function make_info(helper_plugin_extension_extension $extension) { + $default = $this->getLang('unknown'); + $return = '<dl class="details">'; + + if (!$extension->isBundled()) { + $return .= '<dt>'.$this->getLang('downloadurl').'</dt>'; + $return .= '<dd>'; + $return .= ($extension->getDownloadURL() ? $this->shortlink($extension->getDownloadURL()) : $default); + $return .= '</dd>'; + + $return .= '<dt>'.$this->getLang('repository').'</dt>'; + $return .= '<dd>'; + $return .= ($extension->getSourcerepoURL() ? $this->shortlink($extension->getSourcerepoURL()) : $default); + $return .= '</dd>'; + } + + if ($extension->isInstalled()) { + if ($extension->getInstalledVersion()) { + $return .= '<dt>'.$this->getLang('installed_version').'</dt>'; + $return .= '<dd>'; + $return .= hsc($extension->getInstalledVersion()); + $return .= '</dd>'; + } else { + $return .= '<dt>'.$this->getLang('install_date').'</dt>'; + $return .= '<dd>'; + $return .= ($extension->getUpdateDate() ? hsc($extension->getUpdateDate()) : $this->getLang('unknown')); + $return .= '</dd>'; + } + } + if (!$extension->isInstalled() || $extension->updateAvailable()) { + $return .= '<dt>'.$this->getLang('available_version').'</dt>'; + $return .= '<dd>'; + $return .= ($extension->getLastUpdate() ? hsc($extension->getLastUpdate()) : $this->getLang('unknown')); + $return .= '</dd>'; + } + + if($extension->getInstallDate()) { + $return .= '<dt>'.$this->getLang('installed').'</dt>'; + $return .= '<dd>'; + $return .= hsc($extension->getInstallDate()); + $return .= '</dd>'; + } + + $return .= '<dt>'.$this->getLang('provides').'</dt>'; + $return .= '<dd>'; + $return .= ($extension->getTypes() ? hsc(implode(', ', $extension->getTypes())) : $default); + $return .= '</dd>'; + + if($extension->getCompatibleVersions()) { + $return .= '<dt>'.$this->getLang('compatible').'</dt>'; + $return .= '<dd>'; + foreach ($extension->getCompatibleVersions() as $date => $version) { + $return .= $version['label'].' ('.$date.'), '; + } + $return .= '</dd>'; + } + if($extension->getDependencies()) { + $return .= '<dt>'.$this->getLang('depends').'</dt>'; + $return .= '<dd>'; + $return .= $this->make_linklist($extension->getDependencies()); + $return .= '</dd>'; + } + + if($extension->getSimilarExtensions()) { + $return .= '<dt>'.$this->getLang('similar').'</dt>'; + $return .= '<dd>'; + $return .= $this->make_linklist($extension->getSimilarExtensions()); + $return .= '</dd>'; + } + + if($extension->getConflicts()) { + $return .= '<dt>'.$this->getLang('conflicts').'</dt>'; + $return .= '<dd>'; + $return .= $this->make_linklist($extension->getConflicts()); + $return .= '</dd>'; + } + if ($extension->getDonationURL()) { + $return .= '<a href="'.hsc($extension->getDonationURL()).'" class="donate" title="'.$this->getLang('donate').'"></a>'; + } + $return .= '</dl>'; + return $return; + } + + /** + * Generate a list of links for extensions + * @param array $links The links + * @return string The HTML code + */ + function make_linklist($links) { + $return = ''; + foreach ($links as $link) { + $dokulink = hsc($link); + if (strpos($link, 'template:') !== 0) $dokulink = 'plugin:'.$dokulink; + $return .= '<a href="http://www.dokuwiki.org/'.$dokulink.'" title="'.$dokulink.'" class="interwiki iw_doku">'.$link.'</a> '; + } + return $return; + } + + /** + * Display the action buttons if they are possible + * + * @param helper_plugin_extension_extension $extension The extension + * @return string The HTML code + */ + function make_actions(helper_plugin_extension_extension $extension) { + $return = ''; + if (!$extension->isInstalled() && $extension->canModify() === true) { + $return .= $this->make_action('install', $extension); + } elseif ($extension->canModify()) { + if (!$extension->isBundled()) { + $return .= $this->make_action('uninstall', $extension); + if ($extension->getDownloadURL()) { + if ($extension->updateAvailable()) { + $return .= $this->make_action('update', $extension); + } else { + $return .= $this->make_action('reinstall', $extension); + } + } + } + if (!$extension->isProtected()) { + if ($extension->isEnabled()) { + $return .= $this->make_action('disable', $extension); + } else { + $return .= $this->make_action('enable', $extension); + } + } + } + + if (!$extension->isInstalled()) { + $return .= ' <span class="version">'.$this->getLang('available_version').' '; + $return .= ($extension->getLastUpdate() ? hsc($extension->getLastUpdate()) : $this->getLang('unknown')).'</span>'; + } + + return $return; + } + + /** + * Display an action button for an extension + * + * @param string $action The action + * @param helper_plugin_extension_extension $extension The extension + * @param bool $showinfo If the info block is shown + * @return string The HTML code + */ + function make_action($action, $extension, $showinfo = false) { + $title = $revertAction = $extraClass = ''; + + switch ($action) { + case 'info': + $title = 'title="'.$this->getLang('btn_info').'"'; + if ($showinfo) { + $revertAction = '-'; + $extraClass = 'close'; + } + break; + case 'install': + case 'reinstall': + $title = 'title="'.$extension->getDownloadURL().'"'; + break; + } + + $classes = 'button '.$action.' '.$extraClass; + $name = 'fn['.$action.']['.$revertAction.hsc($extension->getInstallName()).']'; + + return '<input class="'.$classes.'" name="'.$name.'" type="submit" value="'.$this->getLang('btn_'.$action).'" '.$title.' />'; + } +} diff --git a/lib/plugins/extension/helper/repository.php b/lib/plugins/extension/helper/repository.php new file mode 100644 index 000000000..37b9bc02c --- /dev/null +++ b/lib/plugins/extension/helper/repository.php @@ -0,0 +1,112 @@ +<?php +/** + * DokuWiki Plugin extension (Helper Component) + * + * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html + * @author Michael Hamann <michael@content-space.de> + */ + +if (!defined('EXTENSION_REPOSITORY_API_ENDPOINT')) + define('EXTENSION_REPSITORY_API', 'http://www.dokuwiki.org/lib/plugins/pluginrepo/api.php'); + +// must be run within Dokuwiki +if(!defined('DOKU_INC')) die(); + +/** + * Class helper_plugin_extension_repository provides access to the extension repository on dokuwiki.org + */ +class helper_plugin_extension_repository extends DokuWiki_Plugin { + private $loaded_extensions = array(); + private $has_access = null; + /** + * Initialize the repository (cache), fetches data for all installed plugins + */ + public function init() { + /* @var Doku_Plugin_Controller $plugin_controller */ + global $plugin_controller; + if ($this->hasAccess()) { + $list = $plugin_controller->getList('', true); + $request_data = array('fmt' => 'php'); + $request_needed = false; + foreach ($list as $name) { + $cache = new cache('##extension_manager##'.$name, 'repo'); + $result = null; + if (!isset($this->loaded_extensions[$name]) && $this->hasAccess() && !$cache->useCache(array('age' => 3600 * 24))) { + $this->loaded_extensions[$name] = true; + $request_data['ext'][] = $name; + $request_needed = true; + } + } + + if ($request_needed) { + $httpclient = new DokuHTTPClient(); + $data = $httpclient->post(EXTENSION_REPSITORY_API, $request_data); + if ($data !== false) { + $extensions = unserialize($data); + foreach ($extensions as $extension) { + $cache = new cache('##extension_manager##'.$extension['plugin'], 'repo'); + $cache->storeCache(serialize($extension)); + } + } else { + $this->has_access = false; + } + } + } + } + + /** + * If repository access is available + * + * @return bool If repository access is available + */ + public function hasAccess() { + if ($this->has_access === null) { + $cache = new cache('##extension_manager###hasAccess', 'repo'); + $result = null; + if (!$cache->useCache(array('age' => 3600 * 24))) { + $httpclient = new DokuHTTPClient(); + $httpclient->timeout = 5; + $data = $httpclient->get(EXTENSION_REPSITORY_API.'?cmd=ping'); + if ($data !== false) { + $this->has_access = true; + $cache->storeCache(1); + } else { + $this->has_access = false; + $cache->storeCache(0); + } + } else { + $this->has_access = ($cache->retrieveCache(false) == 1); + } + } + return $this->has_access; + } + + /** + * Get the remote data of an individual plugin or template + * + * @param string $name The plugin name to get the data for, template names need to be prefix by 'template:' + * @return array The data or null if nothing was found (possibly no repository access) + */ + public function getData($name) { + $cache = new cache('##extension_manager##'.$name, 'repo'); + $result = null; + if (!isset($this->loaded_extensions[$name]) && $this->hasAccess() && !$cache->useCache(array('age' => 3600 * 24))) { + $this->loaded_extensions[$name] = true; + $httpclient = new DokuHTTPClient(); + $data = $httpclient->get(EXTENSION_REPSITORY_API.'?fmt=php&ext[]='.urlencode($name)); + if ($data !== false) { + $result = unserialize($data); + $cache->storeCache(serialize($result[0])); + return $result[0]; + } else { + $this->has_access = false; + } + } + if (file_exists($cache->cache)) { + return unserialize($cache->retrieveCache(false)); + } + return array(); + } +} + +// vim:ts=4:sw=4:et: diff --git a/lib/plugins/extension/images/disabled.png b/lib/plugins/extension/images/disabled.png Binary files differnew file mode 100644 index 000000000..7a0dbb3b5 --- /dev/null +++ b/lib/plugins/extension/images/disabled.png diff --git a/lib/plugins/extension/images/donate.png b/lib/plugins/extension/images/donate.png Binary files differnew file mode 100644 index 000000000..b26ceb66e --- /dev/null +++ b/lib/plugins/extension/images/donate.png diff --git a/lib/plugins/extension/images/down.png b/lib/plugins/extension/images/down.png Binary files differnew file mode 100644 index 000000000..df7beda4e --- /dev/null +++ b/lib/plugins/extension/images/down.png diff --git a/lib/plugins/extension/images/enabled.png b/lib/plugins/extension/images/enabled.png Binary files differnew file mode 100644 index 000000000..7c051cda1 --- /dev/null +++ b/lib/plugins/extension/images/enabled.png diff --git a/lib/plugins/extension/images/icons.xcf b/lib/plugins/extension/images/icons.xcf Binary files differnew file mode 100644 index 000000000..a99747d81 --- /dev/null +++ b/lib/plugins/extension/images/icons.xcf diff --git a/lib/plugins/extension/images/license.txt b/lib/plugins/extension/images/license.txt new file mode 100644 index 000000000..254b9cdf6 --- /dev/null +++ b/lib/plugins/extension/images/license.txt @@ -0,0 +1,4 @@ +enabled.png - CC-BY-ND, (c) Emey87 http://www.iconfinder.com/icondetails/65590/48/lightbulb_icon +disabled.png - CC-BY-ND, (c) Emey87 http://www.iconfinder.com/icondetails/65589/48/idea_lightbulb_off_icon +plugin.png - public domain, (c) nicubunu, http://openclipart.org/detail/15093/blue-jigsaw-piece-07-by-nicubunu +template.png - public domain, (c) mathec, http://openclipart.org/detail/166596/palette-by-mathec diff --git a/lib/plugins/extension/images/plugin.png b/lib/plugins/extension/images/plugin.png Binary files differnew file mode 100644 index 000000000..b9133681f --- /dev/null +++ b/lib/plugins/extension/images/plugin.png diff --git a/lib/plugins/extension/images/template.png b/lib/plugins/extension/images/template.png Binary files differnew file mode 100644 index 000000000..383c4d0a3 --- /dev/null +++ b/lib/plugins/extension/images/template.png diff --git a/lib/plugins/extension/images/up.png b/lib/plugins/extension/images/up.png Binary files differnew file mode 100644 index 000000000..ec9337715 --- /dev/null +++ b/lib/plugins/extension/images/up.png diff --git a/lib/plugins/extension/lang/en/lang.php b/lib/plugins/extension/lang/en/lang.php new file mode 100644 index 000000000..b3a451ecc --- /dev/null +++ b/lib/plugins/extension/lang/en/lang.php @@ -0,0 +1,121 @@ +<?php +/** + * English language file for extension plugin + * + * @author Michael Hamann <michael@content-space.de> + * @author Christopher Smith <chris@jalakai.co.uk> + */ + +// menu entry for admin plugins +$lang['menu'] = 'Extension manager'; + +// custom language strings for the plugin +$lang['notimplemented'] = 'This feature hasn\'t been implemented yet'; +$lang['alreadyenabled'] = 'This extension has already been enabled'; +$lang['alreadydisabled'] = 'This extension has already been disabled'; +$lang['pluginlistsaveerror'] = 'There was an error saving the plugin list'; +$lang['unknownauthor'] = 'Unknown author'; +$lang['unknownversion'] = 'Unknown version'; + +// extension list +$lang['btn_info'] = 'Show more info'; +$lang['btn_update'] = 'Update'; +$lang['btn_uninstall'] = 'Uninstall'; +$lang['btn_enable'] = 'Enable'; +$lang['btn_disable'] = 'Disable'; +//$lang['btn_disable_all'] = 'Disable all'; +//$lang['btn_settings'] = 'Settings'; +$lang['btn_install'] = 'Install'; +$lang['btn_reinstall'] = 'Re-install'; +//$lang['btn_disdown'] = 'Download as Disabled'; +//$lang['btn_dependown'] = 'Download with dependencies'; + +$lang['extensionby'] = '<strong>%s</strong> by %s'; +$lang['popularity'] = 'Popularity: %s'; +$lang['homepage_link'] = 'Docs'; +$lang['bugs_features'] = 'Bugs'; +$lang['author_hint'] = 'Search extensions by this author'; +$lang['tag_hint'] = 'Search extensions with this tag'; +$lang['installed'] = 'Installed:'; +$lang['lastupdate'] = 'Last updated:'; +$lang['downloadurl'] = 'Download URL:'; +$lang['repository'] = 'Repository:'; +$lang['unknown'] = '<em>unknown</em>'; +$lang['installed_version'] = 'Installed version:'; +$lang['install_date'] = 'Your last update:'; +$lang['available_version'] = 'Version:'; +$lang['compatible'] = 'Compatible with:'; +$lang['depends'] = 'Depends on:'; +$lang['similar'] = 'Similar to:'; +$lang['conflicts'] = 'Conflicts with:'; +$lang['donate'] = 'Donate'; +$lang['bundled'] = 'bundled'; +$lang['manual_install'] = 'manual install'; + +$lang['msg_tpl_uninstalled'] = 'Template %s uninstalled'; +$lang['msg_tpl_uninstalled'] = 'Template %s could not be uninstalled'; +$lang['msg_uninstalled'] = 'Plugin %s uninstalled'; +$lang['msg_uninstalled'] = 'Plugin %s could not be uninstalled'; + +$lang['msg_tpl_enabled'] = 'Template %s enabled'; +$lang['msg_tpl_notenabled'] = 'Template %s could not be enabled, check file permissions'; +$lang['msg_enabled'] = 'Plugin %s enabled'; +$lang['msg_notenabled'] = 'Plugin %s could not be enabled, check file permissions'; + +$lang['msg_disabled'] = 'Plugin %s disabled'; +$lang['msg_notdisabled'] = 'Plugin %s could not be disabled, check file permissions'; + +$lang['msg_url_failed'] = 'URL [%s] could not be downloaded.<br /> %s'; +$lang['msg_download_failed'] = 'Plugin %s could not be downloaded.<br /> %s'; +$lang['msg_download_success'] = 'Plugin %s installed successfully'; +$lang['msg_tpl_download_failed'] = 'Template %s could not be downloaded.<br /> %s'; +$lang['msg_tpl_download_success'] = 'Template %s installed successfully'; +$lang['msg_download_pkg_success'] = '%s extension package successfully installed (%s)'; +$lang['msg_tpl_download_pkg_success'] = '%s extension package successfully installed (%s)'; + +$lang['msg_update_success'] = 'Plugin %s successfully updated'; +$lang['msg_update_failed'] = 'Update of plugin %s failed.<br /> %s'; +$lang['msg_tpl_update_success'] = 'Template %s successfully updated'; +$lang['msg_tpl_update_failed'] = 'Update of template %s failed.<br /> %s'; +$lang['msg_update_pkg_success'] = '%s extension package successfully updated (%s)'; +$lang['msg_tpl_update_pkg_success'] = '%s extension package successfully updated (%s)'; + +$lang['msg_reinstall_success'] = 'Plugin %s re-installed successfully'; +$lang['msg_reinstall_failed'] = 'Failed to re-install plugin %s.<br /> %s'; +$lang['msg_tpl_reinstall_success'] = 'Template %s re-installed successfully'; +$lang['msg_tpl_reinstall_failed'] = 'Failed to re-install template %s.<br /> %s'; +$lang['msg_reinstall_pkg_success'] = '%s extension package successfully reinstalled (%s)'; +$lang['msg_tpl_reinstall_pkg_success'] = '%s extension package successfully reinstalled (%s)'; + +// info titles +$lang['plugin'] = 'Plugin'; +$lang['provides'] = 'Provides:'; +$lang['noinfo'] = 'This plugin returned no information, it may be invalid.'; +$lang['name'] = 'Name:'; +$lang['date'] = 'Date:'; +$lang['type'] = 'Type:'; +$lang['desc'] = 'Description:'; +$lang['author'] = 'Author:'; +$lang['www'] = 'Web:'; + +// error messages +$lang['needed_by'] = 'Needed by:'; +$lang['not_writable'] = 'DokuWiki can not write to the folder'; +$lang['missing_dependency'] = '<strong>Missing or disabled dependency:</strong> %s'; +$lang['security_issue'] = '<strong>Security Issue:</strong> %s'; +$lang['security_warning'] = '<strong>Security Warning:</strong> %s'; +$lang['update_available'] = '<strong>Update:</strong> New version %s is available.'; +$lang['wrong_folder'] = '<strong>Plugin installed incorrectly:</strong> Rename plugin directory "%s" to "%s".'; +$lang['url_change'] = '<strong>URL changed:</strong> Download URL has changed since last download. Check if the new URL is valid before updating the extension.<br />New: %s<br />Old: %s'; +$lang['gitmanaged'] = 'Extension installed with git'; +$lang['bundled_source'] = 'Bundled with DokuWiki source'; +$lang['no_url'] = 'No download URL'; +$lang['no_manager'] = 'Could not find manager.dat file'; + +$lang['error_badurl'] = 'URL ends with slash - unable to determine file name from the url'; +$lang['error_dircreate'] = 'Unable to create temporary folder to receive download'; +$lang['error_download'] = 'Unable to download the file: %s'; +$lang['error_decompress'] = 'Unable to decompress the downloaded file. This maybe as a result of a bad download, in which case you should try again; or the compression format may be unknown, in which case you will need to download and install manually'; +$lang['error_findfolder'] = 'Unable to identify extension directory, you need to download and install manually'; +$lang['error_copy'] = 'There was a file copy error while attempting to install files for directory <em>%s</em>: the disk could be full or file access permissions may be incorrect. This may have resulted in a partially installed plugin and leave your wiki installation unstable'; +//Setup VIM: ex: et ts=4 : diff --git a/lib/plugins/extension/plugin.info.txt b/lib/plugins/extension/plugin.info.txt new file mode 100644 index 000000000..3c4469ad7 --- /dev/null +++ b/lib/plugins/extension/plugin.info.txt @@ -0,0 +1,7 @@ +base extension +author Michael Hamann +email michael@content-space.de +date 2013-08-01 +name extension plugin +desc Extension manager +url https://www.dokuwiki.org/plugin:extension diff --git a/lib/plugins/extension/style.css b/lib/plugins/extension/style.css new file mode 100644 index 000000000..1cce52be8 --- /dev/null +++ b/lib/plugins/extension/style.css @@ -0,0 +1,368 @@ +/* + * Extension plugin styles + * + * @author Christopher Smith <chris@jalakai.co.uk> + * @author Piyush Mishra <me@piyushmishra.com> + * @author Håkan Sandell <sandell.hakan@gmail.com> + * @author Anika Henke <anika@selfthinker.org> + */ + +/* + * tab support not included in "Angua", copied from _mediamanager.css + */ +#extension__manager .panelHeader { + background-color: __background_alt__; + margin-bottom: 10px; + padding: 10px; + text-align: left; + /* to counter partly missing border of ul.tabs: */ + border-top: 1px solid __border__; + margin-top: -1px; +} + +#extension__manager .panelHeader p { + float: left; + font-weight: normal; + font-size: 1em; + padding: 0; + margin: 0 0 3px; +} +[dir=rtl] #extension__manager .panelHeader p { + float: right; +} + +#extension__manager ul.tabs div.message { + display: inline; + margin: 0 .5em; +} + +/* + * general layout + */ +#extension__manager h2 { + margin-left: 0; +} + +#extension__manager a.taglink { + padding: 1px 4px; + background-color: __background_neu__; + border-radius: 3px; +} +[dir=rtl] #extension__manager a.taglink { + display: inline-block; + line-height: 1.2; +} + +#extension__manager .panelHeader div.error { + margin-top: 0; + float: left; +} +[dir=rtl] #extension__manager .panelHeader div.error { + float: right; +} + +/* + * search & url download forms + */ +#extension__manager form.search, +#extension__manager form.btn_reload { + float: right; +} +[dir=rtl] #extension__manager form.search, +[dir=rtl] #extension__manager form.btn_reload { + float: left; +} + +#extension__manager div.search form.search { + float: none; +} + +#extension__manager .tagcloud { + width: 55%; + float: left; + margin: 0 0.5em 1em 0; +} +[dir=rtl] #extension__manager .tagcloud { + float: right; + margin: 0 0 1em .5em; +} + +#extension__manager .tagcloud a.taglink { + background-color: inherit; +} + +#extension__manager div.search { + width: 44%; + float: left; +} +[dir=rtl] #extension__manager div.search { + float: right; +} + +#extension__manager fieldset { + margin-top: 0.5em; + width: auto; +} + +#extension__manager fieldset p { + margin: 0.5em; + text-align: justify; + font-size: 85%; +} + +/* tag cloud */ +#extension__manager a.cl0 { font-size: 0.7em; } +#extension__manager a.cl1 { font-size: 0.9em; } +#extension__manager a.cl2 { font-size: 1em; } +#extension__manager a.cl3 { font-size: 1.3em; } +#extension__manager a.cl4 { font-size: 1.6em; } +#extension__manager a.cl5 { font-size: 1.9em; } + + +#extension__manager .extensionList input.button { + margin: 0 .3em .3em 0; +} +[dir=rtl] #extension__manager .extensionList input.button { + margin: 0 0 .3em .3em; +} + +/* + * extensions table + */ +#extension__manager .extensionList { + margin-left: 0; + margin-right: 0; + padding: 0; + list-style: none; +} + +#extension__manager .extensionList li { + margin: 0 0 .5em; + padding: 0 0 .5em; + color: __text__; + border-bottom: 1px solid __border__; + overflow: hidden; +} + +#extension__manager .legend { + position: relative; + width: 75%; + float: left; +} +[dir=rtl] #extension__manager .legend { + float: right; +} + +#extension__manager .legend > div { + padding: 0 .5em 0 132px; + border-right: 1px solid __background_alt__; + overflow: hidden; +} +[dir=rtl] #extension__manager .legend > div { + padding: 0 132px 0 .5em; + border-left: 1px solid __background_alt__; + border-right-width: 0; +} + +#extension__manager .enabled div.screenshot span { + background: transparent url(images/enabled.png) no-repeat 2px 2px; +} + +#extension__manager .disabled div.screenshot span { + background: transparent url(images/disabled.png) no-repeat 2px 2px; +} + +#extension__manager .legend div.screenshot { + margin-top: 4px; + margin-left: -132px; + max-width: 120px; + float: left; +} +[dir=rtl] #extension__manager .legend div.screenshot { + margin-left: 0; + margin-right: -132px; + float: right; +} + +#extension__manager .legend div.screenshot span { + min-height: 24px; + min-width: 24px; + position: absolute; + left: 0; +} +[dir=rtl] #extension__manager .legend div.screenshot span { + left: auto; + right: 0; +} + +#extension__manager .legend h2 { + width: 100%; + float: right; + margin: 0.2em 0 0.5em; + font-size: 100%; + font-weight: normal; + border: none; +} +[dir=rtl] #extension__manager .legend h2 { + float: left; +} + +#extension__manager .legend h2 strong { + font-size: 120%; + font-weight: bold; + vertical-align: baseline; +} + +#extension__manager .legend p { + margin: 0 0 0.6em 0; +} + +#extension__manager .legend span.linkbar { + font-size: 85%; +} + +#extension__manager .legend span.linkbar a.urlextern + a.taglink, +#extension__manager .legend span.linkbar a.interwiki + a.taglink { + margin-left: 0.4em; +} +[dir=rtl] #extension__manager .legend span.linkbar a.urlextern + a.taglink, +[dir=rtl] #extension__manager .legend span.linkbar a.interwiki + a.taglink { + margin-left: 0; +} +[dir=rtl] #extension__manager .legend span.linkbar a.urlextern, +[dir=rtl] #extension__manager .legend span.linkbar a.interwiki { + margin-left: .4em; +} + +#extension__manager .legend input.button { + background: transparent url(images/up.png) no-repeat 0 0; + box-shadow: none; + border-width: 0; + height: 14px; + text-indent: -99999px; + float: right; + margin: .5em 0 0; +} +[dir=rtl] #extension__manager .legend input.button { + float: left; + margin: .5em 0 0; +} + +#extension__manager .legend input.button.close { + background: transparent url(images/down.png) no-repeat 0 0; +} + +#extension__manager .legend div.popularity { + background-color: __background__; + border: 1px solid silver; + height: .4em; + margin: 0 auto; + padding: 1px; + width: 5.5em; + position: absolute; + right: .5em; + top: 0.2em; +} +[dir=rtl] #extension__manager .legend div.popularity { + right: auto; + left: .5em; +} + +#extension__manager .legend div.popularity div { + background-color: __border__; + height: 100%; +} + +#extension__manager .legend div.popularity div span { + display: none;/* @todo: hide accessibly */ +} + +#extension__manager .actions { + padding: 0; + font-size: 95%; + width: 25%; + float: right; + text-align: right; +} +[dir=rtl] #extension__manager .actions { + float: left; + text-align: left; +} + +#extension__manager .actions .version { + display: block; +} + +#extension__manager .actions p { + margin: 0.2em 0; + text-align: center; +} + +/* + * extensions table, detailed info box + */ +#extension__manager dl.details { + margin: 0.4em 0 0 0; + font-size: 85%; + border-top: 1px solid __background_alt__; + clear: both; +} + +#extension__manager dl.details dt { + clear: left; + float: left; + width: 25%; + margin: 0; + text-align: right; + font-weight: normal; + padding: 0.2em 5px 0 0; +} +[dir=rtl] #extension__manager dl.details dt { + clear: right; + float: right; + text-align: left; + padding: 0.2em 0 0 5px; +} + +#extension__manager dl.details dd { + margin-left: 25%; + font-weight: bold; + padding: 0.2em 0 0 5px; +} +[dir=rtl] #extension__manager dl.details dd { + margin-left: 0; + margin-right: 25%; + padding: 0.2em 5px 0 0 ; +} + +#extension__manager dl.details dd a { + font-weight: normal; +} + +#extension__manager #info__popup { + z-index: 20; + overflow: hidden; + opacity: 0.9; + border: 1px solid __border__; + background-color: __border__; /*background_other__;*/ + text-align: left; + padding: 0.2em; +} +[dir=rtl] #extension__manager #info__popup { + text-align: right; +} + +#extension__manager div.msg { + margin: 0.4em 0 0 0; +} + +#extension__manager ul.tabs div.msg { + display: inline; + margin-left: 0.4em; +} +[dir=rtl] #extension__manager ul.tabs div.msg { + margin-left: 0; + margin-right: 0.4em; +} + +/* end admin plugin styles */ |