You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

536 lines
14 KiB

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