diff options
author | Andreas Gohr <andi@splitbrain.org> | 2024-04-25 08:55:13 +0200 |
---|---|---|
committer | Andreas Gohr <andi@splitbrain.org> | 2024-12-04 10:51:13 +0100 |
commit | 4fd6a1d7ae34e34afb3c0bae47639222f884a1b5 (patch) | |
tree | a595794a6f049e286d96da14663f96df6868cbf4 /lib/plugins/extension | |
parent | b2a05b76de6c1d1e38212dff43776aaa41a22894 (diff) | |
download | dokuwiki-4fd6a1d7ae34e34afb3c0bae47639222f884a1b5.tar.gz dokuwiki-4fd6a1d7ae34e34afb3c0bae47639222f884a1b5.zip |
Extension Manager: First go at reimplementing the GUI
still a long road ahead
Diffstat (limited to 'lib/plugins/extension')
-rw-r--r-- | lib/plugins/extension/Extension.php | 56 | ||||
-rw-r--r-- | lib/plugins/extension/Gui.php | 61 | ||||
-rw-r--r-- | lib/plugins/extension/GuiExtension.php | 565 | ||||
-rw-r--r-- | lib/plugins/extension/Installer.php | 29 | ||||
-rw-r--r-- | lib/plugins/extension/Manager.php | 2 | ||||
-rw-r--r-- | lib/plugins/extension/Notice.php | 183 | ||||
-rw-r--r-- | lib/plugins/extension/admin.php | 21 | ||||
-rw-r--r-- | lib/plugins/extension/cli.php | 69 | ||||
-rw-r--r-- | lib/plugins/extension/lang/en/lang.php | 1 |
9 files changed, 932 insertions, 55 deletions
diff --git a/lib/plugins/extension/Extension.php b/lib/plugins/extension/Extension.php index e15c6c302..1e95472a2 100644 --- a/lib/plugins/extension/Extension.php +++ b/lib/plugins/extension/Extension.php @@ -140,14 +140,18 @@ class Extension // region Getters /** + * @param bool $wrap If true, the id is wrapped in backticks * @return string The extension id (same as base but prefixed with "template:" for templates) */ - public function getId() + public function getId($wrap=false) { if ($this->type === self::TYPE_TEMPLATE) { - return self::TYPE_TEMPLATE . ':' . $this->base; + $id = self::TYPE_TEMPLATE . ':' . $this->base; + } else { + $id = $this->base; } - return $this->base; + if($wrap) $id = "`$id`"; + return $id; } /** @@ -289,6 +293,17 @@ class Extension } /** + * Get the types of components this extension provides + * + * @todo for installed extensions this could be read from the filesystem instead of relying on the meta data + * @return array int -> type + */ + public function getComponentTypes () + { + return $this->getTag('types', []); + } + + /** * Get a list of extension ids this extension depends on * * @return string[] @@ -408,6 +423,7 @@ class Extension */ public function isInWrongFolder() { + if(!$this->isInstalled()) return false; return $this->getInstallDir() != $this->currentDir; } @@ -435,7 +451,7 @@ class Extension */ public function hasChangedURL() { - $last = $this->getManager()->getDownloadUrl(); + $last = $this->getManager()->getDownloadURL(); if(!$last) return false; return $last !== $this->getDownloadURL(); } @@ -445,7 +461,7 @@ class Extension * * @return bool */ - public function updateAvailable() + public function isUpdateAvailable() { if($this->isBundled()) return false; // bundled extensions are never updated $self = $this->getInstalledVersion(); @@ -579,6 +595,36 @@ class Extension return $this->getRemoteTag('donationurl'); } + /** + * Get a list of extensions that are similar to this one + * + * @return string[] + */ + public function getSimilarList() + { + return $this->getRemoteTag('similar', []); + } + + /** + * Get a list of extensions that are marked as conflicting with this one + * + * @return string[] + */ + public function getConflictList() + { + return $this->getRemoteTag('conflicts', []); + } + + /** + * Get a list of DokuWiki versions this plugin is marked as compatible with + * + * @return string[][] date -> version + */ + public function getCompatibleVersions() + { + return $this->getRemoteTag('compatible', []); + } + // endregion // region Actions diff --git a/lib/plugins/extension/Gui.php b/lib/plugins/extension/Gui.php new file mode 100644 index 000000000..4038cdfef --- /dev/null +++ b/lib/plugins/extension/Gui.php @@ -0,0 +1,61 @@ +<?php + +namespace dokuwiki\plugin\extension; + +class Gui +{ + protected $tabs = ['plugins', 'templates', 'search', 'install']; + + protected $helper; + + + public function __construct() + { + $this->helper = plugin_load('helper', 'extension'); + } + + + public function getLang($msg) + { + return $this->helper->getLang($msg); + } + + /** + * 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 = [ + '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/lib/plugins/extension/GuiExtension.php b/lib/plugins/extension/GuiExtension.php new file mode 100644 index 000000000..d26c389ab --- /dev/null +++ b/lib/plugins/extension/GuiExtension.php @@ -0,0 +1,565 @@ +<?php + +namespace dokuwiki\plugin\extension; + +class GuiExtension extends Gui +{ + const THUMB_WIDTH = 120; + const THUMB_HEIGHT = 70; + + + protected Extension $extension; + + public function __construct(Extension $extension) + { + parent::__construct(); + $this->extension = $extension; + } + + + public function render() + { + + $classes = $this->getClasses(); + + $html = "<section class=\"$classes\">"; + $html .= $this->thumbnail(); + $html .= $this->popularity(); + $html .= $this->info(); + $html .= $this->notices(); + $html .= $this->mainLinks(); + $html .= $this->details(); + $html .= $this->actions(); + + + $html .= '</section>'; + + return $html; + } + + // region sections + + /** + * Get the link and image tag for the screenshot/thumbnail + * + * @return string The HTML code + */ + protected function thumbnail() + { + $screen = $this->extension->getScreenshotURL(); + $thumb = $this->extension->getThumbnailURL(); + + $link = []; + $img = [ + 'width' => self::THUMB_WIDTH, + 'height' => self::THUMB_HEIGHT, + 'alt' => '', + ]; + + if ($screen) { + $link = [ + 'href' => $screen, + 'target' => '_blank', + 'class' => 'extension_screenshot', + 'title' => sprintf($this->getLang('screenshot'), $this->extension->getDisplayName()) + ]; + + $img['src'] = $thumb; + $img['alt'] = $link['title']; + } elseif ($this->extension->isTemplate()) { + $img['src'] = DOKU_BASE . 'lib/plugins/extension/images/template.png'; + } else { + $img['src'] = DOKU_BASE . 'lib/plugins/extension/images/plugin.png'; + } + + $html = '<div class="screenshot">'; + if ($link) $html .= '<a ' . buildAttributes($link) . '>'; + $html .= '<img ' . buildAttributes($img) . ' />'; + if ($link) $html .= '</a>'; + $html .= '</div>'; + + return $html; + + } + + /** + * The main information about the extension + * + * @return string + */ + protected function info() + { + $html = '<h2>'; + $html .= '<bdi>' . hsc($this->extension->getDisplayName()) . '</bdi>'; + if($this->extension->isBundled()) { + $html .= ' <span class="version">' . hsc('<'.$this->getLang('status_bundled').'>') . '</span>'; + } elseif($this->extension->getInstalledVersion()) { + $html .= ' <span class="version">' . hsc($this->extension->getInstalledVersion()) . '</span>'; + } + $html .= '</h2>'; + + $html .= $this->author(); + + $html .= '<p>' . hsc($this->extension->getDescription()) . '</p>'; + + return $html; + } + + /** + * Display the available notices for the extension + * + * @return string + */ + protected function notices() + { + $notices = Notice::list($this->extension); + + $html = ''; + foreach ($notices as $type => $messages) { + foreach ($messages as $message) { + $message = hsc($message); + $message = preg_replace('/`([^`]+)`/', '<bdi>$1</bdi>', $message); + $html .= '<div class="msg ' . $type . '">' . $message . '</div>'; + } + } + return $html; + } + + /** + * Generate the link bar HTML code + * + * @return string The HTML code + */ + public function mainLinks() + { + $html = '<div class="linkbar">'; + + + $homepage = $this->extension->getURL(); + if ($homepage) { + $params = $this->prepareLinkAttributes($homepage, 'homepage'); + $html .= ' <a ' . buildAttributes($params, true) . '>' . $this->getLang('homepage_link') . '</a>'; + } + + $bugtracker = $this->extension->getBugtrackerURL(); + if ($bugtracker) { + $params = $this->prepareLinkAttributes($bugtracker, 'bugs'); + $html .= ' <a ' . buildAttributes($params, true) . '>' . $this->getLang('bugs_features') . '</a>'; + } + + if ($this->extension->getDonationURL()) { + $params = $this->prepareLinkAttributes($this->extension->getDonationURL(), 'donate'); + $html .= ' <a ' . buildAttributes($params, true) . '>' . $this->getLang('donate_action') . '</a>'; + } + + + + $html .= '</div>'; + + return $html; + } + + /** + * Create the details section + * + * @return string + */ + protected function details() + { + $html = '<details>'; + $html .= '<summary>' . 'FIXME label' . '</summary>'; + + + $default = $this->getLang('unknown'); + $list = []; + + if (!$this->extension->isBundled()) { + $list['downloadurl'] = $this->shortlink($this->extension->getDownloadURL(), 'download', $default); + $list['repository'] = $this->shortlink($this->extension->getSourcerepoURL(), 'repo', $default); + } + + if ($this->extension->isInstalled()) { + if ($this->extension->isBundled()) { + $list['installed_version'] = $this->getLang('status_bundled'); + } else { + if ($this->extension->getInstalledVersion()) { + $list['installed_version'] = hsc($this->extension->getInstalledVersion()); + } + if (!$this->extension->isBundled()) { + $updateDate = $this->extension->getManager()->getLastUpdate(); + $list['install_date'] = $updateDate ? hsc($updateDate) : $default; + } + } + } + + if (!$this->extension->isInstalled() || $this->extension->isUpdateAvailable()) { + $list['available_version'] = $this->extension->getLastUpdate() + ? hsc($this->extension->getLastUpdate()) + : $default; + } + + + if (!$this->extension->isBundled() && $this->extension->getCompatibleVersions()) { + $list['compatible'] = join(', ', array_map( + function ($date, $version) { + return '<bdi>' . $version['label'] . ' (' . $date . ')</bdi>'; + }, + array_keys($this->extension->getCompatibleVersions()), + array_values($this->extension->getCompatibleVersions()) + )); + } + + $tags = $this->extension->getTags(); + if ($tags) { + $list['tags'] = join(', ', array_map(function ($tag) { + $url = $this->tabURL('search', ['q' => 'tag:' . $tag]); + return '<bdi><a href="' . $url . '">' . hsc($tag) . '</a></bdi>'; + }, $tags)); + } + + if ($this->extension->getDependencyList()) { + $list['depends'] = $this->linkExtensions($this->extension->getDependencyList()); + } + + if ($this->extension->getSimilarList()) { + $list['similar'] = $this->linkExtensions($this->extension->getSimilarList()); + } + + if ($this->extension->getConflictList()) { + $list['conflicts'] = $this->linkExtensions($this->extension->getConflictList()); + } + + foreach ($list as $key => $value) { + $html .= '<dt>' . $this->getLang($key) . '</dt>'; + $html .= '<dd>' . $value . '</dd>'; + } + + $html .= '</details>'; + return $html; + } + + /** + * Generate a link to the author of the extension + * + * @return string The HTML code of the link + */ + protected function author() + { + if (!$this->extension->getAuthor()) { + return '<em class="author">' . $this->getLang('unknown_author') . '</em>'; + } + + $mailid = $this->extension->getEmailID(); + if ($mailid) { + $url = $this->tabURL('search', ['q' => 'authorid:' . $mailid]); + $html = '<a href="' . $url . '" class="author" title="' . $this->getLang('author_hint') . '" >' . + '<img src="//www.gravatar.com/avatar/' . $mailid . + '?s=60&d=mm" width="20" height="20" alt="" /> ' . + hsc($this->extension->getAuthor()) . '</a>'; + } else { + $html = '<span class="author">' . hsc($this->extension->getAuthor()) . '</span>'; + } + return '<bdi>' . $html . '</bdi>'; + } + + /** + * The popularity bar + * + * @return string + */ + protected function popularity() + { + $popularity = $this->extension->getPopularity(); + if (!$popularity) return ''; + if ($this->extension->isBundled()) return ''; + + $popularityText = sprintf($this->getLang('popularity'), round($popularity * 100, 2)); + return '<div class="popularity" title="' . $popularityText . '">' . + '<div style="width: ' . ($popularity * 100) . '%;">' . + '<span class="a11y">' . $popularityText . '</span>' . + '</div></div>'; + + } + + protected function actions() + { + global $conf; + + $html = ''; + $actions = []; + $errors = []; + + // show the available version if there is one + if ($this->extension->getDownloadURL() && $this->extension->getLastUpdate()) { + $html .= ' <span class="version">' . $this->getLang('available_version') . ' ' . + hsc($this->extension->getLastUpdate()) . '</span>'; + } + + // gather available actions and possible errors to show + try { + Installer::ensurePermissions($this->extension); + + if ($this->extension->isInstalled()) { + + if (!$this->extension->isProtected()) $actions[] = 'uninstall'; + if ($this->extension->getDownloadURL()) { + $actions[] = $this->extension->isUpdateAvailable() ? 'update' : 'reinstall'; + + if ($this->extension->isGitControlled()) { + $errors[] = $this->getLang('git'); + } + } + + if (!$this->extension->isProtected() && !$this->extension->isTemplate()) { // no enable/disable for templates + $actions[] = $this->extension->isEnabled() ? 'disable' : 'enable'; + + if ( + $this->extension->isEnabled() && + in_array('Auth', $this->extension->getComponentTypes()) && + $conf['authtype'] != $this->extension->getID() + ) { + $errors[] = $this->getLang('auth'); + } + } + } else { + if ($this->extension->getDownloadURL()) { + $actions[] = 'install'; + } + } + } catch (\Exception $e) { + $errors[] = $e->getMessage(); + } + + foreach ($actions as $action) { + $html .= '<button name="fn[' . $action . '][' . $this->extension->getID() . ']" class="button" type="submit">' . + $this->getLang('btn_' . $action) . '</button>'; + } + + foreach ($errors as $error) { + $html .= '<div class="msg error">' . hsc($error) . '</div>'; + } + + return $html; + } + + + // endregion + // region utility functions + + /** + * Create the classes representing the state of the extension + * + * @return string + */ + protected function getClasses() + { + $classes = ['extension', $this->extension->getType()]; + if ($this->extension->isInstalled()) $classes[] = 'installed'; + if ($this->extension->isUpdateAvailable()) $classes[] = 'update'; + $classes[] = $this->extension->isEnabled() ? 'enabled' : 'disabled'; + return implode(' ', $classes); + } + + /** + * Create an attributes array for a link + * + * Handles interwiki links to dokuwiki.org + * + * @param string $url The URL to link to + * @param string $class Additional classes to add + * @return array + */ + protected function prepareLinkAttributes($url, $class) + { + global $conf; + + $attributes = [ + 'href' => $url, + 'class' => 'urlextern', + 'target' => $conf['target']['extern'], + 'rel' => 'noopener', + 'title' => $url, + ]; + + if ($conf['relnofollow']) { + $attributes['rel'] .= ' ugc nofollow'; + } + + if (preg_match('/^https?:\/\/(www\.)?dokuwiki\.org\//i', $url)) { + $attributes['class'] = 'interwiki iw_doku'; + $attributes['target'] = $conf['target']['interwiki']; + $attributes['rel'] = ''; + } + + $attributes['class'] .= ' ' . $class; + return $attributes; + } + + /** + * Create a link from the given URL + * + * Shortens the URL for display + * + * @param string $url + * @param string $class Additional classes to add + * @param string $fallback If URL is empty return this fallback + * @return string HTML link + */ + protected function shortlink($url, $class, $fallback = '') + { + if (!$url) return hsc($fallback); + + $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); + + $params = $this->prepareLinkAttributes($url, $class); + $html = '<a ' . buildAttributes($params, true) . '>' . hsc($name) . '</a>'; + return $html; + } + + /** + * Generate a list of links for extensions + * + * Links to the search tab with the extension name + * + * @param array $extensions The extension names + * @return string The HTML code + */ + public function linkExtensions($extensions) + { + $html = ''; + foreach ($extensions as $link) { + $html .= '<bdi><a href="' . + $this->tabURL('search', ['q' => 'ext:' . $link]) . '">' . + hsc($link) . '</a></bdi>, '; + } + return rtrim($html, ', '); + } + + // endregion + + + /** + * Extension main description + * + * @return string The HTML code + */ + public function makeLegend() + { + $html = '<div>'; + $html .= '<h2>'; + $html .= sprintf( + $this->getLang('extensionby'), + '<bdi>' . hsc($this->extension->getDisplayName()) . '</bdi>', + $this->author() + ); + $html .= '</h2>' . DOKU_LF; + + $html .= $this->makeScreenshot(); + + $popularity = $this->extension->getPopularity(); + if ($popularity !== false && !$this->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 ($this->extension->getDescription()) { + $html .= '<p><bdi>'; + $html .= hsc($this->extension->getDescription()) . ' '; + $html .= '</bdi></p>' . DOKU_LF; + } + + $html .= $this->makeLinkbar(); + $html .= $this->makeInfo(); + $html .= $this->makeNoticeArea(); + $html .= '</div>' . DOKU_LF; + return $html; + } + + + /** + * Plugin/template details + * + * @return string The HTML code + */ + public + function makeInfo() + { + $default = $this->getLang('unknown'); + + + $list = []; + + $list['status'] = $this->makeStatus(); + + + if ($this->extension->getDonationURL()) { + $list['donate'] = '<a href="' . $this->extension->getDonationURL() . '" class="donate">' . + $this->getLang('donate_action') . '</a>'; + } + + if (!$this->extension->isBundled()) { + $list['downloadurl'] = $this->shortlink($this->extension->getDownloadURL(), $default); + $list['repository'] = $this->shortlink($this->extension->getSourcerepoURL(), $default); + } + + if ($this->extension->isInstalled()) { + if ($this->extension->getInstalledVersion()) { + $list['installed_version'] = hsc($this->extension->getInstalledVersion()); + } + if (!$this->extension->isBundled()) { + $updateDate = $this->extension->getManager()->getLastUpdate(); + $list['install_date'] = $updateDate ? hsc($updateDate) : $default; + } + } + + if (!$this->extension->isInstalled() || $this->extension->isUpdateAvailable()) { + $list['available_version'] = $this->extension->getLastUpdate() + ? hsc($this->extension->getLastUpdate()) + : $default; + } + + + if (!$this->extension->isBundled() && $this->extension->getCompatibleVersions()) { + $html .= '<dt>' . $this->getLang('compatible') . '</dt>'; + $html .= '<dd>'; + foreach ($this->extension->getCompatibleVersions() as $date => $version) { + $html .= '<bdi>' . $version['label'] . ' (' . $date . ')</bdi>, '; + } + $html = rtrim($html, ', '); + $html .= '</dd>'; + } + if ($this->extension->getDependencyList()) { + $html .= '<dt>' . $this->getLang('depends') . '</dt>'; + $html .= '<dd>'; + $html .= $this->makeLinkList($extension->getDependencies()); + $html .= '</dd>'; + } + + if ($this->extension->getSimilarExtensions()) { + $html .= '<dt>' . $this->getLang('similar') . '</dt>'; + $html .= '<dd>'; + $html .= $this->makeLinkList($extension->getSimilarExtensions()); + $html .= '</dd>'; + } + + if ($this->extension->getConflicts()) { + $html .= '<dt>' . $this->getLang('conflicts') . '</dt>'; + $html .= '<dd>'; + $html .= $this->makeLinkList($extension->getConflicts()); + $html .= '</dd>'; + } + $html .= '</dl>' . DOKU_LF; + return $html; + } + + +} diff --git a/lib/plugins/extension/Installer.php b/lib/plugins/extension/Installer.php index 1de59cd53..90f2fea47 100644 --- a/lib/plugins/extension/Installer.php +++ b/lib/plugins/extension/Installer.php @@ -165,7 +165,7 @@ class Installer } // check PHP requirements - $this->ensurePhpCompatibility($extension); + self::ensurePhpCompatibility($extension); // install dependencies first foreach ($extension->getDependencyList() as $id) { @@ -175,6 +175,7 @@ class Installer } // now install the extension + self::ensurePermissions($extension); $this->dircopy( $extension->getCurrentDir(), $extension->getInstallDir() @@ -206,6 +207,8 @@ class Installer throw new Exception('error_uninstall_protected', [$extension->getId()]); } + self::ensurePermissions($extension); + if (!io_rmdir($extension->getInstallDir(), true)) { throw new Exception('msg_delete_failed', [$extension->getId()]); } @@ -320,7 +323,7 @@ class Installer * @param Extension $extension * @throws Exception */ - protected function ensurePhpCompatibility(Extension $extension) + public static function ensurePhpCompatibility(Extension $extension) { $min = $extension->getMinimumPHPVersion(); if ($min && version_compare(PHP_VERSION, $min, '<')) { @@ -333,6 +336,28 @@ class Installer } } + /** + * Ensure the file permissions are correct before attempting to install + * + * @throws Exception if the permissions are not correct + */ + public static function ensurePermissions(Extension $extension) + { + $target = $extension->getInstallDir(); + + // updates + if (file_exists($target)) { + if (!is_writable($target)) throw new Exception('noperms'); + return; + } + + // new installs + $target = dirname($target); + if (!is_writable($target)) { + if ($extension->isTemplate()) throw new Exception('notplperms'); + throw new Exception('nopluginperms'); + } + } /** * Get a base name from an archive name (we don't trust) diff --git a/lib/plugins/extension/Manager.php b/lib/plugins/extension/Manager.php index a4e31905a..c288b305d 100644 --- a/lib/plugins/extension/Manager.php +++ b/lib/plugins/extension/Manager.php @@ -80,7 +80,7 @@ class Manager return $this->data['updated'] ?? $this->data['installed'] ?? ''; } - public function getDownloadUrl() + public function getDownloadURL() { return $this->data['downloadurl'] ?? ''; } diff --git a/lib/plugins/extension/Notice.php b/lib/plugins/extension/Notice.php new file mode 100644 index 000000000..327271b38 --- /dev/null +++ b/lib/plugins/extension/Notice.php @@ -0,0 +1,183 @@ +<?php + + +namespace dokuwiki\plugin\extension; + +class Notice +{ + const INFO = 'info'; + const WARNING = 'warning'; + const ERROR = 'error'; + const SECURITY = 'security'; + + public const ICONS = [ + self::INFO => 'ⓘ', + self::WARNING => '↯', + self::ERROR => '⚠', + self::SECURITY => '☠', + ]; + + protected $notices = [ + self::INFO => [], + self::WARNING => [], + self::ERROR => [], + self::SECURITY => [], + ]; + + /** @var \helper_plugin_extension */ + protected $helper; + + /** @var Extension */ + protected Extension $extension; + + /** + * Not public, use list() instead + * @param Extension $extension + */ + protected function __construct(Extension $extension) + { + $this->helper = plugin_load('helper', 'extension'); + $this->extension = $extension; + + $this->checkDependencies(); + $this->checkConflicts(); + $this->checkSecurity(); + $this->checkFolder(); + $this->checkPHPVersion(); + $this->checkUpdateMessage(); + $this->checkURLChange(); + } + + /** + * Get all notices for the extension + * + * @return string[][] array of notices grouped by type + */ + public static function list(Extension $extension): array + { + $self = new self($extension); + return $self->notices; + } + + /** + * Access a language string + * + * @param string $msg + * @return string + */ + protected function getLang($msg) + { + return strip_tags($this->helper->getLang($msg)); // FIXME existing strings should be adjusted + } + + /** + * Check that all dependencies are met + * @return void + */ + protected function checkDependencies() + { + if (!$this->extension->isInstalled()) return; + + $dependencies = $this->extension->getDependencyList(); + $missing = []; + foreach ($dependencies as $dependency) { + $dep = Extension::createFromId($dependency); + if (!$dep->isInstalled()) $missing[] = $dep; + } + if(!$missing) return; + + $this->notices[self::ERROR][] = sprintf( + $this->getLang('missing_dependency'), + join(', ', array_map(static fn(Extension $dep) => $dep->getId(true), $missing)) + ); + } + + /** + * Check if installed dependencies are conflicting + * @return void + */ + protected function checkConflicts() + { + $conflicts = $this->extension->getConflictList(); + $found = []; + foreach ($conflicts as $conflict) { + $dep = Extension::createFromId($conflict); + if ($dep->isInstalled()) $found[] = $dep; + } + if(!$found) return; + + $this->notices[self::WARNING][] = sprintf( + $this->getLang('found_conflict'), + join(', ', array_map(static fn(Extension $dep) => $dep->getId(true), $found)) + ); + } + + /** + * Check for security issues + * @return void + */ + protected function checkSecurity() + { + if ($issue = $this->extension->getSecurityIssue()) { + $this->notices[self::SECURITY][] = sprintf($this->getLang('security_issue'), $issue); + } + if ($issue = $this->extension->getSecurityWarning()) { + $this->notices[self::SECURITY][] = sprintf($this->getLang('security_issue'), $issue); + } + } + + /** + * Check if the extension is installed in correct folder + * @return void + */ + protected function checkFolder() + { + if (!$this->extension->isInWrongFolder()) return; + + $this->notices[self::ERROR][] = sprintf( + $this->getLang('wrong_folder'), + basename($this->extension->getCurrentDir()), + basename($this->extension->getInstallDir()) + ); + } + + /** + * Check PHP requirements + * @return void + */ + protected function checkPHPVersion() + { + try { + Installer::ensurePhpCompatibility($this->extension); + } catch (\Exception $e) { + $this->notices[self::ERROR][] = $e->getMessage(); + } + } + + /** + * Check for update message + * @return void + */ + protected function checkUpdateMessage() + { + // FIXME should we only display this for installed extensions? + if ($msg = $this->extension->getUpdateMessage()) { + $this->notices[self::WARNING][] = sprintf($this->getLang('update_message'), $msg); + } + } + + /** + * Check for URL changes + * @return void + */ + protected function checkURLChange() + { + if (!$this->extension->hasChangedURL()) return; + $this->notices[self::WARNING][] = sprintf( + $this->getLang('url_change'), + $this->extension->getDownloadURL(), + $this->extension->getManager()->getDownloadURL() + ); + } + +} diff --git a/lib/plugins/extension/admin.php b/lib/plugins/extension/admin.php index 9dad12761..f26da7a6c 100644 --- a/lib/plugins/extension/admin.php +++ b/lib/plugins/extension/admin.php @@ -161,7 +161,26 @@ class admin_plugin_extension extends AdminPlugin */ public function html() { - echo '<h1>' . $this->getLang('menu') . '</h1>' . DOKU_LF; + echo '<h1>' . $this->getLang('menu') . '</h1>'; + + $ext = \dokuwiki\plugin\extension\Extension::createFromId('aichat'); + $gui = new \dokuwiki\plugin\extension\GuiExtension($ext); + echo $gui->render(); + + $ext = \dokuwiki\plugin\extension\Extension::createFromId('gallery'); + $gui = new \dokuwiki\plugin\extension\GuiExtension($ext); + echo $gui->render(); + + $ext = \dokuwiki\plugin\extension\Extension::createFromId('extension'); + $gui = new \dokuwiki\plugin\extension\GuiExtension($ext); + echo $gui->render(); + + $ext = \dokuwiki\plugin\extension\Extension::createFromId('top'); + $gui = new \dokuwiki\plugin\extension\GuiExtension($ext); + echo $gui->render(); + + return; + echo '<div id="extension__manager">' . DOKU_LF; $this->gui->tabNavigation(); diff --git a/lib/plugins/extension/cli.php b/lib/plugins/extension/cli.php index 01bc3b1c8..de500b0b4 100644 --- a/lib/plugins/extension/cli.php +++ b/lib/plugins/extension/cli.php @@ -5,6 +5,7 @@ use dokuwiki\plugin\extension\Exception as ExtensionException; use dokuwiki\plugin\extension\Extension; use dokuwiki\plugin\extension\Installer; use dokuwiki\plugin\extension\Local; +use dokuwiki\plugin\extension\Notice; use dokuwiki\plugin\extension\Repository; use splitbrain\phpcli\Colors; use splitbrain\phpcli\Exception; @@ -29,10 +30,10 @@ class cli_plugin_extension extends CLIPlugin $options->setHelp( "Manage plugins and templates for this DokuWiki instance\n\n" . "Status codes:\n" . - " i - installed ☠ - security issue\n" . - " b - bundled with DokuWiki ⚠ - security warning\n" . - " g - installed via git ↯ - update message\n" . - " d - disabled ⮎ - URL changed\n" . + " i - installed " . Notice::ICONS[Notice::SECURITY] . " - security issue\n" . + " b - bundled with DokuWiki " . Notice::ICONS[Notice::ERROR] . " - extension error\n" . + " g - installed via git " . Notice::ICONS[Notice::WARNING] . " - extension warning\n" . + " d - disabled " . Notice::ICONS[Notice::INFO] . " - extension info\n" . " u - update available\n" ); @@ -126,7 +127,7 @@ class cli_plugin_extension extends CLIPlugin $local = new Local(); $extensions = []; foreach ($local->getExtensions() as $ext) { - if($ext->updateAvailable()) $extensions[] = $ext->getID(); + if ($ext->isUpdateAvailable()) $extensions[] = $ext->getID(); } return $this->cmdInstall($extensions); } @@ -214,10 +215,10 @@ class cli_plugin_extension extends CLIPlugin } $processed = $installer->getProcessed(); - foreach($processed as $id => $status){ - if($status == Installer::STATUS_INSTALLED) { + foreach ($processed as $id => $status) { + if ($status == Installer::STATUS_INSTALLED) { $this->success(sprintf($this->getLang('msg_install_success'), $id)); - } else if($status == Installer::STATUS_UPDATED) { + } else if ($status == Installer::STATUS_UPDATED) { $this->success(sprintf($this->getLang('msg_update_success'), $id)); } } @@ -306,11 +307,11 @@ class cli_plugin_extension extends CLIPlugin continue; } - - if ($ext->getSecurityIssue()) $status .= '☠'; - if ($ext->getSecurityWarning()) $status .= '⚠'; - if ($ext->getUpdateMessage()) $status .= '↯'; - if ($ext->hasChangedURL()) $status .= '⮎'; + $notices = Notice::list($ext); + if ($notices[Notice::SECURITY]) $status .= Notice::ICONS[Notice::SECURITY]; + if ($notices[Notice::ERROR]) $status .= Notice::ICONS[Notice::ERROR]; + if ($notices[Notice::WARNING]) $status .= Notice::ICONS[Notice::WARNING]; + if ($notices[Notice::INFO]) $status .= Notice::ICONS[Notice::INFO]; echo $tr->format( [20, 5, 12, '*'], @@ -340,39 +341,15 @@ class cli_plugin_extension extends CLIPlugin ['', $ext->getDescription()], [null, Colors::C_CYAN] ); - if ($ext->getSecurityWarning()) { - echo $tr->format( - [7, '*'], - ['', '⚠ ' . $ext->getSecurityWarning()], - [null, Colors::C_YELLOW] - ); - } - if ($ext->getSecurityIssue()) { - echo $tr->format( - [7, '*'], - ['', '☠ ' . $ext->getSecurityIssue()], - [null, Colors::C_LIGHTRED] - ); - } - if ($ext->getUpdateMessage()) { - echo $tr->format( - [7, '*'], - ['', '↯ ' . $ext->getUpdateMessage()], - [null, Colors::C_LIGHTBLUE] - ); - } - if ($ext->hasChangedURL()) { - $msg = $this->getLang('url_change'); - $msg = str_replace('<br>',"\n", $msg); - $msg = str_replace('<br/>',"\n", $msg); - $msg = str_replace('<br />',"\n", $msg); - $msg = strip_tags($msg); - - echo $tr->format( - [7, '*'], - ['', '⮎ ' . sprintf($msg, $ext->getDownloadURL(), $ext->getManager()->getDownloadUrl())], - [null, Colors::C_BLUE] - ); + foreach ($notices as $type => $msgs) { + if (!$msgs) continue; + foreach ($msgs as $msg) { + echo $tr->format( + [7, '*'], + ['', Notice::ICONS[$type] . ' ' . $msg], + [null, Colors::C_LIGHTBLUE] + ); + } } } } diff --git a/lib/plugins/extension/lang/en/lang.php b/lib/plugins/extension/lang/en/lang.php index 22593865e..eaaaf9581 100644 --- a/lib/plugins/extension/lang/en/lang.php +++ b/lib/plugins/extension/lang/en/lang.php @@ -74,6 +74,7 @@ $lang['msg_upload_failed'] = 'Uploading the file failed: %s'; $lang['msg_nooverwrite'] = 'Extension %s already exists so it is not being overwritten; to overwrite, tick the overwrite option'; $lang['missing_dependency'] = '<strong>Missing or disabled dependency:</strong> %s'; +$lang['found_conflict'] = '<strong>This extension is marked as conflictig with the following installed extensions:</strong> %s'; $lang['security_issue'] = '<strong>Security Issue:</strong> %s'; $lang['security_warning'] = '<strong>Security Warning:</strong> %s'; $lang['update_message'] = '<strong>Update Message:</strong> %s'; |