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(); } - } /**