summaryrefslogtreecommitdiff
path: root/includes/Hooks.php
blob: dffc7bcfe859c7d9f60610b2f7a18ce7b0c92f1b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
<?php

/**
 * A tool for running hook functions.
 *
 * Copyright 2004, 2005 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 *
 * @author Evan Prodromou <evan@wikitravel.org>
 * @see hooks.txt
 * @file
 */

/**
 * @since 1.18
 */
class MWHookException extends MWException {
}

/**
 * Hooks class.
 *
 * Used to supersede $wgHooks, because globals are EVIL.
 *
 * @since 1.18
 */
class Hooks {
	/**
	 * Array of events mapped to an array of callbacks to be run
	 * when that event is triggered.
	 */
	protected static $handlers = array();

	/**
	 * Attach an event handler to a given hook.
	 *
	 * @param string $name Name of hook
	 * @param callable $callback Callback function to attach
	 *
	 * @since 1.18
	 */
	public static function register( $name, $callback ) {
		if ( !isset( self::$handlers[$name] ) ) {
			self::$handlers[$name] = array();
		}

		self::$handlers[$name][] = $callback;
	}

	/**
	 * Clears hooks registered via Hooks::register(). Does not touch $wgHooks.
	 * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
	 *
	 * @param string $name The name of the hook to clear.
	 *
	 * @since 1.21
	 * @throws MWException If not in testing mode.
	 */
	public static function clear( $name ) {
		if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
			throw new MWException( 'Cannot reset hooks in operation.' );
		}

		unset( self::$handlers[$name] );
	}

	/**
	 * Returns true if a hook has a function registered to it.
	 * The function may have been registered either via Hooks::register or in $wgHooks.
	 *
	 * @since 1.18
	 *
	 * @param string $name Name of hook
	 * @return bool True if the hook has a function registered to it
	 */
	public static function isRegistered( $name ) {
		global $wgHooks;
		return !empty( $wgHooks[$name] ) || !empty( self::$handlers[$name] );
	}

	/**
	 * Returns an array of all the event functions attached to a hook
	 * This combines functions registered via Hooks::register and with $wgHooks.
	 *
	 * @since 1.18
	 *
	 * @param string $name Name of the hook
	 * @return array
	 */
	public static function getHandlers( $name ) {
		global $wgHooks;

		if ( !self::isRegistered( $name ) ) {
			return array();
		} elseif ( !isset( self::$handlers[$name] ) ) {
			return $wgHooks[$name];
		} elseif ( !isset( $wgHooks[$name] ) ) {
			return self::$handlers[$name];
		} else {
			return array_merge( self::$handlers[$name], $wgHooks[$name] );
		}
	}

	/**
	 * Call hook functions defined in Hooks::register and $wgHooks.
	 *
	 * For a certain hook event, fetch the array of hook events and
	 * process them. Determine the proper callback for each hook and
	 * then call the actual hook using the appropriate arguments.
	 * Finally, process the return value and return/throw accordingly.
	 *
	 * @param string $event Event name
	 * @param array $args Array of parameters passed to hook functions
	 * @param string|null $deprecatedVersion Optionally, mark hook as deprecated with version number
	 * @return bool True if no handler aborted the hook
	 *
	 * @throws Exception
	 * @throws FatalError
	 * @throws MWException
	 * @since 1.22 A hook function is not required to return a value for
	 *   processing to continue. Not returning a value (or explicitly
	 *   returning null) is equivalent to returning true.
	 */
	public static function run( $event, array $args = array(), $deprecatedVersion = null ) {
		$profiler = Profiler::instance();
		$eventPS = $profiler->scopedProfileIn( 'hook: ' . $event );

		foreach ( self::getHandlers( $event ) as $hook ) {
			// Turn non-array values into an array. (Can't use casting because of objects.)
			if ( !is_array( $hook ) ) {
				$hook = array( $hook );
			}

			if ( !array_filter( $hook ) ) {
				// Either array is empty or it's an array filled with null/false/empty.
				continue;
			} elseif ( is_array( $hook[0] ) ) {
				// First element is an array, meaning the developer intended
				// the first element to be a callback. Merge it in so that
				// processing can be uniform.
				$hook = array_merge( $hook[0], array_slice( $hook, 1 ) );
			}

			/**
			 * $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 ( $hook[0] instanceof Closure ) {
				$func = "hook-$event-closure";
				$callback = array_shift( $hook );
			} elseif ( is_object( $hook[0] ) ) {
				$object = array_shift( $hook );
				$method = array_shift( $hook );

				// If no method was specified, default to on$event.
				if ( $method === null ) {
					$method = "on$event";
				}

				$func = get_class( $object ) . '::' . $method;
				$callback = array( $object, $method );
			} elseif ( is_string( $hook[0] ) ) {
				$func = $callback = array_shift( $hook );
			} else {
				throw new MWException( 'Unknown datatype in hooks for ' . $event . "\n" );
			}

			// Run autoloader (workaround for call_user_func_array bug)
			// and throw error if not callable.
			if ( !is_callable( $callback ) ) {
				throw new MWException( 'Invalid callback ' . $func . ' in hooks for ' . $event . "\n" );
			}

			/*
			 * Call the hook. The documentation of call_user_func_array says
			 * false is returned on failure. However, if the function signature
			 * does not match the call signature, PHP will issue an warning and
			 * return null instead. The following code catches that warning and
			 * provides better error message.
			 */
			$retval = null;
			$badhookmsg = null;
			$hook_args = array_merge( $hook, $args );

			// Profile first in case the Profiler causes errors
			$funcPS = $profiler->scopedProfileIn( $func );
			set_error_handler( 'Hooks::hookErrorHandler' );

			// mark hook as deprecated, if deprecation version is specified
			if ( $deprecatedVersion !== null ) {
				wfDeprecated( "$event hook (used in $func)", $deprecatedVersion );
			}

			try {
				$retval = call_user_func_array( $callback, $hook_args );
			} catch ( MWHookException $e ) {
				$badhookmsg = $e->getMessage();
			} catch ( Exception $e ) {
				restore_error_handler();
				throw $e;
			}

			restore_error_handler();
			$profiler->scopedProfileOut( $funcPS );

			// Process the return value.
			if ( is_string( $retval ) ) {
				// String returned means error.
				throw new FatalError( $retval );
			} elseif ( $badhookmsg !== null ) {
				// Exception was thrown from Hooks::hookErrorHandler.
				throw new MWException(
					'Detected bug in an extension! ' .
					"Hook $func has invalid call signature; " . $badhookmsg
				);
			} elseif ( $retval === false ) {
				// False was returned. Stop processing, but no error.
				return false;
			}
		}

		return true;
	}

	/**
	 * Handle PHP errors issued inside a hook. Catch errors that have to do with
	 * a function expecting a reference, and let all others pass through.
	 *
	 * This REALLY should be protected... but it's public for compatibility
	 *
	 * @since 1.18
	 *
	 * @param int $errno Error number (unused)
	 * @param string $errstr Error message
	 * @throws MWHookException If the error has to do with the function signature
	 * @return bool Always returns false
	 */
	public static function hookErrorHandler( $errno, $errstr ) {
		if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) {
			throw new MWHookException( $errstr, $errno );
		}
		return false;
	}
}