aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/lib/plugins/extension
diff options
context:
space:
mode:
authorAndreas Gohr <andi@splitbrain.org>2024-04-25 08:55:13 +0200
committerAndreas Gohr <andi@splitbrain.org>2024-12-04 10:51:13 +0100
commit4fd6a1d7ae34e34afb3c0bae47639222f884a1b5 (patch)
treea595794a6f049e286d96da14663f96df6868cbf4 /lib/plugins/extension
parentb2a05b76de6c1d1e38212dff43776aaa41a22894 (diff)
downloaddokuwiki-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.php56
-rw-r--r--lib/plugins/extension/Gui.php61
-rw-r--r--lib/plugins/extension/GuiExtension.php565
-rw-r--r--lib/plugins/extension/Installer.php29
-rw-r--r--lib/plugins/extension/Manager.php2
-rw-r--r--lib/plugins/extension/Notice.php183
-rw-r--r--lib/plugins/extension/admin.php21
-rw-r--r--lib/plugins/extension/cli.php69
-rw-r--r--lib/plugins/extension/lang/en/lang.php1
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&amp;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';