about summary refs log tree commit diff stats
path: root/wiki/inc/Menu
diff options
context:
space:
mode:
Diffstat (limited to 'wiki/inc/Menu')
-rw-r--r--wiki/inc/Menu/AbstractMenu.php95
-rw-r--r--wiki/inc/Menu/DetailMenu.php21
-rw-r--r--wiki/inc/Menu/Item/AbstractItem.php253
-rw-r--r--wiki/inc/Menu/Item/Admin.php24
-rw-r--r--wiki/inc/Menu/Item/Back.php29
-rw-r--r--wiki/inc/Menu/Item/Backlink.php18
-rw-r--r--wiki/inc/Menu/Item/Edit.php65
-rw-r--r--wiki/inc/Menu/Item/ImgBackto.php24
-rw-r--r--wiki/inc/Menu/Item/Index.php27
-rw-r--r--wiki/inc/Menu/Item/Login.php29
-rw-r--r--wiki/inc/Menu/Item/Media.php21
-rw-r--r--wiki/inc/Menu/Item/MediaManager.php32
-rw-r--r--wiki/inc/Menu/Item/Profile.php24
-rw-r--r--wiki/inc/Menu/Item/Recent.php20
-rw-r--r--wiki/inc/Menu/Item/Register.php24
-rw-r--r--wiki/inc/Menu/Item/Resendpwd.php24
-rw-r--r--wiki/inc/Menu/Item/Revert.php26
-rw-r--r--wiki/inc/Menu/Item/Revisions.php21
-rw-r--r--wiki/inc/Menu/Item/Subscribe.php24
-rw-r--r--wiki/inc/Menu/Item/Top.php36
-rw-r--r--wiki/inc/Menu/MenuInterface.php20
-rw-r--r--wiki/inc/Menu/MobileMenu.php91
-rw-r--r--wiki/inc/Menu/PageMenu.php23
-rw-r--r--wiki/inc/Menu/SiteMenu.php20
-rw-r--r--wiki/inc/Menu/UserMenu.php21
25 files changed, 1012 insertions, 0 deletions
diff --git a/wiki/inc/Menu/AbstractMenu.php b/wiki/inc/Menu/AbstractMenu.php
new file mode 100644
index 0000000..ce021ab
--- /dev/null
+++ b/wiki/inc/Menu/AbstractMenu.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+use dokuwiki\Menu\Item\AbstractItem;
+
+/**
+ * Class AbstractMenu
+ *
+ * Basic menu functionality. A menu defines a list of AbstractItem that shall be shown.
+ * It contains convenience functions to display the menu in HTML, but template authors can also
+ * just accesst the items via getItems() and create the HTML as however they see fit.
+ */
+abstract class AbstractMenu implements MenuInterface {
+
+    /** @var string[] list of Item classes to load */
+    protected $types = array();
+
+    /** @var int the context this menu is used in */
+    protected $context = AbstractItem::CTX_DESKTOP;
+
+    /** @var string view identifier to be set in the event */
+    protected $view = '';
+
+    /**
+     * AbstractMenu constructor.
+     *
+     * @param int $context the context this menu is used in
+     */
+    public function __construct($context = AbstractItem::CTX_DESKTOP) {
+        $this->context = $context;
+    }
+
+    /**
+     * Get the list of action items in this menu
+     *
+     * @return AbstractItem[]
+     * @triggers MENU_ITEMS_ASSEMBLY
+     */
+    public function getItems() {
+        $data = array(
+            'view' => $this->view,
+            'items' => array(),
+        );
+        trigger_event('MENU_ITEMS_ASSEMBLY', $data, array($this, 'loadItems'));
+        return $data['items'];
+    }
+
+    /**
+     * Default action for the MENU_ITEMS_ASSEMBLY event
+     *
+     * @see getItems()
+     * @param array $data The plugin data
+     */
+    public function loadItems(&$data) {
+        foreach($this->types as $class) {
+            try {
+                $class = "\\dokuwiki\\Menu\\Item\\$class";
+                /** @var AbstractItem $item */
+                $item = new $class();
+                if(!$item->visibleInContext($this->context)) continue;
+                $data['items'][] = $item;
+            } catch(\RuntimeException $ignored) {
+                // item not available
+            }
+        }
+    }
+
+    /**
+     * Generate HTML list items for this menu
+     *
+     * This is a convenience method for template authors. If you need more fine control over the
+     * output, use getItems() and build the HTML yourself
+     *
+     * @param string|false $classprefix create a class from type with this prefix, false for no class
+     * @param bool $svg add the SVG link
+     * @return string
+     */
+    public function getListItems($classprefix = '', $svg = true) {
+        $html = '';
+        foreach($this->getItems() as $item) {
+            if($classprefix !== false) {
+                $class = ' class="' . $classprefix . $item->getType() . '"';
+            } else {
+                $class = '';
+            }
+
+            $html .= "<li$class>";
+            $html .= $item->asHtmlLink(false, $svg);
+            $html .= '</li>';
+        }
+        return $html;
+    }
+
+}
diff --git a/wiki/inc/Menu/DetailMenu.php b/wiki/inc/Menu/DetailMenu.php
new file mode 100644
index 0000000..27c0c6f
--- /dev/null
+++ b/wiki/inc/Menu/DetailMenu.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+/**
+ * Class DetailMenu
+ *
+ * This menu offers options on an image detail view. It usually displayed similar to
+ * the PageMenu.
+ */
+class DetailMenu extends AbstractMenu {
+
+    protected $view = 'detail';
+
+    protected $types = array(
+        'MediaManager',
+        'ImgBackto',
+        'Top',
+    );
+
+}
diff --git a/wiki/inc/Menu/Item/AbstractItem.php b/wiki/inc/Menu/Item/AbstractItem.php
new file mode 100644
index 0000000..45ead55
--- /dev/null
+++ b/wiki/inc/Menu/Item/AbstractItem.php
@@ -0,0 +1,253 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class AbstractItem
+ *
+ * This class defines a single Item to be displayed in one of DokuWiki's menus. Plugins
+ * can extend those menus through action plugins and add their own instances of this class,
+ * overwriting some of its properties.
+ *
+ * Items may be shown multiple times in different contexts. Eg. for the default template
+ * all menus are shown in a Dropdown list on mobile, but are split into several places on
+ * desktop. The item's $context property can be used to hide the item depending on the current
+ * context.
+ *
+ * Children usually just need to overwrite the different properties, but for complex things
+ * the accessors may be overwritten instead.
+ */
+abstract class AbstractItem {
+
+    /** menu item is to be shown on desktop screens only */
+    const CTX_DESKTOP = 1;
+    /** menu item is to be shown on mobile screens only */
+    const CTX_MOBILE = 2;
+    /** menu item is to be shown in all contexts */
+    const CTX_ALL = 3;
+
+    /** @var string name of the action, usually the lowercase class name */
+    protected $type = '';
+    /** @var string optional keyboard shortcut */
+    protected $accesskey = '';
+    /** @var string the page id this action links to */
+    protected $id = '';
+    /** @var string the method to be used when this action is used in a form */
+    protected $method = 'get';
+    /** @var array parameters for the action (should contain the do parameter) */
+    protected $params = array();
+    /** @var bool when true, a rel=nofollow should be used */
+    protected $nofollow = true;
+    /** @var string this item's label may contain a placeholder, which is replaced with this */
+    protected $replacement = '';
+    /** @var string the full path to the SVG icon of this menu item */
+    protected $svg = DOKU_INC . 'lib/images/menu/00-default_checkbox-blank-circle-outline.svg';
+    /** @var string can be set to overwrite the default lookup in $lang.btn_* */
+    protected $label = '';
+    /** @var string the tooltip title, defaults to $label */
+    protected $title = '';
+    /** @var int the context this titme is shown in */
+    protected $context = self::CTX_ALL;
+
+    /**
+     * AbstractItem constructor.
+     *
+     * Sets the dynamic properties
+     *
+     * Children should always call the parent constructor!
+     *
+     * @throws \RuntimeException when the action is disabled
+     */
+    public function __construct() {
+        global $ID;
+        $this->id = $ID;
+        $this->type = $this->getType();
+        $this->params['do'] = $this->type;
+
+        if(!actionOK($this->type)) throw new \RuntimeException("action disabled: {$this->type}");
+    }
+
+    /**
+     * Return this item's label
+     *
+     * When the label property was set, it is simply returned. Otherwise, the action's type
+     * is used to look up the translation in the main language file and, if used, the replacement
+     * is applied.
+     *
+     * @return string
+     */
+    public function getLabel() {
+        if($this->label !== '') return $this->label;
+
+        /** @var array $lang */
+        global $lang;
+        $label = $lang['btn_' . $this->type];
+        if(strpos($label, '%s')) {
+            $label = sprintf($label, $this->replacement);
+        }
+        if($label === '') $label = '[' . $this->type . ']';
+        return $label;
+    }
+
+    /**
+     * Return this item's title
+     *
+     * This title should be used to display a tooltip (using the HTML title attribute). If
+     * a title property was not explicitly set, the label will be returned.
+     *
+     * @return string
+     */
+    public function getTitle() {
+        if($this->title === '') return $this->getLabel();
+        return $this->title;
+    }
+
+    /**
+     * Return the link this item links to
+     *
+     * Basically runs wl() on $id and $params. However if the ID is a hash it is used directly
+     * as the link
+     *
+     * Please note that the generated URL is *not* XML escaped.
+     *
+     * @see wl()
+     * @return string
+     */
+    public function getLink() {
+        if($this->id[0] == '#') {
+            return $this->id;
+        } else {
+            return wl($this->id, $this->params, false, '&');
+        }
+    }
+
+    /**
+     * Convenience method to get the attributes for constructing an <a> element
+     *
+     * @see buildAttributes()
+     * @param string|false $classprefix create a class from type with this prefix, false for no class
+     * @return array
+     */
+    public function getLinkAttributes($classprefix = 'menuitem ') {
+        $attr = array(
+            'href' => $this->getLink(),
+            'title' => $this->getTitle(),
+        );
+        if($this->isNofollow()) $attr['rel'] = 'nofollow';
+        if($this->getAccesskey()) {
+            $attr['accesskey'] = $this->getAccesskey();
+            $attr['title'] .= ' [' . $this->getAccesskey() . ']';
+        }
+        if($classprefix !== false) $attr['class'] = $classprefix . $this->getType();
+
+        return $attr;
+    }
+
+    /**
+     * Convenience method to create a full <a> element
+     *
+     * Wraps around the label and SVG image
+     *
+     * @param string|false $classprefix create a class from type with this prefix, false for no class
+     * @param bool $svg add SVG icon to the link
+     * @return string
+     */
+    public function asHtmlLink($classprefix = 'menuitem ', $svg = true) {
+        $attr = buildAttributes($this->getLinkAttributes($classprefix));
+        $html = "<a $attr>";
+        if($svg) {
+            $html .= '<span>' . hsc($this->getLabel()) . '</span>';
+            $html .= inlineSVG($this->getSvg());
+        } else {
+            $html .= hsc($this->getLabel());
+        }
+        $html .= "</a>";
+
+        return $html;
+    }
+
+    /**
+     * Convenience method to create a <button> element inside it's own form element
+     *
+     * Uses html_btn()
+     *
+     * @return string
+     */
+    public function asHtmlButton() {
+        return html_btn(
+            $this->getType(),
+            $this->id,
+            $this->getAccesskey(),
+            $this->getParams(),
+            $this->method,
+            $this->getTitle(),
+            $this->getLabel(),
+            $this->getSvg()
+        );
+    }
+
+    /**
+     * Should this item be shown in the given context
+     *
+     * @param int $ctx the current context
+     * @return bool
+     */
+    public function visibleInContext($ctx) {
+        return (bool) ($ctx & $this->context);
+    }
+
+    /**
+     * @return string the name of this item
+     */
+    public function getType() {
+        if($this->type === '') {
+            $this->type = strtolower(substr(strrchr(get_class($this), '\\'), 1));
+        }
+        return $this->type;
+    }
+
+    /**
+     * @return string
+     */
+    public function getAccesskey() {
+        return $this->accesskey;
+    }
+
+    /**
+     * @return array
+     */
+    public function getParams() {
+        return $this->params;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isNofollow() {
+        return $this->nofollow;
+    }
+
+    /**
+     * @return string
+     */
+    public function getSvg() {
+        return $this->svg;
+    }
+
+    /**
+     * Return this Item's settings as an array as used in tpl_get_action()
+     *
+     * @return array
+     */
+    public function getLegacyData() {
+        return array(
+            'accesskey' => $this->accesskey ?: null,
+            'type' => $this->type,
+            'id' => $this->id,
+            'method' => $this->method,
+            'params' => $this->params,
+            'nofollow' => $this->nofollow,
+            'replacement' => $this->replacement
+        );
+    }
+}
diff --git a/wiki/inc/Menu/Item/Admin.php b/wiki/inc/Menu/Item/Admin.php
new file mode 100644
index 0000000..7302f0f
--- /dev/null
+++ b/wiki/inc/Menu/Item/Admin.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Admin
+ *
+ * Opens the Admin screen. Only shown to managers or above
+ */
+class Admin extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $INFO;
+        parent::__construct();
+
+        $this->svg = DOKU_INC . 'lib/images/menu/settings.svg';
+
+        if(!$INFO['ismanager']) {
+            throw new \RuntimeException("admin is for managers only");
+        }
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Back.php b/wiki/inc/Menu/Item/Back.php
new file mode 100644
index 0000000..a7cc1d9
--- /dev/null
+++ b/wiki/inc/Menu/Item/Back.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Back
+ *
+ * Navigates back up one namepspace. This is currently not used in any menu. Templates
+ * would need to add this item manually.
+ */
+class Back extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $ID;
+        parent::__construct();
+
+        $parent = tpl_getparent($ID);
+        if(!$parent) {
+            throw new \RuntimeException("No parent for back action");
+        }
+
+        $this->id = $parent;
+        $this->params = array('do' => '');
+        $this->accesskey = 'b';
+        $this->svg = DOKU_INC . 'lib/images/menu/12-back_arrow-left.svg';
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Backlink.php b/wiki/inc/Menu/Item/Backlink.php
new file mode 100644
index 0000000..6dc242b
--- /dev/null
+++ b/wiki/inc/Menu/Item/Backlink.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Backlink
+ *
+ * Shows the backlinks for the current page
+ */
+class Backlink extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        parent::__construct();
+        $this->svg = DOKU_INC . 'lib/images/menu/08-backlink_link-variant.svg';
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Edit.php b/wiki/inc/Menu/Item/Edit.php
new file mode 100644
index 0000000..5de1778
--- /dev/null
+++ b/wiki/inc/Menu/Item/Edit.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Edit
+ *
+ * Most complex item. Shows the edit button but mutates to show, draft and create based on
+ * current state.
+ */
+class Edit extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $ACT;
+        global $INFO;
+        global $REV;
+
+        parent::__construct();
+
+        if($ACT === 'show') {
+            $this->method = 'post';
+            if($INFO['writable']) {
+                $this->accesskey = 'e';
+                if(!empty($INFO['draft'])) {
+                    $this->type = 'draft';
+                    $this->params['do'] = 'draft';
+                } else {
+                    $this->params['rev'] = $REV;
+                    if(!$INFO['exists']) {
+                        $this->type = 'create';
+                    }
+                }
+            } else {
+                if(!actionOK($this->type)) throw new \RuntimeException("action disabled: source");
+                $params['rev'] = $REV;
+                $this->type = 'source';
+                $this->accesskey = 'v';
+            }
+        } else {
+            $this->params = array('do' => '');
+            $this->type = 'show';
+            $this->accesskey = 'v';
+        }
+
+        $this->setIcon();
+    }
+
+    /**
+     * change the icon according to what type the edit button has
+     */
+    protected function setIcon() {
+        $icons = array(
+            'edit' => '01-edit_pencil.svg',
+            'create' => '02-create_pencil.svg',
+            'draft' => '03-draft_android-studio.svg',
+            'show' => '04-show_file-document.svg',
+            'source' => '05-source_file-xml.svg',
+        );
+        if(isset($icons[$this->type])) {
+            $this->svg = DOKU_INC . 'lib/images/menu/' . $icons[$this->type];
+        }
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/ImgBackto.php b/wiki/inc/Menu/Item/ImgBackto.php
new file mode 100644
index 0000000..72820a5
--- /dev/null
+++ b/wiki/inc/Menu/Item/ImgBackto.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class ImgBackto
+ *
+ * Links back to the originating page from a detail image view
+ */
+class ImgBackto extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $ID;
+        parent::__construct();
+
+        $this->svg = DOKU_INC . 'lib/images/menu/12-back_arrow-left.svg';
+        $this->type = 'img_backto';
+        $this->params = array();
+        $this->accesskey = 'b';
+        $this->replacement = $ID;
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Index.php b/wiki/inc/Menu/Item/Index.php
new file mode 100644
index 0000000..4132673
--- /dev/null
+++ b/wiki/inc/Menu/Item/Index.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Index
+ *
+ * Shows the sitemap
+ */
+class Index extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $conf;
+        global $ID;
+        parent::__construct();
+
+        $this->accesskey = 'x';
+        $this->svg = DOKU_INC . 'lib/images/menu/file-tree.svg';
+
+        // allow searchbots to get to the sitemap from the homepage (when dokuwiki isn't providing a sitemap.xml)
+        if($conf['start'] == $ID && !$conf['sitemap']) {
+            $this->nofollow = false;
+        }
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Login.php b/wiki/inc/Menu/Item/Login.php
new file mode 100644
index 0000000..671f6a7
--- /dev/null
+++ b/wiki/inc/Menu/Item/Login.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Login
+ *
+ * Show a login or logout item, based on the current state
+ */
+class Login extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $INPUT;
+        parent::__construct();
+
+        $this->svg = DOKU_INC . 'lib/images/menu/login.svg';
+        $this->params['sectok'] = getSecurityToken();
+        if($INPUT->server->has('REMOTE_USER')) {
+            if(!actionOK('logout')) {
+                throw new \RuntimeException("logout disabled");
+            }
+            $this->params['do'] = 'logout';
+            $this->type = 'logout';
+            $this->svg = DOKU_INC . 'lib/images/menu/logout.svg';
+        }
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Media.php b/wiki/inc/Menu/Item/Media.php
new file mode 100644
index 0000000..0e5f47b
--- /dev/null
+++ b/wiki/inc/Menu/Item/Media.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Media
+ *
+ * Opens the media manager
+ */
+class Media extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $ID;
+        parent::__construct();
+
+        $this->svg = DOKU_INC . 'lib/images/menu/folder-multiple-image.svg';
+        $this->params['ns'] = getNS($ID);
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/MediaManager.php b/wiki/inc/Menu/Item/MediaManager.php
new file mode 100644
index 0000000..8549d20
--- /dev/null
+++ b/wiki/inc/Menu/Item/MediaManager.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class MediaManager
+ *
+ * Opens the current image in the media manager. Used on image detail view.
+ */
+class MediaManager extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $IMG;
+        parent::__construct();
+
+        $imgNS = getNS($IMG);
+        $authNS = auth_quickaclcheck("$imgNS:*");
+        if($authNS < AUTH_UPLOAD) {
+            throw new \RuntimeException("media manager link only with upload permissions");
+        }
+
+        $this->svg = DOKU_INC . 'lib/images/menu/11-mediamanager_folder-image.svg';
+        $this->type = 'mediaManager';
+        $this->params = array(
+            'ns' => $imgNS,
+            'image' => $IMG,
+            'do' => 'media'
+        );
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Profile.php b/wiki/inc/Menu/Item/Profile.php
new file mode 100644
index 0000000..2b4ceeb
--- /dev/null
+++ b/wiki/inc/Menu/Item/Profile.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Profile
+ *
+ * Open the user's profile
+ */
+class Profile extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $INPUT;
+        parent::__construct();
+
+        if(!$INPUT->server->str('REMOTE_USER')) {
+            throw new \RuntimeException("profile is only for logged in users");
+        }
+
+        $this->svg = DOKU_INC . 'lib/images/menu/account-card-details.svg';
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Recent.php b/wiki/inc/Menu/Item/Recent.php
new file mode 100644
index 0000000..ff90ce6
--- /dev/null
+++ b/wiki/inc/Menu/Item/Recent.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Recent
+ *
+ * Show the site wide recent changes
+ */
+class Recent extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        parent::__construct();
+
+        $this->accesskey = 'r';
+        $this->svg = DOKU_INC . 'lib/images/menu/calendar-clock.svg';
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Register.php b/wiki/inc/Menu/Item/Register.php
new file mode 100644
index 0000000..615146e
--- /dev/null
+++ b/wiki/inc/Menu/Item/Register.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Register
+ *
+ * Open the view to register a new account
+ */
+class Register extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $INPUT;
+        parent::__construct();
+
+        if($INPUT->server->str('REMOTE_USER')) {
+            throw new \RuntimeException("no register when already logged in");
+        }
+
+        $this->svg = DOKU_INC . 'lib/images/menu/account-plus.svg';
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Resendpwd.php b/wiki/inc/Menu/Item/Resendpwd.php
new file mode 100644
index 0000000..7ddc6b0
--- /dev/null
+++ b/wiki/inc/Menu/Item/Resendpwd.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Resendpwd
+ *
+ * Access the "forgot password" dialog
+ */
+class Resendpwd extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $INPUT;
+        parent::__construct();
+
+        if($INPUT->server->str('REMOTE_USER')) {
+            throw new \RuntimeException("no resendpwd when already logged in");
+        }
+
+        $this->svg = DOKU_INC . 'lib/images/menu/lock-reset.svg';
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Revert.php b/wiki/inc/Menu/Item/Revert.php
new file mode 100644
index 0000000..a360c68
--- /dev/null
+++ b/wiki/inc/Menu/Item/Revert.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Revert
+ *
+ * Quick revert to the currently shown page revision
+ */
+class Revert extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $REV;
+        global $INFO;
+        parent::__construct();
+
+        if(!$INFO['ismanager'] || !$REV || !$INFO['writable']) {
+            throw new \RuntimeException('revert not available');
+        }
+        $this->params['rev'] = $REV;
+        $this->params['sectok'] = getSecurityToken();
+        $this->svg = DOKU_INC . 'lib/images/menu/06-revert_replay.svg';
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Revisions.php b/wiki/inc/Menu/Item/Revisions.php
new file mode 100644
index 0000000..3009a79
--- /dev/null
+++ b/wiki/inc/Menu/Item/Revisions.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Revisions
+ *
+ * Access the old revisions of the current page
+ */
+class Revisions extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        parent::__construct();
+
+        $this->accesskey = 'o';
+        $this->type = 'revs';
+        $this->svg = DOKU_INC . 'lib/images/menu/07-revisions_history.svg';
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Subscribe.php b/wiki/inc/Menu/Item/Subscribe.php
new file mode 100644
index 0000000..1c9d335
--- /dev/null
+++ b/wiki/inc/Menu/Item/Subscribe.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Subscribe
+ *
+ * Access the subscription management view
+ */
+class Subscribe extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        global $INPUT;
+        parent::__construct();
+
+        if(!$INPUT->server->str('REMOTE_USER')) {
+            throw new \RuntimeException("subscribe is only for logged in users");
+        }
+
+        $this->svg = DOKU_INC . 'lib/images/menu/09-subscribe_email-outline.svg';
+    }
+
+}
diff --git a/wiki/inc/Menu/Item/Top.php b/wiki/inc/Menu/Item/Top.php
new file mode 100644
index 0000000..a05c4f1
--- /dev/null
+++ b/wiki/inc/Menu/Item/Top.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace dokuwiki\Menu\Item;
+
+/**
+ * Class Top
+ *
+ * Scroll back to the top. Uses a hash as $id which is handled special in getLink().
+ * Not shown in mobile context
+ */
+class Top extends AbstractItem {
+
+    /** @inheritdoc */
+    public function __construct() {
+        parent::__construct();
+
+        $this->svg = DOKU_INC . 'lib/images/menu/10-top_arrow-up.svg';
+        $this->accesskey = 't';
+        $this->params = array('do' => '');
+        $this->id = '#dokuwiki__top';
+        $this->context = self::CTX_DESKTOP;
+    }
+
+    /**
+     * Convenience method to create a <button> element
+     *
+     * Uses html_topbtn()
+     *
+     * @todo this does currently not support the SVG icon
+     * @return string
+     */
+    public function asHtmlButton() {
+        return html_topbtn();
+    }
+
+}
diff --git a/wiki/inc/Menu/MenuInterface.php b/wiki/inc/Menu/MenuInterface.php
new file mode 100644
index 0000000..91dde9d
--- /dev/null
+++ b/wiki/inc/Menu/MenuInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+use dokuwiki\Menu\Item\AbstractItem;
+
+/**
+ * Interface MenuInterface
+ *
+ * Defines what a Menu provides
+ */
+Interface MenuInterface {
+
+    /**
+     * Get the list of action items in this menu
+     *
+     * @return AbstractItem[]
+     */
+    public function getItems();
+}
diff --git a/wiki/inc/Menu/MobileMenu.php b/wiki/inc/Menu/MobileMenu.php
new file mode 100644
index 0000000..29e17d1
--- /dev/null
+++ b/wiki/inc/Menu/MobileMenu.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+use dokuwiki\Menu\Item\AbstractItem;
+
+/**
+ * Class MobileMenu
+ *
+ * Note: this does not inherit from AbstractMenu because it is not working like the other
+ * menus. This is a meta menu, aggregating the items from the other menus and offering a combined
+ * view. The idea is to use this on mobile devices, thus the context is fixed to CTX_MOBILE
+ */
+class MobileMenu implements MenuInterface {
+
+    /**
+     * Returns all items grouped by view
+     *
+     * @return AbstractItem[][]
+     */
+    public function getGroupedItems() {
+        $pagemenu = new PageMenu(AbstractItem::CTX_MOBILE);
+        $sitemenu = new SiteMenu(AbstractItem::CTX_MOBILE);
+        $usermenu = new UserMenu(AbstractItem::CTX_MOBILE);
+
+        return array(
+            'page' => $pagemenu->getItems(),
+            'site' => $sitemenu->getItems(),
+            'user' => $usermenu->getItems()
+        );
+    }
+
+    /**
+     * Get all items in a flat array
+     *
+     * This returns the same format as AbstractMenu::getItems()
+     *
+     * @return AbstractItem[]
+     */
+    public function getItems() {
+        $menu = $this->getGroupedItems();
+        return call_user_func_array('array_merge', array_values($menu));
+    }
+
+    /**
+     * Print a dropdown menu with all DokuWiki actions
+     *
+     * Note: this will not use any pretty URLs
+     *
+     * @param string $empty empty option label
+     * @param string $button submit button label
+     * @return string
+     */
+    public function getDropdown($empty = '', $button = '&gt;') {
+        global $ID;
+        global $REV;
+        /** @var string[] $lang */
+        global $lang;
+        global $INPUT;
+
+        $html = '<form action="' . script() . '" method="get" accept-charset="utf-8">';
+        $html .= '<div class="no">';
+        $html .= '<input type="hidden" name="id" value="' . $ID . '" />';
+        if($REV) $html .= '<input type="hidden" name="rev" value="' . $REV . '" />';
+        if($INPUT->server->str('REMOTE_USER')) {
+            $html .= '<input type="hidden" name="sectok" value="' . getSecurityToken() . '" />';
+        }
+
+        $html .= '<select name="do" class="edit quickselect" title="' . $lang['tools'] . '">';
+        $html .= '<option value="">' . $empty . '</option>';
+
+        foreach($this->getGroupedItems() as $tools => $items) {
+            $html .= '<optgroup label="' . $lang[$tools . '_tools'] . '">';
+            foreach($items as $item) {
+                $params = $item->getParams();
+                $html .= '<option value="' . $params['do'] . '">';
+                $html .= hsc($item->getLabel());
+                $html .= '</option>';
+            }
+            $html .= '</optgroup>';
+        }
+
+        $html .= '</select>';
+        $html .= '<button type="submit">' . $button . '</button>';
+        $html .= '</div>';
+        $html .= '</form>';
+
+        return $html;
+    }
+
+}
diff --git a/wiki/inc/Menu/PageMenu.php b/wiki/inc/Menu/PageMenu.php
new file mode 100644
index 0000000..9c0a55e
--- /dev/null
+++ b/wiki/inc/Menu/PageMenu.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+/**
+ * Class PageMenu
+ *
+ * Actions manipulating the current page. Shown as a floating menu in the dokuwiki template
+ */
+class PageMenu extends AbstractMenu {
+
+    protected $view = 'page';
+
+    protected $types = array(
+        'Edit',
+        'Revert',
+        'Revisions',
+        'Backlink',
+        'Subscribe',
+        'Top',
+    );
+
+}
diff --git a/wiki/inc/Menu/SiteMenu.php b/wiki/inc/Menu/SiteMenu.php
new file mode 100644
index 0000000..dba6888
--- /dev/null
+++ b/wiki/inc/Menu/SiteMenu.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+/**
+ * Class SiteMenu
+ *
+ * Actions that are not bound to an individual page but provide toolsfor the whole wiki.
+ */
+class SiteMenu extends AbstractMenu {
+
+    protected $view = 'site';
+
+    protected $types = array(
+        'Recent',
+        'Media',
+        'Index'
+    );
+
+}
diff --git a/wiki/inc/Menu/UserMenu.php b/wiki/inc/Menu/UserMenu.php
new file mode 100644
index 0000000..01028d3
--- /dev/null
+++ b/wiki/inc/Menu/UserMenu.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\Menu;
+
+/**
+ * Class UserMenu
+ *
+ * Actions related to the current user
+ */
+class UserMenu extends AbstractMenu {
+
+    protected $view = 'user';
+
+    protected $types = array(
+        'Profile',
+        'Admin',
+        'Register',
+        'Login',
+    );
+
+}