diff --git a/_test/tests/inc/Action/general.test.php b/_test/tests/inc/Action/general.test.php
new file mode 100644
index 0000000000000000000000000000000000000000..5e6402836f9a386cb9f4b0d493c0e833c49c6f5b
--- /dev/null
+++ b/_test/tests/inc/Action/general.test.php
@@ -0,0 +1,182 @@
+<?php
+
+use dokuwiki\Action\AbstractAclAction;
+use dokuwiki\Action\AbstractUserAction;
+use dokuwiki\Action\Exception\ActionAclRequiredException;
+use dokuwiki\Action\Exception\ActionDisabledException;
+use dokuwiki\Action\Exception\ActionUserRequiredException;
+
+class action_general extends DokuWikiTest {
+
+    public function dataProvider() {
+        return array(
+            array('Login', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('Logout', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('Search', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('Recent', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('Profile', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('ProfileDelete', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('Index', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('Sitemap', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('Denied', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('Register', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('Resendpwd', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('Backlink', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+
+            array('Revert', AUTH_ADMIN, array('exists' => true, 'ismanager' => false)),
+            array('Revert', AUTH_EDIT, array('exists' => true, 'ismanager' => true)),
+
+            array('Admin', AUTH_ADMIN, array('exists' => true, 'ismanager' => false)),
+            array('Admin', AUTH_READ, array('exists' => true, 'ismanager' => true)), // let in, check later again
+
+            array('Check', AUTH_READ, array('exists' => true, 'ismanager' => false)), // sensible?
+            array('Diff', AUTH_READ, array('exists' => true, 'ismanager' => false)),
+            array('Show', AUTH_READ, array('exists' => true, 'ismanager' => false)),
+            array('Subscribe', AUTH_READ, array('exists' => true, 'ismanager' => false)),
+            array('Locked', AUTH_READ, array('exists' => true, 'ismanager' => false)),
+            array('Source', AUTH_READ, array('exists' => true, 'ismanager' => false)),
+            array('Export', AUTH_READ, array('exists' => true, 'ismanager' => false)),
+            array('Media', AUTH_READ, array('exists' => true, 'ismanager' => false)),
+            array('Revisions', AUTH_READ, array('exists' => true, 'ismanager' => false)),
+
+            array('Draftdel', AUTH_EDIT, array('exists' => true, 'ismanager' => false)),
+
+            // aliases
+            array('Cancel', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+            array('Recover', AUTH_NONE, array('exists' => true, 'ismanager' => false)),
+
+            // EDITING existing page
+            array('Save', AUTH_EDIT, array('exists' => true, 'ismanager' => false)),
+            array('Conflict', AUTH_EDIT, array('exists' => true, 'ismanager' => false)),
+            array('Draft', AUTH_EDIT, array('exists' => true, 'ismanager' => false)),
+            //the edit function will check again and do a source show
+            //when no AUTH_EDIT available:
+            array('Edit', AUTH_READ, array('exists' => true, 'ismanager' => false)),
+            array('Preview', AUTH_READ, array('exists' => true, 'ismanager' => false)),
+
+            // EDITING new page
+            array('Save', AUTH_CREATE, array('exists' => false, 'ismanager' => false)),
+            array('Conflict', AUTH_CREATE, array('exists' => false, 'ismanager' => false)),
+            array('Draft', AUTH_CREATE, array('exists' => false, 'ismanager' => false)),
+            array('Edit', AUTH_CREATE, array('exists' => false, 'ismanager' => false)),
+            array('Preview', AUTH_CREATE, array('exists' => false, 'ismanager' => false)),
+        );
+    }
+
+    /**
+     * @dataProvider dataProvider
+     * @param $name
+     * @param $expected
+     * @param $info
+     */
+    public function testMinimumPermissions($name, $expected, $info) {
+        global $INFO;
+        $INFO = $info;
+
+        $classname = 'dokuwiki\\Action\\' . $name;
+        /** @var \dokuwiki\Action\AbstractAction $class */
+        $class = new $classname();
+
+        $this->assertSame($expected, $class->minimumPermission());
+    }
+
+    /**
+     * All actions should handle the disableactions setting
+     *
+     * @dataProvider dataProvider
+     * @param $name
+     */
+    public function testBaseClassActionOkPermission($name) {
+        $this->assertTrue(true); // mark as not risky
+        if($name == 'Show') return; // disabling show does not work
+
+        $classname = 'dokuwiki\\Action\\' . $name;
+        /** @var \dokuwiki\Action\AbstractAction $class */
+        $class = new $classname();
+
+        global $conf;
+        $conf['useacl'] = 1;
+        $conf['subscribers'] = 1;
+        $conf['disableactions'] = '';
+        $_SERVER['REMOTE_USER'] = 'someone';
+
+        try {
+            \dokuwiki\ActionRouter::getInstance(true)->checkAction($class);
+        } catch(\Exception $e) {
+            $this->assertNotSame(ActionDisabledException::class, get_class($e));
+        }
+
+        $conf['disableactions'] = $class->getActionName();
+
+        try {
+            \dokuwiki\ActionRouter::getInstance(true)->checkAction($class);
+        } catch(\Exception $e) {
+            $this->assertSame(ActionDisabledException::class, get_class($e), $e);
+        }
+    }
+
+    /**
+     * Actions inheriting from AbstractAclAction should have an ACL enabled check
+     *
+     * @dataProvider dataProvider
+     * @param $name
+     */
+    public function testBaseClassAclPermission($name) {
+        $classname = 'dokuwiki\\Action\\' . $name;
+        /** @var \dokuwiki\Action\AbstractAction $class */
+        $class = new $classname();
+        $this->assertTrue(true); // mark as not risky
+        if(!is_a($class, AbstractAclAction::class)) return;
+
+        global $conf;
+        $conf['useacl'] = 1;
+        $conf['subscribers'] = 1;
+
+        try {
+            $class->checkPermissions();
+        } catch(\Exception $e) {
+            $this->assertNotSame(ActionAclRequiredException::class, get_class($e));
+        }
+
+        $conf['useacl'] = 0;
+
+        try {
+            $class->checkPermissions();
+        } catch(\Exception $e) {
+            $this->assertSame(ActionAclRequiredException::class, get_class($e));
+        }
+    }
+
+    /**
+     * Actions inheriting from AbstractUserAction should have user check
+     *
+     * @dataProvider dataProvider
+     * @param $name
+     */
+    public function testBaseClassUserPermission($name) {
+        $classname = 'dokuwiki\\Action\\' . $name;
+        /** @var \dokuwiki\Action\AbstractAction $class */
+        $class = new $classname();
+        $this->assertTrue(true); // mark as not risky
+        if(!is_a($class, AbstractUserAction::class)) return;
+
+        global $conf;
+        $conf['useacl'] = 1;
+        $conf['subscribers'] = 1;
+        $_SERVER['REMOTE_USER'] = 'test';
+
+        try {
+            $class->checkPermissions();
+        } catch(\Exception $e) {
+            $this->assertNotSame(ActionUserRequiredException::class, get_class($e));
+        }
+
+        unset($_SERVER['REMOTE_USER']);
+
+        try {
+            $class->checkPermissions();
+        } catch(\Exception $e) {
+            $this->assertSame(ActionUserRequiredException::class, get_class($e));
+        }
+    }
+}
diff --git a/inc/Action/AbstractAclAction.php b/inc/Action/AbstractAclAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..76639b066da392b7f623327b51bc2a22505641f1
--- /dev/null
+++ b/inc/Action/AbstractAclAction.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAclRequiredException;
+
+/**
+ * Class AbstractAclAction
+ *
+ * An action that requires the ACL subsystem to be enabled (eg. useacl=1)
+ *
+ * @package dokuwiki\Action
+ */
+abstract class AbstractAclAction extends AbstractAction {
+
+    /** @inheritdoc */
+    public function checkPermissions() {
+        parent::checkPermissions();
+        global $conf;
+        global $auth;
+        if(!$conf['useacl']) throw new ActionAclRequiredException();
+        if(!$auth) throw new ActionAclRequiredException();
+    }
+
+}
diff --git a/inc/Action/AbstractAction.php b/inc/Action/AbstractAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..8c2467f86d54e1222419613d843bda388331f208
--- /dev/null
+++ b/inc/Action/AbstractAction.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionDisabledException;
+use dokuwiki\Action\Exception\ActionException;
+use dokuwiki\Action\Exception\FatalException;
+
+/**
+ * Class AbstractAction
+ *
+ * Base class for all actions
+ *
+ * @package dokuwiki\Action
+ */
+abstract class AbstractAction {
+
+    /** @var string holds the name of the action (lowercase class name, no namespace) */
+    protected $actionname;
+
+    /**
+     * AbstractAction constructor.
+     *
+     * @param string $actionname the name of this action (see getActionName() for caveats)
+     */
+    public function __construct($actionname = '') {
+        if($actionname !== '') {
+            $this->actionname = $actionname;
+        } else {
+            // http://stackoverflow.com/a/27457689/172068
+            $this->actionname = strtolower(substr(strrchr(get_class($this), '\\'), 1));
+        }
+    }
+
+    /**
+     * Return the minimum permission needed
+     *
+     * This needs to return one of the AUTH_* constants. It will be checked against
+     * the current user and page after checkPermissions() ran through. If it fails,
+     * the user will be shown the Denied action.
+     *
+     * @return int
+     */
+    abstract public function minimumPermission();
+
+    /**
+     * Check permissions are correct to run this action
+     *
+     * @throws ActionException
+     * @return void
+     */
+    public function checkPermissions() {
+    }
+
+    /**
+     * Process data
+     *
+     * This runs before any output is sent to the browser.
+     *
+     * Throw an Exception if a different action should be run after this step.
+     *
+     * @throws ActionException
+     * @return void
+     */
+    public function preProcess() {
+    }
+
+    /**
+     * Output whatever content is wanted within tpl_content();
+     *
+     * @fixme we may want to return a Ui class here
+     */
+    public function tplContent() {
+        throw new FatalException('No content for Action ' . $this->actionname);
+    }
+
+    /**
+     * Returns the name of this action
+     *
+     * This is usually the lowercased class name, but may differ for some actions.
+     * eg. the export_ modes or for the Plugin action.
+     *
+     * @return string
+     */
+    public function getActionName() {
+        return $this->actionname;
+    }
+}
diff --git a/inc/Action/AbstractAliasAction.php b/inc/Action/AbstractAliasAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..f61f39a88948235e1ae81a438bd24ded0f73b885
--- /dev/null
+++ b/inc/Action/AbstractAliasAction.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\FatalException;
+
+/**
+ * Class AbstractAliasAction
+ *
+ * An action that is an alias for another action. Skips the minimumPermission check
+ *
+ * Be sure to implement preProcess() and throw an ActionAbort exception
+ * with the proper action.
+ *
+ * @package dokuwiki\Action
+ */
+abstract class AbstractAliasAction extends AbstractAction {
+
+    /** @inheritdoc */
+    function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    public function preProcess() {
+        throw new FatalException('Alias Actions need to implement preProcess to load the aliased action');
+    }
+
+}
diff --git a/inc/Action/AbstractUserAction.php b/inc/Action/AbstractUserAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..8a3a19f75c4f6d888b81bd2eac050f70aad65b2d
--- /dev/null
+++ b/inc/Action/AbstractUserAction.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionUserRequiredException;
+
+/**
+ * Class AbstractUserAction
+ *
+ * An action that requires a logged in user
+ *
+ * @package dokuwiki\Action
+ */
+abstract class AbstractUserAction extends AbstractAclAction {
+
+    /** @inheritdoc */
+    public function checkPermissions() {
+        parent::checkPermissions();
+        global $INPUT;
+        if(!$INPUT->server->str('REMOTE_USER')) {
+            throw new ActionUserRequiredException();
+        }
+    }
+
+}
diff --git a/inc/Action/Admin.php b/inc/Action/Admin.php
new file mode 100644
index 0000000000000000000000000000000000000000..b1f9095ee59c0d345801cabb1a5ae7d3cab2107f
--- /dev/null
+++ b/inc/Action/Admin.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Admin
+ *
+ * Action to show the admin interface or admin plugins
+ *
+ * @package dokuwiki\Action
+ */
+class Admin extends AbstractUserAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        global $INFO;
+
+        if($INFO['ismanager']) {
+            return AUTH_READ; // let in check later
+        } else {
+            return AUTH_ADMIN;
+        }
+    }
+
+    public function checkPermissions() {
+        parent::checkPermissions();
+
+        global $INFO;
+        if(!$INFO['ismanager']) {
+            throw new ActionException('denied');
+        }
+    }
+
+    public function preProcess() {
+        global $INPUT;
+        global $INFO;
+
+        // retrieve admin plugin name from $_REQUEST['page']
+        if(($page = $INPUT->str('page', '', true)) != '') {
+            /** @var $plugin \DokuWiki_Admin_Plugin */
+            if($plugin = plugin_getRequestAdminPlugin()) { // FIXME this method does also permission checking
+                if($plugin->forAdminOnly() && !$INFO['isadmin']) {
+                    throw new ActionException('denied');
+                }
+                $plugin->handle();
+            }
+        }
+    }
+
+    public function tplContent() {
+        tpl_admin();
+    }
+
+}
diff --git a/inc/Action/Backlink.php b/inc/Action/Backlink.php
new file mode 100644
index 0000000000000000000000000000000000000000..0337917b35b363ab7145cf54d5dfa1cff389072f
--- /dev/null
+++ b/inc/Action/Backlink.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Backlink
+ *
+ * Shows which pages link to the current page
+ *
+ * @package dokuwiki\Action
+ */
+class Backlink extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        html_backlinks();
+    }
+
+}
diff --git a/inc/Action/Cancel.php b/inc/Action/Cancel.php
new file mode 100644
index 0000000000000000000000000000000000000000..c3e185534d94f97ad7e75e9138b135a7f75cacfe
--- /dev/null
+++ b/inc/Action/Cancel.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Cancel
+ *
+ * Alias for show. Aborts editing
+ *
+ * @package dokuwiki\Action
+ */
+class Cancel extends AbstractAliasAction {
+
+    public function preProcess() {
+        // continue with draftdel -> redirect -> show
+        throw new ActionAbort('draftdel');
+    }
+
+}
diff --git a/inc/Action/Check.php b/inc/Action/Check.php
new file mode 100644
index 0000000000000000000000000000000000000000..36ae8e8bd279da7f4581ca3f0b385b32e1097a03
--- /dev/null
+++ b/inc/Action/Check.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Check
+ *
+ * Adds some debugging info before aborting to show
+ *
+ * @package dokuwiki\Action
+ */
+class Check extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_READ;
+    }
+
+    public function preProcess() {
+        check();
+        throw new ActionAbort();
+    }
+
+}
diff --git a/inc/Action/Conflict.php b/inc/Action/Conflict.php
new file mode 100644
index 0000000000000000000000000000000000000000..d880b5b28f48d2ad7180eef33e0a7a8eb0e16b3c
--- /dev/null
+++ b/inc/Action/Conflict.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Conflict
+ *
+ * Show the conflict resolution screen
+ *
+ * @package dokuwiki\Action
+ */
+class Conflict extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        global $INFO;
+        if($INFO['exists']) {
+            return AUTH_EDIT;
+        } else {
+            return AUTH_CREATE;
+        }
+    }
+
+    public function tplContent() {
+        global $PRE;
+        global $TEXT;
+        global $SUF;
+        global $SUM;
+
+        html_conflict(con($PRE, $TEXT, $SUF), $SUM);
+        html_diff(con($PRE, $TEXT, $SUF), false);
+    }
+
+}
diff --git a/inc/Action/Denied.php b/inc/Action/Denied.php
new file mode 100644
index 0000000000000000000000000000000000000000..c8e01926265cb86dd3b373cb2c69c4830b966dca
--- /dev/null
+++ b/inc/Action/Denied.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Denied
+ *
+ * Show the access denied screen
+ *
+ * @package dokuwiki\Action
+ */
+class Denied extends AbstractAclAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    public function tplContent() {
+        html_denied();
+    }
+
+}
diff --git a/inc/Action/Diff.php b/inc/Action/Diff.php
new file mode 100644
index 0000000000000000000000000000000000000000..b14b1d04ed91c4bb47fc739b4c339080a9d4143c
--- /dev/null
+++ b/inc/Action/Diff.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Diff
+ *
+ * Show the differences between two revisions
+ *
+ * @package dokuwiki\Action
+ */
+class Diff extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_READ;
+    }
+
+    /** @inheritdoc */
+    public function preProcess() {
+        global $INPUT;
+
+        // store the selected diff type in cookie
+        $difftype = $INPUT->str('difftype');
+        if(!empty($difftype)) {
+            set_doku_pref('difftype', $difftype);
+        }
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        html_diff();
+    }
+
+}
diff --git a/inc/Action/Draft.php b/inc/Action/Draft.php
new file mode 100644
index 0000000000000000000000000000000000000000..ab678c2942eda21c13ddfe763cc3d1e78dfb8feb
--- /dev/null
+++ b/inc/Action/Draft.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Draft
+ *
+ * Screen to see and recover a draft
+ *
+ * @package dokuwiki\Action
+ * @fixme combine with Recover?
+ */
+class Draft extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        global $INFO;
+        if($INFO['exists']) {
+            return AUTH_EDIT;
+        } else {
+            return AUTH_CREATE;
+        }
+    }
+
+    /** @inheritdoc */
+    public function checkPermissions() {
+        parent::checkPermissions();
+        global $INFO;
+        if(!file_exists($INFO['draft'])) throw new ActionException('edit');
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        html_draft();
+    }
+
+}
diff --git a/inc/Action/Draftdel.php b/inc/Action/Draftdel.php
new file mode 100644
index 0000000000000000000000000000000000000000..77378f7cb8086ee0e5c65508585bc70326e2f698
--- /dev/null
+++ b/inc/Action/Draftdel.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Draftdel
+ *
+ * Delete a draft
+ *
+ * @package dokuwiki\Action
+ */
+class Draftdel extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_EDIT;
+    }
+
+    /**
+     * Delete an existing draft if any
+     *
+     * Reads draft information from $INFO. Redirects to show, afterwards.
+     *
+     * @throws ActionAbort
+     */
+    public function preProcess() {
+        global $INFO;
+        @unlink($INFO['draft']);
+        $INFO['draft'] = null;
+
+        throw new ActionAbort('redirect');
+    }
+
+}
diff --git a/inc/Action/Edit.php b/inc/Action/Edit.php
new file mode 100644
index 0000000000000000000000000000000000000000..7483516811ccd85e9315846921540a0aae19a2c9
--- /dev/null
+++ b/inc/Action/Edit.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Edit
+ *
+ * Handle editing
+ *
+ * @package dokuwiki\Action
+ */
+class Edit extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        global $INFO;
+        if($INFO['exists']) {
+            return AUTH_READ; // we check again below
+        } else {
+            return AUTH_CREATE;
+        }
+    }
+
+    /**
+     * @inheritdoc falls back to 'source' if page not writable
+     */
+    public function checkPermissions() {
+        parent::checkPermissions();
+        global $INFO;
+
+        // no edit permission? view source
+        if($INFO['exists'] && !$INFO['writable']) {
+            throw new ActionAbort('source');
+        }
+    }
+
+    /** @inheritdoc */
+    public function preProcess() {
+        global $ID;
+        global $INFO;
+
+        global $TEXT;
+        global $RANGE;
+        global $PRE;
+        global $SUF;
+        global $REV;
+        global $SUM;
+        global $lang;
+        global $DATE;
+
+        if(!isset($TEXT)) {
+            if($INFO['exists']) {
+                if($RANGE) {
+                    list($PRE, $TEXT, $SUF) = rawWikiSlices($RANGE, $ID, $REV);
+                } else {
+                    $TEXT = rawWiki($ID, $REV);
+                }
+            } else {
+                $TEXT = pageTemplate($ID);
+            }
+        }
+
+        //set summary default
+        if(!$SUM) {
+            if($REV) {
+                $SUM = sprintf($lang['restored'], dformat($REV));
+            } elseif(!$INFO['exists']) {
+                $SUM = $lang['created'];
+            }
+        }
+
+        // Use the date of the newest revision, not of the revision we edit
+        // This is used for conflict detection
+        if(!$DATE) $DATE = @filemtime(wikiFN($ID));
+
+        //check if locked by anyone - if not lock for my self
+        $lockedby = checklock($ID);
+        if($lockedby) {
+            throw new ActionAbort('locked');
+        };
+        lock($ID);
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        html_edit();
+    }
+
+}
diff --git a/inc/Action/Exception/ActionAbort.php b/inc/Action/Exception/ActionAbort.php
new file mode 100644
index 0000000000000000000000000000000000000000..9c188bb4b30ea0ac7ffddd3956fe5efd4cb74eac
--- /dev/null
+++ b/inc/Action/Exception/ActionAbort.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class ActionAbort
+ *
+ * Strictly speaking not an Exception but an expected execution path. Used to
+ * signal when one action is done and another should take over.
+ *
+ * If you want to signal the same but under some error condition use ActionException
+ * or one of it's decendants.
+ *
+ * The message will NOT be shown to the enduser
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class ActionAbort extends ActionException {
+
+}
diff --git a/inc/Action/Exception/ActionAclRequiredException.php b/inc/Action/Exception/ActionAclRequiredException.php
new file mode 100644
index 0000000000000000000000000000000000000000..64a2c61e3e156da01873eed41bfaefe891240067
--- /dev/null
+++ b/inc/Action/Exception/ActionAclRequiredException.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class ActionAclRequiredException
+ *
+ * Thrown by AbstractACLAction when an action requires that the ACL subsystem is
+ * enabled but it isn't. You should not use it
+ *
+ * The message will NOT be shown to the enduser
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class ActionAclRequiredException extends ActionException {
+
+}
diff --git a/inc/Action/Exception/ActionDisabledException.php b/inc/Action/Exception/ActionDisabledException.php
new file mode 100644
index 0000000000000000000000000000000000000000..40a0c7dd70fdc1f3c8a6c2066ec6ce3e83e15ae9
--- /dev/null
+++ b/inc/Action/Exception/ActionDisabledException.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class ActionDisabledException
+ *
+ * Thrown when the requested action has been disabled. Eg. through the 'disableactions'
+ * config setting. You should probably not use it.
+ *
+ * The message will NOT be shown to the enduser, but a generic information will be shown.
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class ActionDisabledException extends ActionException {
+
+}
diff --git a/inc/Action/Exception/ActionException.php b/inc/Action/Exception/ActionException.php
new file mode 100644
index 0000000000000000000000000000000000000000..381584c15b483c7901ba2efd07d156c7e5d74eaf
--- /dev/null
+++ b/inc/Action/Exception/ActionException.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class ActionException
+ *
+ * This exception and its subclasses signal that the current action should be
+ * aborted and a different action should be used instead. The new action can
+ * be given as parameter in the constructor. Defaults to 'show'
+ *
+ * The message will NOT be shown to the enduser
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class ActionException extends \Exception {
+
+    /** @var string the new action */
+    protected $newaction;
+
+    /** @var bool should the exception's message be shown to the user? */
+    protected $displayToUser = false;
+
+    /**
+     * ActionException constructor.
+     *
+     * When no new action is given 'show' is assumed. For requests that originated in a POST,
+     * a 'redirect' is used which will cause a redirect to the 'show' action.
+     *
+     * @param string|null $newaction the action that should be used next
+     * @param string $message optional message, will not be shown except for some dub classes
+     */
+    public function __construct($newaction = null, $message = '') {
+        global $INPUT;
+        parent::__construct($message);
+        if(is_null($newaction)) {
+            if(strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post') {
+                $newaction = 'redirect';
+            } else {
+                $newaction = 'show';
+            }
+        }
+
+        $this->newaction = $newaction;
+    }
+
+    /**
+     * Returns the action to use next
+     *
+     * @return string
+     */
+    public function getNewAction() {
+        return $this->newaction;
+    }
+
+    /**
+     * Should this Exception's message be shown to the user?
+     *
+     * @param null|bool $set when null is given, the current setting is not changed
+     * @return bool
+     */
+    public function displayToUser($set = null) {
+        if(!is_null($set)) $this->displayToUser = $set;
+        return $set;
+    }
+}
diff --git a/inc/Action/Exception/ActionUserRequiredException.php b/inc/Action/Exception/ActionUserRequiredException.php
new file mode 100644
index 0000000000000000000000000000000000000000..aab06cca11fd4f6be10bc59cdd4ae2ac407df329
--- /dev/null
+++ b/inc/Action/Exception/ActionUserRequiredException.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class ActionUserRequiredException
+ *
+ * Thrown by AbstractUserAction when an action requires that a user is logged
+ * in but it isn't. You should not use it.
+ *
+ * The message will NOT be shown to the enduser
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class ActionUserRequiredException extends ActionException {
+
+}
diff --git a/inc/Action/Exception/FatalException.php b/inc/Action/Exception/FatalException.php
new file mode 100644
index 0000000000000000000000000000000000000000..5f2516fa2edd0304a98b41dd1a3dbe7d6dbc3bbe
--- /dev/null
+++ b/inc/Action/Exception/FatalException.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class FatalException
+ *
+ * A fatal exception during handling the action
+ *
+ * Will abort all handling and display some info to the user. The HTTP status code
+ * can be defined.
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class FatalException extends \Exception {
+
+    protected $status;
+
+    /**
+     * FatalException constructor.
+     *
+     * @param string $message the message to send
+     * @param int $status the HTTP status to send
+     * @param null|\Exception $previous previous exception
+     */
+    public function __construct($message = 'A fatal error occured', $status = 500, $previous = null) {
+        parent::__construct($message, $status, $previous);
+    }
+}
diff --git a/inc/Action/Exception/NoActionException.php b/inc/Action/Exception/NoActionException.php
new file mode 100644
index 0000000000000000000000000000000000000000..1c4e4d0944ebe6402b573ae5198dd5d75f8e59c0
--- /dev/null
+++ b/inc/Action/Exception/NoActionException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace dokuwiki\Action\Exception;
+
+/**
+ * Class NoActionException
+ *
+ * Thrown in the ActionRouter when a wanted action can not be found. Triggers
+ * the unknown action event
+ *
+ * @package dokuwiki\Action\Exception
+ */
+class NoActionException extends \Exception {
+
+}
diff --git a/inc/Action/Export.php b/inc/Action/Export.php
new file mode 100644
index 0000000000000000000000000000000000000000..1eec27ec3a260e5677cfe684cce59b7f80d79526
--- /dev/null
+++ b/inc/Action/Export.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Export
+ *
+ * Handle exporting by calling the appropriate renderer
+ *
+ * @package dokuwiki\Action
+ */
+class Export extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_READ;
+    }
+
+    /**
+     * Export a wiki page for various formats
+     *
+     * Triggers ACTION_EXPORT_POSTPROCESS
+     *
+     *  Event data:
+     *    data['id']      -- page id
+     *    data['mode']    -- requested export mode
+     *    data['headers'] -- export headers
+     *    data['output']  -- export output
+     *
+     * @author Andreas Gohr <andi@splitbrain.org>
+     * @author Michael Klier <chi@chimeric.de>
+     * @inheritdoc
+     */
+    public function preProcess() {
+        global $ID;
+        global $REV;
+        global $conf;
+        global $lang;
+
+        $pre = '';
+        $post = '';
+        $headers = array();
+
+        // search engines: never cache exported docs! (Google only currently)
+        $headers['X-Robots-Tag'] = 'noindex';
+
+        $mode = substr($this->actionname, 7);
+        switch($mode) {
+            case 'raw':
+                $headers['Content-Type'] = 'text/plain; charset=utf-8';
+                $headers['Content-Disposition'] = 'attachment; filename=' . noNS($ID) . '.txt';
+                $output = rawWiki($ID, $REV);
+                break;
+            case 'xhtml':
+                $pre .= '<!DOCTYPE html>' . DOKU_LF;
+                $pre .= '<html lang="' . $conf['lang'] . '" dir="' . $lang['direction'] . '">' . DOKU_LF;
+                $pre .= '<head>' . DOKU_LF;
+                $pre .= '  <meta charset="utf-8" />' . DOKU_LF; // FIXME improve wrapper
+                $pre .= '  <title>' . $ID . '</title>' . DOKU_LF;
+
+                // get metaheaders
+                ob_start();
+                tpl_metaheaders();
+                $pre .= ob_get_clean();
+
+                $pre .= '</head>' . DOKU_LF;
+                $pre .= '<body>' . DOKU_LF;
+                $pre .= '<div class="dokuwiki export">' . DOKU_LF;
+
+                // get toc
+                $pre .= tpl_toc(true);
+
+                $headers['Content-Type'] = 'text/html; charset=utf-8';
+                $output = p_wiki_xhtml($ID, $REV, false);
+
+                $post .= '</div>' . DOKU_LF;
+                $post .= '</body>' . DOKU_LF;
+                $post .= '</html>' . DOKU_LF;
+                break;
+            case 'xhtmlbody':
+                $headers['Content-Type'] = 'text/html; charset=utf-8';
+                $output = p_wiki_xhtml($ID, $REV, false);
+                break;
+            default:
+                $output = p_cached_output(wikiFN($ID, $REV), $mode, $ID);
+                $headers = p_get_metadata($ID, "format $mode");
+                break;
+        }
+
+        // prepare event data
+        $data = array();
+        $data['id'] = $ID;
+        $data['mode'] = $mode;
+        $data['headers'] = $headers;
+        $data['output'] =& $output;
+
+        trigger_event('ACTION_EXPORT_POSTPROCESS', $data);
+
+        if(!empty($data['output'])) {
+            if(is_array($data['headers'])) foreach($data['headers'] as $key => $val) {
+                header("$key: $val");
+            }
+            print $pre . $data['output'] . $post;
+            exit;
+        }
+
+        throw new ActionAbort();
+    }
+
+}
diff --git a/inc/Action/Index.php b/inc/Action/Index.php
new file mode 100644
index 0000000000000000000000000000000000000000..c87a3f89cd97ace23baffae2d5d83346084b25d6
--- /dev/null
+++ b/inc/Action/Index.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Index
+ *
+ * Show the human readable sitemap. Do not confuse with Sitemap
+ *
+ * @package dokuwiki\Action
+ */
+class Index extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        global $IDX;
+        html_index($IDX);
+    }
+
+}
diff --git a/inc/Action/Locked.php b/inc/Action/Locked.php
new file mode 100644
index 0000000000000000000000000000000000000000..3ff2c5b8016be2977978d8dcdefd6f94a2ffe73d
--- /dev/null
+++ b/inc/Action/Locked.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Locked
+ *
+ * Show a locked screen when a page is locked
+ *
+ * @package dokuwiki\Action
+ */
+class Locked extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_READ;
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        html_locked();
+    }
+
+}
diff --git a/inc/Action/Login.php b/inc/Action/Login.php
new file mode 100644
index 0000000000000000000000000000000000000000..6e4aeb01ab2900515fb6331f65f9ac7e571852a2
--- /dev/null
+++ b/inc/Action/Login.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Login
+ *
+ * The login form. Actual logins are handled in inc/auth.php
+ *
+ * @package dokuwiki\Action
+ */
+class Login extends AbstractAclAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /** @inheritdoc */
+    public function checkPermissions() {
+        global $INPUT;
+        parent::checkPermissions();
+        if($INPUT->server->has('REMOTE_USER')) {
+            // nothing to do
+            throw new ActionException();
+        }
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        html_login();
+    }
+
+}
diff --git a/inc/Action/Logout.php b/inc/Action/Logout.php
new file mode 100644
index 0000000000000000000000000000000000000000..2abf968f6203952d52c9fa0f7b14c2f4c5ee24aa
--- /dev/null
+++ b/inc/Action/Logout.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionDisabledException;
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Logout
+ *
+ * Log out a user
+ *
+ * @package dokuwiki\Action
+ */
+class Logout extends AbstractUserAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /** @inheritdoc */
+    public function checkPermissions() {
+        parent::checkPermissions();
+
+        /** @var \DokuWiki_Auth_Plugin $auth */
+        global $auth;
+        if(!$auth->canDo('logout')) throw new ActionDisabledException();
+    }
+
+    /** @inheritdoc */
+    public function preProcess() {
+        global $ID;
+        global $INPUT;
+
+        // when logging out during an edit session, unlock the page
+        $lockedby = checklock($ID);
+        if($lockedby == $INPUT->server->str('REMOTE_USER')) {
+            unlock($ID);
+        }
+
+        // do the logout stuff and redirect to login
+        auth_logoff();
+        send_redirect(wl($ID, array('do' => 'login')));
+
+        // should never be reached
+        throw new ActionException('login');
+    }
+
+}
diff --git a/inc/Action/Media.php b/inc/Action/Media.php
new file mode 100644
index 0000000000000000000000000000000000000000..77a2a6f0d45cbd3d21c0d0d60dca2c166fac70c3
--- /dev/null
+++ b/inc/Action/Media.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Media
+ *
+ * The full screen media manager
+ *
+ * @package dokuwiki\Action
+ */
+class Media extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_READ;
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        tpl_media();
+    }
+
+}
diff --git a/inc/Action/Plugin.php b/inc/Action/Plugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..c3e16bf875520e10df3bff225a7b76f1e78a041c
--- /dev/null
+++ b/inc/Action/Plugin.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Plugin
+ *
+ * Used to run action plugins
+ *
+ * @package dokuwiki\Action
+ */
+class Plugin extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /**
+     * Outputs nothing but a warning unless an action plugin overwrites it
+     *
+     * @inheritdoc
+     * @triggers TPL_ACT_UNKNOWN
+     */
+    public function tplContent() {
+        $evt = new \Doku_Event('TPL_ACT_UNKNOWN', $this->actionname);
+        if($evt->advise_before()) {
+            msg('Failed to handle action: ' . hsc($this->actionname), -1);
+        }
+        $evt->advise_after();
+    }
+}
diff --git a/inc/Action/Preview.php b/inc/Action/Preview.php
new file mode 100644
index 0000000000000000000000000000000000000000..850b2049afed7197a7884e6c648d346c2ab3334f
--- /dev/null
+++ b/inc/Action/Preview.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Preview
+ *
+ * preview during editing
+ *
+ * @package dokuwiki\Action
+ */
+class Preview extends Edit {
+
+    /** @inheritdoc */
+    public function preProcess() {
+        header('X-XSS-Protection: 0');
+        $this->savedraft();
+        parent::preProcess();
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        global $TEXT;
+        html_edit();
+        html_show($TEXT);
+    }
+
+    /**
+     * Saves a draft on preview
+     */
+    protected function savedraft() {
+        global $INFO;
+        global $ID;
+        global $INPUT;
+        global $conf;
+
+        if(!$conf['usedraft']) return;
+        if(!$INPUT->post->has('wikitext')) return;
+
+        // ensure environment (safeguard when used via AJAX)
+        assert(isset($INFO['client']), 'INFO.client should have been set');
+        assert(isset($ID), 'ID should have been set');
+
+        $draft = array(
+            'id' => $ID,
+            'prefix' => substr($INPUT->post->str('prefix'), 0, -1),
+            'text' => $INPUT->post->str('wikitext'),
+            'suffix' => $INPUT->post->str('suffix'),
+            'date' => $INPUT->post->int('date'),
+            'client' => $INFO['client'],
+        );
+        $cname = getCacheName($draft['client'] . $ID, '.draft');
+        if(io_saveFile($cname, serialize($draft))) {
+            $INFO['draft'] = $cname;
+        }
+    }
+
+}
diff --git a/inc/Action/Profile.php b/inc/Action/Profile.php
new file mode 100644
index 0000000000000000000000000000000000000000..1ebe51fec700a7f0d3db52b28ab1935609671e78
--- /dev/null
+++ b/inc/Action/Profile.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionDisabledException;
+
+/**
+ * Class Profile
+ *
+ * Handle the profile form
+ *
+ * @package dokuwiki\Action
+ */
+class Profile extends AbstractUserAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /** @inheritdoc */
+    public function checkPermissions() {
+        parent::checkPermissions();
+
+        /** @var \DokuWiki_Auth_Plugin $auth */
+        global $auth;
+        if(!$auth->canDo('Profile')) throw new ActionDisabledException();
+    }
+
+    /** @inheritdoc */
+    public function preProcess() {
+        global $lang;
+        if(updateprofile()) {
+            msg($lang['profchanged'], 1);
+            throw new ActionAbort('show');
+        }
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        html_updateprofile();
+    }
+
+}
diff --git a/inc/Action/ProfileDelete.php b/inc/Action/ProfileDelete.php
new file mode 100644
index 0000000000000000000000000000000000000000..5be5ff57870660f06e5c0ed5ae97edbe2b2eb269
--- /dev/null
+++ b/inc/Action/ProfileDelete.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionDisabledException;
+
+/**
+ * Class ProfileDelete
+ *
+ * Delete a user account
+ *
+ * @package dokuwiki\Action
+ */
+class ProfileDelete extends AbstractUserAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /** @inheritdoc */
+    public function checkPermissions() {
+        parent::checkPermissions();
+
+        /** @var \DokuWiki_Auth_Plugin $auth */
+        global $auth;
+        if(!$auth->canDo('delUser')) throw new ActionDisabledException();
+    }
+
+    /** @inheritdoc */
+    public function preProcess() {
+        global $lang;
+        if(auth_deleteprofile()) {
+            msg($lang['profdeleted'], 1);
+            throw new ActionAbort('show');
+        } else {
+            throw new ActionAbort('profile');
+        }
+    }
+
+}
diff --git a/inc/Action/Recent.php b/inc/Action/Recent.php
new file mode 100644
index 0000000000000000000000000000000000000000..4fb3e41544aed46413fa5967bef9c32450a2b8df
--- /dev/null
+++ b/inc/Action/Recent.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Recent
+ *
+ * The recent changes view
+ *
+ * @package dokuwiki\Action
+ */
+class Recent extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /** @inheritdoc */
+    public function preProcess() {
+        global $INPUT;
+        $show_changes = $INPUT->str('show_changes');
+        if(!empty($show_changes)) {
+            set_doku_pref('show_changes', $show_changes);
+        }
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        global $INPUT;
+        html_recent((int) $INPUT->extract('first')->int('first'));
+    }
+
+}
diff --git a/inc/Action/Recover.php b/inc/Action/Recover.php
new file mode 100644
index 0000000000000000000000000000000000000000..7966396b90bb28ed94b2dea95660f91ee80b5c1f
--- /dev/null
+++ b/inc/Action/Recover.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Recover
+ *
+ * Recover a draft
+ *
+ * @package dokuwiki\Action
+ */
+class Recover extends AbstractAliasAction {
+
+    /** @inheritdoc */
+    public function preProcess() {
+        throw new ActionAbort('edit');
+    }
+
+}
diff --git a/inc/Action/Redirect.php b/inc/Action/Redirect.php
new file mode 100644
index 0000000000000000000000000000000000000000..0e989d1ffed4a9a61e68b159ea036186db3e030f
--- /dev/null
+++ b/inc/Action/Redirect.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Redirect
+ *
+ * Used to redirect to the current page with the last edited section as a target if found
+ *
+ * @package dokuwiki\Action
+ */
+class Redirect extends AbstractAliasAction {
+
+    /**
+     * Redirect to the show action, trying to jump to the previously edited section
+     *
+     * @triggers ACTION_SHOW_REDIRECT
+     * @throws ActionAbort
+     */
+    public function preProcess() {
+        global $PRE;
+        global $TEXT;
+        global $INPUT;
+        global $ID;
+        global $ACT;
+
+        $opts = array(
+            'id' => $ID,
+            'preact' => $ACT
+        );
+        //get section name when coming from section edit
+        if($INPUT->has('hid')) {
+            // Use explicitly transmitted header id
+            $opts['fragment'] = $INPUT->str('hid');
+        } else if($PRE && preg_match('/^\s*==+([^=\n]+)/', $TEXT, $match)) {
+            // Fallback to old mechanism
+            $check = false; //Byref
+            $opts['fragment'] = sectionID($match[0], $check);
+        }
+
+        // execute the redirect
+        trigger_event('ACTION_SHOW_REDIRECT', $opts, array($this, 'redirect'));
+
+        // should never be reached
+        throw new ActionAbort();
+    }
+
+    /**
+     * Execute the redirect
+     *
+     * Default action for ACTION_SHOW_REDIRECT
+     *
+     * @param array $opts id and fragment for the redirect and the preact
+     */
+    public function redirect($opts) {
+        $go = wl($opts['id'], '', true);
+        if(isset($opts['fragment'])) $go .= '#' . $opts['fragment'];
+
+        //show it
+        send_redirect($go);
+    }
+}
diff --git a/inc/Action/Register.php b/inc/Action/Register.php
new file mode 100644
index 0000000000000000000000000000000000000000..c97d3f8582d0f0f6c6f9467c17476a1fe47e3051
--- /dev/null
+++ b/inc/Action/Register.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionDisabledException;
+
+/**
+ * Class Register
+ *
+ * Self registering a new user
+ *
+ * @package dokuwiki\Action
+ */
+class Register extends AbstractAclAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /** @inheritdoc */
+    public function checkPermissions() {
+        parent::checkPermissions();
+
+        /** @var \DokuWiki_Auth_Plugin $auth */
+        global $auth;
+        global $conf;
+        if(isset($conf['openregister']) && !$conf['openregister']) throw new ActionDisabledException();
+        if(!$auth->canDo('addUser')) throw new ActionDisabledException();
+    }
+
+    /** @inheritdoc */
+    public function preProcess() {
+        if(register()) { // FIXME could be moved from auth to here
+            throw new ActionAbort('login');
+        }
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        html_register();
+    }
+
+}
diff --git a/inc/Action/Resendpwd.php b/inc/Action/Resendpwd.php
new file mode 100644
index 0000000000000000000000000000000000000000..466e078a41fa54fa3e7d440165fd9b4f5a9f38a7
--- /dev/null
+++ b/inc/Action/Resendpwd.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionDisabledException;
+
+/**
+ * Class Resendpwd
+ *
+ * Handle password recovery
+ *
+ * @package dokuwiki\Action
+ */
+class Resendpwd extends AbstractAclAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /** @inheritdoc */
+    public function checkPermissions() {
+        parent::checkPermissions();
+
+        /** @var \DokuWiki_Auth_Plugin $auth */
+        global $auth;
+        global $conf;
+        if(isset($conf['resendpasswd']) && !$conf['resendpasswd']) throw new ActionDisabledException(); //legacy option
+        if(!$auth->canDo('modPass')) throw new ActionDisabledException();
+    }
+
+    /** @inheritdoc */
+    public function preProcess() {
+        if($this->resendpwd()) {
+            throw new ActionAbort('login');
+        }
+    }
+
+    /**
+     * Send a  new password
+     *
+     * This function handles both phases of the password reset:
+     *
+     *   - handling the first request of password reset
+     *   - validating the password reset auth token
+     *
+     * @author Benoit Chesneau <benoit@bchesneau.info>
+     * @author Chris Smith <chris@jalakai.co.uk>
+     * @author Andreas Gohr <andi@splitbrain.org>
+     * @fixme this should be split up into multiple methods
+     * @return bool true on success, false on any error
+     */
+    function resendpwd() {
+        global $lang;
+        global $conf;
+        /* @var \DokuWiki_Auth_Plugin $auth */
+        global $auth;
+        global $INPUT;
+
+        if(!actionOK('resendpwd')) {
+            msg($lang['resendna'], -1);
+            return false;
+        }
+
+        $token = preg_replace('/[^a-f0-9]+/', '', $INPUT->str('pwauth'));
+
+        if($token) {
+            // we're in token phase - get user info from token
+
+            $tfile = $conf['cachedir'] . '/' . $token{0} . '/' . $token . '.pwauth';
+            if(!file_exists($tfile)) {
+                msg($lang['resendpwdbadauth'], -1);
+                $INPUT->remove('pwauth');
+                return false;
+            }
+            // token is only valid for 3 days
+            if((time() - filemtime($tfile)) > (3 * 60 * 60 * 24)) {
+                msg($lang['resendpwdbadauth'], -1);
+                $INPUT->remove('pwauth');
+                @unlink($tfile);
+                return false;
+            }
+
+            $user = io_readfile($tfile);
+            $userinfo = $auth->getUserData($user, $requireGroups = false);
+            if(!$userinfo['mail']) {
+                msg($lang['resendpwdnouser'], -1);
+                return false;
+            }
+
+            if(!$conf['autopasswd']) { // we let the user choose a password
+                $pass = $INPUT->str('pass');
+
+                // password given correctly?
+                if(!$pass) return false;
+                if($pass != $INPUT->str('passchk')) {
+                    msg($lang['regbadpass'], -1);
+                    return false;
+                }
+
+                // change it
+                if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
+                    msg($lang['proffail'], -1);
+                    return false;
+                }
+
+            } else { // autogenerate the password and send by mail
+
+                $pass = auth_pwgen($user);
+                if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
+                    msg($lang['proffail'], -1);
+                    return false;
+                }
+
+                if(auth_sendPassword($user, $pass)) {
+                    msg($lang['resendpwdsuccess'], 1);
+                } else {
+                    msg($lang['regmailfail'], -1);
+                }
+            }
+
+            @unlink($tfile);
+            return true;
+
+        } else {
+            // we're in request phase
+
+            if(!$INPUT->post->bool('save')) return false;
+
+            if(!$INPUT->post->str('login')) {
+                msg($lang['resendpwdmissing'], -1);
+                return false;
+            } else {
+                $user = trim($auth->cleanUser($INPUT->post->str('login')));
+            }
+
+            $userinfo = $auth->getUserData($user, $requireGroups = false);
+            if(!$userinfo['mail']) {
+                msg($lang['resendpwdnouser'], -1);
+                return false;
+            }
+
+            // generate auth token
+            $token = md5(auth_randombytes(16)); // random secret
+            $tfile = $conf['cachedir'] . '/' . $token{0} . '/' . $token . '.pwauth';
+            $url = wl('', array('do' => 'resendpwd', 'pwauth' => $token), true, '&');
+
+            io_saveFile($tfile, $user);
+
+            $text = rawLocale('pwconfirm');
+            $trep = array(
+                'FULLNAME' => $userinfo['name'],
+                'LOGIN' => $user,
+                'CONFIRM' => $url
+            );
+
+            $mail = new \Mailer();
+            $mail->to($userinfo['name'] . ' <' . $userinfo['mail'] . '>');
+            $mail->subject($lang['regpwmail']);
+            $mail->setBody($text, $trep);
+            if($mail->send()) {
+                msg($lang['resendpwdconfirm'], 1);
+            } else {
+                msg($lang['regmailfail'], -1);
+            }
+            return true;
+        }
+        // never reached
+    }
+
+}
diff --git a/inc/Action/Revert.php b/inc/Action/Revert.php
new file mode 100644
index 0000000000000000000000000000000000000000..ca35374f251858ae3e7148f4b8cefecafd403034
--- /dev/null
+++ b/inc/Action/Revert.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Revert
+ *
+ * Quick revert to an old revision
+ *
+ * @package dokuwiki\Action
+ */
+class Revert extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        global $INFO;
+        if($INFO['ismanager']) {
+            return AUTH_EDIT;
+        } else {
+            return AUTH_ADMIN;
+        }
+    }
+
+    /**
+     *
+     * @inheritdoc
+     * @throws ActionAbort
+     * @throws ActionException
+     * @todo check for writability of the current page ($INFO might do it wrong and check the attic version)
+     */
+    public function preProcess() {
+        if(!checkSecurityToken()) throw new ActionException();
+
+        global $ID;
+        global $REV;
+        global $lang;
+
+        // when no revision is given, delete current one
+        // FIXME this feature is not exposed in the GUI currently
+        $text = '';
+        $sum = $lang['deleted'];
+        if($REV) {
+            $text = rawWiki($ID, $REV);
+            if(!$text) throw new ActionException(); //something went wrong
+            $sum = sprintf($lang['restored'], dformat($REV));
+        }
+
+        // spam check
+        if(checkwordblock($text)) {
+            msg($lang['wordblock'], -1);
+            throw new ActionException('edit');
+        }
+
+        saveWikiText($ID, $text, $sum, false);
+        msg($sum, 1);
+        $REV = '';
+
+        // continue with draftdel -> redirect -> show
+        throw new ActionAbort('draftdel');
+    }
+
+}
diff --git a/inc/Action/Revisions.php b/inc/Action/Revisions.php
new file mode 100644
index 0000000000000000000000000000000000000000..b8db531c78ae88d69dbfd5b12c68ee717ef872aa
--- /dev/null
+++ b/inc/Action/Revisions.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Revisions
+ *
+ * Show the list of old revisions of the current page
+ *
+ * @package dokuwiki\Action
+ */
+class Revisions extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_READ;
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        global $INPUT;
+        html_revisions($INPUT->int('first'));
+    }
+}
diff --git a/inc/Action/Save.php b/inc/Action/Save.php
new file mode 100644
index 0000000000000000000000000000000000000000..0b247298303e67d422914458b3dabd929113d7cc
--- /dev/null
+++ b/inc/Action/Save.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionException;
+
+/**
+ * Class Save
+ *
+ * Save at the end of an edit session
+ *
+ * @package dokuwiki\Action
+ */
+class Save extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        global $INFO;
+        if($INFO['exists']) {
+            return AUTH_EDIT;
+        } else {
+            return AUTH_CREATE;
+        }
+    }
+
+    /** @inheritdoc */
+    public function preProcess() {
+        if(!checkSecurityToken()) throw new ActionException('preview');
+
+        global $ID;
+        global $DATE;
+        global $PRE;
+        global $TEXT;
+        global $SUF;
+        global $SUM;
+        global $lang;
+        global $INFO;
+        global $INPUT;
+
+        //spam check
+        if(checkwordblock()) {
+            msg($lang['wordblock'], -1);
+            throw new ActionException('edit');
+        }
+        //conflict check
+        if($DATE != 0 && $INFO['meta']['date']['modified'] > $DATE) {
+            throw new ActionException('conflict');
+        }
+
+        //save it
+        saveWikiText($ID, con($PRE, $TEXT, $SUF, true), $SUM, $INPUT->bool('minor')); //use pretty mode for con
+        //unlock it
+        unlock($ID);
+
+        // continue with draftdel -> redirect -> show
+        throw new ActionAbort('draftdel');
+    }
+
+}
diff --git a/inc/Action/Search.php b/inc/Action/Search.php
new file mode 100644
index 0000000000000000000000000000000000000000..d4833f4539c7e828fc4b79003e596cb7a6a2bdde
--- /dev/null
+++ b/inc/Action/Search.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+
+/**
+ * Class Search
+ *
+ * Search for pages and content
+ *
+ * @package dokuwiki\Action
+ */
+class Search extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /**
+     * we only search if a search word was given
+     *
+     * @inheritdoc
+     */
+    public function checkPermissions() {
+        parent::checkPermissions();
+        global $QUERY;
+        $s = cleanID($QUERY);
+        if($s === '') throw new ActionAbort();
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        html_search();
+    }
+}
diff --git a/inc/Action/Show.php b/inc/Action/Show.php
new file mode 100644
index 0000000000000000000000000000000000000000..6dbe9a15c253755484c7fc384bd4e1e660f46374
--- /dev/null
+++ b/inc/Action/Show.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Created by IntelliJ IDEA.
+ * User: andi
+ * Date: 2/10/17
+ * Time: 4:32 PM
+ */
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Show
+ *
+ * The default action of showing a page
+ *
+ * @package dokuwiki\Action
+ */
+class Show extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_READ;
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        html_show();
+    }
+
+}
diff --git a/inc/Action/Sitemap.php b/inc/Action/Sitemap.php
new file mode 100644
index 0000000000000000000000000000000000000000..025c5153c9d59a4f7bad216fc5554f8114ef08b4
--- /dev/null
+++ b/inc/Action/Sitemap.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\FatalException;
+
+/**
+ * Class Sitemap
+ *
+ * Generate an XML sitemap for search engines. Do not confuse with Index
+ *
+ * @package dokuwiki\Action
+ */
+class Sitemap extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_NONE;
+    }
+
+    /**
+     * Handle sitemap delivery
+     *
+     * @author Michael Hamann <michael@content-space.de>
+     * @throws FatalException
+     * @inheritdoc
+     */
+    public function preProcess() {
+        global $conf;
+
+        if($conf['sitemap'] < 1 || !is_numeric($conf['sitemap'])) {
+            throw new FatalException(404, 'Sitemap generation is disabled');
+        }
+
+        $sitemap = \Sitemapper::getFilePath();
+        if(\Sitemapper::sitemapIsCompressed()) {
+            $mime = 'application/x-gzip';
+        } else {
+            $mime = 'application/xml; charset=utf-8';
+        }
+
+        // Check if sitemap file exists, otherwise create it
+        if(!is_readable($sitemap)) {
+            \Sitemapper::generate();
+        }
+
+        if(is_readable($sitemap)) {
+            // Send headers
+            header('Content-Type: ' . $mime);
+            header('Content-Disposition: attachment; filename=' . utf8_basename($sitemap));
+
+            http_conditionalRequest(filemtime($sitemap));
+
+            // Send file
+            //use x-sendfile header to pass the delivery to compatible webservers
+            http_sendfile($sitemap);
+
+            readfile($sitemap);
+            exit;
+        }
+
+        throw new FatalException(500, 'Could not read the sitemap file - bad permissions?');
+    }
+
+}
diff --git a/inc/Action/Source.php b/inc/Action/Source.php
new file mode 100644
index 0000000000000000000000000000000000000000..fa3c88a4f648088b9494501c34282f1e68ffd6f1
--- /dev/null
+++ b/inc/Action/Source.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace dokuwiki\Action;
+
+/**
+ * Class Source
+ *
+ * Show the source of a page
+ *
+ * @package dokuwiki\Action
+ */
+class Source extends AbstractAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_READ;
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        html_edit(); // FIXME is this correct? Should we split it off completely?
+    }
+
+}
diff --git a/inc/Action/Subscribe.php b/inc/Action/Subscribe.php
new file mode 100644
index 0000000000000000000000000000000000000000..94920c428784dc332abf2d557da296647eec6ece
--- /dev/null
+++ b/inc/Action/Subscribe.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace dokuwiki\Action;
+
+use dokuwiki\Action\Exception\ActionAbort;
+use dokuwiki\Action\Exception\ActionDisabledException;
+
+/**
+ * Class Subscribe
+ *
+ * E-Mail subscription handling
+ *
+ * @package dokuwiki\Action
+ */
+class Subscribe extends AbstractUserAction {
+
+    /** @inheritdoc */
+    public function minimumPermission() {
+        return AUTH_READ;
+    }
+
+    /** @inheritdoc */
+    public function checkPermissions() {
+        parent::checkPermissions();
+
+        global $conf;
+        if(isset($conf['subscribers']) && !$conf['subscribers']) throw new ActionDisabledException();
+    }
+
+    /** @inheritdoc */
+    public function preProcess() {
+        try {
+            $this->handleSubscribeData();
+        } catch(ActionAbort $e) {
+            throw $e;
+        } catch(\Exception $e) {
+            msg($e->getMessage(), -1);
+        }
+    }
+
+    /** @inheritdoc */
+    public function tplContent() {
+        tpl_subscribe();
+    }
+
+    /**
+     * Handle page 'subscribe'
+     *
+     * @author Adrian Lang <lang@cosmocode.de>
+     * @throws \Exception if (un)subscribing fails
+     * @throws ActionAbort when (un)subscribing worked
+     */
+    protected function handleSubscribeData() {
+        global $lang;
+        global $INFO;
+        global $INPUT;
+
+        // get and preprocess data.
+        $params = array();
+        foreach(array('target', 'style', 'action') as $param) {
+            if($INPUT->has("sub_$param")) {
+                $params[$param] = $INPUT->str("sub_$param");
+            }
+        }
+
+        // any action given? if not just return and show the subscription page
+        if(empty($params['action']) || !checkSecurityToken()) return;
+
+        // Handle POST data, may throw exception.
+        trigger_event('ACTION_HANDLE_SUBSCRIBE', $params, array($this, 'handlePostData'));
+
+        $target = $params['target'];
+        $style = $params['style'];
+        $action = $params['action'];
+
+        // Perform action.
+        $sub = new \Subscription();
+        if($action == 'unsubscribe') {
+            $ok = $sub->remove($target, $INPUT->server->str('REMOTE_USER'), $style);
+        } else {
+            $ok = $sub->add($target, $INPUT->server->str('REMOTE_USER'), $style);
+        }
+
+        if($ok) {
+            msg(
+                sprintf(
+                    $lang["subscr_{$action}_success"], hsc($INFO['userinfo']['name']),
+                    prettyprint_id($target)
+                ), 1
+            );
+            throw new ActionAbort('redirect');
+        } else {
+            throw new \Exception(
+                sprintf(
+                    $lang["subscr_{$action}_error"],
+                    hsc($INFO['userinfo']['name']),
+                    prettyprint_id($target)
+                )
+            );
+        }
+    }
+
+    /**
+     * Validate POST data
+     *
+     * Validates POST data for a subscribe or unsubscribe request. This is the
+     * default action for the event ACTION_HANDLE_SUBSCRIBE.
+     *
+     * @author Adrian Lang <lang@cosmocode.de>
+     *
+     * @param array &$params the parameters: target, style and action
+     * @throws \Exception
+     */
+    public function handlePostData(&$params) {
+        global $INFO;
+        global $lang;
+        global $INPUT;
+
+        // Get and validate parameters.
+        if(!isset($params['target'])) {
+            throw new \Exception('no subscription target given');
+        }
+        $target = $params['target'];
+        $valid_styles = array('every', 'digest');
+        if(substr($target, -1, 1) === ':') {
+            // Allow “list” subscribe style since the target is a namespace.
+            $valid_styles[] = 'list';
+        }
+        $style = valid_input_set(
+            'style', $valid_styles, $params,
+            'invalid subscription style given'
+        );
+        $action = valid_input_set(
+            'action', array('subscribe', 'unsubscribe'),
+            $params, 'invalid subscription action given'
+        );
+
+        // Check other conditions.
+        if($action === 'subscribe') {
+            if($INFO['userinfo']['mail'] === '') {
+                throw new \Exception($lang['subscr_subscribe_noaddress']);
+            }
+        } elseif($action === 'unsubscribe') {
+            $is = false;
+            foreach($INFO['subscribed'] as $subscr) {
+                if($subscr['target'] === $target) {
+                    $is = true;
+                }
+            }
+            if($is === false) {
+                throw new \Exception(
+                    sprintf(
+                        $lang['subscr_not_subscribed'],
+                        $INPUT->server->str('REMOTE_USER'),
+                        prettyprint_id($target)
+                    )
+                );
+            }
+            // subscription_set deletes a subscription if style = null.
+            $style = null;
+        }
+
+        $params = compact('target', 'style', 'action');
+    }
+
+}
diff --git a/inc/ActionRouter.php b/inc/ActionRouter.php
new file mode 100644
index 0000000000000000000000000000000000000000..cb2df70adc3a7b30231362855bed0e763a838895
--- /dev/null
+++ b/inc/ActionRouter.php
@@ -0,0 +1,228 @@
+<?php
+
+namespace dokuwiki;
+
+use dokuwiki\Action\AbstractAction;
+use dokuwiki\Action\Exception\ActionDisabledException;
+use dokuwiki\Action\Exception\ActionException;
+use dokuwiki\Action\Exception\FatalException;
+use dokuwiki\Action\Exception\NoActionException;
+use dokuwiki\Action\Plugin;
+
+/**
+ * Class ActionRouter
+ * @package dokuwiki
+ */
+class ActionRouter {
+
+    /** @var  AbstractAction */
+    protected $action;
+
+    /** @var  ActionRouter */
+    protected static $instance;
+
+    /** @var int transition counter */
+    protected $transitions = 0;
+
+    /** maximum loop */
+    const MAX_TRANSITIONS = 5;
+
+    /** @var string[] the actions disabled in the configuration */
+    protected $disabled;
+
+    /**
+     * ActionRouter constructor. Singleton, thus protected!
+     *
+     * Sets up the correct action based on the $ACT global. Writes back
+     * the selected action to $ACT
+     */
+    protected function __construct() {
+        global $ACT;
+        global $conf;
+
+        $this->disabled = explode(',', $conf['disableactions']);
+        $this->disabled = array_map('trim', $this->disabled);
+        $this->transitions = 0;
+
+        $ACT = act_clean($ACT);
+        $this->setupAction($ACT);
+        $ACT = $this->action->getActionName();
+    }
+
+    /**
+     * Get the singleton instance
+     *
+     * @param bool $reinit
+     * @return ActionRouter
+     */
+    public static function getInstance($reinit = false) {
+        if((self::$instance === null) || $reinit) {
+            self::$instance = new ActionRouter();
+        }
+        return self::$instance;
+    }
+
+    /**
+     * Setup the given action
+     *
+     * Instantiates the right class, runs permission checks and pre-processing and
+     * sets $action
+     *
+     * @param string $actionname
+     * @triggers ACTION_ACT_PREPROCESS
+     */
+    protected function setupAction($actionname) {
+        $presetup = $actionname;
+
+        try {
+            $this->action = $this->loadAction($actionname);
+            $this->checkAction($this->action);
+            $this->action->preProcess();
+
+        } catch(ActionException $e) {
+            // we should have gotten a new action
+            $actionname = $e->getNewAction();
+
+            // this one should trigger a user message
+            if(is_a($e, ActionDisabledException::class)) {
+                msg('Action disabled: ' . hsc($presetup), -1);
+            }
+
+            // some actions may request the display of a message
+            if($e->displayToUser()) {
+                msg(hsc($e->getMessage()), -1);
+            }
+
+            // do setup for new action
+            $this->transitionAction($presetup, $actionname);
+
+        } catch(NoActionException $e) {
+            // give plugins an opportunity to process the actionname
+            $evt = new \Doku_Event('ACTION_ACT_PREPROCESS', $actionname);
+            if($evt->advise_before()) {
+                if($actionname == $presetup) {
+                    // no plugin changed the action, complain and switch to show
+                    msg('Action unknown: ' . hsc($actionname), -1);
+                    $actionname = 'show';
+                }
+                $this->transitionAction($presetup, $actionname);
+            } else {
+                // event said the action should be kept, assume action plugin will handle it later
+                $this->action = new Plugin($actionname);
+            }
+            $evt->advise_after();
+
+        } catch(\Exception $e) {
+            $this->handleFatalException($e);
+        }
+    }
+
+    /**
+     * Transitions from one action to another
+     *
+     * Basically just calls setupAction() again but does some checks before.
+     *
+     * @param string $from current action name
+     * @param string $to new action name
+     * @param null|ActionException $e any previous exception that caused the transition
+     */
+    protected function transitionAction($from, $to, $e = null) {
+        $this->transitions++;
+
+        // no infinite recursion
+        if($from == $to) {
+            $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
+        }
+
+        // larger loops will be caught here
+        if($this->transitions >= self::MAX_TRANSITIONS) {
+            $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
+        }
+
+        // do the recursion
+        $this->setupAction($to);
+    }
+
+    /**
+     * Aborts all processing with a message
+     *
+     * When a FataException instanc is passed, the code is treated as Status code
+     *
+     * @param \Exception|FatalException $e
+     */
+    protected function handleFatalException(\Exception $e) {
+        if(is_a($e, FatalException::class)) {
+            http_status($e->getCode());
+        } else {
+            http_status(500);
+        }
+        $msg = 'Something unforseen has happened: ' . $e->getMessage();
+        nice_die(hsc($msg));
+    }
+
+    /**
+     * Load the given action
+     *
+     * This translates the given name to a class name by uppercasing the first letter.
+     * Underscores translate to camelcase names. For actions with underscores, the different
+     * parts are removed beginning from the end until a matching class is found. The instatiated
+     * Action will always have the full original action set as Name
+     *
+     * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
+     *
+     * @param $actionname
+     * @return AbstractAction
+     * @throws NoActionException
+     */
+    public function loadAction($actionname) {
+        $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
+        $parts = explode('_', $actionname);
+        while($parts) {
+            $load = join('_', $parts);
+            $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
+            if(class_exists($class)) {
+                return new $class($actionname);
+            }
+            array_pop($parts);
+        }
+
+        throw new NoActionException();
+    }
+
+    /**
+     * Execute all the checks to see if this action can be executed
+     *
+     * @param AbstractAction $action
+     * @throws ActionDisabledException
+     * @throws ActionException
+     */
+    public function checkAction(AbstractAction $action) {
+        global $INFO;
+        global $ID;
+
+        if(in_array($action->getActionName(), $this->disabled)) {
+            throw new ActionDisabledException();
+        }
+
+        $action->checkPermissions();
+
+        if(isset($INFO)) {
+            $perm = $INFO['perm'];
+        } else {
+            $perm = auth_quickaclcheck($ID);
+        }
+
+        if($perm < $action->minimumPermission()) {
+            throw new ActionException('denied');
+        }
+    }
+
+    /**
+     * Returns the action handling the current request
+     *
+     * @return AbstractAction
+     */
+    public function getAction() {
+        return $this->action;
+    }
+}
diff --git a/inc/Tpl/Action.php b/inc/Tpl/Action.php
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/inc/Tpl/ActionInfo.php b/inc/Tpl/ActionInfo.php
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/inc/Tpl/Menu.php b/inc/Tpl/Menu.php
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/inc/actions.php b/inc/actions.php
index 0e100bbfa8efad8d3d8d998d314def3969409d12..22954ae7a227fed107b8845ce92ff96a07a36e9f 100644
--- a/inc/actions.php
+++ b/inc/actions.php
@@ -8,194 +8,19 @@
 
 if(!defined('DOKU_INC')) die('meh.');
 
-/**
- * Call the needed action handlers
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- * @triggers ACTION_ACT_PREPROCESS
- * @triggers ACTION_HEADERS_SEND
- */
-function act_dispatch(){
-    global $ACT;
-    global $ID;
-    global $INFO;
-    global $QUERY;
-    /* @var Input $INPUT */
-    global $INPUT;
-    global $lang;
-    global $conf;
-
-    $preact = $ACT;
-
-    // give plugins an opportunity to process the action
-    $evt = new Doku_Event('ACTION_ACT_PREPROCESS',$ACT);
-
-    $headers = array();
-    if ($evt->advise_before()) {
-
-        //sanitize $ACT
-        $ACT = act_validate($ACT);
-
-        //check if searchword was given - else just show
-        $s = cleanID($QUERY);
-        if($ACT == 'search' && empty($s)){
-            $ACT = 'show';
-        }
-
-        //login stuff
-        if(in_array($ACT,array('login','logout'))){
-            $ACT = act_auth($ACT);
-        }
-
-        //check if user is asking to (un)subscribe a page
-        if($ACT == 'subscribe') {
-            try {
-                $ACT = act_subscription($ACT);
-            } catch (Exception $e) {
-                msg($e->getMessage(), -1);
-            }
-        }
-
-        //display some info
-        if($ACT == 'check'){
-            check();
-            $ACT = 'show';
-        }
-
-        //check permissions
-        $ACT = act_permcheck($ACT);
-
-        //sitemap
-        if ($ACT == 'sitemap'){
-            act_sitemap($ACT);
-        }
 
-        //recent changes
-        if ($ACT == 'recent'){
-            $show_changes = $INPUT->str('show_changes');
-            if (!empty($show_changes)) {
-                set_doku_pref('show_changes', $show_changes);
-            }
-        }
-
-        //diff
-        if ($ACT == 'diff'){
-            $difftype = $INPUT->str('difftype');
-            if (!empty($difftype)) {
-                set_doku_pref('difftype', $difftype);
-            }
-        }
-
-        //register
-        if($ACT == 'register' && $INPUT->post->bool('save') && register()){
-            $ACT = 'login';
-        }
-
-        if ($ACT == 'resendpwd' && act_resendpwd()) {
-            $ACT = 'login';
-        }
-
-        // user profile changes
-        if (in_array($ACT, array('profile','profile_delete'))) {
-            if(!$INPUT->server->str('REMOTE_USER')) {
-                $ACT = 'login';
-            } else {
-                switch ($ACT) {
-                    case 'profile' :
-                        if(updateprofile()) {
-                            msg($lang['profchanged'],1);
-                            $ACT = 'show';
-                        }
-                        break;
-                    case 'profile_delete' :
-                        if(auth_deleteprofile()){
-                            msg($lang['profdeleted'],1);
-                            $ACT = 'show';
-                        } else {
-                            $ACT = 'profile';
-                        }
-                        break;
-                }
-            }
-        }
-
-        //revert
-        if($ACT == 'revert'){
-            if(checkSecurityToken()){
-                $ACT = act_revert($ACT);
-            }else{
-                $ACT = 'show';
-            }
-        }
-
-        //save
-        if($ACT == 'save'){
-            if(checkSecurityToken()){
-                $ACT = act_save($ACT);
-            }else{
-                $ACT = 'preview';
-            }
-        }
-
-        //cancel conflicting edit
-        if($ACT == 'cancel')
-            $ACT = 'show';
-
-        //draft deletion
-        if($ACT == 'draftdel')
-            $ACT = act_draftdel($ACT);
-
-        //draft saving on preview
-        if($ACT == 'preview') {
-            $headers[] = "X-XSS-Protection: 0";
-            $ACT = act_draftsave($ACT);
-        }
-
-        //edit
-        if(in_array($ACT, array('edit', 'preview', 'recover'))) {
-            $ACT = act_edit($ACT);
-        }else{
-            unlock($ID); //try to unlock
-        }
-
-        //handle export
-        if(substr($ACT,0,7) == 'export_')
-            $ACT = act_export($ACT);
-
-        //handle admin tasks
-        if($ACT == 'admin'){
-            // retrieve admin plugin name from $_REQUEST['page']
-            if (($page = $INPUT->str('page', '', true)) != '') {
-                /** @var $plugin DokuWiki_Admin_Plugin */
-                if ($plugin = plugin_getRequestAdminPlugin()){
-                    $plugin->handle();
-                }
-            }
-        }
-
-        // check permissions again - the action may have changed
-        $ACT = act_permcheck($ACT);
-    }  // end event ACTION_ACT_PREPROCESS default action
-    $evt->advise_after();
-    // Make sure plugs can handle 'denied'
-    if($conf['send404'] && $ACT == 'denied') {
-        http_status(403);
-    }
-    unset($evt);
-
-    // when action 'show', the intial not 'show' and POST, do a redirect
-    if($ACT == 'show' && $preact != 'show' && strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post'){
-        act_redirect($ID,$preact);
-    }
-
-    global $INFO;
-    global $conf;
-    global $license;
+function act_dispatch(){
+    $router = \dokuwiki\ActionRouter::getInstance(); // is this needed here or could we delegate it to tpl_content() later?
 
-    //call template FIXME: all needed vars available?
-    $headers[] = 'Content-Type: text/html; charset=utf-8';
+    $headers = array('Content-Type: text/html; charset=utf-8');
     trigger_event('ACTION_HEADERS_SEND',$headers,'act_sendheaders');
 
+    // clear internal variables
+    unset($router);
+    unset($headers);
+    // make all globals available to the template
+    extract($GLOBALS);
+
     include(template('main.php'));
     // output for the commands is now handled in inc/templates.php
     // in function tpl_content()
@@ -234,628 +59,3 @@ function act_clean($act){
     if($act === '') $act = 'show';
     return $act;
 }
-
-/**
- * Sanitize and validate action commands.
- *
- * Add all allowed commands here.
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- *
- * @param array|string $act
- * @return string
- */
-function act_validate($act) {
-    global $conf;
-    global $INFO;
-
-    $act = act_clean($act);
-
-    // check if action is disabled
-    if(!actionOK($act)){
-        msg('Command disabled: '.hsc($act),-1);
-        return 'show';
-    }
-
-    //disable all acl related commands if ACL is disabled
-    if(!$conf['useacl'] && in_array($act,array('login','logout','register','admin',
-                    'subscribe','unsubscribe','profile','revert',
-                    'resendpwd','profile_delete'))){
-        msg('Command unavailable: '.hsc($act),-1);
-        return 'show';
-    }
-
-    //is there really a draft?
-    if($act == 'draft' && !file_exists($INFO['draft'])) return 'edit';
-
-    if(!in_array($act,array('login','logout','register','save','cancel','edit','draft',
-                    'preview','search','show','check','index','revisions',
-                    'diff','recent','backlink','admin','subscribe','revert',
-                    'unsubscribe','profile','profile_delete','resendpwd','recover',
-                    'draftdel','sitemap','media')) && substr($act,0,7) != 'export_' ) {
-        msg('Command unknown: '.hsc($act),-1);
-        return 'show';
-    }
-    return $act;
-}
-
-/**
- * Run permissionchecks
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- *
- * @param string $act action command
- * @return string action command
- */
-function act_permcheck($act){
-    global $INFO;
-
-    if(in_array($act,array('save','preview','edit','recover'))){
-        if($INFO['exists']){
-            if($act == 'edit'){
-                //the edit function will check again and do a source show
-                //when no AUTH_EDIT available
-                $permneed = AUTH_READ;
-            }else{
-                $permneed = AUTH_EDIT;
-            }
-        }else{
-            $permneed = AUTH_CREATE;
-        }
-    }elseif(in_array($act,array('login','search','recent','profile','profile_delete','index', 'sitemap'))){
-        $permneed = AUTH_NONE;
-    }elseif($act == 'revert'){
-        $permneed = AUTH_ADMIN;
-        if($INFO['ismanager']) $permneed = AUTH_EDIT;
-    }elseif($act == 'register'){
-        $permneed = AUTH_NONE;
-    }elseif($act == 'resendpwd'){
-        $permneed = AUTH_NONE;
-    }elseif($act == 'admin'){
-        if($INFO['ismanager']){
-            // if the manager has the needed permissions for a certain admin
-            // action is checked later
-            $permneed = AUTH_READ;
-        }else{
-            $permneed = AUTH_ADMIN;
-        }
-    }else{
-        $permneed = AUTH_READ;
-    }
-    if($INFO['perm'] >= $permneed) return $act;
-
-    return 'denied';
-}
-
-/**
- * Handle 'draftdel'
- *
- * Deletes the draft for the current page and user
- *
- * @param string $act action command
- * @return string action command
- */
-function act_draftdel($act){
-    global $INFO;
-    @unlink($INFO['draft']);
-    $INFO['draft'] = null;
-    return 'show';
-}
-
-/**
- * Saves a draft on preview
- *
- * @todo this currently duplicates code from ajax.php :-/
- *
- * @param string $act action command
- * @return string action command
- */
-function act_draftsave($act){
-    global $INFO;
-    global $ID;
-    global $INPUT;
-    global $conf;
-    if($conf['usedraft'] && $INPUT->post->has('wikitext')) {
-        $draft = array('id'     => $ID,
-                'prefix' => substr($INPUT->post->str('prefix'), 0, -1),
-                'text'   => $INPUT->post->str('wikitext'),
-                'suffix' => $INPUT->post->str('suffix'),
-                'date'   => $INPUT->post->int('date'),
-                'client' => $INFO['client'],
-                );
-        $cname = getCacheName($draft['client'].$ID,'.draft');
-        if(io_saveFile($cname,serialize($draft))){
-            $INFO['draft'] = $cname;
-        }
-    }
-    return $act;
-}
-
-/**
- * Handle 'save'
- *
- * Checks for spam and conflicts and saves the page.
- * Does a redirect to show the page afterwards or
- * returns a new action.
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- *
- * @param string $act action command
- * @return string action command
- */
-function act_save($act){
-    global $ID;
-    global $DATE;
-    global $PRE;
-    global $TEXT;
-    global $SUF;
-    global $SUM;
-    global $lang;
-    global $INFO;
-    global $INPUT;
-
-    //spam check
-    if(checkwordblock()) {
-        msg($lang['wordblock'], -1);
-        return 'edit';
-    }
-    //conflict check
-    if($DATE != 0 && $INFO['meta']['date']['modified'] > $DATE )
-        return 'conflict';
-
-    //save it
-    saveWikiText($ID,con($PRE,$TEXT,$SUF,true),$SUM,$INPUT->bool('minor')); //use pretty mode for con
-    //unlock it
-    unlock($ID);
-
-    //delete draft
-    act_draftdel($act);
-    session_write_close();
-
-    // when done, show page
-    return 'show';
-}
-
-/**
- * Revert to a certain revision
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- *
- * @param string $act action command
- * @return string action command
- */
-function act_revert($act){
-    global $ID;
-    global $REV;
-    global $lang;
-    /* @var Input $INPUT */
-    global $INPUT;
-    // FIXME $INFO['writable'] currently refers to the attic version
-    // global $INFO;
-    // if (!$INFO['writable']) {
-    //     return 'show';
-    // }
-
-    // when no revision is given, delete current one
-    // FIXME this feature is not exposed in the GUI currently
-    $text = '';
-    $sum  = $lang['deleted'];
-    if($REV){
-        $text = rawWiki($ID,$REV);
-        if(!$text) return 'show'; //something went wrong
-        $sum = sprintf($lang['restored'], dformat($REV));
-    }
-
-    // spam check
-
-    if (checkwordblock($text)) {
-        msg($lang['wordblock'], -1);
-        return 'edit';
-    }
-
-    saveWikiText($ID,$text,$sum,false);
-    msg($sum,1);
-
-    //delete any draft
-    act_draftdel($act);
-    session_write_close();
-
-    // when done, show current page
-    $INPUT->server->set('REQUEST_METHOD','post'); //should force a redirect
-    $REV = '';
-    return 'show';
-}
-
-/**
- * Do a redirect after receiving post data
- *
- * Tries to add the section id as hash mark after section editing
- *
- * @param string $id page id
- * @param string $preact action command before redirect
- */
-function act_redirect($id,$preact){
-    global $PRE;
-    global $TEXT;
-
-    $opts = array(
-            'id'       => $id,
-            'preact'   => $preact
-            );
-    //get section name when coming from section edit
-    if($PRE && preg_match('/^\s*==+([^=\n]+)/',$TEXT,$match)){
-        $check = false; //Byref
-        $opts['fragment'] = sectionID($match[0], $check);
-    }
-
-    trigger_event('ACTION_SHOW_REDIRECT',$opts,'act_redirect_execute');
-}
-
-/**
- * Execute the redirect
- *
- * @param array $opts id and fragment for the redirect and the preact
- */
-function act_redirect_execute($opts){
-    $go = wl($opts['id'],'',true);
-    if(isset($opts['fragment'])) $go .= '#'.$opts['fragment'];
-
-    //show it
-    send_redirect($go);
-}
-
-/**
- * Handle 'login', 'logout'
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- *
- * @param string $act action command
- * @return string action command
- */
-function act_auth($act){
-    global $ID;
-    global $INFO;
-    /* @var Input $INPUT */
-    global $INPUT;
-
-    //already logged in?
-    if($INPUT->server->has('REMOTE_USER') && $act=='login'){
-        return 'show';
-    }
-
-    //handle logout
-    if($act=='logout'){
-        $lockedby = checklock($ID); //page still locked?
-        if($lockedby == $INPUT->server->str('REMOTE_USER')){
-            unlock($ID); //try to unlock
-        }
-
-        // do the logout stuff
-        auth_logoff();
-
-        // rebuild info array
-        $INFO = pageinfo();
-
-        act_redirect($ID,'login');
-    }
-
-    return $act;
-}
-
-/**
- * Handle 'edit', 'preview', 'recover'
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- *
- * @param string $act action command
- * @return string action command
- */
-function act_edit($act){
-    global $ID;
-    global $INFO;
-
-    global $TEXT;
-    global $RANGE;
-    global $PRE;
-    global $SUF;
-    global $REV;
-    global $SUM;
-    global $lang;
-    global $DATE;
-
-    if (!isset($TEXT)) {
-        if ($INFO['exists']) {
-            if ($RANGE) {
-                list($PRE,$TEXT,$SUF) = rawWikiSlices($RANGE,$ID,$REV);
-            } else {
-                $TEXT = rawWiki($ID,$REV);
-            }
-        } else {
-            $TEXT = pageTemplate($ID);
-        }
-    }
-
-    //set summary default
-    if(!$SUM){
-        if($REV){
-            $SUM = sprintf($lang['restored'], dformat($REV));
-        }elseif(!$INFO['exists']){
-            $SUM = $lang['created'];
-        }
-    }
-
-    // Use the date of the newest revision, not of the revision we edit
-    // This is used for conflict detection
-    if(!$DATE) $DATE = @filemtime(wikiFN($ID));
-
-    //check if locked by anyone - if not lock for my self
-    //do not lock when the user can't edit anyway
-    if ($INFO['writable']) {
-        $lockedby = checklock($ID);
-        if($lockedby) return 'locked';
-
-        lock($ID);
-    }
-
-    return $act;
-}
-
-/**
- * Export a wiki page for various formats
- *
- * Triggers ACTION_EXPORT_POSTPROCESS
- *
- *  Event data:
- *    data['id']      -- page id
- *    data['mode']    -- requested export mode
- *    data['headers'] -- export headers
- *    data['output']  -- export output
- *
- * @author Andreas Gohr <andi@splitbrain.org>
- * @author Michael Klier <chi@chimeric.de>
- *
- * @param string $act action command
- * @return string action command
- */
-function act_export($act){
-    global $ID;
-    global $REV;
-    global $conf;
-    global $lang;
-
-    $pre = '';
-    $post = '';
-    $headers = array();
-
-    // search engines: never cache exported docs! (Google only currently)
-    $headers['X-Robots-Tag'] = 'noindex';
-
-    $mode = substr($act,7);
-    switch($mode) {
-        case 'raw':
-            $headers['Content-Type'] = 'text/plain; charset=utf-8';
-            $headers['Content-Disposition'] = 'attachment; filename='.noNS($ID).'.txt';
-            $output = rawWiki($ID,$REV);
-            break;
-        case 'xhtml':
-            $pre .= '<!DOCTYPE html>' . DOKU_LF;
-            $pre .= '<html lang="'.$conf['lang'].'" dir="'.$lang['direction'].'">' . DOKU_LF;
-            $pre .= '<head>' . DOKU_LF;
-            $pre .= '  <meta charset="utf-8" />' . DOKU_LF;
-            $pre .= '  <title>'.$ID.'</title>' . DOKU_LF;
-
-            // get metaheaders
-            ob_start();
-            tpl_metaheaders();
-            $pre .= ob_get_clean();
-
-            $pre .= '</head>' . DOKU_LF;
-            $pre .= '<body>' . DOKU_LF;
-            $pre .= '<div class="dokuwiki export">' . DOKU_LF;
-
-            // get toc
-            $pre .= tpl_toc(true);
-
-            $headers['Content-Type'] = 'text/html; charset=utf-8';
-            $output = p_wiki_xhtml($ID,$REV,false);
-
-            $post .= '</div>' . DOKU_LF;
-            $post .= '</body>' . DOKU_LF;
-            $post .= '</html>' . DOKU_LF;
-            break;
-        case 'xhtmlbody':
-            $headers['Content-Type'] = 'text/html; charset=utf-8';
-            $output = p_wiki_xhtml($ID,$REV,false);
-            break;
-        default:
-            $output = p_cached_output(wikiFN($ID,$REV), $mode, $ID);
-            $headers = p_get_metadata($ID,"format $mode");
-            break;
-    }
-
-    // prepare event data
-    $data = array();
-    $data['id'] = $ID;
-    $data['mode'] = $mode;
-    $data['headers'] = $headers;
-    $data['output'] =& $output;
-
-    trigger_event('ACTION_EXPORT_POSTPROCESS', $data);
-
-    if(!empty($data['output'])){
-        if(is_array($data['headers'])) foreach($data['headers'] as $key => $val){
-            header("$key: $val");
-        }
-        print $pre.$data['output'].$post;
-        exit;
-    }
-    return 'show';
-}
-
-/**
- * Handle sitemap delivery
- *
- * @author Michael Hamann <michael@content-space.de>
- *
- * @param string $act action command
- */
-function act_sitemap($act) {
-    global $conf;
-
-    if ($conf['sitemap'] < 1 || !is_numeric($conf['sitemap'])) {
-        http_status(404);
-        print "Sitemap generation is disabled.";
-        exit;
-    }
-
-    $sitemap = Sitemapper::getFilePath();
-    if (Sitemapper::sitemapIsCompressed()) {
-        $mime = 'application/x-gzip';
-    }else{
-        $mime = 'application/xml; charset=utf-8';
-    }
-
-    // Check if sitemap file exists, otherwise create it
-    if (!is_readable($sitemap)) {
-        Sitemapper::generate();
-    }
-
-    if (is_readable($sitemap)) {
-        // Send headers
-        header('Content-Type: '.$mime);
-        header('Content-Disposition: attachment; filename='.utf8_basename($sitemap));
-
-        http_conditionalRequest(filemtime($sitemap));
-
-        // Send file
-        //use x-sendfile header to pass the delivery to compatible webservers
-        http_sendfile($sitemap);
-
-        readfile($sitemap);
-        exit;
-    }
-
-    http_status(500);
-    print "Could not read the sitemap file - bad permissions?";
-    exit;
-}
-
-/**
- * Handle page 'subscribe'
- *
- * Throws exception on error.
- *
- * @author Adrian Lang <lang@cosmocode.de>
- *
- * @param string $act action command
- * @return string action command
- * @throws Exception if (un)subscribing fails
- */
-function act_subscription($act){
-    global $lang;
-    global $INFO;
-    global $ID;
-    /* @var Input $INPUT */
-    global $INPUT;
-
-    // subcriptions work for logged in users only
-    if(!$INPUT->server->str('REMOTE_USER')) return 'show';
-
-    // get and preprocess data.
-    $params = array();
-    foreach(array('target', 'style', 'action') as $param) {
-        if ($INPUT->has("sub_$param")) {
-            $params[$param] = $INPUT->str("sub_$param");
-        }
-    }
-
-    // any action given? if not just return and show the subscription page
-    if(empty($params['action']) || !checkSecurityToken()) return $act;
-
-    // Handle POST data, may throw exception.
-    trigger_event('ACTION_HANDLE_SUBSCRIBE', $params, 'subscription_handle_post');
-
-    $target = $params['target'];
-    $style  = $params['style'];
-    $action = $params['action'];
-
-    // Perform action.
-    $sub = new Subscription();
-    if($action == 'unsubscribe'){
-        $ok = $sub->remove($target, $INPUT->server->str('REMOTE_USER'), $style);
-    }else{
-        $ok = $sub->add($target, $INPUT->server->str('REMOTE_USER'), $style);
-    }
-
-    if($ok) {
-        msg(sprintf($lang["subscr_{$action}_success"], hsc($INFO['userinfo']['name']),
-                    prettyprint_id($target)), 1);
-        act_redirect($ID, $act);
-    } else {
-        throw new Exception(sprintf($lang["subscr_{$action}_error"],
-                                    hsc($INFO['userinfo']['name']),
-                                    prettyprint_id($target)));
-    }
-
-    // Assure that we have valid data if act_redirect somehow fails.
-    $INFO['subscribed'] = $sub->user_subscription();
-    return 'show';
-}
-
-/**
- * Validate POST data
- *
- * Validates POST data for a subscribe or unsubscribe request. This is the
- * default action for the event ACTION_HANDLE_SUBSCRIBE.
- *
- * @author Adrian Lang <lang@cosmocode.de>
- *
- * @param array &$params the parameters: target, style and action
- * @throws Exception
- */
-function subscription_handle_post(&$params) {
-    global $INFO;
-    global $lang;
-    /* @var Input $INPUT */
-    global $INPUT;
-
-    // Get and validate parameters.
-    if (!isset($params['target'])) {
-        throw new Exception('no subscription target given');
-    }
-    $target = $params['target'];
-    $valid_styles = array('every', 'digest');
-    if (substr($target, -1, 1) === ':') {
-        // Allow “list” subscribe style since the target is a namespace.
-        $valid_styles[] = 'list';
-    }
-    $style  = valid_input_set('style', $valid_styles, $params,
-                              'invalid subscription style given');
-    $action = valid_input_set('action', array('subscribe', 'unsubscribe'),
-                              $params, 'invalid subscription action given');
-
-    // Check other conditions.
-    if ($action === 'subscribe') {
-        if ($INFO['userinfo']['mail'] === '') {
-            throw new Exception($lang['subscr_subscribe_noaddress']);
-        }
-    } elseif ($action === 'unsubscribe') {
-        $is = false;
-        foreach($INFO['subscribed'] as $subscr) {
-            if ($subscr['target'] === $target) {
-                $is = true;
-            }
-        }
-        if ($is === false) {
-            throw new Exception(sprintf($lang['subscr_not_subscribed'],
-                                        $INPUT->server->str('REMOTE_USER'),
-                                        prettyprint_id($target)));
-        }
-        // subscription_set deletes a subscription if style = null.
-        $style = null;
-    }
-
-    $params = compact('target', 'style', 'action');
-}
-
-//Setup VIM: ex: et ts=2 :
diff --git a/inc/html.php b/inc/html.php
index 1a0963afa602eacd10a0484ce1ed772b53e16d9c..e7bc072e66b64dd4a60e16226de231fb83125e26 100644
--- a/inc/html.php
+++ b/inc/html.php
@@ -91,7 +91,7 @@ function html_denied() {
 function html_secedit($text,$show=true){
     global $INFO;
 
-    $regexp = '#<!-- EDIT(\d+) ([A-Z_]+) (?:"([^"]*)" )?\[(\d+-\d*)\] -->#';
+    $regexp = '#<!-- EDIT(\d+) ([A-Z_]+) (?:"([^"]*)" )(?:"([^"]*)" )?\[(\d+-\d*)\] -->#';
 
     if(!$INFO['writable'] || !$show || $INFO['rev']){
         return preg_replace($regexp,'',$text);
@@ -114,8 +114,9 @@ function html_secedit($text,$show=true){
 function html_secedit_button($matches){
     $data = array('secid'  => $matches[1],
                   'target' => strtolower($matches[2]),
+                  'hid' => strtolower($matches[4]),
                   'range'  => $matches[count($matches) - 1]);
-    if (count($matches) === 5) {
+    if (count($matches) === 6) {
         $data['name'] = $matches[3];
     }
 
@@ -1849,6 +1850,9 @@ function html_edit(){
     }
 
     $form->addHidden('target', $data['target']);
+    if ($INPUT->has('hid')) {
+        $form->addHidden('hid', $INPUT->str('hid'));
+    }
     $form->addElement(form_makeOpenTag('div', array('id'=>'wiki__editbar', 'class'=>'editBar')));
     $form->addElement(form_makeOpenTag('div', array('id'=>'size__ctl')));
     $form->addElement(form_makeCloseTag('div'));
diff --git a/inc/load.php b/inc/load.php
index 6c3c83f8e1725962a20f10cd1f035554cb538ffe..14f8db3f798693775c571e43dff9b90255597a69 100644
--- a/inc/load.php
+++ b/inc/load.php
@@ -133,8 +133,11 @@ function load_autoload($name){
 
     // our own namespace
     if(substr($name, 0, 9) == 'dokuwiki/') {
-        require substr($name, 9) . '.php';
-        return true;
+        $file = DOKU_INC . 'inc/' . substr($name, 9) . '.php';
+        if(file_exists($file)) {
+            require $file;
+            return true;
+        }
     }
 
     // Plugin loading
diff --git a/inc/parser/xhtml.php b/inc/parser/xhtml.php
index 7a991eca581bcd48704fc52c0f8a0dbffd7e047b..dbac1a9822bd73f9ce0447a8b4f768f6e497450a 100644
--- a/inc/parser/xhtml.php
+++ b/inc/parser/xhtml.php
@@ -66,8 +66,8 @@ class Doku_Renderer_xhtml extends Doku_Renderer {
      *
      * @author Adrian Lang <lang@cosmocode.de>
      */
-    public function startSectionEdit($start, $type, $title = null) {
-        $this->sectionedits[] = array(++$this->lastsecid, $start, $type, $title);
+    public function startSectionEdit($start, $type, $title = null, $hid = null) {
+        $this->sectionedits[] = array(++$this->lastsecid, $start, $type, $title, $hid);
         return 'sectionedit'.$this->lastsecid;
     }
 
@@ -78,8 +78,8 @@ class Doku_Renderer_xhtml extends Doku_Renderer {
      *
      * @author Adrian Lang <lang@cosmocode.de>
      */
-    public function finishSectionEdit($end = null) {
-        list($id, $start, $type, $title) = array_pop($this->sectionedits);
+    public function finishSectionEdit($end = null, $hid = null) {
+        list($id, $start, $type, $title, $hid) = array_pop($this->sectionedits);
         if(!is_null($end) && $end <= $start) {
             return;
         }
@@ -87,6 +87,9 @@ class Doku_Renderer_xhtml extends Doku_Renderer {
         if(!is_null($title)) {
             $this->doc .= '"'.str_replace('"', '', $title).'" ';
         }
+        if(!is_null($hid)) {
+            $this->doc .= '"'.$hid.'" ';
+        }
         $this->doc .= "[$start-".(is_null($end) ? '' : $end).'] -->';
     }
 
@@ -217,7 +220,7 @@ class Doku_Renderer_xhtml extends Doku_Renderer {
         // write the header
         $this->doc .= DOKU_LF.'<h'.$level;
         if($level <= $conf['maxseclevel']) {
-            $this->doc .= ' class="'.$this->startSectionEdit($pos, 'section', $text).'"';
+            $this->doc .= ' class="'.$this->startSectionEdit($pos, 'section', $text, $hid).'"';
         }
         $this->doc .= ' id="'.$hid.'">';
         $this->doc .= $this->_xmlEntities($text);
diff --git a/inc/template.php b/inc/template.php
index b962e8d239d82ed0ef8b3b9e12a03c65cbc907ae..f3837c1fa360256f9c011d45b58c03a7f3284caa 100644
--- a/inc/template.php
+++ b/inc/template.php
@@ -93,90 +93,13 @@ function tpl_content($prependTOC = true) {
  * @return bool
  */
 function tpl_content_core() {
-    global $ACT;
-    global $TEXT;
-    global $PRE;
-    global $SUF;
-    global $SUM;
-    global $IDX;
-    global $INPUT;
-
-    switch($ACT) {
-        case 'show':
-            html_show();
-            break;
-        /** @noinspection PhpMissingBreakStatementInspection */
-        case 'locked':
-            html_locked();
-        case 'edit':
-        case 'recover':
-            html_edit();
-            break;
-        case 'preview':
-            html_edit();
-            html_show($TEXT);
-            break;
-        case 'draft':
-            html_draft();
-            break;
-        case 'search':
-            html_search();
-            break;
-        case 'revisions':
-            html_revisions($INPUT->int('first'));
-            break;
-        case 'diff':
-            html_diff();
-            break;
-        case 'recent':
-            $show_changes = $INPUT->str('show_changes');
-            if (empty($show_changes)) {
-                $show_changes = get_doku_pref('show_changes', $show_changes);
-            }
-            html_recent($INPUT->extract('first')->int('first'), $show_changes);
-            break;
-        case 'index':
-            html_index($IDX); #FIXME can this be pulled from globals? is it sanitized correctly?
-            break;
-        case 'backlink':
-            html_backlinks();
-            break;
-        case 'conflict':
-            html_conflict(con($PRE, $TEXT, $SUF), $SUM);
-            html_diff(con($PRE, $TEXT, $SUF), false);
-            break;
-        case 'login':
-            html_login();
-            break;
-        case 'register':
-            html_register();
-            break;
-        case 'resendpwd':
-            html_resendpwd();
-            break;
-        case 'denied':
-            html_denied();
-            break;
-        case 'profile' :
-            html_updateprofile();
-            break;
-        case 'admin':
-            tpl_admin();
-            break;
-        case 'subscribe':
-            tpl_subscribe();
-            break;
-        case 'media':
-            tpl_media();
-            break;
-        default:
-            $evt = new Doku_Event('TPL_ACT_UNKNOWN', $ACT);
-            if($evt->advise_before()) {
-                msg("Failed to handle command: ".hsc($ACT), -1);
-            }
-            $evt->advise_after();
-            unset($evt);
-            return false;
+    $router = \dokuwiki\ActionRouter::getInstance();
+    try {
+        $router->getAction()->tplContent();
+    } catch(\dokuwiki\Action\Exception\FatalException $e) {
+        // there was no content for the action
+        msg(hsc($e->getMessage()), -1);
+        return false;
     }
     return true;
 }
diff --git a/lib/exe/ajax.php b/lib/exe/ajax.php
index 475e4a4ba0fb3efadcef42babc2f7d157213e6ac..d62b3a5c7d2fa20eb71f478a3b030fb0af483c71 100644
--- a/lib/exe/ajax.php
+++ b/lib/exe/ajax.php
@@ -122,19 +122,23 @@ function ajax_suggestions() {
  * Andreas Gohr <andi@splitbrain.org>
  */
 function ajax_lock(){
-    global $conf;
     global $lang;
     global $ID;
     global $INFO;
     global $INPUT;
 
     $ID = cleanID($INPUT->post->str('id'));
-    if(empty($ID)) return;
-
+    if($ID === '') return;
     $INFO = pageinfo();
-
-    if (!$INFO['writable']) {
-        echo 'Permission denied';
+    if(isset($INFO['draft'])) unset($INFO['draft']);
+
+    // the preview action does locking and draft saving for us
+    try {
+        $preview = new \dokuwiki\Action\Preview();
+        $preview->checkPermissions();
+        $preview->preProcess();
+    } catch(\dokuwiki\Action\Exception\ActionException $e) {
+        echo $e->getMessage();
         return;
     }
 
@@ -143,23 +147,9 @@ function ajax_lock(){
         echo 1;
     }
 
-    if($conf['usedraft'] && $INPUT->post->str('wikitext')){
-        $client = $_SERVER['REMOTE_USER'];
-        if(!$client) $client = clientIP(true);
-
-        $draft = array('id'     => $ID,
-                'prefix' => substr($INPUT->post->str('prefix'), 0, -1),
-                'text'   => $INPUT->post->str('wikitext'),
-                'suffix' => $INPUT->post->str('suffix'),
-                'date'   => $INPUT->post->int('date'),
-                'client' => $client,
-                );
-        $cname = getCacheName($draft['client'].$ID,'.draft');
-        if(io_saveFile($cname,serialize($draft))){
-            echo $lang['draftdate'].' '.dformat();
-        }
+    if(isset($INFO['draft'])) {
+        echo $lang['draftdate'].' '.dformat();
     }
-
 }
 
 /**