diff options
Diffstat (limited to 'platform/www/lib/plugins/extension/helper')
-rw-r--r-- | platform/www/lib/plugins/extension/helper/extension.php | 1298 | ||||
-rw-r--r-- | platform/www/lib/plugins/extension/helper/gui.php | 237 | ||||
-rw-r--r-- | platform/www/lib/plugins/extension/helper/list.php | 674 | ||||
-rw-r--r-- | platform/www/lib/plugins/extension/helper/repository.php | 203 |
4 files changed, 2412 insertions, 0 deletions
diff --git a/platform/www/lib/plugins/extension/helper/extension.php b/platform/www/lib/plugins/extension/helper/extension.php new file mode 100644 index 0000000..5ddf332 --- /dev/null +++ b/platform/www/lib/plugins/extension/helper/extension.php @@ -0,0 +1,1298 @@ +<?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> + */ + +use dokuwiki\HTTP\DokuHTTPClient; +use dokuwiki\Extension\PluginController; + +/** + * Class helper_plugin_extension_extension represents a single extension (plugin or template) + */ +class helper_plugin_extension_extension extends DokuWiki_Plugin +{ + private $id; + private $base; + private $is_template = false; + private $localInfo; + private $remoteInfo; + private $managerData; + /** @var helper_plugin_extension_repository $repository */ + private $repository = null; + + /** @var array list of temporary directories */ + private $temporary = array(); + + /** @var string where templates are installed to */ + private $tpllib = ''; + + /** + * helper_plugin_extension_extension constructor. + */ + public function __construct() + { + $this->tpllib = dirname(tpl_incdir()).'/'; + } + + /** + * Destructor + * + * deletes any dangling temporary directories + */ + public function __destruct() + { + foreach ($this->temporary as $dir) { + io_rmdir($dir, true); + } + } + + /** + * @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 $id The id of the extension (prefixed with template: for templates) + * @return bool If some (local or remote) data was found + */ + public function setExtension($id) + { + $id = cleanID($id); + $this->id = $id; + $this->base = $id; + + if (substr($id, 0, 9) == 'template:') { + $this->base = substr($id, 9); + $this->is_template = true; + } else { + $this->is_template = false; + } + + $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->getID()); + + return ($this->localInfo || $this->remoteInfo); + } + + /** + * 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 under git control + * + * @return bool + */ + public function isGitControlled() + { + if (!$this->isInstalled()) return false; + return is_dir($this->getInstallDir().'/.git'); + } + + /** + * 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->id, + array( + 'authad', 'authldap', 'authpdo', 'authplain', + 'acl', 'config', 'extension', 'info', 'popularity', 'revert', + 'safefnrecode', 'styling', 'testing', 'usermanager', + 'template:dokuwiki', + ) + ); + } + + /** + * If the extension is protected against any modification (disable/uninstall) + * + * @return bool if the extension is protected + */ + public function isProtected() + { + // never allow deinstalling the current auth plugin: + global $conf; + if ($this->id == $conf['authtype']) return true; + + /** @var PluginController $plugin_controller */ + global $plugin_controller; + $cascade = $plugin_controller->getCascade(); + return (isset($cascade['protected'][$this->id]) && $cascade['protected'][$this->id]); + } + + /** + * 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->base != $this->getBase(); + } + + /** + * If the extension is enabled + * + * @return bool If the extension is enabled + */ + public function isEnabled() + { + global $conf; + if ($this->isTemplate()) { + return ($conf['template'] == $this->getBase()); + } + + /* @var PluginController $plugin_controller */ + global $plugin_controller; + return $plugin_controller->isEnabled($this->base); + } + + /** + * If the extension should be updated, i.e. if an updated version is available + * + * @return bool If an update is available + */ + public function updateAvailable() + { + if (!$this->isInstalled()) return false; + if ($this->isBundled()) return false; + $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 ID of the extension + * + * This is the same as getName() for plugins, for templates it's getName() prefixed with 'template:' + * + * @return string + */ + public function getID() + { + return $this->id; + } + + /** + * Get the name of the installation directory + * + * @return string The name of the installation directory + */ + public function getInstallName() + { + return $this->base; + } + + // 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->base; + } + + /** + * Get the display name of the extension + * + * @return string The display name + */ + public function getDisplayName() + { + if (!empty($this->localInfo['name'])) return $this->localInfo['name']; + if (!empty($this->remoteInfo['name'])) return $this->remoteInfo['name']; + return $this->base; + } + + /** + * 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 $this->getInstallDate(); + } + + /** + * 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 PluginController $plugin_controller */ + global $plugin_controller; + $dependencies = $this->getDependencies(); + $missing_dependencies = array(); + foreach ($dependencies as $dependency) { + if (!$plugin_controller->isEnabled($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['conflicts']; + 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']; + 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 $this->tpllib.$this->base; + } else { + return DOKU_PLUGIN.$this->base; + } + } + + /** + * 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 error string + */ + public function canModify() + { + if ($this->isInstalled()) { + if (!is_writable($this->getInstallDir())) { + return 'noperms'; + } + } + + if ($this->isTemplate() && !is_writable($this->tpllib)) { + return 'notplperms'; + } elseif (!is_writable(DOKU_PLUGIN)) { + return 'nopluginperms'; + } + return true; + } + + /** + * Install an extension from a user upload + * + * @param string $field name of the upload file + * @param boolean $overwrite overwrite folder if the extension name is the same + * @throws Exception when something goes wrong + * @return array The list of installed extensions + */ + public function installFromUpload($field, $overwrite = true) + { + if ($_FILES[$field]['error']) { + throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')'); + } + + $tmp = $this->mkTmpDir(); + if (!$tmp) throw new Exception($this->getLang('error_dircreate')); + + // filename may contain the plugin name for old style plugins... + $basename = basename($_FILES[$field]['name']); + $basename = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $basename); + $basename = preg_replace('/[\W]+/', '', $basename); + + if (!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")) { + throw new Exception($this->getLang('msg_upload_failed')); + } + + try { + $installed = $this->installArchive("$tmp/upload.archive", $overwrite, $basename); + $this->updateManagerData('', $installed); + $this->removeDeletedfiles($installed); + // purge cache + $this->purgeCache(); + } catch (Exception $e) { + throw $e; + } + return $installed; + } + + /** + * Install an extension from a remote URL + * + * @param string $url + * @param boolean $overwrite overwrite folder if the extension name is the same + * @throws Exception when something goes wrong + * @return array The list of installed extensions + */ + public function installFromURL($url, $overwrite = true) + { + try { + $path = $this->download($url); + $installed = $this->installArchive($path, $overwrite); + $this->updateManagerData($url, $installed); + $this->removeDeletedfiles($installed); + + // purge cache + $this->purgeCache(); + } catch (Exception $e) { + throw $e; + } + return $installed; + } + + /** + * Install or update the extension + * + * @throws \Exception when something goes wrong + * @return array The list of installed extensions + */ + public function installOrUpdate() + { + $url = $this->getDownloadURL(); + $path = $this->download($url); + $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase()); + $this->updateManagerData($url, $installed); + + // refresh extension information + if (!isset($installed[$this->getID()])) { + throw new Exception('Error, the requested extension hasn\'t been installed or updated'); + } + $this->removeDeletedfiles($installed); + $this->setExtension($this->getID()); + $this->purgeCache(); + return $installed; + } + + /** + * Uninstall the extension + * + * @return bool If the plugin was sucessfully uninstalled + */ + public function uninstall() + { + $this->purgeCache(); + return io_rmdir($this->getInstallDir(), true); + } + + /** + * Enable the extension + * + * @return bool|string True or an error message + */ + public function enable() + { + if ($this->isTemplate()) return $this->getLang('notimplemented'); + if (!$this->isInstalled()) return $this->getLang('notinstalled'); + if ($this->isEnabled()) return $this->getLang('alreadyenabled'); + + /* @var PluginController $plugin_controller */ + global $plugin_controller; + if ($plugin_controller->enable($this->base)) { + $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 PluginController $plugin_controller */ + global $plugin_controller; + if (!$this->isInstalled()) return $this->getLang('notinstalled'); + if (!$this->isEnabled()) return $this->getLang('alreadydisabled'); + if ($plugin_controller->disable($this->base)) { + $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()) { + $path = $this->getInstallDir().'/'; + $plugin = null; + + foreach (PluginController::PLUGIN_TYPES as $type) { + if (file_exists($path.$type.'.php')) { + $plugin = plugin_load($type, $this->base); + 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->base.'_'.substr($cp, 0, -4)); + if ($plugin) break; + } + if ($plugin) break; + closedir($dh); + } + } + + if ($plugin) { + /* @var DokuWiki_Plugin $plugin */ + $this->localInfo = $plugin->getInfo(); + } + } + } + + /** + * Save the given URL and current datetime in the manager.dat file of all installed extensions + * + * @param string $url Where the extension was downloaded from. (empty for manual installs via upload) + * @param array $installed Optional list of installed plugins + */ + protected function updateManagerData($url = '', $installed = null) + { + $origID = $this->getID(); + + if (is_null($installed)) { + $installed = array($origID); + } + + foreach ($installed as $ext => $info) { + if ($this->getID() != $ext) $this->setExtension($ext); + if ($url) { + $this->managerData['downloadurl'] = $url; + } elseif (isset($this->managerData['downloadurl'])) { + unset($this->managerData['downloadurl']); + } + if (isset($this->managerData['installed'])) { + $this->managerData['updated'] = date('r'); + } else { + $this->managerData['installed'] = date('r'); + } + $this->writeManagerData(); + } + + if ($this->getID() != $origID) $this->setExtension($origID); + } + + /** + * 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, DOKU_LF), 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); + } + + /** + * Returns a temporary directory + * + * The directory is registered for cleanup when the class is destroyed + * + * @return false|string + */ + protected function mkTmpDir() + { + $dir = io_mktmpdir(); + if (!$dir) return false; + $this->temporary[] = $dir; + return $dir; + } + + /** + * downloads a file from the net and saves it + * + * - $file is the directory where the file should be saved + * - if successful will return the name used for the saved file, false otherwise + * + * @author Andreas Gohr <andi@splitbrain.org> + * @author Chris Smith <chris@jalakai.co.uk> + * + * @param string $url url to download + * @param string $file path to file or directory where to save + * @param string $defaultName fallback for name of download + * @return bool|string if failed false, otherwise true or the name of the file in the given dir + */ + protected function downloadToFile($url, $file, $defaultName = '') + { + global $conf; + $http = new DokuHTTPClient(); + $http->max_bodysize = 0; + $http->timeout = 25; //max. 25 sec + $http->keep_alive = false; // we do single ops here, no need for keep-alive + $http->agent = 'DokuWiki HTTP Client (Extension Manager)'; + + $data = $http->get($url); + if ($data === false) return false; + + $name = ''; + if (isset($http->resp_headers['content-disposition'])) { + $content_disposition = $http->resp_headers['content-disposition']; + $match = array(); + if (is_string($content_disposition) && + preg_match('/attachment;\s*filename\s*=\s*"([^"]*)"/i', $content_disposition, $match) + ) { + $name = \dokuwiki\Utf8\PhpString::basename($match[1]); + } + + } + + if (!$name) { + if (!$defaultName) return false; + $name = $defaultName; + } + + $file = $file.$name; + + $fileexists = file_exists($file); + $fp = @fopen($file,"w"); + if (!$fp) return false; + fwrite($fp, $data); + fclose($fp); + if (!$fileexists and $conf['fperm']) chmod($file, $conf['fperm']); + return $name; + } + + /** + * Download an archive to a protected path + * + * @param string $url The url to get the archive from + * @throws Exception when something goes wrong + * @return string The path where the archive was saved + */ + public function download($url) + { + // check the url + if (!preg_match('/https?:\/\//i', $url)) { + throw new Exception($this->getLang('error_badurl')); + } + + // try to get the file from the path (used as plugin name fallback) + $file = parse_url($url, PHP_URL_PATH); + if (is_null($file)) { + $file = md5($url); + } else { + $file = \dokuwiki\Utf8\PhpString::basename($file); + } + + // create tmp directory for download + if (!($tmp = $this->mkTmpDir())) { + throw new Exception($this->getLang('error_dircreate')); + } + + // download + if (!$file = $this->downloadToFile($url, $tmp.'/', $file)) { + io_rmdir($tmp, true); + throw new Exception(sprintf($this->getLang('error_download'), + '<bdi>'.hsc($url).'</bdi>') + ); + } + + return $tmp.'/'.$file; + } + + /** + * @param string $file The path to the archive that shall be installed + * @param bool $overwrite If an already installed plugin should be overwritten + * @param string $base The basename of the plugin if it's known + * @throws Exception when something went wrong + * @return array list of installed extensions + */ + public function installArchive($file, $overwrite = false, $base = '') + { + $installed_extensions = array(); + + // create tmp directory for decompression + if (!($tmp = $this->mkTmpDir())) { + throw new Exception($this->getLang('error_dircreate')); + } + + // add default base folder if specified to handle case where zip doesn't contain this + if ($base && !@mkdir($tmp.'/'.$base)) { + throw new Exception($this->getLang('error_dircreate')); + } + + // decompress + $this->decompress($file, "$tmp/".$base); + + // search $tmp/$base for the folder(s) that has been created + // move the folder(s) to lib/.. + $result = array('old'=>array(), 'new'=>array()); + $default = ($this->isTemplate() ? 'template' : 'plugin'); + if (!$this->findFolders($result, $tmp.'/'.$base, $default)) { + throw new Exception($this->getLang('error_findfolder')); + } + + // choose correct result array + if (count($result['new'])) { + $install = $result['new']; + } else { + $install = $result['old']; + } + + if (!count($install)) { + throw new Exception($this->getLang('error_findfolder')); + } + + // now install all found items + foreach ($install as $item) { + // where to install? + if ($item['type'] == 'template') { + $target_base_dir = $this->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)) { + // this info message is not being exposed via exception, + // so that it's not interrupting the installation + msg(sprintf($this->getLang('msg_nooverwrite'), $item['base'])); + continue; + } + + $action = file_exists($target) ? 'update' : 'install'; + + // copy action + if ($this->dircopy($item['tmp'], $target)) { + // return info + $id = $item['base']; + if ($item['type'] == 'template') { + $id = 'template:'.$id; + } + $installed_extensions[$id] = array( + 'base' => $item['base'], + 'type' => $item['type'], + 'action' => $action + ); + } else { + throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF, + '<bdi>'.$item['base'].'</bdi>') + ); + } + } + + // cleanup + if ($tmp) io_rmdir($tmp, true); + + return $installed_extensions; + } + + /** + * 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 $directory - the temp directory where the package was unpacked to + * @param string $default_type - type used if no info.txt available + * @param string $subdir - a subdirectory. do not set. used by recursion + * @return bool - false on error + */ + protected function findFolders(&$result, $directory, $default_type = 'plugin', $subdir = '') + { + $this_dir = "$directory$subdir"; + $dh = @opendir($this_dir); + if (!$dh) return false; + + $found_dirs = array(); + $found_files = 0; + $found_template_parts = 0; + while (false !== ($f = readdir($dh))) { + if ($f == '.' || $f == '..') continue; + + if (is_dir("$this_dir/$f")) { + $found_dirs[] = "$subdir/$f"; + } else { + // it's a file -> check for config + $found_files++; + switch ($f) { + case 'plugin.info.txt': + case 'template.info.txt': + // we have found a clear marker, save and return + $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; + return true; + + case 'main.php': + case 'details.php': + case 'mediamanager.php': + case 'style.ini': + $found_template_parts++; + break; + } + } + } + closedir($dh); + + // files where found but no info.txt - use old method + if ($found_files) { + $info = array(); + $info['tmp'] = $this_dir; + // does this look like a template or should we use the default type? + if ($found_template_parts >= 2) { + $info['type'] = 'template'; + } else { + $info['type'] = $default_type; + } + + $result['old'][] = $info; + return true; + } + + // we have no files yet -> recurse + foreach ($found_dirs as $found_dir) { + $this->findFolders($result, $directory, $default_type, "$found_dir"); + } + return true; + } + + /** + * Decompress a given file to the given target directory + * + * Determines the compression type from the file extension + * + * @param string $file archive to extract + * @param string $target directory to extract to + * @throws Exception + * @return bool + */ + 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->guessArchiveType($file); + if (in_array($ext, array('tar', 'bz', 'gz'))) { + try { + $tar = new \splitbrain\PHPArchive\Tar(); + $tar->open($file); + $tar->extract($target); + } catch (\splitbrain\PHPArchive\ArchiveIOException $e) { + throw new Exception($this->getLang('error_decompress').' '.$e->getMessage()); + } + + return true; + } elseif ($ext == 'zip') { + try { + $zip = new \splitbrain\PHPArchive\Zip(); + $zip->open($file); + $zip->extract($target); + } catch (\splitbrain\PHPArchive\ArchiveIOException $e) { + throw new Exception($this->getLang('error_decompress').' '.$e->getMessage()); + } + + return true; + } + + // the only case when we don't get one of the recognized archive types is + // when the archive file can't be read + throw new Exception($this->getLang('error_decompress').' 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|false false if the file can't be read, otherwise an "extension" + */ + private function guessArchiveType($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 + * + * @param string $src filename path to file + * @param string $dst filename path to file + * @return bool|int|string + */ + 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 { + $existed = file_exists($dst); + + if (!@copy($src, $dst)) return false; + if (!$existed && $conf['fperm']) chmod($dst, $conf['fperm']); + @touch($dst, filemtime($src)); + } + + return true; + } + + /** + * Delete outdated files from updated plugins + * + * @param array $installed + */ + private function removeDeletedfiles($installed) + { + foreach ($installed as $id => $extension) { + // only on update + if ($extension['action'] == 'install') continue; + + // get definition file + if ($extension['type'] == 'template') { + $extensiondir = $this->tpllib; + } else { + $extensiondir = DOKU_PLUGIN; + } + $extensiondir = $extensiondir . $extension['base'] .'/'; + $definitionfile = $extensiondir . 'deleted.files'; + if (!file_exists($definitionfile)) continue; + + // delete the old files + $list = file($definitionfile); + + foreach ($list as $line) { + $line = trim(preg_replace('/#.*$/', '', $line)); + if (!$line) continue; + $file = $extensiondir . $line; + if (!file_exists($file)) continue; + + io_rmdir($file, true); + } + } + } +} + +// vim:ts=4:sw=4:et: diff --git a/platform/www/lib/plugins/extension/helper/gui.php b/platform/www/lib/plugins/extension/helper/gui.php new file mode 100644 index 0000000..919eb2c --- /dev/null +++ b/platform/www/lib/plugins/extension/helper/gui.php @@ -0,0 +1,237 @@ +<?php +/** + * DokuWiki Plugin extension (Helper Component) + * + * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html + * @author Andreas Gohr <andi@splitbrain.org> + */ + +use dokuwiki\Form\Form; + +/** + * Class helper_plugin_extension_list takes care of the overall GUI + */ +class helper_plugin_extension_gui extends DokuWiki_Plugin +{ + protected $tabs = array('plugins', 'templates', 'search', 'install'); + + /** @var string the extension that should have an open info window FIXME currently broken */ + protected $infoFor = ''; + + /** + * Constructor + * + * initializes requested info window + */ + public function __construct() + { + global $INPUT; + $this->infoFor = $INPUT->str('info'); + } + + /** + * display the plugin tab + */ + public function tabPlugins() + { + echo '<div class="panelHeader">'; + echo $this->locale_xhtml('intro_plugins'); + echo '</div>'; + + $pluginlist = plugin_list('', true); + /* @var helper_plugin_extension_extension $extension */ + $extension = $this->loadHelper('extension_extension'); + /* @var helper_plugin_extension_list $list */ + $list = $this->loadHelper('extension_list'); + + $form = new Form([ + 'action' => $this->tabURL('', [], '&'), + 'id' => 'extension__list', + ]); + $list->startForm(); + foreach ($pluginlist as $name) { + $extension->setExtension($name); + $list->addRow($extension, $extension->getID() == $this->infoFor); + } + $list->endForm(); + $form->addHTML($list->render(true)); + echo $form->toHTML(); + } + + /** + * Display the template tab + */ + public function tabTemplates() + { + echo '<div class="panelHeader">'; + echo $this->locale_xhtml('intro_templates'); + echo '</div>'; + + // FIXME do we have a real way? + $tpllist = glob(DOKU_INC.'lib/tpl/*', GLOB_ONLYDIR); + $tpllist = array_map('basename', $tpllist); + sort($tpllist); + + /* @var helper_plugin_extension_extension $extension */ + $extension = $this->loadHelper('extension_extension'); + /* @var helper_plugin_extension_list $list */ + $list = $this->loadHelper('extension_list'); + + $form = new Form([ + 'action' => $this->tabURL('', [], '&'), + 'id' => 'extension__list', + ]); + $list->startForm(); + foreach ($tpllist as $name) { + $extension->setExtension("template:$name"); + $list->addRow($extension, $extension->getID() == $this->infoFor); + } + $list->endForm(); + $form->addHTML($list->render(true)); + echo $form->toHTML(); + } + + /** + * Display the search tab + */ + public function tabSearch() + { + global $INPUT; + echo '<div class="panelHeader">'; + echo $this->locale_xhtml('intro_search'); + echo '</div>'; + + $form = new Form([ + 'action' => $this->tabURL('', [], '&'), + 'class' => 'search', + ]); + $form->addTagOpen('div')->addClass('no'); + $form->addTextInput('q', $this->getLang('search_for')) + ->addClass('edit') + ->val($INPUT->str('q')); + $form->addButton('submit', $this->getLang('search')) + ->attrs(['type' => 'submit', 'title' => $this->getLang('search')]); + $form->addTagClose('div'); + echo $form->toHTML(); + + if (!$INPUT->bool('q')) return; + + /* @var helper_plugin_extension_repository $repository FIXME should we use some gloabl instance? */ + $repository = $this->loadHelper('extension_repository'); + $result = $repository->search($INPUT->str('q')); + + /* @var helper_plugin_extension_extension $extension */ + $extension = $this->loadHelper('extension_extension'); + /* @var helper_plugin_extension_list $list */ + $list = $this->loadHelper('extension_list'); + + $form = new Form([ + 'action' => $this->tabURL('', [], '&'), + 'id' => 'extension__list', + ]); + $list->startForm(); + if ($result) { + foreach ($result as $name) { + $extension->setExtension($name); + $list->addRow($extension, $extension->getID() == $this->infoFor); + } + } else { + $list->nothingFound(); + } + $list->endForm(); + $form->addHTML($list->render(true)); + echo $form->toHTML(); + } + + /** + * Display the template tab + */ + public function tabInstall() + { + global $lang; + echo '<div class="panelHeader">'; + echo $this->locale_xhtml('intro_install'); + echo '</div>'; + + $form = new Form([ + 'action' => $this->tabURL('', [], '&'), + 'enctype' => 'multipart/form-data', + 'class' => 'install', + ]); + $form->addTagOpen('div')->addClass('no'); + $form->addTextInput('installurl', $this->getLang('install_url')) + ->addClass('block') + ->attrs(['type' => 'url']); + $form->addTag('br'); + $form->addTextInput('installfile', $this->getLang('install_upload')) + ->addClass('block') + ->attrs(['type' => 'file']); + $form->addTag('br'); + $form->addCheckbox('overwrite', $lang['js']['media_overwrt']) + ->addClass('block'); + $form->addTag('br'); + $form->addButton('', $this->getLang('btn_install')) + ->attrs(['type' => 'submit', 'title' => $this->getLang('btn_install')]); + $form->addTagClose('div'); + echo $form->toHTML(); + } + + /** + * Print the tab navigation + * + * @fixme style active one + */ + public function tabNavigation() + { + echo '<ul class="tabs">'; + foreach ($this->tabs as $tab) { + $url = $this->tabURL($tab); + if ($this->currentTab() == $tab) { + $class = ' active'; + } else { + $class = ''; + } + echo '<li class="'.$tab.$class.'"><a href="'.$url.'">'.$this->getLang('tab_'.$tab).'</a></li>'; + } + echo '</ul>'; + } + + /** + * Return the currently selected tab + * + * @return string + */ + public function currentTab() + { + global $INPUT; + + $tab = $INPUT->str('tab', 'plugins', true); + if (!in_array($tab, $this->tabs)) $tab = 'plugins'; + return $tab; + } + + /** + * Create an URL inside the extension manager + * + * @param string $tab tab to load, empty for current tab + * @param array $params associative array of parameter to set + * @param string $sep seperator to build the URL + * @param bool $absolute create absolute URLs? + * @return string + */ + public function tabURL($tab = '', $params = [], $sep = '&', $absolute = false) + { + global $ID; + global $INPUT; + + if (!$tab) $tab = $this->currentTab(); + $defaults = array( + 'do' => 'admin', + 'page' => 'extension', + 'tab' => $tab, + ); + if ($tab == 'search') $defaults['q'] = $INPUT->str('q'); + + return wl($ID, array_merge($defaults, $params), $absolute, $sep); + } +} diff --git a/platform/www/lib/plugins/extension/helper/list.php b/platform/www/lib/plugins/extension/helper/list.php new file mode 100644 index 0000000..647575b --- /dev/null +++ b/platform/www/lib/plugins/extension/helper/list.php @@ -0,0 +1,674 @@ +<?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> + */ + +/** + * Class helper_plugin_extension_list takes care of creating a HTML list of extensions + */ +class helper_plugin_extension_list extends DokuWiki_Plugin +{ + protected $form = ''; + /** @var helper_plugin_extension_gui */ + protected $gui; + + /** + * Constructor + * + * loads additional helpers + */ + public function __construct() + { + $this->gui = plugin_load('helper', 'extension_gui'); + } + + /** + * Initialize the extension table form + */ + public function startForm() + { + $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 + */ + public function addRow(helper_plugin_extension_extension $extension, $showinfo = false) + { + $this->startRow($extension); + $this->populateColumn('legend', $this->makeLegend($extension, $showinfo)); + $this->populateColumn('actions', $this->makeActions($extension)); + $this->endRow(); + } + + /** + * 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 + */ + public function addHeader($id, $header, $level = 2) + { + $this->form .='<h'.$level.' id="'.$id.'">'.hsc($header).'</h'.$level.'>'.DOKU_LF; + } + + /** + * Adds a paragraph to the form + * + * @param string $data The content + */ + public function addParagraph($data) + { + $this->form .= '<p>'.hsc($data).'</p>'.DOKU_LF; + } + + /** + * Add hidden fields to the form with the given data + * + * @param array $data key-value list of fields and their values to add + */ + public function addHidden(array $data) + { + $this->form .= '<div class="no">'; + foreach ($data as $key => $value) { + $this->form .= '<input type="hidden" name="'.hsc($key).'" value="'.hsc($value).'" />'; + } + $this->form .= '</div>'.DOKU_LF; + } + + /** + * Add closing tags + */ + public function endForm() + { + $this->form .= '</ul>'; + } + + /** + * Show message when no results are found + */ + public function nothingFound() + { + global $lang; + $this->form .= '<li class="notfound">'.$lang['nothingfound'].'</li>'; + } + + /** + * Print the form + * + * @param bool $returnonly whether to return html or print + */ + public function render($returnonly = false) + { + if ($returnonly) return $this->form; + echo $this->form; + } + + /** + * Start the HTML for the row for the extension + * + * @param helper_plugin_extension_extension $extension The extension + */ + private function startRow(helper_plugin_extension_extension $extension) + { + $this->form .= '<li id="extensionplugin__'.hsc($extension->getID()). + '" class="'.$this->makeClass($extension).'">'; + } + + /** + * Add a column with the given class and content + * @param string $class The class name + * @param string $html The content + */ + private function populateColumn($class, $html) + { + $this->form .= '<div class="'.$class.' col">'.$html.'</div>'.DOKU_LF; + } + + /** + * End the row + */ + private function endRow() + { + $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 + */ + public function makeHomepageLink(helper_plugin_extension_extension $extension) + { + global $conf; + $url = $extension->getURL(); + if (strtolower(parse_url($url, PHP_URL_HOST)) == 'www.dokuwiki.org') { + $linktype = 'interwiki'; + } else { + $linktype = 'extern'; + } + $param = array( + 'href' => $url, + 'title' => $url, + 'class' => ($linktype == 'extern') ? 'urlextern' : 'interwiki iw_doku', + 'target' => $conf['target'][$linktype], + 'rel' => ($linktype == 'extern') ? 'noopener' : '', + ); + if ($linktype == 'extern' && $conf['relnofollow']) { + $param['rel'] = implode(' ', [$param['rel'], 'ugc nofollow']); + } + $html = ' <a '. buildAttributes($param, true).'>'. + $this->getLang('homepage_link').'</a>'; + return $html; + } + + /** + * Generate the class name for the row of the extension + * + * @param helper_plugin_extension_extension $extension The extension object + * @return string The class name + */ + public function makeClass(helper_plugin_extension_extension $extension) + { + $class = ($extension->isTemplate()) ? 'template' : 'plugin'; + if ($extension->isInstalled()) { + $class.=' installed'; + $class.= ($extension->isEnabled()) ? ' enabled':' disabled'; + if ($extension->updateAvailable()) $class .= ' updatable'; + } + 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 + */ + public function makeAuthor(helper_plugin_extension_extension $extension) + { + if ($extension->getAuthor()) { + $mailid = $extension->getEmailID(); + if ($mailid) { + $url = $this->gui->tabURL('search', array('q' => 'authorid:'.$mailid)); + $html = '<a href="'.$url.'" class="author" title="'.$this->getLang('author_hint').'" >'. + '<img src="//www.gravatar.com/avatar/'.$mailid. + '?s=20&d=mm" width="20" height="20" alt="" /> '. + hsc($extension->getAuthor()).'</a>'; + } else { + $html = '<span class="author">'.hsc($extension->getAuthor()).'</span>'; + } + $html = '<bdi>'.$html.'</bdi>'; + } else { + $html = '<em class="author">'.$this->getLang('unknown_author').'</em>'.DOKU_LF; + } + return $html; + } + + /** + * Get the link and image tag for the screenshot/thumbnail + * + * @param helper_plugin_extension_extension $extension The extension object + * @return string The HTML code + */ + public function makeScreenshot(helper_plugin_extension_extension $extension) + { + $screen = $extension->getScreenshotURL(); + $thumb = $extension->getThumbnailURL(); + + if ($screen) { + // use protocol independent URLs for images coming from us #595 + $screen = str_replace('http://www.dokuwiki.org', '//www.dokuwiki.org', $screen); + $thumb = str_replace('http://www.dokuwiki.org', '//www.dokuwiki.org', $thumb); + + $title = sprintf($this->getLang('screenshot'), hsc($extension->getDisplayName())); + $img = '<a href="'.hsc($screen).'" target="_blank" class="extension_screenshot">'. + '<img alt="'.$title.'" width="120" height="70" src="'.hsc($thumb).'" />'. + '</a>'; + } elseif ($extension->isTemplate()) { + $img = '<img alt="" width="120" height="70" src="'.DOKU_BASE. + 'lib/plugins/extension/images/template.png" />'; + } else { + $img = '<img alt="" width="120" height="70" src="'.DOKU_BASE. + 'lib/plugins/extension/images/plugin.png" />'; + } + $html = '<div class="screenshot" >'.$img.'<span></span></div>'.DOKU_LF; + return $html; + } + + /** + * Extension main description + * + * @param helper_plugin_extension_extension $extension The extension object + * @param bool $showinfo Show the info section + * @return string The HTML code + */ + public function makeLegend(helper_plugin_extension_extension $extension, $showinfo = false) + { + $html = '<div>'; + $html .= '<h2>'; + $html .= sprintf( + $this->getLang('extensionby'), + '<bdi>'.hsc($extension->getDisplayName()).'</bdi>', + $this->makeAuthor($extension) + ); + $html .= '</h2>'.DOKU_LF; + + $html .= $this->makeScreenshot($extension); + + $popularity = $extension->getPopularity(); + if ($popularity !== false && !$extension->isBundled()) { + $popularityText = sprintf($this->getLang('popularity'), round($popularity*100, 2)); + $html .= '<div class="popularity" title="'.$popularityText.'">'. + '<div style="width: '.($popularity * 100).'%;">'. + '<span class="a11y">'.$popularityText.'</span>'. + '</div></div>'.DOKU_LF; + } + + if ($extension->getDescription()) { + $html .= '<p><bdi>'; + $html .= hsc($extension->getDescription()).' '; + $html .= '</bdi></p>'.DOKU_LF; + } + + $html .= $this->makeLinkbar($extension); + + if ($showinfo) { + $url = $this->gui->tabURL(''); + $class = 'close'; + } else { + $url = $this->gui->tabURL('', array('info' => $extension->getID())); + $class = ''; + } + $html .= ' <a href="'.$url.'#extensionplugin__'.$extension->getID(). + '" class="info '.$class.'" title="'.$this->getLang('btn_info'). + '" data-extid="'.$extension->getID().'">'.$this->getLang('btn_info').'</a>'; + + if ($showinfo) { + $html .= $this->makeInfo($extension); + } + $html .= $this->makeNoticeArea($extension); + $html .= '</div>'.DOKU_LF; + return $html; + } + + /** + * Generate the link bar HTML code + * + * @param helper_plugin_extension_extension $extension The extension instance + * @return string The HTML code + */ + public function makeLinkbar(helper_plugin_extension_extension $extension) + { + global $conf; + $html = '<div class="linkbar">'; + $html .= $this->makeHomepageLink($extension); + + $bugtrackerURL = $extension->getBugtrackerURL(); + if ($bugtrackerURL) { + if (strtolower(parse_url($bugtrackerURL, PHP_URL_HOST)) == 'www.dokuwiki.org') { + $linktype = 'interwiki'; + } else { + $linktype = 'extern'; + } + $param = array( + 'href' => $bugtrackerURL, + 'title' => $bugtrackerURL, + 'class' => 'bugs', + 'target' => $conf['target'][$linktype], + 'rel' => ($linktype == 'extern') ? 'noopener' : '', + ); + if ($conf['relnofollow']) { + $param['rel'] = implode(' ', [$param['rel'], 'ugc nofollow']); + } + $html .= ' <a '.buildAttributes($param, true).'>'. + $this->getLang('bugs_features').'</a>'; + } + if ($extension->getTags()) { + $first = true; + $html .= ' <span class="tags">'.$this->getLang('tags').' '; + foreach ($extension->getTags() as $tag) { + if (!$first) { + $html .= ', '; + } else { + $first = false; + } + $url = $this->gui->tabURL('search', ['q' => 'tag:'.$tag]); + $html .= '<bdi><a href="'.$url.'">'.hsc($tag).'</a></bdi>'; + } + $html .= '</span>'; + } + $html .= '</div>'.DOKU_LF; + return $html; + } + + /** + * Notice area + * + * @param helper_plugin_extension_extension $extension The extension + * @return string The HTML code + */ + public function makeNoticeArea(helper_plugin_extension_extension $extension) + { + $html = ''; + $missing_dependencies = $extension->getMissingDependencies(); + if (!empty($missing_dependencies)) { + $html .= '<div class="msg error">' . + sprintf( + $this->getLang('missing_dependency'), + '<bdi>' . implode(', ', $missing_dependencies) . '</bdi>' + ) . + '</div>'; + } + if ($extension->isInWrongFolder()) { + $html .= '<div class="msg error">' . + sprintf( + $this->getLang('wrong_folder'), + '<bdi>' . hsc($extension->getInstallName()) . '</bdi>', + '<bdi>' . hsc($extension->getBase()) . '</bdi>' + ) . + '</div>'; + } + if (($securityissue = $extension->getSecurityIssue()) !== false) { + $html .= '<div class="msg error">'. + sprintf($this->getLang('security_issue'), '<bdi>'.hsc($securityissue).'</bdi>'). + '</div>'; + } + if (($securitywarning = $extension->getSecurityWarning()) !== false) { + $html .= '<div class="msg notify">'. + sprintf($this->getLang('security_warning'), '<bdi>'.hsc($securitywarning).'</bdi>'). + '</div>'; + } + if ($extension->updateAvailable()) { + $html .= '<div class="msg notify">'. + sprintf($this->getLang('update_available'), hsc($extension->getLastUpdate())). + '</div>'; + } + if ($extension->hasDownloadURLChanged()) { + $html .= '<div class="msg notify">' . + sprintf( + $this->getLang('url_change'), + '<bdi>' . hsc($extension->getDownloadURL()) . '</bdi>', + '<bdi>' . hsc($extension->getLastDownloadURL()) . '</bdi>' + ) . + '</div>'; + } + return $html.DOKU_LF; + } + + /** + * Create a link from the given URL + * + * Shortens the URL for display + * + * @param string $url + * @return string HTML link + */ + public function shortlink($url) + { + $link = parse_url($url); + + $base = $link['host']; + if (!empty($link['port'])) $base .= $base.':'.$link['port']; + $long = $link['path']; + if (!empty($link['query'])) $long .= $link['query']; + + $name = shorten($base, $long, 55); + + $html = '<a href="'.hsc($url).'" class="urlextern">'.hsc($name).'</a>'; + return $html; + } + + /** + * Plugin/template details + * + * @param helper_plugin_extension_extension $extension The extension + * @return string The HTML code + */ + public function makeInfo(helper_plugin_extension_extension $extension) + { + $default = $this->getLang('unknown'); + $html = '<dl class="details">'; + + $html .= '<dt>'.$this->getLang('status').'</dt>'; + $html .= '<dd>'.$this->makeStatus($extension).'</dd>'; + + if ($extension->getDonationURL()) { + $html .= '<dt>'.$this->getLang('donate').'</dt>'; + $html .= '<dd>'; + $html .= '<a href="'.$extension->getDonationURL().'" class="donate">'. + $this->getLang('donate_action').'</a>'; + $html .= '</dd>'; + } + + if (!$extension->isBundled()) { + $html .= '<dt>'.$this->getLang('downloadurl').'</dt>'; + $html .= '<dd><bdi>'; + $html .= ($extension->getDownloadURL() + ? $this->shortlink($extension->getDownloadURL()) + : $default); + $html .= '</bdi></dd>'; + + $html .= '<dt>'.$this->getLang('repository').'</dt>'; + $html .= '<dd><bdi>'; + $html .= ($extension->getSourcerepoURL() + ? $this->shortlink($extension->getSourcerepoURL()) + : $default); + $html .= '</bdi></dd>'; + } + + if ($extension->isInstalled()) { + if ($extension->getInstalledVersion()) { + $html .= '<dt>'.$this->getLang('installed_version').'</dt>'; + $html .= '<dd>'; + $html .= hsc($extension->getInstalledVersion()); + $html .= '</dd>'; + } + if (!$extension->isBundled()) { + $html .= '<dt>'.$this->getLang('install_date').'</dt>'; + $html .= '<dd>'; + $html .= ($extension->getUpdateDate() + ? hsc($extension->getUpdateDate()) + : $this->getLang('unknown')); + $html .= '</dd>'; + } + } + if (!$extension->isInstalled() || $extension->updateAvailable()) { + $html .= '<dt>'.$this->getLang('available_version').'</dt>'; + $html .= '<dd>'; + $html .= ($extension->getLastUpdate() + ? hsc($extension->getLastUpdate()) + : $this->getLang('unknown')); + $html .= '</dd>'; + } + + $html .= '<dt>'.$this->getLang('provides').'</dt>'; + $html .= '<dd><bdi>'; + $html .= ($extension->getTypes() + ? hsc(implode(', ', $extension->getTypes())) + : $default); + $html .= '</bdi></dd>'; + + if (!$extension->isBundled() && $extension->getCompatibleVersions()) { + $html .= '<dt>'.$this->getLang('compatible').'</dt>'; + $html .= '<dd>'; + foreach ($extension->getCompatibleVersions() as $date => $version) { + $html .= '<bdi>'.$version['label'].' ('.$date.')</bdi>, '; + } + $html = rtrim($html, ', '); + $html .= '</dd>'; + } + if ($extension->getDependencies()) { + $html .= '<dt>'.$this->getLang('depends').'</dt>'; + $html .= '<dd>'; + $html .= $this->makeLinkList($extension->getDependencies()); + $html .= '</dd>'; + } + + if ($extension->getSimilarExtensions()) { + $html .= '<dt>'.$this->getLang('similar').'</dt>'; + $html .= '<dd>'; + $html .= $this->makeLinkList($extension->getSimilarExtensions()); + $html .= '</dd>'; + } + + if ($extension->getConflicts()) { + $html .= '<dt>'.$this->getLang('conflicts').'</dt>'; + $html .= '<dd>'; + $html .= $this->makeLinkList($extension->getConflicts()); + $html .= '</dd>'; + } + $html .= '</dl>'.DOKU_LF; + return $html; + } + + /** + * Generate a list of links for extensions + * + * @param array $ext The extensions + * @return string The HTML code + */ + public function makeLinkList($ext) + { + $html = ''; + foreach ($ext as $link) { + $html .= '<bdi><a href="'. + $this->gui->tabURL('search', array('q'=>'ext:'.$link)).'">'. + hsc($link).'</a></bdi>, '; + } + return rtrim($html, ', '); + } + + /** + * Display the action buttons if they are possible + * + * @param helper_plugin_extension_extension $extension The extension + * @return string The HTML code + */ + public function makeActions(helper_plugin_extension_extension $extension) + { + global $conf; + $html = ''; + $errors = ''; + + if ($extension->isInstalled()) { + if (($canmod = $extension->canModify()) === true) { + if (!$extension->isProtected()) { + $html .= $this->makeAction('uninstall', $extension); + } + if ($extension->getDownloadURL()) { + if ($extension->updateAvailable()) { + $html .= $this->makeAction('update', $extension); + } else { + $html .= $this->makeAction('reinstall', $extension); + } + } + } else { + $errors .= '<p class="permerror">'.$this->getLang($canmod).'</p>'; + } + + if (!$extension->isProtected() && !$extension->isTemplate()) { // no enable/disable for templates + if ($extension->isEnabled()) { + $html .= $this->makeAction('disable', $extension); + } else { + $html .= $this->makeAction('enable', $extension); + } + } + + if ($extension->isGitControlled()) { + $errors .= '<p class="permerror">'.$this->getLang('git').'</p>'; + } + + if ($extension->isEnabled() && + in_array('Auth', $extension->getTypes()) && + $conf['authtype'] != $extension->getID() + ) { + $errors .= '<p class="permerror">'.$this->getLang('auth').'</p>'; + } + } else { + if (($canmod = $extension->canModify()) === true) { + if ($extension->getDownloadURL()) { + $html .= $this->makeAction('install', $extension); + } + } else { + $errors .= '<div class="permerror">'.$this->getLang($canmod).'</div>'; + } + } + + if (!$extension->isInstalled() && $extension->getDownloadURL()) { + $html .= ' <span class="version">'.$this->getLang('available_version').' '; + $html .= ($extension->getLastUpdate() + ? hsc($extension->getLastUpdate()) + : $this->getLang('unknown')).'</span>'; + } + + return $html.' '.$errors.DOKU_LF; + } + + /** + * Display an action button for an extension + * + * @param string $action The action + * @param helper_plugin_extension_extension $extension The extension + * @return string The HTML code + */ + public function makeAction($action, $extension) + { + $title = ''; + + switch ($action) { + case 'install': + case 'reinstall': + $title = 'title="'.hsc($extension->getDownloadURL()).'"'; + break; + } + + $classes = 'button '.$action; + $name = 'fn['.$action.']['.hsc($extension->getID()).']'; + + $html = '<button class="'.$classes.'" name="'.$name.'" type="submit" '.$title.'>'. + $this->getLang('btn_'.$action).'</button> '; + return $html; + } + + /** + * Plugin/template status + * + * @param helper_plugin_extension_extension $extension The extension + * @return string The description of all relevant statusses + */ + public function makeStatus(helper_plugin_extension_extension $extension) + { + $status = array(); + + if ($extension->isInstalled()) { + $status[] = $this->getLang('status_installed'); + if ($extension->isProtected()) { + $status[] = $this->getLang('status_protected'); + } else { + $status[] = $extension->isEnabled() + ? $this->getLang('status_enabled') + : $this->getLang('status_disabled'); + } + } else { + $status[] = $this->getLang('status_not_installed'); + } + if (!$extension->canModify()) $status[] = $this->getLang('status_unmodifiable'); + if ($extension->isBundled()) $status[] = $this->getLang('status_bundled'); + $status[] = $extension->isTemplate() + ? $this->getLang('status_template') + : $this->getLang('status_plugin'); + return implode(', ', $status); + } +} diff --git a/platform/www/lib/plugins/extension/helper/repository.php b/platform/www/lib/plugins/extension/helper/repository.php new file mode 100644 index 0000000..712baa0 --- /dev/null +++ b/platform/www/lib/plugins/extension/helper/repository.php @@ -0,0 +1,203 @@ +<?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> + */ + +use dokuwiki\Cache\Cache; +use dokuwiki\HTTP\DokuHTTPClient; +use dokuwiki\Extension\PluginController; + +/** + * Class helper_plugin_extension_repository provides access to the extension repository on dokuwiki.org + */ +class helper_plugin_extension_repository extends DokuWiki_Plugin +{ + + const EXTENSION_REPOSITORY_API = 'http://www.dokuwiki.org/lib/plugins/pluginrepo/api.php'; + + private $loaded_extensions = array(); + private $has_access = null; + + /** + * Initialize the repository (cache), fetches data for all installed plugins + */ + public function init() + { + /* @var PluginController $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'); + + 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(self::EXTENSION_REPOSITORY_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 + * + * @param bool $usecache use cached result if still valid + * @return bool If repository access is available + */ + public function hasAccess($usecache = true) { + if ($this->has_access === null) { + $cache = new Cache('##extension_manager###hasAccess', '.repo'); + + if (!$cache->useCache(array('age' => 60*10, 'purge' => !$usecache))) { + $httpclient = new DokuHTTPClient(); + $httpclient->timeout = 5; + $data = $httpclient->get(self::EXTENSION_REPOSITORY_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'); + + 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(self::EXTENSION_REPOSITORY_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(); + } + + /** + * Search for plugins or templates using the given query string + * + * @param string $q the query string + * @return array a list of matching extensions + */ + public function search($q) + { + $query = $this->parseQuery($q); + $query['fmt'] = 'php'; + + $httpclient = new DokuHTTPClient(); + $data = $httpclient->post(self::EXTENSION_REPOSITORY_API, $query); + if ($data === false) return array(); + $result = unserialize($data); + + $ids = array(); + + // store cache info for each extension + foreach ($result as $ext) { + $name = $ext['plugin']; + $cache = new Cache('##extension_manager##'.$name, '.repo'); + $cache->storeCache(serialize($ext)); + $ids[] = $name; + } + + return $ids; + } + + /** + * Parses special queries from the query string + * + * @param string $q + * @return array + */ + protected function parseQuery($q) + { + $parameters = array( + 'tag' => array(), + 'mail' => array(), + 'type' => array(), + 'ext' => array() + ); + + // extract tags + if (preg_match_all('/(^|\s)(tag:([\S]+))/', $q, $matches, PREG_SET_ORDER)) { + foreach ($matches as $m) { + $q = str_replace($m[2], '', $q); + $parameters['tag'][] = $m[3]; + } + } + // extract author ids + if (preg_match_all('/(^|\s)(authorid:([\S]+))/', $q, $matches, PREG_SET_ORDER)) { + foreach ($matches as $m) { + $q = str_replace($m[2], '', $q); + $parameters['mail'][] = $m[3]; + } + } + // extract extensions + if (preg_match_all('/(^|\s)(ext:([\S]+))/', $q, $matches, PREG_SET_ORDER)) { + foreach ($matches as $m) { + $q = str_replace($m[2], '', $q); + $parameters['ext'][] = $m[3]; + } + } + // extract types + if (preg_match_all('/(^|\s)(type:([\S]+))/', $q, $matches, PREG_SET_ORDER)) { + foreach ($matches as $m) { + $q = str_replace($m[2], '', $q); + $parameters['type'][] = $m[3]; + } + } + + // FIXME make integer from type value + + $parameters['q'] = trim($q); + return $parameters; + } +} + +// vim:ts=4:sw=4:et: |