| Index: trunk/phase3/docs/hooks.doc |
| — | — | @@ -0,0 +1,228 @@ |
| | 2 | +HOOKS.DOC |
| | 3 | + |
| | 4 | +This document describes how event hooks work in MediaWiki; how to add |
| | 5 | +hooks for an event; and how to run hooks for an event. |
| | 6 | + |
| | 7 | +==Glossary== |
| | 8 | + |
| | 9 | +event |
| | 10 | + Something that happens with the wiki. For example: a user logs |
| | 11 | + in. A wiki page is saved. A wiki page is deleted. Often there are |
| | 12 | + two events associated with a single action: one before the code |
| | 13 | + is run to make the event happen, and one after. Each event has a |
| | 14 | + name, preferably in CamelCase. For example, 'UserLogin', |
| | 15 | + 'ArticleSave', 'ArticleSaveComplete', 'ArticleDelete'. |
| | 16 | + |
| | 17 | +hook |
| | 18 | + A clump of code and data that should be run when an event |
| | 19 | + happens. This can be either a function and a chunk of data, or an |
| | 20 | + object and a method. |
| | 21 | + |
| | 22 | +hook function |
| | 23 | + The function part of a hook. |
| | 24 | + |
| | 25 | +==Rationale== |
| | 26 | + |
| | 27 | +Hooks allow us to decouple optionally-run code from code that is run |
| | 28 | +for everyone. It allows MediaWiki hackers, third-party developers and |
| | 29 | +local administrators to define code that will be run at certain points |
| | 30 | +in the mainline code, and to modify the data run by that mainline |
| | 31 | +code. Hooks can keep mainline code simple, and make it easier to |
| | 32 | +write extensions. Hooks are a principled alternative to local patches. |
| | 33 | + |
| | 34 | +Consider, for example, two options in MediaWiki. One reverses the |
| | 35 | +order of a title before displaying the article; the other converts the |
| | 36 | +title to all uppercase letters. Currently, in MediaWiki code, we |
| | 37 | +handle this as follows: |
| | 38 | + |
| | 39 | + function showAnArticle($article) { |
| | 40 | + global $wgReverseTitle, $wgCapitalizeTitle; |
| | 41 | + |
| | 42 | + if ($wgReverseTitle) { |
| | 43 | + wfReverseTitle($article); |
| | 44 | + } |
| | 45 | + |
| | 46 | + if ($wgCapitalizeTitle) { |
| | 47 | + wfCapitalizeTitle($article); |
| | 48 | + } |
| | 49 | + |
| | 50 | + # code to actually show the article goes here |
| | 51 | + } |
| | 52 | + |
| | 53 | +An extension writer, or a local admin, will often add custom code to |
| | 54 | +the function -- with or without a global variable. For example, |
| | 55 | +someone wanting email notification when an article is shown may add: |
| | 56 | + |
| | 57 | + function showAnArticle($article) { |
| | 58 | + global $wgReverseTitle, $wgCapitalizeTitle; |
| | 59 | + |
| | 60 | + if ($wgReverseTitle) { |
| | 61 | + wfReverseTitle($article); |
| | 62 | + } |
| | 63 | + |
| | 64 | + if ($wgCapitalizeTitle) { |
| | 65 | + wfCapitalizeTitle($article); |
| | 66 | + } |
| | 67 | + |
| | 68 | + # code to actually show the article goes here |
| | 69 | + |
| | 70 | + if ($wgNotifyArticle) { |
| | 71 | + wfNotifyArticleShow($article)); |
| | 72 | + } |
| | 73 | + } |
| | 74 | + |
| | 75 | +Using a hook-running strategy, we can avoid having all this |
| | 76 | +option-specific stuff in our mainline code. Using hooks, the function |
| | 77 | +becomes: |
| | 78 | + |
| | 79 | + function showAnArticle($article) { |
| | 80 | + if (wfRunHooks('ArticleShow', $article)) { |
| | 81 | + # code to actually show the article goes here |
| | 82 | + wfRunHooks('ArticleShowComplete', $article); |
| | 83 | + } |
| | 84 | + } |
| | 85 | + |
| | 86 | +We've cleaned up the code here by removing clumps of weird, |
| | 87 | +infrequently used code and moving them off somewhere else. It's much |
| | 88 | +easier for someone working with this code to see what's _really_ going |
| | 89 | +on, and make changes or fix bugs. |
| | 90 | + |
| | 91 | +In addition, we can take all the code that deals with the little-used |
| | 92 | +title-reversing options (say) and put it in one place. Instead of |
| | 93 | +having a little title-reversing if-block spread all over the codebase |
| | 94 | +in showAnArticle, deleteAnArticle, exportArticle, etc., we can |
| | 95 | +concentrate it all in an extension file: |
| | 96 | + |
| | 97 | + function reverseArticleTitle($article) { |
| | 98 | + # ... |
| | 99 | + } |
| | 100 | + |
| | 101 | + function reverseForExport($article) { |
| | 102 | + # ... |
| | 103 | + } |
| | 104 | + |
| | 105 | +The setup function for the extension just has to add its hook |
| | 106 | +functions to the appropriate events: |
| | 107 | + |
| | 108 | + setupTitleReversingExtension() { |
| | 109 | + global $wgHooks; |
| | 110 | + |
| | 111 | + $wgHooks['ArticleShow'][] = reverseArticleTitle; |
| | 112 | + $wgHooks['ArticleDelete'][] = reverseArticleTitle; |
| | 113 | + $wgHooks['ArticleExport'][] = reverseForExport; |
| | 114 | + } |
| | 115 | + |
| | 116 | +Having all this code related to the title-reversion option in one |
| | 117 | +place means that it's easier to read and understand; you don't have to |
| | 118 | +do a grep-find to see where the $wgReverseTitle variable is used, say. |
| | 119 | + |
| | 120 | +If the code is well enough isolated, it can even be excluded when not |
| | 121 | +used -- making for some slight savings in memory and time at runtime. |
| | 122 | +Admins who want to have all the reversed titles can add: |
| | 123 | + |
| | 124 | + require_once('extensions/ReverseTitle.php'); |
| | 125 | + |
| | 126 | +...to their LocalSettings.php file; those of us who don't want or need |
| | 127 | +it can just leave it out. |
| | 128 | + |
| | 129 | +The extensions don't even have to be shipped with MediaWiki; they |
| | 130 | +could be provided by a third-party developer or written by the admin |
| | 131 | +him/herself. |
| | 132 | + |
| | 133 | +==Writing hooks== |
| | 134 | + |
| | 135 | +A hook is a chunk of code run at some particular event. It consists of: |
| | 136 | + |
| | 137 | + * a function with some optional accompanying data, or |
| | 138 | + * an object with a method and some optional accompanying data. |
| | 139 | + |
| | 140 | +Hooks are registered by adding them to the global $wgHooks array for a |
| | 141 | +given event. All the following are valid ways to define hooks: |
| | 142 | + |
| | 143 | + $wgHooks['EventName'][] = someFunction; # function, no data |
| | 144 | + $wgHooks['EventName'][] = array(someFunction, $someData); |
| | 145 | + $wgHooks['EventName'][] = array(someFunction); # weird, but OK |
| | 146 | + |
| | 147 | + $wgHooks['EventName'][] = $object; # object only |
| | 148 | + $wgHooks['EventName'][] = array($object, 'someMethod'); |
| | 149 | + $wgHooks['EventName'][] = array($object, 'someMethod', $someData); |
| | 150 | + $wgHooks['EventName'][] = array($object); # weird but OK |
| | 151 | + |
| | 152 | +When an event occurs, the function (or object method) will be called |
| | 153 | +with the optional data provided as well as event-specific parameters. |
| | 154 | +The above examples would result in the following code being executed |
| | 155 | +when 'EventName' happened: |
| | 156 | + |
| | 157 | + # function, no data |
| | 158 | + someFunction($param1, $param2) |
| | 159 | + # function with data |
| | 160 | + someFunction($someData, $param1, $param2) |
| | 161 | + |
| | 162 | + # object only |
| | 163 | + $object->onEventName($param1, $param2) |
| | 164 | + # object with method |
| | 165 | + $object->someMethod($param1, $param2) |
| | 166 | + # object with method and data |
| | 167 | + $object->someMethod($someData, $param1, $param2) |
| | 168 | + |
| | 169 | +Note that when an object is the hook, and there's no specified method, |
| | 170 | +the default method called is 'onEventName'. For different events this |
| | 171 | +would be different: 'onArticleSave', 'onUserLogin', etc. |
| | 172 | + |
| | 173 | +The extra data is useful if we want to use the same function or object |
| | 174 | +for different purposes. For example: |
| | 175 | + |
| | 176 | + $wgHooks['ArticleSaveComplete'][] = array(ircNotify, 'TimStarling'); |
| | 177 | + $wgHooks['ArticleSaveComplete'][] = array(ircNotify, 'brion'); |
| | 178 | + |
| | 179 | +This code would result in ircNotify being run twice when an article is |
| | 180 | +saved: once for 'TimStarling', and once for 'brion'. |
| | 181 | + |
| | 182 | +Hooks can return three possible values: |
| | 183 | + * true: the hook has operated successfully |
| | 184 | + * "some string": an error occurred; processing should |
| | 185 | + stop and the error should be shown to the user |
| | 186 | + * false: the hook has successfully done the work |
| | 187 | + necessary and the calling function should skip |
| | 188 | + |
| | 189 | +The last result would be for cases where the hook function replaces |
| | 190 | +the main functionality. For example, if you wanted to authenticate |
| | 191 | +users to a custom system (LDAP, another PHP program, whatever), you |
| | 192 | +could do: |
| | 193 | + |
| | 194 | + $wgHooks['UserLogin'][] = array(ldapLogin, $ldapServer); |
| | 195 | + |
| | 196 | + function ldapLogin($username, $password) { |
| | 197 | + # log user into LDAP |
| | 198 | + return false; |
| | 199 | + } |
| | 200 | + |
| | 201 | +Returning false makes less sense for events where the action is |
| | 202 | +complete, and will probably be ignored. |
| | 203 | + |
| | 204 | +==Using hooks== |
| | 205 | + |
| | 206 | +A calling function or method uses the wfRunHooks() function to run |
| | 207 | +the hooks related to a particular event, like so: |
| | 208 | + |
| | 209 | + class Article { |
| | 210 | + # ... |
| | 211 | + function protect() { |
| | 212 | + global $wgUser; |
| | 213 | + if (wfRunHooks('ArticleProtect', $this, $wgUser)) { |
| | 214 | + # protect the article |
| | 215 | + wfRunHooks('ArticleProtectComplete', $this, $wgUser); |
| | 216 | + } |
| | 217 | + } |
| | 218 | + |
| | 219 | +wfRunHooks() returns true if the calling function should continue |
| | 220 | +processing (the hooks ran OK, or there are no hooks to run), or false |
| | 221 | +if it shouldn't (an error occurred, or one of the hooks handled the |
| | 222 | +action already). Checking the return value matters more for "before" |
| | 223 | +hooks than for "complete" hooks. |
| | 224 | + |
| | 225 | +==Existing hooks== |
| | 226 | + |
| | 227 | +The following list of hooks exist in the code right now: |
| | 228 | + |
| | 229 | +TBD |
| Property changes on: trunk/phase3/docs/hooks.doc |
| ___________________________________________________________________ |
| Added: svn:eol-style |
| 1 | 230 | + native |
| Added: svn:keywords |
| 2 | 231 | + Author Date Id Revision |
| Index: trunk/phase3/includes/Setup.php |
| — | — | @@ -58,6 +58,7 @@ |
| 59 | 59 | wfProfileIn( $fname.'-includes' ); |
| 60 | 60 | |
| 61 | 61 | require_once( 'GlobalFunctions.php' ); |
| | 62 | +require_once( 'Hooks.php' ); |
| 62 | 63 | require_once( 'Namespace.php' ); |
| 63 | 64 | require_once( 'RecentChange.php' ); |
| 64 | 65 | require_once( 'User.php' ); |
| — | — | @@ -76,7 +77,7 @@ |
| 77 | 78 | require_once( 'WebRequest.php' ); |
| 78 | 79 | require_once( 'LoadBalancer.php' ); |
| 79 | 80 | require_once( 'HistoryBlob.php' ); |
| 80 | | - |
| | 81 | + |
| 81 | 82 | $wgRequest = new WebRequest(); |
| 82 | 83 | |
| 83 | 84 | |
| Index: trunk/phase3/includes/Hooks.php |
| — | — | @@ -0,0 +1,130 @@ |
| | 2 | +<?php |
| | 3 | +/** |
| | 4 | + * Hooks.php -- a tool for running hook functions |
| | 5 | + * Copyright 2004, Evan Prodromou <evan@wikitravel.org>. |
| | 6 | + * |
| | 7 | + * This program is free software; you can redistribute it and/or modify |
| | 8 | + * it under the terms of the GNU General Public License as published by |
| | 9 | + * the Free Software Foundation; either version 2 of the License, or |
| | 10 | + * (at your option) any later version. |
| | 11 | + * |
| | 12 | + * This program is distributed in the hope that it will be useful, |
| | 13 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| | 14 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| | 15 | + * GNU General Public License for more details. |
| | 16 | + * |
| | 17 | + * You should have received a copy of the GNU General Public License |
| | 18 | + * along with this program; if not, write to the Free Software |
| | 19 | + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| | 20 | + * |
| | 21 | + * @author <evan@wikitravel.org> |
| | 22 | + * @package MediaWiki |
| | 23 | + * @seealso hooks.doc |
| | 24 | + */ |
| | 25 | + |
| | 26 | +if (defined('MEDIAWIKI')) { |
| | 27 | + |
| | 28 | + /* |
| | 29 | + * Because programmers assign to $wgHooks, we need to be very |
| | 30 | + * careful about its contents. So, there's a lot more error-checking |
| | 31 | + * in here than would normally be necessary. |
| | 32 | + */ |
| | 33 | + |
| | 34 | + function wfRunHooks() { |
| | 35 | + |
| | 36 | + global $wgHooks; |
| | 37 | + |
| | 38 | + if (!is_array($wgHooks)) { |
| | 39 | + wfDieDebugBacktrace("Global hooks array is not an array!\n"); |
| | 40 | + return false; |
| | 41 | + } |
| | 42 | + |
| | 43 | + $args = func_get_args(); |
| | 44 | + |
| | 45 | + if (count($args) < 1) { |
| | 46 | + wfDieDebugBacktrace("No event name given for wfRunHooks().\n"); |
| | 47 | + return false; |
| | 48 | + } |
| | 49 | + |
| | 50 | + $event = array_shift($args); |
| | 51 | + |
| | 52 | + if (!array_key_exists($wgHooks, $event)) { |
| | 53 | + return true; |
| | 54 | + } |
| | 55 | + |
| | 56 | + if (!is_array($wgHooks[$event])) { |
| | 57 | + wfDieDebugBacktrace("Hooks array for event '$event' is not an array!\n"); |
| | 58 | + return false; |
| | 59 | + } |
| | 60 | + |
| | 61 | + foreach ($wgHooks[$event] as $hook) { |
| | 62 | + |
| | 63 | + $object = NULL; |
| | 64 | + $method = NULL; |
| | 65 | + $func = NULL; |
| | 66 | + $data = NULL; |
| | 67 | + $have_data = false; |
| | 68 | + |
| | 69 | + /* $hook can be: a function, an object, an array of $function and $data, |
| | 70 | + * an array of just a function, an array of object and method, or an |
| | 71 | + * array of object, method, and data. |
| | 72 | + */ |
| | 73 | + |
| | 74 | + if (is_array($hook)) { |
| | 75 | + if (count($hook) < 1) { |
| | 76 | + wfDieDebugBacktrace("Empty array in hooks for " . $event . "\n"); |
| | 77 | + } else if (is_object($hook[0])) { |
| | 78 | + $object = $hook[0]; |
| | 79 | + if (count($hook) < 2) { |
| | 80 | + $method = "on" . $event; |
| | 81 | + } else { |
| | 82 | + $method = $hook[1]; |
| | 83 | + if (count($hook) > 2) { |
| | 84 | + $data = $hook[2]; |
| | 85 | + $have_data = true; |
| | 86 | + } |
| | 87 | + } |
| | 88 | + } else if (is_string($hook[0])) { |
| | 89 | + $func = $hook[0]; |
| | 90 | + if (count($hook) > 1) { |
| | 91 | + $data = $hook[1]; |
| | 92 | + $have_data = true; |
| | 93 | + } |
| | 94 | + } else { |
| | 95 | + wfDieDebugBacktrace("Unknown datatype in hooks for " . $event . "\n"); |
| | 96 | + } |
| | 97 | + } else if (is_string($hook)) { # functions look like strings, too |
| | 98 | + $func = $hook; |
| | 99 | + } else if (is_object($hook)) { |
| | 100 | + $object = $hook; |
| | 101 | + $method = "on" . $event; |
| | 102 | + } else { |
| | 103 | + wfDieDebugBacktrace("Unknown datatype in hooks for " . $event . "\n"); |
| | 104 | + } |
| | 105 | + |
| | 106 | + if ($have_data) { |
| | 107 | + $hook_args = array_merge(array($data), $args); |
| | 108 | + } else { |
| | 109 | + $hook_args = $args; |
| | 110 | + } |
| | 111 | + |
| | 112 | + if ($object) { |
| | 113 | + $retval = call_user_func_array(array($object, $method), $hook_args); |
| | 114 | + } else { |
| | 115 | + $retval = call_user_func_array($func, $hook_args); |
| | 116 | + } |
| | 117 | + |
| | 118 | + if (is_string($retval)) { |
| | 119 | + global $wgOut; |
| | 120 | + $wgOut->fatalError($retval); |
| | 121 | + return false; |
| | 122 | + } else if (!$retval) { |
| | 123 | + return false; |
| | 124 | + } |
| | 125 | + } |
| | 126 | + |
| | 127 | + return true; |
| | 128 | + } |
| | 129 | +} |
| | 130 | + |
| | 131 | +?> |
| Property changes on: trunk/phase3/includes/Hooks.php |
| ___________________________________________________________________ |
| Added: svn:eol-style |
| 1 | 132 | + native |
| Added: svn:keywords |
| 2 | 133 | + Author Date Id Revision |
| Index: trunk/phase3/includes/DefaultSettings.php |
| — | — | @@ -857,6 +857,18 @@ |
| 858 | 858 | */ |
| 859 | 859 | $wgAuth = null; |
| 860 | 860 | |
| | 861 | +/** |
| | 862 | + * Global list of hooks. |
| | 863 | + * Add a hook by doing: |
| | 864 | + * $wgHooks['event_name'][] = $function; |
| | 865 | + * or: |
| | 866 | + * $wgHooks['event_name'][] = array($function, $data); |
| | 867 | + * or: |
| | 868 | + * $wgHooks['event_name'][] = array($object, 'method'); |
| | 869 | + */ |
| | 870 | + |
| | 871 | +$wgHooks = array(); |
| | 872 | + |
| 861 | 873 | } else { |
| 862 | 874 | die(); |
| 863 | 875 | } |