· 5 years ago · Mar 04, 2021, 07:44 PM
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 */
18final 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 particular action
68 * are executed. Lower numbers correspond with earlier execution,
69 * and functions with the same priority are executed in the order
70 * 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
76 $priority_existed = isset( $this->callbacks[ $priority ] );
77
78 $this->callbacks[ $priority ][ $idx ] = array(
79 'function' => $function_to_add,
80 'accepted_args' => $accepted_args,
81 );
82
83 // If we're adding a new priority to the list, put them back in sorted order.
84 if ( ! $priority_existed && count( $this->callbacks ) > 1 ) {
85 ksort( $this->callbacks, SORT_NUMERIC );
86 }
87
88 if ( $this->nesting_level > 0 ) {
89 $this->resort_active_iterations( $priority, $priority_existed );
90 }
91 }
92
93 /**
94 * Handles resetting callback priority keys mid-iteration.
95 *
96 * @since 4.7.0
97 *
98 * @param bool|int $new_priority Optional. The priority of the new filter being added. Default false,
99 * for no priority being added.
100 * @param bool $priority_existed Optional. Flag for whether the priority already existed before the new
101 * filter was added. Default false.
102 */
103 private function resort_active_iterations( $new_priority = false, $priority_existed = false ) {
104 $new_priorities = array_keys( $this->callbacks );
105
106 // If there are no remaining hooks, clear out all running iterations.
107 if ( ! $new_priorities ) {
108 foreach ( $this->iterations as $index => $iteration ) {
109 $this->iterations[ $index ] = $new_priorities;
110 }
111 return;
112 }
113
114 $min = min( $new_priorities );
115 foreach ( $this->iterations as $index => &$iteration ) {
116 $current = current( $iteration );
117 // If we're already at the end of this iteration, just leave the array pointer where it is.
118 if ( false === $current ) {
119 continue;
120 }
121
122 $iteration = $new_priorities;
123
124 if ( $current < $min ) {
125 array_unshift( $iteration, $current );
126 continue;
127 }
128
129 while ( current( $iteration ) < $current ) {
130 if ( false === next( $iteration ) ) {
131 break;
132 }
133 }
134
135 // If we have a new priority that didn't exist, but ::apply_filters() or ::do_action() thinks it's the current priority...
136 if ( $new_priority === $this->current_priority[ $index ] && ! $priority_existed ) {
137 /*
138 * ...and the new priority is the same as what $this->iterations thinks is the previous
139 * priority, we need to move back to it.
140 */
141
142 if ( false === current( $iteration ) ) {
143 // If we've already moved off the end of the array, go back to the last element.
144 $prev = end( $iteration );
145 } else {
146 // Otherwise, just go back to the previous element.
147 $prev = prev( $iteration );
148 }
149 if ( false === $prev ) {
150 // Start of the array. Reset, and go about our day.
151 reset( $iteration );
152 } elseif ( $new_priority !== $prev ) {
153 // Previous wasn't the same. Move forward again.
154 next( $iteration );
155 }
156 }
157 }
158 unset( $iteration );
159 }
160
161 /**
162 * Unhooks a function or method from a specific filter action.
163 *
164 * @since 4.7.0
165 *
166 * @param string $tag The filter hook to which the function to be removed is hooked.
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. Default empty.
193 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
194 * @return bool|int The priority of that hook is returned, or false if the function is not attached.
195 */
196 public function has_filter( $tag = '', $function_to_check = false ) {
197 if ( false === $function_to_check ) {
198 return $this->has_filters();
199 }
200
201 $function_key = _wp_filter_build_unique_id( $tag, $function_to_check, false );
202 if ( ! $function_key ) {
203 return false;
204 }
205
206 foreach ( $this->callbacks as $priority => $callbacks ) {
207 if ( isset( $callbacks[ $function_key ] ) ) {
208 return $priority;
209 }
210 }
211
212 return false;
213 }
214
215 /**
216 * Checks if any callbacks have been registered for this hook.
217 *
218 * @since 4.7.0
219 *
220 * @return bool True if callbacks have been registered for the current hook, otherwise false.
221 */
222 public function has_filters() {
223 foreach ( $this->callbacks as $callbacks ) {
224 if ( $callbacks ) {
225 return true;
226 }
227 }
228 return false;
229 }
230
231 /**
232 * Removes all callbacks from the current filter.
233 *
234 * @since 4.7.0
235 *
236 * @param int|bool $priority Optional. The priority number to remove. Default false.
237 */
238 public function remove_all_filters( $priority = false ) {
239 if ( ! $this->callbacks ) {
240 return;
241 }
242
243 if ( false === $priority ) {
244 $this->callbacks = array();
245 } elseif ( isset( $this->callbacks[ $priority ] ) ) {
246 unset( $this->callbacks[ $priority ] );
247 }
248
249 if ( $this->nesting_level > 0 ) {
250 $this->resort_active_iterations();
251 }
252 }
253
254 /**
255 * Calls the callback functions that have been added to a filter hook.
256 *
257 * @since 4.7.0
258 *
259 * @param mixed $value The value to filter.
260 * @param array $args Additional parameters to pass to the callback functions.
261 * This array is expected to include $value at index 0.
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 ] = current( $this->iterations[ $nesting_level ] );
276 $priority = $this->current_priority[ $nesting_level ];
277
278 foreach ( $this->callbacks[ $priority ] as $the_ ) {
279 if ( ! $this->doing_action ) {
280 $args[0] = $value;
281 }
282
283 // Avoid the array_slice() if possible.
284 if ( 0 == $the_['accepted_args'] ) {
285 $value = call_user_func( $the_['function'] );
286 } elseif ( $the_['accepted_args'] >= $num_args ) {
287 $value = call_user_func_array( $the_['function'], $args );
288 } else {
289 $value = call_user_func_array( $the_['function'], array_slice( $args, 0, (int) $the_['accepted_args'] ) );
290 }
291 }
292 } while ( false !== next( $this->iterations[ $nesting_level ] ) );
293
294 unset( $this->iterations[ $nesting_level ] );
295 unset( $this->current_priority[ $nesting_level ] );
296
297 $this->nesting_level--;
298
299 return $value;
300 }
301
302 /**
303 * Calls the callback functions that have been added to an action hook.
304 *
305 * @since 4.7.0
306 *
307 * @param array $args Parameters to pass to the callback functions.
308 */
309 public function do_action( $args ) {
310 $this->doing_action = true;
311 $this->apply_filters( '', $args );
312
313 // If there are recursive calls to the current action, we haven't finished it until we get to the last one.
314 if ( ! $this->nesting_level ) {
315 $this->doing_action = false;
316 }
317 }
318
319 /**
320 * Processes the functions hooked into the 'all' hook.
321 *
322 * @since 4.7.0
323 *
324 * @param array $args Arguments to pass to the hook callbacks. Passed by reference.
325 */
326 public function do_all_hook( &$args ) {
327 $nesting_level = $this->nesting_level++;
328 $this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
329
330 do {
331 $priority = current( $this->iterations[ $nesting_level ] );
332 foreach ( $this->callbacks[ $priority ] as $the_ ) {
333 call_user_func_array( $the_['function'], $args );
334 }
335 } while ( false !== next( $this->iterations[ $nesting_level ] ) );
336
337 unset( $this->iterations[ $nesting_level ] );
338 $this->nesting_level--;
339 }
340
341 /**
342 * Return the current priority level of the currently running iteration of the hook.
343 *
344 * @since 4.7.0
345 *
346 * @return int|false If the hook is running, return the current priority level. If it isn't running, return false.
347 */
348 public function current_priority() {
349 if ( false === current( $this->iterations ) ) {
350 return false;
351 }
352
353 return current( current( $this->iterations ) );
354 }
355
356 /**
357 * Normalizes filters set up before WordPress has initialized to WP_Hook objects.
358 *
359 * The `$filters` parameter should be an array keyed by hook name, with values
360 * containing either:
361 *
362 * - A `WP_Hook` instance
363 * - An array of callbacks keyed by their priorities
364 *
365 * Examples:
366 *
367 * $filters = array(
368 * 'wp_fatal_error_handler_enabled' => array(
369 * 10 => array(
370 * array(
371 * 'accepted_args' => 0,
372 * 'function' => function() {
373 * return false;
374 * },
375 * ),
376 * ),
377 * ),
378 * );
379 *
380 * @since 4.7.0
381 *
382 * @param array $filters Filters to normalize. See documentation above for details.
383 * @return WP_Hook[] Array of normalized filters.
384 */
385 public static function build_preinitialized_hooks( $filters ) {
386 /** @var WP_Hook[] $normalized */
387 $normalized = array();
388
389 foreach ( $filters as $tag => $callback_groups ) {
390 if ( is_object( $callback_groups ) && $callback_groups instanceof WP_Hook ) {
391 $normalized[ $tag ] = $callback_groups;
392 continue;
393 }
394 $hook = new WP_Hook();
395
396 // Loop through callback groups.
397 foreach ( $callback_groups as $priority => $callbacks ) {
398
399 // Loop through callbacks.
400 foreach ( $callbacks as $cb ) {
401 $hook->add_filter( $tag, $cb['function'], $priority, $cb['accepted_args'] );
402 }
403 }
404 $normalized[ $tag ] = $hook;
405 }
406 return $normalized;
407 }
408
409 /**
410 * Determines whether an offset value exists.
411 *
412 * @since 4.7.0
413 *
414 * @link https://www.php.net/manual/en/arrayaccess.offsetexists.php
415 *
416 * @param mixed $offset An offset to check for.
417 * @return bool True if the offset exists, false otherwise.
418 */
419 public function offsetExists( $offset ) {
420 return isset( $this->callbacks[ $offset ] );
421 }
422
423 /**
424 * Retrieves a value at a specified offset.
425 *
426 * @since 4.7.0
427 *
428 * @link https://www.php.net/manual/en/arrayaccess.offsetget.php
429 *
430 * @param mixed $offset The offset to retrieve.
431 * @return mixed If set, the value at the specified offset, null otherwise.
432 */
433 public function offsetGet( $offset ) {
434 return isset( $this->callbacks[ $offset ] ) ? $this->callbacks[ $offset ] : null;
435 }
436
437 /**
438 * Sets a value at a specified offset.
439 *
440 * @since 4.7.0
441 *
442 * @link https://www.php.net/manual/en/arrayaccess.offsetset.php
443 *
444 * @param mixed $offset The offset to assign the value to.
445 * @param mixed $value The value to set.
446 */
447 public function offsetSet( $offset, $value ) {
448 if ( is_null( $offset ) ) {
449 $this->callbacks[] = $value;
450 } else {
451 $this->callbacks[ $offset ] = $value;
452 }
453 }
454
455 /**
456 * Unsets a specified offset.
457 *
458 * @since 4.7.0
459 *
460 * @link https://www.php.net/manual/en/arrayaccess.offsetunset.php
461 *
462 * @param mixed $offset The offset to unset.
463 */
464 public function offsetUnset( $offset ) {
465 unset( $this->callbacks[ $offset ] );
466 }
467
468 /**
469 * Returns the current element.
470 *
471 * @since 4.7.0
472 *
473 * @link https://www.php.net/manual/en/iterator.current.php
474 *
475 * @return array Of callbacks at current priority.
476 */
477 public function current() {
478 return current( $this->callbacks );
479 }
480
481 /**
482 * Moves forward to the next element.
483 *
484 * @since 4.7.0
485 *
486 * @link https://www.php.net/manual/en/iterator.next.php
487 *
488 * @return array Of callbacks at next priority.
489 */
490 public function next() {
491 return next( $this->callbacks );
492 }
493
494 /**
495 * Returns the key of the current element.
496 *
497 * @since 4.7.0
498 *
499 * @link https://www.php.net/manual/en/iterator.key.php
500 *
501 * @return mixed Returns current priority on success, or NULL on failure
502 */
503 public function key() {
504 return key( $this->callbacks );
505 }
506
507 /**
508 * Checks if current position is valid.
509 *
510 * @since 4.7.0
511 *
512 * @link https://www.php.net/manual/en/iterator.valid.php
513 *
514 * @return bool Whether the current position is valid.
515 */
516 public function valid() {
517 return key( $this->callbacks ) !== null;
518 }
519
520 /**
521 * Rewinds the Iterator to the first element.
522 *
523 * @since 4.7.0
524 *
525 * @link https://www.php.net/manual/en/iterator.rewind.php
526 */
527 public function rewind() {
528 reset( $this->callbacks );
529 }
530
531}
532