Add a system of hooks to allow third-party code to run before, after, or
authorEvan Prodromou <evanprodromou@users.mediawiki.org>
Sat, 27 Nov 2004 21:43:06 +0000 (21:43 +0000)
committerEvan Prodromou <evanprodromou@users.mediawiki.org>
Sat, 27 Nov 2004 21:43:06 +0000 (21:43 +0000)
instead of -- MediaWiki code for particular events (article rollback,
user ban, etc.). Framework is in place; hooks are not yet in place
in the mainline code.

docs/hooks.doc [new file with mode: 0644]
includes/DefaultSettings.php
includes/Hooks.php [new file with mode: 0644]
includes/Setup.php

diff --git a/docs/hooks.doc b/docs/hooks.doc
new file mode 100644 (file)
index 0000000..57ffddf
--- /dev/null
@@ -0,0 +1,228 @@
+HOOKS.DOC
+
+This document describes how event hooks work in MediaWiki; how to add
+hooks for an event; and how to run hooks for an event.
+
+==Glossary==
+
+event
+     Something that happens with the wiki. For example: a user logs
+     in. A wiki page is saved. A wiki page is deleted. Often there are
+     two events associated with a single action: one before the code
+     is run to make the event happen, and one after. Each event has a
+     name, preferably in CamelCase. For example, 'UserLogin',
+     'ArticleSave', 'ArticleSaveComplete', 'ArticleDelete'.
+
+hook
+     A clump of code and data that should be run when an event
+     happens. This can be either a function and a chunk of data, or an
+     object and a method.
+     
+hook function
+     The function part of a hook.
+     
+==Rationale==
+
+Hooks allow us to decouple optionally-run code from code that is run
+for everyone. It allows MediaWiki hackers, third-party developers and
+local administrators to define code that will be run at certain points
+in the mainline code, and to modify the data run by that mainline
+code. Hooks can keep mainline code simple, and make it easier to
+write extensions. Hooks are a principled alternative to local patches.
+
+Consider, for example, two options in MediaWiki. One reverses the
+order of a title before displaying the article; the other converts the
+title to all uppercase letters. Currently, in MediaWiki code, we
+handle this as follows:
+
+    function showAnArticle($article) {
+        global $wgReverseTitle, $wgCapitalizeTitle;
+       
+       if ($wgReverseTitle) {
+           wfReverseTitle($article);
+        }
+       
+       if ($wgCapitalizeTitle) {
+          wfCapitalizeTitle($article);
+        }
+       
+       # code to actually show the article goes here
+    }
+    
+An extension writer, or a local admin, will often add custom code to
+the function -- with or without a global variable. For example,
+someone wanting email notification when an article is shown may add:
+
+    function showAnArticle($article) {
+        global $wgReverseTitle, $wgCapitalizeTitle;
+       
+       if ($wgReverseTitle) {
+           wfReverseTitle($article);
+        }
+       
+       if ($wgCapitalizeTitle) {
+          wfCapitalizeTitle($article);
+        }
+
+       # code to actually show the article goes here
+       
+       if ($wgNotifyArticle) {
+          wfNotifyArticleShow($article));
+        }
+    }
+
+Using a hook-running strategy, we can avoid having all this
+option-specific stuff in our mainline code. Using hooks, the function
+becomes:
+
+    function showAnArticle($article) {
+       if (wfRunHooks('ArticleShow', $article)) {
+           # code to actually show the article goes here
+           wfRunHooks('ArticleShowComplete', $article);
+       }
+    }
+
+We've cleaned up the code here by removing clumps of weird,
+infrequently used code and moving them off somewhere else. It's much
+easier for someone working with this code to see what's _really_ going
+on, and make changes or fix bugs.
+
+In addition, we can take all the code that deals with the little-used
+title-reversing options (say) and put it in one place. Instead of
+having a little title-reversing if-block spread all over the codebase
+in showAnArticle, deleteAnArticle, exportArticle, etc., we can
+concentrate it all in an extension file:
+
+    function reverseArticleTitle($article) {
+        # ...
+    }
+
+    function reverseForExport($article) {
+        # ...
+    }
+
+The setup function for the extension just has to add its hook
+functions to the appropriate events:
+
+    setupTitleReversingExtension() {
+        global $wgHooks;
+       
+       $wgHooks['ArticleShow'][] = reverseArticleTitle;
+       $wgHooks['ArticleDelete'][] = reverseArticleTitle;
+       $wgHooks['ArticleExport'][] = reverseForExport;
+    }
+
+Having all this code related to the title-reversion option in one
+place means that it's easier to read and understand; you don't have to
+do a grep-find to see where the $wgReverseTitle variable is used, say.
+
+If the code is well enough isolated, it can even be excluded when not
+used -- making for some slight savings in memory and time at runtime.
+Admins who want to have all the reversed titles can add:
+
+    require_once('extensions/ReverseTitle.php');
+    
+...to their LocalSettings.php file; those of us who don't want or need
+it can just leave it out.
+
+The extensions don't even have to be shipped with MediaWiki; they
+could be provided by a third-party developer or written by the admin
+him/herself.
+
+==Writing hooks==
+
+A hook is a chunk of code run at some particular event. It consists of:
+
+  * a function with some optional accompanying data, or
+  * an object with a method and some optional accompanying data.
+
+Hooks are registered by adding them to the global $wgHooks array for a
+given event. All the following are valid ways to define hooks:
+
+      $wgHooks['EventName'][] = someFunction; # function, no data
+      $wgHooks['EventName'][] = array(someFunction, $someData);
+      $wgHooks['EventName'][] = array(someFunction); # weird, but OK
+      
+      $wgHooks['EventName'][] = $object; # object only
+      $wgHooks['EventName'][] = array($object, 'someMethod');
+      $wgHooks['EventName'][] = array($object, 'someMethod', $someData);
+      $wgHooks['EventName'][] = array($object); # weird but OK
+
+When an event occurs, the function (or object method) will be called
+with the optional data provided as well as event-specific parameters.
+The above examples would result in the following code being executed
+when 'EventName' happened:
+
+      # function, no data
+      someFunction($param1, $param2)
+      # function with data
+      someFunction($someData, $param1, $param2)
+
+      # object only
+      $object->onEventName($param1, $param2)
+      # object with method
+      $object->someMethod($param1, $param2)
+      # object with method and data
+      $object->someMethod($someData, $param1, $param2)
+      
+Note that when an object is the hook, and there's no specified method,
+the default method called is 'onEventName'. For different events this
+would be different: 'onArticleSave', 'onUserLogin', etc.
+
+The extra data is useful if we want to use the same function or object
+for different purposes. For example:
+
+      $wgHooks['ArticleSaveComplete'][] = array(ircNotify, 'TimStarling');
+      $wgHooks['ArticleSaveComplete'][] = array(ircNotify, 'brion');
+
+This code would result in ircNotify being run twice when an article is
+saved: once for 'TimStarling', and once for 'brion'.
+
+Hooks can return three possible values:
+  * true: the hook has operated successfully
+  * "some string": an error occurred; processing should
+         stop and the error should be shown to the user
+  * false: the hook has successfully done the work
+          necessary and the calling function should skip
+       
+The last result would be for cases where the hook function replaces
+the main functionality. For example, if you wanted to authenticate
+users to a custom system (LDAP, another PHP program, whatever), you
+could do:
+
+    $wgHooks['UserLogin'][] = array(ldapLogin, $ldapServer);
+                               
+    function ldapLogin($username, $password) {
+        # log user into LDAP
+       return false;
+    }
+
+Returning false makes less sense for events where the action is
+complete, and will probably be ignored.
+
+==Using hooks==
+
+A calling function or method uses the wfRunHooks() function to run
+the hooks related to a particular event, like so:
+
+    class Article { 
+        # ...
+       function protect() {
+           global $wgUser;
+           if (wfRunHooks('ArticleProtect', $this, $wgUser)) {
+               # protect the article
+               wfRunHooks('ArticleProtectComplete', $this, $wgUser);
+           }
+       }
+                                                   
+wfRunHooks() returns true if the calling function should continue
+processing (the hooks ran OK, or there are no hooks to run), or false
+if it shouldn't (an error occurred, or one of the hooks handled the
+action already). Checking the return value matters more for "before"
+hooks than for "complete" hooks.
+
+==Existing hooks==
+
+The following list of hooks exist in the code right now:
+
+TBD
index aa46220..b8899d3 100644 (file)
@@ -857,6 +857,18 @@ $wgWhitelistRead = array ( ':Accueil', ':Main_Page');
  */
 $wgAuth = null;
 
+/**
+ * Global list of hooks.
+ * Add a hook by doing:
+ *     $wgHooks['event_name'][] = $function;
+ * or:
+ *     $wgHooks['event_name'][] = array($function, $data);
+ * or:
+ *     $wgHooks['event_name'][] = array($object, 'method');
+ */
+       
+$wgHooks = array();
+       
 } else {
        die();
 }
diff --git a/includes/Hooks.php b/includes/Hooks.php
new file mode 100644 (file)
index 0000000..56ca88d
--- /dev/null
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Hooks.php -- a tool for running hook functions
+ * Copyright 2004, Evan Prodromou <evan@wikitravel.org>.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @author <evan@wikitravel.org>
+ * @package MediaWiki
+ * @seealso hooks.doc
+ */
+
+if (defined('MEDIAWIKI')) {
+       
+       /* 
+        * Because programmers assign to $wgHooks, we need to be very
+        * careful about its contents. So, there's a lot more error-checking
+        * in here than would normally be necessary.
+        */
+       
+       function wfRunHooks() {
+               
+               global $wgHooks;
+
+               if (!is_array($wgHooks)) {
+                       wfDieDebugBacktrace("Global hooks array is not an array!\n");
+                       return false;
+               }
+
+               $args = func_get_args();
+
+               if (count($args) < 1) {
+                       wfDieDebugBacktrace("No event name given for wfRunHooks().\n");
+                       return false;
+               }
+
+               $event = array_shift($args);
+
+               if (!array_key_exists($wgHooks, $event)) {
+                       return true;
+               }
+
+               if (!is_array($wgHooks[$event])) {
+                       wfDieDebugBacktrace("Hooks array for event '$event' is not an array!\n");
+                       return false;
+               }
+
+               foreach ($wgHooks[$event] as $hook) {
+                       
+                       $object = NULL;
+                       $method = NULL;
+                       $func = NULL;
+                       $data = NULL;
+                       $have_data = false;
+
+                       /* $hook can be: a function, an object, an array of $function and $data,
+                        * an array of just a function, an array of object and method, or an
+                        * array of object, method, and data.
+                        */
+                       
+                       if (is_array($hook)) {
+                               if (count($hook) < 1) {
+                                       wfDieDebugBacktrace("Empty array in hooks for " . $event . "\n");
+                               } else if (is_object($hook[0])) {
+                                       $object = $hook[0];
+                                       if (count($hook) < 2) {
+                                               $method = "on" . $event;
+                                       } else {
+                                               $method = $hook[1];
+                                               if (count($hook) > 2) {
+                                                       $data = $hook[2];
+                                                       $have_data = true;
+                                               }
+                                       }
+                               } else if (is_string($hook[0])) {
+                                       $func = $hook[0];
+                                       if (count($hook) > 1) {
+                                               $data = $hook[1];
+                                               $have_data = true;
+                                       }
+                               } else {
+                                       wfDieDebugBacktrace("Unknown datatype in hooks for " . $event . "\n");
+                               }
+                       } else if (is_string($hook)) { # functions look like strings, too
+                               $func = $hook;
+                       } else if (is_object($hook)) {
+                               $object = $hook;
+                               $method = "on" . $event;
+                       } else {
+                               wfDieDebugBacktrace("Unknown datatype in hooks for " . $event . "\n");
+                       }
+
+                       if ($have_data) {
+                               $hook_args = array_merge(array($data), $args);
+                       } else {
+                               $hook_args = $args;
+                       }
+                       
+                       if ($object) {
+                               $retval = call_user_func_array(array($object, $method), $hook_args);
+                       } else {
+                               $retval = call_user_func_array($func, $hook_args);
+                       }
+                       
+                       if (is_string($retval)) {
+                               global $wgOut;
+                               $wgOut->fatalError($retval);
+                               return false;
+                       } else if (!$retval) {
+                               return false;
+                       }
+               }
+               
+               return true;
+       }
+}
+
+?>
index 46bd30b..a2eefdb 100644 (file)
@@ -58,6 +58,7 @@ global $wgUseDynamicDates;
 wfProfileIn( $fname.'-includes' );
 
 require_once( 'GlobalFunctions.php' );
+require_once( 'Hooks.php' );
 require_once( 'Namespace.php' );
 require_once( 'RecentChange.php' ); 
 require_once( 'User.php' );
@@ -76,7 +77,7 @@ require_once( 'ParserCache.php' );
 require_once( 'WebRequest.php' );
 require_once( 'LoadBalancer.php' );
 require_once( 'HistoryBlob.php' );
-
+       
 $wgRequest = new WebRequest();