diff options
author | Andreas Gohr <gohr@cosmocode.de> | 2019-10-10 09:55:14 +0200 |
---|---|---|
committer | Andreas Gohr <gohr@cosmocode.de> | 2019-10-10 09:55:14 +0200 |
commit | 31a58aba4c24b34c34ad5764d1a35b7c398c3a2c (patch) | |
tree | 7f4d1546fbb69863a7d366fc1ff647f784853b68 /inc/Extension | |
parent | af7ba5aa0bd10fc0ad9ef983006305b4c5a8ed42 (diff) | |
parent | c0c77cd20b23921c9e893bb70b99f38be153875a (diff) | |
download | dokuwiki-31a58aba4c24b34c34ad5764d1a35b7c398c3a2c.tar.gz dokuwiki-31a58aba4c24b34c34ad5764d1a35b7c398c3a2c.zip |
Merge branch 'psr2'
* psr2: (160 commits)
fixed merge error
Moved parts of the Asian word handling to its own class
ignore snake_case error of substr_replace
fixed some line length errors
ignore PSR2 in the old form class
fix PSR2 error in switch statement
replaced deprecated utf8 functions
formatting cleanup
mark old utf8 functions deprecated
some more PSR2 cleanup
Some cleanup for the UTF-8 stuff
Moved all utf8 methods to their own namespaced classes
Create separate table files for UTF-8 handling
Ignore mixed concerns in loader
Use type safe comparisons in loader
Remove obsolete include
adjust phpcs exclude patterns for new plugin classes
🚚 Move Subscription class to deprecated.php
♻️ Split up ChangesSubscriptionSender into multiple classes
Minor optimizations in PluginController
...
Diffstat (limited to 'inc/Extension')
-rw-r--r-- | inc/Extension/ActionPlugin.php | 22 | ||||
-rw-r--r-- | inc/Extension/AdminPlugin.php | 123 | ||||
-rw-r--r-- | inc/Extension/AuthPlugin.php | 458 | ||||
-rw-r--r-- | inc/Extension/CLIPlugin.php | 13 | ||||
-rw-r--r-- | inc/Extension/Event.php | 202 | ||||
-rw-r--r-- | inc/Extension/EventHandler.php | 108 | ||||
-rw-r--r-- | inc/Extension/Plugin.php | 13 | ||||
-rw-r--r-- | inc/Extension/PluginController.php | 393 | ||||
-rw-r--r-- | inc/Extension/PluginInterface.php | 162 | ||||
-rw-r--r-- | inc/Extension/PluginTrait.php | 256 | ||||
-rw-r--r-- | inc/Extension/RemotePlugin.php | 122 | ||||
-rw-r--r-- | inc/Extension/SyntaxPlugin.php | 132 |
12 files changed, 2004 insertions, 0 deletions
diff --git a/inc/Extension/ActionPlugin.php b/inc/Extension/ActionPlugin.php new file mode 100644 index 000000000..ed6d82038 --- /dev/null +++ b/inc/Extension/ActionPlugin.php @@ -0,0 +1,22 @@ +<?php + +namespace dokuwiki\Extension; + +/** + * Action Plugin Prototype + * + * Handles action hooks within a plugin + * + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) + * @author Christopher Smith <chris@jalakai.co.uk> + */ +abstract class ActionPlugin extends Plugin +{ + + /** + * Registers a callback function for a given event + * + * @param \Doku_Event_Handler $controller + */ + abstract public function register(\Doku_Event_Handler $controller); +} diff --git a/inc/Extension/AdminPlugin.php b/inc/Extension/AdminPlugin.php new file mode 100644 index 000000000..7900a1ec4 --- /dev/null +++ b/inc/Extension/AdminPlugin.php @@ -0,0 +1,123 @@ +<?php + +namespace dokuwiki\Extension; + +/** + * Admin Plugin Prototype + * + * Implements an admin interface in a plugin + * + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) + * @author Christopher Smith <chris@jalakai.co.uk> + */ +abstract class AdminPlugin extends Plugin +{ + + /** + * Return the text that is displayed at the main admin menu + * (Default localized language string 'menu' is returned, override this function for setting another name) + * + * @param string $language language code + * @return string menu string + */ + public function getMenuText($language) + { + $menutext = $this->getLang('menu'); + if (!$menutext) { + $info = $this->getInfo(); + $menutext = $info['name'] . ' ...'; + } + return $menutext; + } + + /** + * Return the path to the icon being displayed in the main admin menu. + * By default it tries to find an 'admin.svg' file in the plugin directory. + * (Override this function for setting another image) + * + * Important: you have to return a single path, monochrome SVG icon! It has to be + * under 2 Kilobytes! + * + * We recommend icons from https://materialdesignicons.com/ or to use a matching + * style. + * + * @return string full path to the icon file + */ + public function getMenuIcon() + { + $plugin = $this->getPluginName(); + return DOKU_PLUGIN . $plugin . '/admin.svg'; + } + + /** + * Determine position in list in admin window + * Lower values are sorted up + * + * @return int + */ + public function getMenuSort() + { + return 1000; + } + + /** + * Carry out required processing + */ + public function handle() + { + // some plugins might not need this + } + + /** + * Output html of the admin page + */ + abstract public function html(); + + /** + * Checks if access should be granted to this admin plugin + * + * @return bool true if the current user may access this admin plugin + */ + public function isAccessibleByCurrentUser() { + $data = []; + $data['instance'] = $this; + $data['hasAccess'] = false; + + $event = new Event('ADMINPLUGIN_ACCESS_CHECK', $data); + if($event->advise_before()) { + if ($this->forAdminOnly()) { + $data['hasAccess'] = auth_isadmin(); + } else { + $data['hasAccess'] = auth_ismanager(); + } + } + $event->advise_after(); + + return $data['hasAccess']; + } + + /** + * Return true for access only by admins (config:superuser) or false if managers are allowed as well + * + * @return bool + */ + public function forAdminOnly() + { + return true; + } + + /** + * Return array with ToC items. Items can be created with the html_mktocitem() + * + * @see html_mktocitem() + * @see tpl_toc() + * + * @return array + */ + public function getTOC() + { + return array(); + } + +} + diff --git a/inc/Extension/AuthPlugin.php b/inc/Extension/AuthPlugin.php new file mode 100644 index 000000000..2123e1320 --- /dev/null +++ b/inc/Extension/AuthPlugin.php @@ -0,0 +1,458 @@ +<?php + +namespace dokuwiki\Extension; + +/** + * Auth Plugin Prototype + * + * allows to authenticate users in a plugin + * + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) + * @author Chris Smith <chris@jalakai.co.uk> + * @author Jan Schumann <js@jschumann-it.com> + */ +abstract class AuthPlugin extends Plugin +{ + public $success = true; + + /** + * Possible things an auth backend module may be able to + * do. The things a backend can do need to be set to true + * in the constructor. + */ + protected $cando = array( + 'addUser' => false, // can Users be created? + 'delUser' => false, // can Users be deleted? + 'modLogin' => false, // can login names be changed? + 'modPass' => false, // can passwords be changed? + 'modName' => false, // can real names be changed? + 'modMail' => false, // can emails be changed? + 'modGroups' => false, // can groups be changed? + 'getUsers' => false, // can a (filtered) list of users be retrieved? + 'getUserCount' => false, // can the number of users be retrieved? + 'getGroups' => false, // can a list of available groups be retrieved? + 'external' => false, // does the module do external auth checking? + 'logout' => true, // can the user logout again? (eg. not possible with HTTP auth) + ); + + /** + * Constructor. + * + * Carry out sanity checks to ensure the object is + * able to operate. Set capabilities in $this->cando + * array here + * + * For future compatibility, sub classes should always include a call + * to parent::__constructor() in their constructors! + * + * Set $this->success to false if checks fail + * + * @author Christopher Smith <chris@jalakai.co.uk> + */ + public function __construct() + { + // the base class constructor does nothing, derived class + // constructors do the real work + } + + /** + * Available Capabilities. [ DO NOT OVERRIDE ] + * + * For introspection/debugging + * + * @author Christopher Smith <chris@jalakai.co.uk> + * @return array + */ + public function getCapabilities() + { + return array_keys($this->cando); + } + + /** + * Capability check. [ DO NOT OVERRIDE ] + * + * Checks the capabilities set in the $this->cando array and + * some pseudo capabilities (shortcutting access to multiple + * ones) + * + * ususal capabilities start with lowercase letter + * shortcut capabilities start with uppercase letter + * + * @author Andreas Gohr <andi@splitbrain.org> + * @param string $cap the capability to check + * @return bool + */ + public function canDo($cap) + { + switch ($cap) { + case 'Profile': + // can at least one of the user's properties be changed? + return ($this->cando['modPass'] || + $this->cando['modName'] || + $this->cando['modMail']); + break; + case 'UserMod': + // can at least anything be changed? + return ($this->cando['modPass'] || + $this->cando['modName'] || + $this->cando['modMail'] || + $this->cando['modLogin'] || + $this->cando['modGroups'] || + $this->cando['modMail']); + break; + default: + // print a helping message for developers + if (!isset($this->cando[$cap])) { + msg("Check for unknown capability '$cap' - Do you use an outdated Plugin?", -1); + } + return $this->cando[$cap]; + } + } + + /** + * Trigger the AUTH_USERDATA_CHANGE event and call the modification function. [ DO NOT OVERRIDE ] + * + * You should use this function instead of calling createUser, modifyUser or + * deleteUsers directly. The event handlers can prevent the modification, for + * example for enforcing a user name schema. + * + * @author Gabriel Birke <birke@d-scribe.de> + * @param string $type Modification type ('create', 'modify', 'delete') + * @param array $params Parameters for the createUser, modifyUser or deleteUsers method. + * The content of this array depends on the modification type + * @return bool|null|int Result from the modification function or false if an event handler has canceled the action + */ + public function triggerUserMod($type, $params) + { + $validTypes = array( + 'create' => 'createUser', + 'modify' => 'modifyUser', + 'delete' => 'deleteUsers', + ); + if (empty($validTypes[$type])) { + return false; + } + + $result = false; + $eventdata = array('type' => $type, 'params' => $params, 'modification_result' => null); + $evt = new Event('AUTH_USER_CHANGE', $eventdata); + if ($evt->advise_before(true)) { + $result = call_user_func_array(array($this, $validTypes[$type]), $evt->data['params']); + $evt->data['modification_result'] = $result; + } + $evt->advise_after(); + unset($evt); + return $result; + } + + /** + * Log off the current user [ OPTIONAL ] + * + * Is run in addition to the ususal logoff method. Should + * only be needed when trustExternal is implemented. + * + * @see auth_logoff() + * @author Andreas Gohr <andi@splitbrain.org> + */ + public function logOff() + { + } + + /** + * Do all authentication [ OPTIONAL ] + * + * Set $this->cando['external'] = true when implemented + * + * If this function is implemented it will be used to + * authenticate a user - all other DokuWiki internals + * will not be used for authenticating, thus + * implementing the checkPass() function is not needed + * anymore. + * + * The function can be used to authenticate against third + * party cookies or Apache auth mechanisms and replaces + * the auth_login() function + * + * The function will be called with or without a set + * username. If the Username is given it was called + * from the login form and the given credentials might + * need to be checked. If no username was given it + * the function needs to check if the user is logged in + * by other means (cookie, environment). + * + * The function needs to set some globals needed by + * DokuWiki like auth_login() does. + * + * @see auth_login() + * @author Andreas Gohr <andi@splitbrain.org> + * + * @param string $user Username + * @param string $pass Cleartext Password + * @param bool $sticky Cookie should not expire + * @return bool true on successful auth + */ + public function trustExternal($user, $pass, $sticky = false) + { + /* some example: + + global $USERINFO; + global $conf; + $sticky ? $sticky = true : $sticky = false; //sanity check + + // do the checking here + + // set the globals if authed + $USERINFO['name'] = 'FIXME'; + $USERINFO['mail'] = 'FIXME'; + $USERINFO['grps'] = array('FIXME'); + $_SERVER['REMOTE_USER'] = $user; + $_SESSION[DOKU_COOKIE]['auth']['user'] = $user; + $_SESSION[DOKU_COOKIE]['auth']['pass'] = $pass; + $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; + return true; + + */ + } + + /** + * Check user+password [ MUST BE OVERRIDDEN ] + * + * Checks if the given user exists and the given + * plaintext password is correct + * + * May be ommited if trustExternal is used. + * + * @author Andreas Gohr <andi@splitbrain.org> + * @param string $user the user name + * @param string $pass the clear text password + * @return bool + */ + public function checkPass($user, $pass) + { + msg("no valid authorisation system in use", -1); + return false; + } + + /** + * Return user info [ MUST BE OVERRIDDEN ] + * + * Returns info about the given user needs to contain + * at least these fields: + * + * name string full name of the user + * mail string email address of the user + * grps array list of groups the user is in + * + * @author Andreas Gohr <andi@splitbrain.org> + * @param string $user the user name + * @param bool $requireGroups whether or not the returned data must include groups + * @return false|array containing user data or false + */ + public function getUserData($user, $requireGroups = true) + { + if (!$this->cando['external']) msg("no valid authorisation system in use", -1); + return false; + } + + /** + * Create a new User [implement only where required/possible] + * + * Returns false if the user already exists, null when an error + * occurred and true if everything went well. + * + * The new user HAS TO be added to the default group by this + * function! + * + * Set addUser capability when implemented + * + * @author Andreas Gohr <andi@splitbrain.org> + * @param string $user + * @param string $pass + * @param string $name + * @param string $mail + * @param null|array $grps + * @return bool|null + */ + public function createUser($user, $pass, $name, $mail, $grps = null) + { + msg("authorisation method does not allow creation of new users", -1); + return null; + } + + /** + * Modify user data [implement only where required/possible] + * + * Set the mod* capabilities according to the implemented features + * + * @author Chris Smith <chris@jalakai.co.uk> + * @param string $user nick of the user to be changed + * @param array $changes array of field/value pairs to be changed (password will be clear text) + * @return bool + */ + public function modifyUser($user, $changes) + { + msg("authorisation method does not allow modifying of user data", -1); + return false; + } + + /** + * Delete one or more users [implement only where required/possible] + * + * Set delUser capability when implemented + * + * @author Chris Smith <chris@jalakai.co.uk> + * @param array $users + * @return int number of users deleted + */ + public function deleteUsers($users) + { + msg("authorisation method does not allow deleting of users", -1); + return 0; + } + + /** + * Return a count of the number of user which meet $filter criteria + * [should be implemented whenever retrieveUsers is implemented] + * + * Set getUserCount capability when implemented + * + * @author Chris Smith <chris@jalakai.co.uk> + * @param array $filter array of field/pattern pairs, empty array for no filter + * @return int + */ + public function getUserCount($filter = array()) + { + msg("authorisation method does not provide user counts", -1); + return 0; + } + + /** + * Bulk retrieval of user data [implement only where required/possible] + * + * Set getUsers capability when implemented + * + * @author Chris Smith <chris@jalakai.co.uk> + * @param int $start index of first user to be returned + * @param int $limit max number of users to be returned, 0 for unlimited + * @param array $filter array of field/pattern pairs, null for no filter + * @return array list of userinfo (refer getUserData for internal userinfo details) + */ + public function retrieveUsers($start = 0, $limit = 0, $filter = null) + { + msg("authorisation method does not support mass retrieval of user data", -1); + return array(); + } + + /** + * Define a group [implement only where required/possible] + * + * Set addGroup capability when implemented + * + * @author Chris Smith <chris@jalakai.co.uk> + * @param string $group + * @return bool + */ + public function addGroup($group) + { + msg("authorisation method does not support independent group creation", -1); + return false; + } + + /** + * Retrieve groups [implement only where required/possible] + * + * Set getGroups capability when implemented + * + * @author Chris Smith <chris@jalakai.co.uk> + * @param int $start + * @param int $limit + * @return array + */ + public function retrieveGroups($start = 0, $limit = 0) + { + msg("authorisation method does not support group list retrieval", -1); + return array(); + } + + /** + * Return case sensitivity of the backend [OPTIONAL] + * + * When your backend is caseinsensitive (eg. you can login with USER and + * user) then you need to overwrite this method and return false + * + * @return bool + */ + public function isCaseSensitive() + { + return true; + } + + /** + * Sanitize a given username [OPTIONAL] + * + * This function is applied to any user name that is given to + * the backend and should also be applied to any user name within + * the backend before returning it somewhere. + * + * This should be used to enforce username restrictions. + * + * @author Andreas Gohr <andi@splitbrain.org> + * @param string $user username + * @return string the cleaned username + */ + public function cleanUser($user) + { + return $user; + } + + /** + * Sanitize a given groupname [OPTIONAL] + * + * This function is applied to any groupname that is given to + * the backend and should also be applied to any groupname within + * the backend before returning it somewhere. + * + * This should be used to enforce groupname restrictions. + * + * Groupnames are to be passed without a leading '@' here. + * + * @author Andreas Gohr <andi@splitbrain.org> + * @param string $group groupname + * @return string the cleaned groupname + */ + public function cleanGroup($group) + { + return $group; + } + + /** + * Check Session Cache validity [implement only where required/possible] + * + * DokuWiki caches user info in the user's session for the timespan defined + * in $conf['auth_security_timeout']. + * + * This makes sure slow authentication backends do not slow down DokuWiki. + * This also means that changes to the user database will not be reflected + * on currently logged in users. + * + * To accommodate for this, the user manager plugin will touch a reference + * file whenever a change is submitted. This function compares the filetime + * of this reference file with the time stored in the session. + * + * This reference file mechanism does not reflect changes done directly in + * the backend's database through other means than the user manager plugin. + * + * Fast backends might want to return always false, to force rechecks on + * each page load. Others might want to use their own checking here. If + * unsure, do not override. + * + * @param string $user - The username + * @author Andreas Gohr <andi@splitbrain.org> + * @return bool + */ + public function useSessionCache($user) + { + global $conf; + return ($_SESSION[DOKU_COOKIE]['auth']['time'] >= @filemtime($conf['cachedir'] . '/sessionpurge')); + } +} diff --git a/inc/Extension/CLIPlugin.php b/inc/Extension/CLIPlugin.php new file mode 100644 index 000000000..8637ccf8c --- /dev/null +++ b/inc/Extension/CLIPlugin.php @@ -0,0 +1,13 @@ +<?php + +namespace dokuwiki\Extension; + +/** + * CLI plugin prototype + * + * Provides DokuWiki plugin functionality on top of php-cli + */ +abstract class CLIPlugin extends \splitbrain\phpcli\CLI implements PluginInterface +{ + use PluginTrait; +} diff --git a/inc/Extension/Event.php b/inc/Extension/Event.php new file mode 100644 index 000000000..bbaa52e55 --- /dev/null +++ b/inc/Extension/Event.php @@ -0,0 +1,202 @@ +<?php +// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps + +namespace dokuwiki\Extension; + +/** + * The Action plugin event + */ +class Event +{ + /** @var string READONLY event name, objects must register against this name to see the event */ + public $name = ''; + /** @var mixed|null READWRITE data relevant to the event, no standardised format, refer to event docs */ + public $data = null; + /** + * @var mixed|null READWRITE the results of the event action, only relevant in "_AFTER" advise + * event handlers may modify this if they are preventing the default action + * to provide the after event handlers with event results + */ + public $result = null; + /** @var bool READONLY if true, event handlers can prevent the events default action */ + public $canPreventDefault = true; + + /** @var bool whether or not to carry out the default action associated with the event */ + protected $runDefault = true; + /** @var bool whether or not to continue propagating the event to other handlers */ + protected $mayContinue = true; + + /** + * event constructor + * + * @param string $name + * @param mixed $data + */ + public function __construct($name, &$data) + { + + $this->name = $name; + $this->data =& $data; + } + + /** + * @return string + */ + public function __toString() + { + return $this->name; + } + + /** + * advise all registered BEFORE handlers of this event + * + * if these methods are used by functions outside of this object, they must + * properly handle correct processing of any default action and issue an + * advise_after() signal. e.g. + * $evt = new dokuwiki\Plugin\Doku_Event(name, data); + * if ($evt->advise_before(canPreventDefault) { + * // default action code block + * } + * $evt->advise_after(); + * unset($evt); + * + * @param bool $enablePreventDefault + * @return bool results of processing the event, usually $this->runDefault + */ + public function advise_before($enablePreventDefault = true) + { + global $EVENT_HANDLER; + + $this->canPreventDefault = $enablePreventDefault; + if ($EVENT_HANDLER !== null) { + $EVENT_HANDLER->process_event($this, 'BEFORE'); + } else { + dbglog($this->name . ':BEFORE event triggered before event system was initialized'); + } + + return (!$enablePreventDefault || $this->runDefault); + } + + /** + * advise all registered AFTER handlers of this event + * + * @param bool $enablePreventDefault + * @see advise_before() for details + */ + public function advise_after() + { + global $EVENT_HANDLER; + + $this->mayContinue = true; + + if ($EVENT_HANDLER !== null) { + $EVENT_HANDLER->process_event($this, 'AFTER'); + } else { + dbglog($this->name . ':AFTER event triggered before event system was initialized'); + } + } + + /** + * trigger + * + * - advise all registered (<event>_BEFORE) handlers that this event is about to take place + * - carry out the default action using $this->data based on $enablePrevent and + * $this->_default, all of which may have been modified by the event handlers. + * - advise all registered (<event>_AFTER) handlers that the event has taken place + * + * @param null|callable $action + * @param bool $enablePrevent + * @return mixed $event->results + * the value set by any <event>_before or <event> handlers if the default action is prevented + * or the results of the default action (as modified by <event>_after handlers) + * or NULL no action took place and no handler modified the value + */ + public function trigger($action = null, $enablePrevent = true) + { + + if (!is_callable($action)) { + $enablePrevent = false; + if ($action !== null) { + trigger_error( + 'The default action of ' . $this . + ' is not null but also not callable. Maybe the method is not public?', + E_USER_WARNING + ); + } + } + + if ($this->advise_before($enablePrevent) && is_callable($action)) { + if (is_array($action)) { + list($obj, $method) = $action; + $this->result = $obj->$method($this->data); + } else { + $this->result = $action($this->data); + } + } + + $this->advise_after(); + + return $this->result; + } + + /** + * stopPropagation + * + * stop any further processing of the event by event handlers + * this function does not prevent the default action taking place + */ + public function stopPropagation() + { + $this->mayContinue = false; + } + + /** + * may the event propagate to the next handler? + * + * @return bool + */ + public function mayPropagate() + { + return $this->mayContinue; + } + + /** + * preventDefault + * + * prevent the default action taking place + */ + public function preventDefault() + { + $this->runDefault = false; + } + + /** + * should the default action be executed? + * + * @return bool + */ + public function mayRunDefault() + { + return $this->runDefault; + } + + /** + * Convenience method to trigger an event + * + * Creates, triggers and destroys an event in one go + * + * @param string $name name for the event + * @param mixed $data event data + * @param callable $action (optional, default=NULL) default action, a php callback function + * @param bool $canPreventDefault (optional, default=true) can hooks prevent the default action + * + * @return mixed the event results value after all event processing is complete + * by default this is the return value of the default action however + * it can be set or modified by event handler hooks + */ + static public function createAndTrigger($name, &$data, $action = null, $canPreventDefault = true) + { + $evt = new Event($name, $data); + return $evt->trigger($action, $canPreventDefault); + } +} diff --git a/inc/Extension/EventHandler.php b/inc/Extension/EventHandler.php new file mode 100644 index 000000000..7bed0fe6f --- /dev/null +++ b/inc/Extension/EventHandler.php @@ -0,0 +1,108 @@ +<?php +// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps + +namespace dokuwiki\Extension; + +/** + * Controls the registration and execution of all events, + */ +class EventHandler +{ + + // public properties: none + + // private properties + protected $hooks = array(); // array of events and their registered handlers + + /** + * event_handler + * + * constructor, loads all action plugins and calls their register() method giving them + * an opportunity to register any hooks they require + */ + public function __construct() + { + + // load action plugins + /** @var ActionPlugin $plugin */ + $plugin = null; + $pluginlist = plugin_list('action'); + + foreach ($pluginlist as $plugin_name) { + $plugin = plugin_load('action', $plugin_name); + + if ($plugin !== null) $plugin->register($this); + } + } + + /** + * register_hook + * + * register a hook for an event + * + * @param string $event string name used by the event, (incl '_before' or '_after' for triggers) + * @param string $advise + * @param object $obj object in whose scope method is to be executed, + * if NULL, method is assumed to be a globally available function + * @param string $method event handler function + * @param mixed $param data passed to the event handler + * @param int $seq sequence number for ordering hook execution (ascending) + */ + public function register_hook($event, $advise, $obj, $method, $param = null, $seq = 0) + { + $seq = (int)$seq; + $doSort = !isset($this->hooks[$event . '_' . $advise][$seq]); + $this->hooks[$event . '_' . $advise][$seq][] = array($obj, $method, $param); + + if ($doSort) { + ksort($this->hooks[$event . '_' . $advise]); + } + } + + /** + * process the before/after event + * + * @param Event $event + * @param string $advise BEFORE or AFTER + */ + public function process_event($event, $advise = '') + { + + $evt_name = $event->name . ($advise ? '_' . $advise : '_BEFORE'); + + if (!empty($this->hooks[$evt_name])) { + foreach ($this->hooks[$evt_name] as $sequenced_hooks) { + foreach ($sequenced_hooks as $hook) { + list($obj, $method, $param) = $hook; + + if ($obj === null) { + $method($event, $param); + } else { + $obj->$method($event, $param); + } + + if (!$event->mayPropagate()) return; + } + } + } + } + + /** + * Check if an event has any registered handlers + * + * When $advise is empty, both BEFORE and AFTER events will be considered, + * otherwise only the given advisory is checked + * + * @param string $name Name of the event + * @param string $advise BEFORE, AFTER or empty + * @return bool + */ + public function hasHandlerForEvent($name, $advise = '') + { + if ($advise) { + return isset($this->hooks[$name . '_' . $advise]); + } + + return isset($this->hooks[$name . '_BEFORE']) || isset($this->hooks[$name . '_AFTER']); + } +} diff --git a/inc/Extension/Plugin.php b/inc/Extension/Plugin.php new file mode 100644 index 000000000..03637fe4d --- /dev/null +++ b/inc/Extension/Plugin.php @@ -0,0 +1,13 @@ +<?php + +namespace dokuwiki\Extension; + +/** + * DokuWiki Base Plugin + * + * Most plugin types inherit from this class + */ +abstract class Plugin implements PluginInterface +{ + use PluginTrait; +} diff --git a/inc/Extension/PluginController.php b/inc/Extension/PluginController.php new file mode 100644 index 000000000..638fd3946 --- /dev/null +++ b/inc/Extension/PluginController.php @@ -0,0 +1,393 @@ +<?php + +namespace dokuwiki\Extension; + +/** + * Class to encapsulate access to dokuwiki plugins + * + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) + * @author Christopher Smith <chris@jalakai.co.uk> + */ +class PluginController +{ + /** @var array the types of plugins DokuWiki supports */ + const PLUGIN_TYPES = ['auth', 'admin', 'syntax', 'action', 'renderer', 'helper', 'remote', 'cli']; + + protected $listByType = []; + /** @var array all installed plugins and their enabled state [plugin=>enabled] */ + protected $masterList = []; + protected $pluginCascade = ['default' => [], 'local' => [], 'protected' => []]; + protected $lastLocalConfigFile = ''; + + /** + * Populates the master list of plugins + */ + public function __construct() + { + $this->loadConfig(); + $this->populateMasterList(); + } + + /** + * Returns a list of available plugins of given type + * + * @param $type string, plugin_type name; + * the type of plugin to return, + * use empty string for all types + * @param $all bool; + * false to only return enabled plugins, + * true to return both enabled and disabled plugins + * + * @return array of + * - plugin names when $type = '' + * - or plugin component names when a $type is given + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + public function getList($type = '', $all = false) + { + + // request the complete list + if (!$type) { + return $all ? array_keys($this->masterList) : array_keys(array_filter($this->masterList)); + } + + if (!isset($this->listByType[$type]['enabled'])) { + $this->listByType[$type]['enabled'] = $this->getListByType($type, true); + } + if ($all && !isset($this->listByType[$type]['disabled'])) { + $this->listByType[$type]['disabled'] = $this->getListByType($type, false); + } + + return $all + ? array_merge($this->listByType[$type]['enabled'], $this->listByType[$type]['disabled']) + : $this->listByType[$type]['enabled']; + } + + /** + * Loads the given plugin and creates an object of it + * + * @param $type string type of plugin to load + * @param $name string name of the plugin to load + * @param $new bool true to return a new instance of the plugin, false to use an already loaded instance + * @param $disabled bool true to load even disabled plugins + * @return PluginInterface|null the plugin object or null on failure + * @author Andreas Gohr <andi@splitbrain.org> + * + */ + public function load($type, $name, $new = false, $disabled = false) + { + + //we keep all loaded plugins available in global scope for reuse + global $DOKU_PLUGINS; + + list($plugin, /* $component */) = $this->splitName($name); + + // check if disabled + if (!$disabled && !$this->isEnabled($plugin)) { + return null; + } + + $class = $type . '_plugin_' . $name; + + //plugin already loaded? + if (!empty($DOKU_PLUGINS[$type][$name])) { + if ($new || !$DOKU_PLUGINS[$type][$name]->isSingleton()) { + return class_exists($class, true) ? new $class : null; + } + + return $DOKU_PLUGINS[$type][$name]; + } + + //construct class and instantiate + if (!class_exists($class, true)) { + + # the plugin might be in the wrong directory + $inf = confToHash(DOKU_PLUGIN . "$plugin/plugin.info.txt"); + if ($inf['base'] && $inf['base'] != $plugin) { + msg( + sprintf( + "Plugin installed incorrectly. Rename plugin directory '%s' to '%s'.", + hsc($plugin), + hsc( + $inf['base'] + ) + ), -1 + ); + } elseif (preg_match('/^' . DOKU_PLUGIN_NAME_REGEX . '$/', $plugin) !== 1) { + msg( + sprintf( + "Plugin name '%s' is not a valid plugin name, only the characters a-z and 0-9 are allowed. " . + 'Maybe the plugin has been installed in the wrong directory?', hsc($plugin) + ), -1 + ); + } + return null; + } + + $DOKU_PLUGINS[$type][$name] = new $class; + return $DOKU_PLUGINS[$type][$name]; + } + + /** + * Whether plugin is disabled + * + * @param string $plugin name of plugin + * @return bool true disabled, false enabled + * @deprecated in favor of the more sensible isEnabled where the return value matches the enabled state + */ + public function isDisabled($plugin) + { + dbg_deprecated('isEnabled()'); + return !$this->isEnabled($plugin); + } + + /** + * Check whether plugin is disabled + * + * @param string $plugin name of plugin + * @return bool true enabled, false disabled + */ + public function isEnabled($plugin) + { + return !empty($this->masterList[$plugin]); + } + + /** + * Disable the plugin + * + * @param string $plugin name of plugin + * @return bool true saving succeed, false saving failed + */ + public function disable($plugin) + { + if (array_key_exists($plugin, $this->pluginCascade['protected'])) return false; + $this->masterList[$plugin] = 0; + return $this->saveList(); + } + + /** + * Enable the plugin + * + * @param string $plugin name of plugin + * @return bool true saving succeed, false saving failed + */ + public function enable($plugin) + { + if (array_key_exists($plugin, $this->pluginCascade['protected'])) return false; + $this->masterList[$plugin] = 1; + return $this->saveList(); + } + + /** + * Returns cascade of the config files + * + * @return array with arrays of plugin configs + */ + public function getCascade() + { + return $this->pluginCascade; + } + + /** + * Read all installed plugins and their current enabled state + */ + protected function populateMasterList() + { + if ($dh = @opendir(DOKU_PLUGIN)) { + $all_plugins = array(); + while (false !== ($plugin = readdir($dh))) { + if ($plugin[0] === '.') continue; // skip hidden entries + if (is_file(DOKU_PLUGIN . $plugin)) continue; // skip files, we're only interested in directories + + if (array_key_exists($plugin, $this->masterList) && $this->masterList[$plugin] == 0) { + $all_plugins[$plugin] = 0; + + } elseif (array_key_exists($plugin, $this->masterList) && $this->masterList[$plugin] == 1) { + $all_plugins[$plugin] = 1; + } else { + $all_plugins[$plugin] = 1; + } + } + $this->masterList = $all_plugins; + if (!file_exists($this->lastLocalConfigFile)) { + $this->saveList(true); + } + } + } + + /** + * Includes the plugin config $files + * and returns the entries of the $plugins array set in these files + * + * @param array $files list of files to include, latter overrides previous + * @return array with entries of the $plugins arrays of the included files + */ + protected function checkRequire($files) + { + $plugins = array(); + foreach ($files as $file) { + if (file_exists($file)) { + include_once($file); + } + } + return $plugins; + } + + /** + * Save the current list of plugins + * + * @param bool $forceSave ; + * false to save only when config changed + * true to always save + * @return bool true saving succeed, false saving failed + */ + protected function saveList($forceSave = false) + { + global $conf; + + if (empty($this->masterList)) return false; + + // Rebuild list of local settings + $local_plugins = $this->rebuildLocal(); + if ($local_plugins != $this->pluginCascade['local'] || $forceSave) { + $file = $this->lastLocalConfigFile; + $out = "<?php\n/*\n * Local plugin enable/disable settings\n" . + " * Auto-generated through plugin/extension manager\n *\n" . + " * NOTE: Plugins will not be added to this file unless there " . + "is a need to override a default setting. Plugins are\n" . + " * enabled by default.\n */\n"; + foreach ($local_plugins as $plugin => $value) { + $out .= "\$plugins['$plugin'] = $value;\n"; + } + // backup current file (remove any existing backup) + if (file_exists($file)) { + $backup = $file . '.bak'; + if (file_exists($backup)) @unlink($backup); + if (!@copy($file, $backup)) return false; + if (!empty($conf['fperm'])) chmod($backup, $conf['fperm']); + } + //check if can open for writing, else restore + return io_saveFile($file, $out); + } + return false; + } + + /** + * Rebuild the set of local plugins + * + * @return array array of plugins to be saved in end($config_cascade['plugins']['local']) + */ + protected function rebuildLocal() + { + //assign to local variable to avoid overwriting + $backup = $this->masterList; + //Can't do anything about protected one so rule them out completely + $local_default = array_diff_key($backup, $this->pluginCascade['protected']); + //Diff between local+default and default + //gives us the ones we need to check and save + $diffed_ones = array_diff_key($local_default, $this->pluginCascade['default']); + //The ones which we are sure of (list of 0s not in default) + $sure_plugins = array_filter($diffed_ones, array($this, 'negate')); + //the ones in need of diff + $conflicts = array_diff_key($local_default, $diffed_ones); + //The final list + return array_merge($sure_plugins, array_diff_assoc($conflicts, $this->pluginCascade['default'])); + } + + /** + * Build the list of plugins and cascade + * + */ + protected function loadConfig() + { + global $config_cascade; + foreach (array('default', 'protected') as $type) { + if (array_key_exists($type, $config_cascade['plugins'])) { + $this->pluginCascade[$type] = $this->checkRequire($config_cascade['plugins'][$type]); + } + } + $local = $config_cascade['plugins']['local']; + $this->lastLocalConfigFile = array_pop($local); + $this->pluginCascade['local'] = $this->checkRequire(array($this->lastLocalConfigFile)); + if (is_array($local)) { + $this->pluginCascade['default'] = array_merge( + $this->pluginCascade['default'], + $this->checkRequire($local) + ); + } + $this->masterList = array_merge( + $this->pluginCascade['default'], + $this->pluginCascade['local'], + $this->pluginCascade['protected'] + ); + } + + /** + * Returns a list of available plugin components of given type + * + * @param string $type plugin_type name; the type of plugin to return, + * @param bool $enabled true to return enabled plugins, + * false to return disabled plugins + * @return array of plugin components of requested type + */ + protected function getListByType($type, $enabled) + { + $master_list = $enabled + ? array_keys(array_filter($this->masterList)) + : array_keys(array_filter($this->masterList, array($this, 'negate'))); + $plugins = array(); + + foreach ($master_list as $plugin) { + + if (file_exists(DOKU_PLUGIN . "$plugin/$type.php")) { + $plugins[] = $plugin; + continue; + } + + $typedir = DOKU_PLUGIN . "$plugin/$type/"; + if (is_dir($typedir)) { + if ($dp = opendir($typedir)) { + while (false !== ($component = readdir($dp))) { + if (strpos($component, '.') === 0 || strtolower(substr($component, -4)) !== '.php') continue; + if (is_file($typedir . $component)) { + $plugins[] = $plugin . '_' . substr($component, 0, -4); + } + } + closedir($dp); + } + } + + }//foreach + + return $plugins; + } + + /** + * Split name in a plugin name and a component name + * + * @param string $name + * @return array with + * - plugin name + * - and component name when available, otherwise empty string + */ + protected function splitName($name) + { + if (!isset($this->masterList[$name])) { + return explode('_', $name, 2); + } + + return array($name, ''); + } + + /** + * Returns inverse boolean value of the input + * + * @param mixed $input + * @return bool inversed boolean value of input + */ + protected function negate($input) + { + return !(bool)$input; + } +} diff --git a/inc/Extension/PluginInterface.php b/inc/Extension/PluginInterface.php new file mode 100644 index 000000000..f2dbe8626 --- /dev/null +++ b/inc/Extension/PluginInterface.php @@ -0,0 +1,162 @@ +<?php + +namespace dokuwiki\Extension; + +/** + * DokuWiki Plugin Interface + * + * Defines the public contract all DokuWiki plugins will adhere to. The actual code + * to do so is defined in dokuwiki\Extension\PluginTrait + * + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) + * @author Christopher Smith <chris@jalakai.co.uk> + */ +interface PluginInterface +{ + /** + * General Info + * + * Needs to return a associative array with the following values: + * + * base - the plugin's base name (eg. the directory it needs to be installed in) + * author - Author of the plugin + * email - Email address to contact the author + * date - Last modified date of the plugin in YYYY-MM-DD format + * name - Name of the plugin + * desc - Short description of the plugin (Text only) + * url - Website with more information on the plugin (eg. syntax description) + */ + public function getInfo(); + + /** + * The type of the plugin inferred from the class name + * + * @return string plugin type + */ + public function getPluginType(); + + /** + * The name of the plugin inferred from the class name + * + * @return string plugin name + */ + public function getPluginName(); + + /** + * The component part of the plugin inferred from the class name + * + * @return string component name + */ + public function getPluginComponent(); + + /** + * Access plugin language strings + * + * to try to minimise unnecessary loading of the strings when the plugin doesn't require them + * e.g. when info plugin is querying plugins for information about themselves. + * + * @param string $id id of the string to be retrieved + * @return string in appropriate language or english if not available + */ + public function getLang($id); + + /** + * retrieve a language dependent file and pass to xhtml renderer for display + * plugin equivalent of p_locale_xhtml() + * + * @param string $id id of language dependent wiki page + * @return string parsed contents of the wiki page in xhtml format + */ + public function locale_xhtml($id); + + /** + * Prepends appropriate path for a language dependent filename + * plugin equivalent of localFN() + * + * @param string $id id of localization file + * @param string $ext The file extension (usually txt) + * @return string wiki text + */ + public function localFN($id, $ext = 'txt'); + + /** + * Reads all the plugins language dependent strings into $this->lang + * this function is automatically called by getLang() + * + * @todo this could be made protected and be moved to the trait only + */ + public function setupLocale(); + + /** + * use this function to access plugin configuration variables + * + * @param string $setting the setting to access + * @param mixed $notset what to return if the setting is not available + * @return mixed + */ + public function getConf($setting, $notset = false); + + /** + * merges the plugin's default settings with any local settings + * this function is automatically called through getConf() + * + * @todo this could be made protected and be moved to the trait only + */ + public function loadConfig(); + + /** + * Loads a given helper plugin (if enabled) + * + * @author Esther Brunner <wikidesign@gmail.com> + * + * @param string $name name of plugin to load + * @param bool $msg if a message should be displayed in case the plugin is not available + * @return PluginInterface|null helper plugin object + */ + public function loadHelper($name, $msg = true); + + /** + * email + * standardised function to generate an email link according to obfuscation settings + * + * @param string $email + * @param string $name + * @param string $class + * @param string $more + * @return string html + */ + public function email($email, $name = '', $class = '', $more = ''); + + /** + * external_link + * standardised function to generate an external link according to conf settings + * + * @param string $link + * @param string $title + * @param string $class + * @param string $target + * @param string $more + * @return string + */ + public function external_link($link, $title = '', $class = '', $target = '', $more = ''); + + /** + * output text string through the parser, allows dokuwiki markup to be used + * very ineffecient for small pieces of data - try not to use + * + * @param string $text wiki markup to parse + * @param string $format output format + * @return null|string + */ + public function render_text($text, $format = 'xhtml'); + + /** + * Allow the plugin to prevent DokuWiki from reusing an instance + * + * @return bool false if the plugin has to be instantiated + */ + public function isSingleton(); +} + + + diff --git a/inc/Extension/PluginTrait.php b/inc/Extension/PluginTrait.php new file mode 100644 index 000000000..f1db0f598 --- /dev/null +++ b/inc/Extension/PluginTrait.php @@ -0,0 +1,256 @@ +<?php + +namespace dokuwiki\Extension; + +/** + * Provides standard DokuWiki plugin behaviour + */ +trait PluginTrait +{ + + protected $localised = false; // set to true by setupLocale() after loading language dependent strings + protected $lang = array(); // array to hold language dependent strings, best accessed via ->getLang() + protected $configloaded = false; // set to true by loadConfig() after loading plugin configuration variables + protected $conf = array(); // array to hold plugin settings, best accessed via ->getConf() + + /** + * @see PluginInterface::getInfo() + */ + public function getInfo() + { + $parts = explode('_', get_class($this)); + $info = DOKU_PLUGIN . '/' . $parts[2] . '/plugin.info.txt'; + if (file_exists($info)) return confToHash($info); + + msg( + 'getInfo() not implemented in ' . get_class($this) . ' and ' . $info . ' not found.<br />' . + 'Verify you\'re running the latest version of the plugin. If the problem persists, send a ' . + 'bug report to the author of the ' . $parts[2] . ' plugin.', -1 + ); + return array( + 'date' => '0000-00-00', + 'name' => $parts[2] . ' plugin', + ); + } + + /** + * @see PluginInterface::isSingleton() + */ + public function isSingleton() + { + return true; + } + + /** + * @see PluginInterface::loadHelper() + */ + public function loadHelper($name, $msg = true) + { + $obj = plugin_load('helper', $name); + if (is_null($obj) && $msg) msg("Helper plugin $name is not available or invalid.", -1); + return $obj; + } + + // region introspection methods + + /** + * @see PluginInterface::getPluginType() + */ + public function getPluginType() + { + list($t) = explode('_', get_class($this), 2); + return $t; + } + + /** + * @see PluginInterface::getPluginName() + */ + public function getPluginName() + { + list(/* $t */, /* $p */, $n) = explode('_', get_class($this), 4); + return $n; + } + + /** + * @see PluginInterface::getPluginComponent() + */ + public function getPluginComponent() + { + list(/* $t */, /* $p */, /* $n */, $c) = explode('_', get_class($this), 4); + return (isset($c) ? $c : ''); + } + + // endregion + // region localization methods + + /** + * @see PluginInterface::getLang() + */ + public function getLang($id) + { + if (!$this->localised) $this->setupLocale(); + + return (isset($this->lang[$id]) ? $this->lang[$id] : ''); + } + + /** + * @see PluginInterface::locale_xhtml() + */ + public function locale_xhtml($id) + { + return p_cached_output($this->localFN($id)); + } + + /** + * @see PluginInterface::localFN() + */ + public function localFN($id, $ext = 'txt') + { + global $conf; + $plugin = $this->getPluginName(); + $file = DOKU_CONF . 'plugin_lang/' . $plugin . '/' . $conf['lang'] . '/' . $id . '.' . $ext; + if (!file_exists($file)) { + $file = DOKU_PLUGIN . $plugin . '/lang/' . $conf['lang'] . '/' . $id . '.' . $ext; + if (!file_exists($file)) { + //fall back to english + $file = DOKU_PLUGIN . $plugin . '/lang/en/' . $id . '.' . $ext; + } + } + return $file; + } + + /** + * @see PluginInterface::setupLocale() + */ + public function setupLocale() + { + if ($this->localised) return; + + global $conf, $config_cascade; // definitely don't invoke "global $lang" + $path = DOKU_PLUGIN . $this->getPluginName() . '/lang/'; + + $lang = array(); + + // don't include once, in case several plugin components require the same language file + @include($path . 'en/lang.php'); + foreach ($config_cascade['lang']['plugin'] as $config_file) { + if (file_exists($config_file . $this->getPluginName() . '/en/lang.php')) { + include($config_file . $this->getPluginName() . '/en/lang.php'); + } + } + + if ($conf['lang'] != 'en') { + @include($path . $conf['lang'] . '/lang.php'); + foreach ($config_cascade['lang']['plugin'] as $config_file) { + if (file_exists($config_file . $this->getPluginName() . '/' . $conf['lang'] . '/lang.php')) { + include($config_file . $this->getPluginName() . '/' . $conf['lang'] . '/lang.php'); + } + } + } + + $this->lang = $lang; + $this->localised = true; + } + + // endregion + // region configuration methods + + /** + * @see PluginInterface::getConf() + */ + public function getConf($setting, $notset = false) + { + + if (!$this->configloaded) { + $this->loadConfig(); + } + + if (isset($this->conf[$setting])) { + return $this->conf[$setting]; + } else { + return $notset; + } + } + + /** + * @see PluginInterface::loadConfig() + */ + public function loadConfig() + { + global $conf; + + $defaults = $this->readDefaultSettings(); + $plugin = $this->getPluginName(); + + foreach ($defaults as $key => $value) { + if (isset($conf['plugin'][$plugin][$key])) continue; + $conf['plugin'][$plugin][$key] = $value; + } + + $this->configloaded = true; + $this->conf =& $conf['plugin'][$plugin]; + } + + /** + * read the plugin's default configuration settings from conf/default.php + * this function is automatically called through getConf() + * + * @return array setting => value + */ + protected function readDefaultSettings() + { + + $path = DOKU_PLUGIN . $this->getPluginName() . '/conf/'; + $conf = array(); + + if (file_exists($path . 'default.php')) { + include($path . 'default.php'); + } + + return $conf; + } + + // endregion + // region output methods + + /** + * @see PluginInterface::email() + */ + public function email($email, $name = '', $class = '', $more = '') + { + if (!$email) return $name; + $email = obfuscate($email); + if (!$name) $name = $email; + $class = "class='" . ($class ? $class : 'mail') . "'"; + return "<a href='mailto:$email' $class title='$email' $more>$name</a>"; + } + + /** + * @see PluginInterface::external_link() + */ + public function external_link($link, $title = '', $class = '', $target = '', $more = '') + { + global $conf; + + $link = htmlentities($link); + if (!$title) $title = $link; + if (!$target) $target = $conf['target']['extern']; + if ($conf['relnofollow']) $more .= ' rel="nofollow"'; + + if ($class) $class = " class='$class'"; + if ($target) $target = " target='$target'"; + if ($more) $more = " " . trim($more); + + return "<a href='$link'$class$target$more>$title</a>"; + } + + /** + * @see PluginInterface::render_text() + */ + public function render_text($text, $format = 'xhtml') + { + return p_render($format, p_get_instructions($text), $info); + } + + // endregion +} diff --git a/inc/Extension/RemotePlugin.php b/inc/Extension/RemotePlugin.php new file mode 100644 index 000000000..33bca980a --- /dev/null +++ b/inc/Extension/RemotePlugin.php @@ -0,0 +1,122 @@ +<?php + +namespace dokuwiki\Extension; + +use dokuwiki\Remote\Api; +use ReflectionException; +use ReflectionMethod; + +/** + * Remote Plugin prototype + * + * Add functionality to the remote API in a plugin + */ +abstract class RemotePlugin extends Plugin +{ + + private $api; + + /** + * Constructor + */ + public function __construct() + { + $this->api = new Api(); + } + + /** + * Get all available methods with remote access. + * + * By default it exports all public methods of a remote plugin. Methods beginning + * with an underscore are skipped. + * + * @return array Information about all provided methods. {@see dokuwiki\Remote\RemoteAPI}. + * @throws ReflectionException + */ + public function _getMethods() + { + $result = array(); + + $reflection = new \ReflectionClass($this); + foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + // skip parent methods, only methods further down are exported + $declaredin = $method->getDeclaringClass()->name; + if ($declaredin === 'dokuwiki\Extension\Plugin' || $declaredin === 'dokuwiki\Extension\RemotePlugin') { + continue; + } + $method_name = $method->name; + if (strpos($method_name, '_') === 0) { + continue; + } + + // strip asterisks + $doc = $method->getDocComment(); + $doc = preg_replace( + array('/^[ \t]*\/\*+[ \t]*/m', '/[ \t]*\*+[ \t]*/m', '/\*+\/\s*$/m', '/\s*\/\s*$/m'), + array('', '', '', ''), + $doc + ); + + // prepare data + $data = array(); + $data['name'] = $method_name; + $data['public'] = 0; + $data['doc'] = $doc; + $data['args'] = array(); + + // get parameter type from doc block type hint + foreach ($method->getParameters() as $parameter) { + $name = $parameter->name; + $type = 'string'; // we default to string + if (preg_match('/^@param[ \t]+([\w|\[\]]+)[ \t]\$' . $name . '/m', $doc, $m)) { + $type = $this->cleanTypeHint($m[1]); + } + $data['args'][] = $type; + } + + // get return type from doc block type hint + if (preg_match('/^@return[ \t]+([\w|\[\]]+)/m', $doc, $m)) { + $data['return'] = $this->cleanTypeHint($m[1]); + } else { + $data['return'] = 'string'; + } + + // add to result + $result[$method_name] = $data; + } + + return $result; + } + + /** + * Matches the given type hint against the valid options for the remote API + * + * @param string $hint + * @return string + */ + protected function cleanTypeHint($hint) + { + $types = explode('|', $hint); + foreach ($types as $t) { + if (substr($t, -2) === '[]') { + return 'array'; + } + if ($t === 'boolean') { + return 'bool'; + } + if (in_array($t, array('array', 'string', 'int', 'double', 'bool', 'null', 'date', 'file'))) { + return $t; + } + } + return 'string'; + } + + /** + * @return Api + */ + protected function getApi() + { + return $this->api; + } + +} diff --git a/inc/Extension/SyntaxPlugin.php b/inc/Extension/SyntaxPlugin.php new file mode 100644 index 000000000..e5dda9bdc --- /dev/null +++ b/inc/Extension/SyntaxPlugin.php @@ -0,0 +1,132 @@ +<?php + +namespace dokuwiki\Extension; + +use \Doku_Handler; +use \Doku_Renderer; + +/** + * Syntax Plugin Prototype + * + * All DokuWiki plugins to extend the parser/rendering mechanism + * need to inherit from this class + * + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) + * @author Andreas Gohr <andi@splitbrain.org> + */ +abstract class SyntaxPlugin extends \dokuwiki\Parsing\ParserMode\Plugin +{ + use PluginTrait; + + protected $allowedModesSetup = false; + + /** + * Syntax Type + * + * Needs to return one of the mode types defined in $PARSER_MODES in Parser.php + * + * @return string + */ + abstract public function getType(); + + /** + * Allowed Mode Types + * + * Defines the mode types for other dokuwiki markup that maybe nested within the + * plugin's own markup. Needs to return an array of one or more of the mode types + * defined in $PARSER_MODES in Parser.php + * + * @return array + */ + public function getAllowedTypes() + { + return array(); + } + + /** + * Paragraph Type + * + * Defines how this syntax is handled regarding paragraphs. This is important + * for correct XHTML nesting. Should return one of the following: + * + * 'normal' - The plugin can be used inside paragraphs + * 'block' - Open paragraphs need to be closed before plugin output + * 'stack' - Special case. Plugin wraps other paragraphs. + * + * @see Doku_Handler_Block + * + * @return string + */ + public function getPType() + { + return 'normal'; + } + + /** + * Handler to prepare matched data for the rendering process + * + * This function can only pass data to render() via its return value - render() + * may be not be run during the object's current life. + * + * Usually you should only need the $match param. + * + * @param string $match The text matched by the patterns + * @param int $state The lexer state for the match + * @param int $pos The character position of the matched text + * @param Doku_Handler $handler The Doku_Handler object + * @return bool|array Return an array with all data you want to use in render, false don't add an instruction + */ + abstract public function handle($match, $state, $pos, Doku_Handler $handler); + + /** + * Handles the actual output creation. + * + * The function must not assume any other of the classes methods have been run + * during the object's current life. The only reliable data it receives are its + * parameters. + * + * The function should always check for the given output format and return false + * when a format isn't supported. + * + * $renderer contains a reference to the renderer object which is + * currently handling the rendering. You need to use it for writing + * the output. How this is done depends on the renderer used (specified + * by $format + * + * The contents of the $data array depends on what the handler() function above + * created + * + * @param string $format output format being rendered + * @param Doku_Renderer $renderer the current renderer object + * @param array $data data created by handler() + * @return boolean rendered correctly? (however, returned value is not used at the moment) + */ + abstract public function render($format, Doku_Renderer $renderer, $data); + + /** + * There should be no need to override this function + * + * @param string $mode + * @return bool + */ + public function accepts($mode) + { + + if (!$this->allowedModesSetup) { + global $PARSER_MODES; + + $allowedModeTypes = $this->getAllowedTypes(); + foreach ($allowedModeTypes as $mt) { + $this->allowedModes = array_merge($this->allowedModes, $PARSER_MODES[$mt]); + } + + $idx = array_search(substr(get_class($this), 7), (array)$this->allowedModes); + if ($idx !== false) { + unset($this->allowedModes[$idx]); + } + $this->allowedModesSetup = true; + } + + return parent::accepts($mode); + } +} |