|
1 <?php |
|
2 /** |
|
3 * Plugin API: WP_Hook class |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage Plugin |
|
7 * @since 4.7.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Core class used to implement action and filter hook functionality. |
|
12 * |
|
13 * @since 4.7.0 |
|
14 * |
|
15 * @see Iterator |
|
16 * @see ArrayAccess |
|
17 */ |
|
18 final class WP_Hook implements Iterator, ArrayAccess { |
|
19 |
|
20 /** |
|
21 * Hook callbacks. |
|
22 * |
|
23 * @since 4.7.0 |
|
24 * @var array |
|
25 */ |
|
26 public $callbacks = array(); |
|
27 |
|
28 /** |
|
29 * The priority keys of actively running iterations of a hook. |
|
30 * |
|
31 * @since 4.7.0 |
|
32 * @var array |
|
33 */ |
|
34 private $iterations = array(); |
|
35 |
|
36 /** |
|
37 * The current priority of actively running iterations of a hook. |
|
38 * |
|
39 * @since 4.7.0 |
|
40 * @var array |
|
41 */ |
|
42 private $current_priority = array(); |
|
43 |
|
44 /** |
|
45 * Number of levels this hook can be recursively called. |
|
46 * |
|
47 * @since 4.7.0 |
|
48 * @var int |
|
49 */ |
|
50 private $nesting_level = 0; |
|
51 |
|
52 /** |
|
53 * Flag for if we're current doing an action, rather than a filter. |
|
54 * |
|
55 * @since 4.7.0 |
|
56 * @var bool |
|
57 */ |
|
58 private $doing_action = false; |
|
59 |
|
60 /** |
|
61 * Hooks a function or method to a specific filter action. |
|
62 * |
|
63 * @since 4.7.0 |
|
64 * |
|
65 * @param string $tag The name of the filter to hook the $function_to_add callback to. |
|
66 * @param callable $function_to_add The callback to be run when the filter is applied. |
|
67 * @param int $priority The order in which the functions associated with a |
|
68 * particular action are executed. Lower numbers correspond with |
|
69 * earlier execution, and functions with the same priority are executed |
|
70 * in the order in which they were added to the action. |
|
71 * @param int $accepted_args The number of arguments the function accepts. |
|
72 */ |
|
73 public function add_filter( $tag, $function_to_add, $priority, $accepted_args ) { |
|
74 $idx = _wp_filter_build_unique_id( $tag, $function_to_add, $priority ); |
|
75 $priority_existed = isset( $this->callbacks[ $priority ] ); |
|
76 |
|
77 $this->callbacks[ $priority ][ $idx ] = array( |
|
78 'function' => $function_to_add, |
|
79 'accepted_args' => $accepted_args |
|
80 ); |
|
81 |
|
82 // if we're adding a new priority to the list, put them back in sorted order |
|
83 if ( ! $priority_existed && count( $this->callbacks ) > 1 ) { |
|
84 ksort( $this->callbacks, SORT_NUMERIC ); |
|
85 } |
|
86 |
|
87 if ( $this->nesting_level > 0 ) { |
|
88 $this->resort_active_iterations( $priority, $priority_existed ); |
|
89 } |
|
90 } |
|
91 |
|
92 /** |
|
93 * Handles reseting callback priority keys mid-iteration. |
|
94 * |
|
95 * @since 4.7.0 |
|
96 * |
|
97 * @param bool|int $new_priority Optional. The priority of the new filter being added. Default false, |
|
98 * for no priority being added. |
|
99 * @param bool $priority_existed Optional. Flag for whether the priority already existed before the new |
|
100 * filter was added. Default false. |
|
101 */ |
|
102 private function resort_active_iterations( $new_priority = false, $priority_existed = false ) { |
|
103 $new_priorities = array_keys( $this->callbacks ); |
|
104 |
|
105 // If there are no remaining hooks, clear out all running iterations. |
|
106 if ( ! $new_priorities ) { |
|
107 foreach ( $this->iterations as $index => $iteration ) { |
|
108 $this->iterations[ $index ] = $new_priorities; |
|
109 } |
|
110 return; |
|
111 } |
|
112 |
|
113 $min = min( $new_priorities ); |
|
114 foreach ( $this->iterations as $index => &$iteration ) { |
|
115 $current = current( $iteration ); |
|
116 // If we're already at the end of this iteration, just leave the array pointer where it is. |
|
117 if ( false === $current ) { |
|
118 continue; |
|
119 } |
|
120 |
|
121 $iteration = $new_priorities; |
|
122 |
|
123 if ( $current < $min ) { |
|
124 array_unshift( $iteration, $current ); |
|
125 continue; |
|
126 } |
|
127 |
|
128 while ( current( $iteration ) < $current ) { |
|
129 if ( false === next( $iteration ) ) { |
|
130 break; |
|
131 } |
|
132 } |
|
133 |
|
134 // If we have a new priority that didn't exist, but ::apply_filters() or ::do_action() thinks it's the current priority... |
|
135 if ( $new_priority === $this->current_priority[ $index ] && ! $priority_existed ) { |
|
136 /* |
|
137 * ... and the new priority is the same as what $this->iterations thinks is the previous |
|
138 * priority, we need to move back to it. |
|
139 */ |
|
140 |
|
141 if ( false === current( $iteration ) ) { |
|
142 // If we've already moved off the end of the array, go back to the last element. |
|
143 $prev = end( $iteration ); |
|
144 } else { |
|
145 // Otherwise, just go back to the previous element. |
|
146 $prev = prev( $iteration ); |
|
147 } |
|
148 if ( false === $prev ) { |
|
149 // Start of the array. Reset, and go about our day. |
|
150 reset( $iteration ); |
|
151 } elseif ( $new_priority !== $prev ) { |
|
152 // Previous wasn't the same. Move forward again. |
|
153 next( $iteration ); |
|
154 } |
|
155 } |
|
156 } |
|
157 unset( $iteration ); |
|
158 } |
|
159 |
|
160 /** |
|
161 * Unhooks a function or method from a specific filter action. |
|
162 * |
|
163 * @since 4.7.0 |
|
164 * |
|
165 * @param string $tag The filter hook to which the function to be removed is hooked. Used |
|
166 * for building the callback ID when SPL is not available. |
|
167 * @param callable $function_to_remove The callback to be removed from running when the filter is applied. |
|
168 * @param int $priority The exact priority used when adding the original filter callback. |
|
169 * @return bool Whether the callback existed before it was removed. |
|
170 */ |
|
171 public function remove_filter( $tag, $function_to_remove, $priority ) { |
|
172 $function_key = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority ); |
|
173 |
|
174 $exists = isset( $this->callbacks[ $priority ][ $function_key ] ); |
|
175 if ( $exists ) { |
|
176 unset( $this->callbacks[ $priority ][ $function_key ] ); |
|
177 if ( ! $this->callbacks[ $priority ] ) { |
|
178 unset( $this->callbacks[ $priority ] ); |
|
179 if ( $this->nesting_level > 0 ) { |
|
180 $this->resort_active_iterations(); |
|
181 } |
|
182 } |
|
183 } |
|
184 return $exists; |
|
185 } |
|
186 |
|
187 /** |
|
188 * Checks if a specific action has been registered for this hook. |
|
189 * |
|
190 * @since 4.7.0 |
|
191 * |
|
192 * @param string $tag Optional. The name of the filter hook. Used for building |
|
193 * the callback ID when SPL is not available. Default empty. |
|
194 * @param callable|bool $function_to_check Optional. The callback to check for. Default false. |
|
195 * @return bool|int The priority of that hook is returned, or false if the function is not attached. |
|
196 */ |
|
197 public function has_filter( $tag = '', $function_to_check = false ) { |
|
198 if ( false === $function_to_check ) { |
|
199 return $this->has_filters(); |
|
200 } |
|
201 |
|
202 $function_key = _wp_filter_build_unique_id( $tag, $function_to_check, false ); |
|
203 if ( ! $function_key ) { |
|
204 return false; |
|
205 } |
|
206 |
|
207 foreach ( $this->callbacks as $priority => $callbacks ) { |
|
208 if ( isset( $callbacks[ $function_key ] ) ) { |
|
209 return $priority; |
|
210 } |
|
211 } |
|
212 |
|
213 return false; |
|
214 } |
|
215 |
|
216 /** |
|
217 * Checks if any callbacks have been registered for this hook. |
|
218 * |
|
219 * @since 4.7.0 |
|
220 * |
|
221 * @return bool True if callbacks have been registered for the current hook, otherwise false. |
|
222 */ |
|
223 public function has_filters() { |
|
224 foreach ( $this->callbacks as $callbacks ) { |
|
225 if ( $callbacks ) { |
|
226 return true; |
|
227 } |
|
228 } |
|
229 return false; |
|
230 } |
|
231 |
|
232 /** |
|
233 * Removes all callbacks from the current filter. |
|
234 * |
|
235 * @since 4.7.0 |
|
236 * |
|
237 * @param int|bool $priority Optional. The priority number to remove. Default false. |
|
238 */ |
|
239 public function remove_all_filters( $priority = false ) { |
|
240 if ( ! $this->callbacks ) { |
|
241 return; |
|
242 } |
|
243 |
|
244 if ( false === $priority ) { |
|
245 $this->callbacks = array(); |
|
246 } else if ( isset( $this->callbacks[ $priority ] ) ) { |
|
247 unset( $this->callbacks[ $priority ] ); |
|
248 } |
|
249 |
|
250 if ( $this->nesting_level > 0 ) { |
|
251 $this->resort_active_iterations(); |
|
252 } |
|
253 } |
|
254 |
|
255 /** |
|
256 * Calls the callback functions added to a filter hook. |
|
257 * |
|
258 * @since 4.7.0 |
|
259 * |
|
260 * @param mixed $value The value to filter. |
|
261 * @param array $args Arguments to pass to callbacks. |
|
262 * @return mixed The filtered value after all hooked functions are applied to it. |
|
263 */ |
|
264 public function apply_filters( $value, $args ) { |
|
265 if ( ! $this->callbacks ) { |
|
266 return $value; |
|
267 } |
|
268 |
|
269 $nesting_level = $this->nesting_level++; |
|
270 |
|
271 $this->iterations[ $nesting_level ] = array_keys( $this->callbacks ); |
|
272 $num_args = count( $args ); |
|
273 |
|
274 do { |
|
275 $this->current_priority[ $nesting_level ] = $priority = current( $this->iterations[ $nesting_level ] ); |
|
276 |
|
277 foreach ( $this->callbacks[ $priority ] as $the_ ) { |
|
278 if( ! $this->doing_action ) { |
|
279 $args[ 0 ] = $value; |
|
280 } |
|
281 |
|
282 // Avoid the array_slice if possible. |
|
283 if ( $the_['accepted_args'] == 0 ) { |
|
284 $value = call_user_func_array( $the_['function'], array() ); |
|
285 } elseif ( $the_['accepted_args'] >= $num_args ) { |
|
286 $value = call_user_func_array( $the_['function'], $args ); |
|
287 } else { |
|
288 $value = call_user_func_array( $the_['function'], array_slice( $args, 0, (int)$the_['accepted_args'] ) ); |
|
289 } |
|
290 } |
|
291 } while ( false !== next( $this->iterations[ $nesting_level ] ) ); |
|
292 |
|
293 unset( $this->iterations[ $nesting_level ] ); |
|
294 unset( $this->current_priority[ $nesting_level ] ); |
|
295 |
|
296 $this->nesting_level--; |
|
297 |
|
298 return $value; |
|
299 } |
|
300 |
|
301 /** |
|
302 * Executes the callback functions hooked on a specific action hook. |
|
303 * |
|
304 * @since 4.7.0 |
|
305 * |
|
306 * @param mixed $args Arguments to pass to the hook callbacks. |
|
307 */ |
|
308 public function do_action( $args ) { |
|
309 $this->doing_action = true; |
|
310 $this->apply_filters( '', $args ); |
|
311 |
|
312 // If there are recursive calls to the current action, we haven't finished it until we get to the last one. |
|
313 if ( ! $this->nesting_level ) { |
|
314 $this->doing_action = false; |
|
315 } |
|
316 } |
|
317 |
|
318 /** |
|
319 * Processes the functions hooked into the 'all' hook. |
|
320 * |
|
321 * @since 4.7.0 |
|
322 * |
|
323 * @param array $args Arguments to pass to the hook callbacks. Passed by reference. |
|
324 */ |
|
325 public function do_all_hook( &$args ) { |
|
326 $nesting_level = $this->nesting_level++; |
|
327 $this->iterations[ $nesting_level ] = array_keys( $this->callbacks ); |
|
328 |
|
329 do { |
|
330 $priority = current( $this->iterations[ $nesting_level ] ); |
|
331 foreach ( $this->callbacks[ $priority ] as $the_ ) { |
|
332 call_user_func_array( $the_['function'], $args ); |
|
333 } |
|
334 } while ( false !== next( $this->iterations[ $nesting_level ] ) ); |
|
335 |
|
336 unset( $this->iterations[ $nesting_level ] ); |
|
337 $this->nesting_level--; |
|
338 } |
|
339 |
|
340 /** |
|
341 * Return the current priority level of the currently running iteration of the hook. |
|
342 * |
|
343 * @since 4.7.0 |
|
344 * |
|
345 * @return int|false If the hook is running, return the current priority level. If it isn't running, return false. |
|
346 */ |
|
347 public function current_priority() { |
|
348 if ( false === current( $this->iterations ) ) { |
|
349 return false; |
|
350 } |
|
351 |
|
352 return current( current( $this->iterations ) ); |
|
353 } |
|
354 |
|
355 /** |
|
356 * Normalizes filters set up before WordPress has initialized to WP_Hook objects. |
|
357 * |
|
358 * @since 4.7.0 |
|
359 * @static |
|
360 * |
|
361 * @param array $filters Filters to normalize. |
|
362 * @return WP_Hook[] Array of normalized filters. |
|
363 */ |
|
364 public static function build_preinitialized_hooks( $filters ) { |
|
365 /** @var WP_Hook[] $normalized */ |
|
366 $normalized = array(); |
|
367 |
|
368 foreach ( $filters as $tag => $callback_groups ) { |
|
369 if ( is_object( $callback_groups ) && $callback_groups instanceof WP_Hook ) { |
|
370 $normalized[ $tag ] = $callback_groups; |
|
371 continue; |
|
372 } |
|
373 $hook = new WP_Hook(); |
|
374 |
|
375 // Loop through callback groups. |
|
376 foreach ( $callback_groups as $priority => $callbacks ) { |
|
377 |
|
378 // Loop through callbacks. |
|
379 foreach ( $callbacks as $cb ) { |
|
380 $hook->add_filter( $tag, $cb['function'], $priority, $cb['accepted_args'] ); |
|
381 } |
|
382 } |
|
383 $normalized[ $tag ] = $hook; |
|
384 } |
|
385 return $normalized; |
|
386 } |
|
387 |
|
388 /** |
|
389 * Determines whether an offset value exists. |
|
390 * |
|
391 * @since 4.7.0 |
|
392 * |
|
393 * @link https://secure.php.net/manual/en/arrayaccess.offsetexists.php |
|
394 * |
|
395 * @param mixed $offset An offset to check for. |
|
396 * @return bool True if the offset exists, false otherwise. |
|
397 */ |
|
398 public function offsetExists( $offset ) { |
|
399 return isset( $this->callbacks[ $offset ] ); |
|
400 } |
|
401 |
|
402 /** |
|
403 * Retrieves a value at a specified offset. |
|
404 * |
|
405 * @since 4.7.0 |
|
406 * |
|
407 * @link https://secure.php.net/manual/en/arrayaccess.offsetget.php |
|
408 * |
|
409 * @param mixed $offset The offset to retrieve. |
|
410 * @return mixed If set, the value at the specified offset, null otherwise. |
|
411 */ |
|
412 public function offsetGet( $offset ) { |
|
413 return isset( $this->callbacks[ $offset ] ) ? $this->callbacks[ $offset ] : null; |
|
414 } |
|
415 |
|
416 /** |
|
417 * Sets a value at a specified offset. |
|
418 * |
|
419 * @since 4.7.0 |
|
420 * |
|
421 * @link https://secure.php.net/manual/en/arrayaccess.offsetset.php |
|
422 * |
|
423 * @param mixed $offset The offset to assign the value to. |
|
424 * @param mixed $value The value to set. |
|
425 */ |
|
426 public function offsetSet( $offset, $value ) { |
|
427 if ( is_null( $offset ) ) { |
|
428 $this->callbacks[] = $value; |
|
429 } else { |
|
430 $this->callbacks[ $offset ] = $value; |
|
431 } |
|
432 } |
|
433 |
|
434 /** |
|
435 * Unsets a specified offset. |
|
436 * |
|
437 * @since 4.7.0 |
|
438 * |
|
439 * @link https://secure.php.net/manual/en/arrayaccess.offsetunset.php |
|
440 * |
|
441 * @param mixed $offset The offset to unset. |
|
442 */ |
|
443 public function offsetUnset( $offset ) { |
|
444 unset( $this->callbacks[ $offset ] ); |
|
445 } |
|
446 |
|
447 /** |
|
448 * Returns the current element. |
|
449 * |
|
450 * @since 4.7.0 |
|
451 * |
|
452 * @link https://secure.php.net/manual/en/iterator.current.php |
|
453 * |
|
454 * @return array Of callbacks at current priority. |
|
455 */ |
|
456 public function current() { |
|
457 return current( $this->callbacks ); |
|
458 } |
|
459 |
|
460 /** |
|
461 * Moves forward to the next element. |
|
462 * |
|
463 * @since 4.7.0 |
|
464 * |
|
465 * @link https://secure.php.net/manual/en/iterator.next.php |
|
466 * |
|
467 * @return array Of callbacks at next priority. |
|
468 */ |
|
469 public function next() { |
|
470 return next( $this->callbacks ); |
|
471 } |
|
472 |
|
473 /** |
|
474 * Returns the key of the current element. |
|
475 * |
|
476 * @since 4.7.0 |
|
477 * |
|
478 * @link https://secure.php.net/manual/en/iterator.key.php |
|
479 * |
|
480 * @return mixed Returns current priority on success, or NULL on failure |
|
481 */ |
|
482 public function key() { |
|
483 return key( $this->callbacks ); |
|
484 } |
|
485 |
|
486 /** |
|
487 * Checks if current position is valid. |
|
488 * |
|
489 * @since 4.7.0 |
|
490 * |
|
491 * @link https://secure.php.net/manual/en/iterator.valid.php |
|
492 * |
|
493 * @return boolean |
|
494 */ |
|
495 public function valid() { |
|
496 return key( $this->callbacks ) !== null; |
|
497 } |
|
498 |
|
499 /** |
|
500 * Rewinds the Iterator to the first element. |
|
501 * |
|
502 * @since 4.7.0 |
|
503 * |
|
504 * @link https://secure.php.net/manual/en/iterator.rewind.php |
|
505 */ |
|
506 public function rewind() { |
|
507 reset( $this->callbacks ); |
|
508 } |
|
509 |
|
510 } |