├── README.md ├── composer.json ├── license.txt └── src └── voku └── helper └── Hooks.php /README.md: -------------------------------------------------------------------------------- 1 | [](https://travis-ci.org/voku/php-hooks) 2 | [](https://coveralls.io/github/voku/php-hooks?branch=master) 3 | [](https://scrutinizer-ci.com/g/voku/php-hooks/?branch=master) 4 | [](https://www.codacy.com/app/voku/php-hooks) 5 | [](https://insight.sensiolabs.com/projects/8ab3148c-61b5-4da6-be80-9018eb0b4441) 6 | [](https://packagist.org/packages/voku/php-hooks) 7 | [](https://packagist.org/packages/voku/php-hooks) 8 | [](https://packagist.org/packages/voku/php-hooks) 9 | [](https://travis-ci.org/voku/php-hooks) 10 | [](https://packagist.org/packages/voku/php-hooks) 11 | 12 | PHP-Hooks 13 | ========= 14 | 15 | The PHP Hooks Class is a fork of the WordPress filters hook system rolled in to a class to be ported into any php based system 16 | * This class is heavily based on the WordPress plugin API and most (if not all) of the code comes from there. 17 | 18 | How to install? 19 | ===== 20 | 21 | ```shell 22 | composer require voku/php-hooks 23 | ``` 24 | 25 | How to use? 26 | ===== 27 | 28 | We start with a simple example ... 29 | 30 | ```php 31 | add_action('header_action','echo_this_in_header'); 36 | 37 | function echo_this_in_header(){ 38 | echo 'this came from a hooked function'; 39 | } 40 | ``` 41 | 42 | then all that is left for you is to call the hooked function when you want anywhere in your application, EX: 43 | 44 | ```php 45 | '; 50 | $hooks->do_action('header_action'); 51 | echo ''; 52 | ``` 53 | 54 | and you output will be: `
131 | * The name of the filter to hook the 132 | * {@link $function_to_add} to. 133 | *
134 | * @param string|array|object $function_to_add135 | * The name of the function to be called 136 | * when the filter is applied. 137 | *
138 | * @param int $priority139 | * [optional] Used to specify the order in 140 | * which the functions associated with a 141 | * particular action are executed (default: 50). 142 | * Lower numbers correspond with earlier execution, 143 | * and functions with the same priority are executed 144 | * in the order in which they were added to the action. 145 | *
146 | * @param string $include_path147 | * [optional] File to include before executing the callback. 148 | *
149 | * 150 | * @return bool 151 | */ 152 | public function add_filter(string $tag, $function_to_add, int $priority = self::PRIORITY_NEUTRAL, string $include_path = null): bool 153 | { 154 | $idx = $this->_filter_build_unique_id($function_to_add); 155 | 156 | $this->filters[$tag][$priority][$idx] = [ 157 | 'function' => $function_to_add, 158 | 'include_path' => \is_string($include_path) ? $include_path : null, 159 | ]; 160 | 161 | unset($this->merged_filters[$tag]); 162 | 163 | return true; 164 | } 165 | 166 | /** 167 | * Removes a function from a specified filter hook. 168 | * 169 | * @param string $tagThe filter hook to which the function to be removed is 170 | * hooked.
171 | * @param string|array|object $function_to_removeThe name of the function which should be removed.
172 | * @param int $priority[optional] The priority of the function (default: 50).
173 | * 174 | * @return bool 175 | */ 176 | public function remove_filter(string $tag, $function_to_remove, int $priority = self::PRIORITY_NEUTRAL): bool 177 | { 178 | $function_to_remove = $this->_filter_build_unique_id($function_to_remove); 179 | 180 | if (!isset($this->filters[$tag][$priority][$function_to_remove])) { 181 | return false; 182 | } 183 | 184 | unset($this->filters[$tag][$priority][$function_to_remove]); 185 | if (empty($this->filters[$tag][$priority])) { 186 | unset($this->filters[$tag][$priority]); 187 | } 188 | 189 | unset($this->merged_filters[$tag]); 190 | 191 | return true; 192 | } 193 | 194 | /** 195 | * Remove all of the hooks from a filter. 196 | * 197 | * @param string $tagThe filter to remove hooks from.
198 | * @param false|int $priorityThe priority number to remove.
199 | * 200 | * @return bool 201 | */ 202 | public function remove_all_filters(string $tag, $priority = false): bool 203 | { 204 | if (isset($this->merged_filters[$tag])) { 205 | unset($this->merged_filters[$tag]); 206 | } 207 | 208 | if (!isset($this->filters[$tag])) { 209 | return true; 210 | } 211 | 212 | if (false !== $priority && isset($this->filters[$tag][$priority])) { 213 | unset($this->filters[$tag][$priority]); 214 | } else { 215 | unset($this->filters[$tag]); 216 | } 217 | 218 | return true; 219 | } 220 | 221 | /** 222 | * Check if any filter has been registered for the given hook. 223 | * 224 | *
225 | *
226 | * INFO: Use !== false to check if it's true!
227 | *
The name of the filter hook.
230 | * @param false|string $function_to_check[optional] Callback function name to check for
231 | * 232 | * @return mixed233 | * If {@link $function_to_check} is omitted, 234 | * returns boolean for whether the hook has 235 | * anything registered. 236 | * When checking a specific function, the priority 237 | * of that hook is returned, or false if the 238 | * function is not attached. 239 | * When using the {@link $function_to_check} argument, 240 | * this function may return a non-boolean value that 241 | * evaluates to false 242 | * (e.g.) 0, so use the === operator for testing the return value. 243 | *
244 | */ 245 | public function has_filter(string $tag, $function_to_check = false) 246 | { 247 | $has = isset($this->filters[$tag]); 248 | if (false === $function_to_check || !$has) { 249 | return $has; 250 | } 251 | 252 | if (!($idx = $this->_filter_build_unique_id($function_to_check))) { 253 | return false; 254 | } 255 | 256 | foreach (\array_keys($this->filters[$tag]) as $priority) { 257 | if (isset($this->filters[$tag][$priority][$idx])) { 258 | return $priority; 259 | } 260 | } 261 | 262 | return false; 263 | } 264 | 265 | /** 266 | * Call the functions added to a filter hook. 267 | * 268 | *
269 | *
270 | * INFO: Additional variables passed to the functions hooked to $tag.
271 | *
The name of the filter hook.
274 | * @param mixed $valueThe value on which the filters hooked to $tag are applied on.
275 | * 276 | * @return mixedThe filtered value after all hooked functions are applied to it.
277 | */ 278 | public function apply_filters(string $tag, $value) 279 | { 280 | $args = []; 281 | 282 | // Do 'all' actions first 283 | if (isset($this->filters['all'])) { 284 | $this->current_filter[] = $tag; 285 | $args = \func_get_args(); 286 | $this->_call_all_hook($args); 287 | } 288 | 289 | if (!isset($this->filters[$tag])) { 290 | if (isset($this->filters['all'])) { 291 | \array_pop($this->current_filter); 292 | } 293 | 294 | return $value; 295 | } 296 | 297 | if (!isset($this->filters['all'])) { 298 | $this->current_filter[] = $tag; 299 | } 300 | 301 | // Sort 302 | if (!isset($this->merged_filters[$tag])) { 303 | \ksort($this->filters[$tag]); 304 | $this->merged_filters[$tag] = true; 305 | } 306 | 307 | \reset($this->filters[$tag]); 308 | 309 | if (empty($args)) { 310 | $args = \func_get_args(); 311 | } 312 | 313 | \array_shift($args); 314 | 315 | do { 316 | foreach ((array)\current($this->filters[$tag]) as $the_) { 317 | if (null !== $the_['function']) { 318 | 319 | if (null !== $the_['include_path']) { 320 | /** @noinspection PhpIncludeInspection */ 321 | include_once $the_['include_path']; 322 | } 323 | 324 | $args[0] = $value; 325 | $value = \call_user_func_array($the_['function'], $args); 326 | } 327 | } 328 | } while (\next($this->filters[$tag]) !== false); 329 | 330 | \array_pop($this->current_filter); 331 | 332 | return $value; 333 | } 334 | 335 | /** 336 | * Execute functions hooked on a specific filter hook, specifying arguments in an array. 337 | * 338 | * @param string $tagThe name of the filter hook.
339 | * @param array $argsThe arguments supplied to the functions hooked to $tag
340 | * 341 | * @return mixedThe filtered value after all hooked functions are applied to it.
342 | */ 343 | public function apply_filters_ref_array(string $tag, array $args) 344 | { 345 | // Do 'all' actions first 346 | if (isset($this->filters['all'])) { 347 | $this->current_filter[] = $tag; 348 | $all_args = \func_get_args(); 349 | $this->_call_all_hook($all_args); 350 | } 351 | 352 | if (!isset($this->filters[$tag])) { 353 | if (isset($this->filters['all'])) { 354 | \array_pop($this->current_filter); 355 | } 356 | 357 | return $args[0]; 358 | } 359 | 360 | if (!isset($this->filters['all'])) { 361 | $this->current_filter[] = $tag; 362 | } 363 | 364 | // Sort 365 | if (!isset($this->merged_filters[$tag])) { 366 | \ksort($this->filters[$tag]); 367 | $this->merged_filters[$tag] = true; 368 | } 369 | 370 | \reset($this->filters[$tag]); 371 | 372 | do { 373 | foreach ((array)\current($this->filters[$tag]) as $the_) { 374 | if (null !== $the_['function']) { 375 | 376 | if (null !== $the_['include_path']) { 377 | /** @noinspection PhpIncludeInspection */ 378 | include_once $the_['include_path']; 379 | } 380 | 381 | $args[0] = \call_user_func_array($the_['function'], $args); 382 | } 383 | } 384 | } while (\next($this->filters[$tag]) !== false); 385 | 386 | \array_pop($this->current_filter); 387 | 388 | return $args[0]; 389 | } 390 | 391 | /** 392 | * Hooks a function on to a specific action. 393 | * 394 | * @param string $tag395 | * The name of the action to which the 396 | * $function_to_add is hooked. 397 | *
398 | * @param string|array $function_to_addThe name of the function you wish to be called.
399 | * @param int $priority400 | * [optional] Used to specify the order in which 401 | * the functions associated with a particular 402 | * action are executed (default: 50). 403 | * Lower numbers correspond with earlier execution, 404 | * and functions with the same priority are executed 405 | * in the order in which they were added to the action. 406 | *
407 | * @param string $include_path[optional] File to include before executing the callback.
408 | * 409 | * @return bool 410 | */ 411 | public function add_action( 412 | string $tag, 413 | $function_to_add, 414 | int $priority = self::PRIORITY_NEUTRAL, 415 | string $include_path = null 416 | ): bool 417 | { 418 | return $this->add_filter($tag, $function_to_add, $priority, $include_path); 419 | } 420 | 421 | /** 422 | * Check if any action has been registered for a hook. 423 | * 424 | *
425 | *
426 | * INFO: Use !== false to check if it's true!
427 | *
The name of the action hook.
430 | * @param false|string $function_to_check[optional]
431 | * 432 | * @return mixed433 | * If $function_to_check is omitted, 434 | * returns boolean for whether the hook has 435 | * anything registered. 436 | * When checking a specific function, 437 | * the priority of that hook is returned, 438 | * or false if the function is not attached. 439 | * When using the $function_to_check 440 | * argument, this function may return a non-boolean 441 | * value that evaluates to false (e.g.) 0, 442 | * so use the === operator for testing the return value. 443 | *
444 | */ 445 | public function has_action(string $tag, $function_to_check = false) 446 | { 447 | return $this->has_filter($tag, $function_to_check); 448 | } 449 | 450 | /** 451 | * Removes a function from a specified action hook. 452 | * 453 | * @param string $tagThe action hook to which the function to be removed is hooked.
454 | * @param mixed $function_to_removeThe name of the function which should be removed.
455 | * @param int $priority[optional] The priority of the function (default: 50).
456 | * 457 | * @return boolWhether the function is removed.
458 | */ 459 | public function remove_action(string $tag, $function_to_remove, int $priority = self::PRIORITY_NEUTRAL): bool 460 | { 461 | return $this->remove_filter($tag, $function_to_remove, $priority); 462 | } 463 | 464 | /** 465 | * Remove all of the hooks from an action. 466 | * 467 | * @param string $tagThe action to remove hooks from.
468 | * @param false|int $priorityThe priority number to remove them from.
469 | * 470 | * @return bool 471 | */ 472 | public function remove_all_actions(string $tag, $priority = false): bool 473 | { 474 | return $this->remove_all_filters($tag, $priority); 475 | } 476 | 477 | /** 478 | * Execute functions hooked on a specific action hook. 479 | * 480 | * @param string $tagThe name of the action to be executed.
481 | * @param mixed $arg482 | * [optional] Additional arguments which are passed on 483 | * to the functions hooked to the action. 484 | *
485 | * 486 | * @return boolWill return false if $tag does not exist in $filter array.
487 | */ 488 | public function do_action(string $tag, $arg = ''): bool 489 | { 490 | if (!\is_array($this->actions)) { 491 | $this->actions = []; 492 | } 493 | 494 | if (isset($this->actions[$tag])) { 495 | ++$this->actions[$tag]; 496 | } else { 497 | $this->actions[$tag] = 1; 498 | } 499 | 500 | // Do 'all' actions first 501 | if (isset($this->filters['all'])) { 502 | $this->current_filter[] = $tag; 503 | $all_args = \func_get_args(); 504 | $this->_call_all_hook($all_args); 505 | } 506 | 507 | if (!isset($this->filters[$tag])) { 508 | if (isset($this->filters['all'])) { 509 | \array_pop($this->current_filter); 510 | } 511 | 512 | return false; 513 | } 514 | 515 | if (!isset($this->filters['all'])) { 516 | $this->current_filter[] = $tag; 517 | } 518 | 519 | $args = []; 520 | 521 | if ( 522 | \is_array($arg) 523 | && 524 | isset($arg[0]) 525 | && 526 | \is_object($arg[0]) 527 | && 528 | 1 == \count($arg) 529 | ) { 530 | $args[] =& $arg[0]; 531 | } else { 532 | $args[] = $arg; 533 | } 534 | 535 | $numArgs = \func_num_args(); 536 | 537 | for ($a = 2; $a < $numArgs; $a++) { 538 | $args[] = \func_get_arg($a); 539 | } 540 | 541 | // Sort 542 | if (!isset($this->merged_filters[$tag])) { 543 | \ksort($this->filters[$tag]); 544 | $this->merged_filters[$tag] = true; 545 | } 546 | 547 | \reset($this->filters[$tag]); 548 | 549 | do { 550 | foreach ((array)\current($this->filters[$tag]) as $the_) { 551 | if (null !== $the_['function']) { 552 | 553 | if (null !== $the_['include_path']) { 554 | /** @noinspection PhpIncludeInspection */ 555 | include_once $the_['include_path']; 556 | } 557 | 558 | \call_user_func_array($the_['function'], $args); 559 | } 560 | } 561 | } while (\next($this->filters[$tag]) !== false); 562 | 563 | \array_pop($this->current_filter); 564 | 565 | return true; 566 | } 567 | 568 | /** 569 | * Execute functions hooked on a specific action hook, specifying arguments in an array. 570 | * 571 | * @param string $tagThe name of the action to be executed.
572 | * @param array $argsThe arguments supplied to the functions hooked to $tag
573 | * 574 | * @return boolWill return false if $tag does not exist in $filter array.
575 | */ 576 | public function do_action_ref_array(string $tag, array $args): bool 577 | { 578 | if (!\is_array($this->actions)) { 579 | $this->actions = []; 580 | } 581 | 582 | if (isset($this->actions[$tag])) { 583 | ++$this->actions[$tag]; 584 | } else { 585 | $this->actions[$tag] = 1; 586 | } 587 | 588 | // Do 'all' actions first 589 | if (isset($this->filters['all'])) { 590 | $this->current_filter[] = $tag; 591 | $all_args = \func_get_args(); 592 | $this->_call_all_hook($all_args); 593 | } 594 | 595 | if (!isset($this->filters[$tag])) { 596 | if (isset($this->filters['all'])) { 597 | \array_pop($this->current_filter); 598 | } 599 | 600 | return false; 601 | } 602 | 603 | if (!isset($this->filters['all'])) { 604 | $this->current_filter[] = $tag; 605 | } 606 | 607 | // Sort 608 | if (!isset($this->merged_filters[$tag])) { 609 | \ksort($this->filters[$tag]); 610 | $this->merged_filters[$tag] = true; 611 | } 612 | 613 | \reset($this->filters[$tag]); 614 | 615 | do { 616 | foreach ((array)\current($this->filters[$tag]) as $the_) { 617 | if (null !== $the_['function']) { 618 | 619 | if (null !== $the_['include_path']) { 620 | /** @noinspection PhpIncludeInspection */ 621 | include_once $the_['include_path']; 622 | } 623 | 624 | \call_user_func_array($the_['function'], $args); 625 | } 626 | } 627 | } while (\next($this->filters[$tag]) !== false); 628 | 629 | \array_pop($this->current_filter); 630 | 631 | return true; 632 | } 633 | 634 | /** 635 | * Retrieve the number of times an action has fired. 636 | * 637 | * @param string $tagThe name of the action hook.
638 | * 639 | * @return intThe number of times action hook $tag is fired.
640 | */ 641 | public function did_action(string $tag): int 642 | { 643 | if (!\is_array($this->actions) || !isset($this->actions[$tag])) { 644 | return 0; 645 | } 646 | 647 | return $this->actions[$tag]; 648 | } 649 | 650 | /** 651 | * Retrieve the name of the current filter or action. 652 | * 653 | * @return stringHook name of the current filter or action.
654 | */ 655 | public function current_filter(): string 656 | { 657 | return \end($this->current_filter); 658 | } 659 | 660 | /** 661 | * Build Unique ID for storage and retrieval. 662 | * 663 | * @param string|array|object $functionUsed for creating unique id.
664 | * 665 | * @return string|false666 | * Unique ID for usage as array key or false if 667 | * $priority === false and $function is an 668 | * object reference, and it does not already have a unique id. 669 | *
670 | */ 671 | private function _filter_build_unique_id($function) 672 | { 673 | if (\is_string($function)) { 674 | return $function; 675 | } 676 | 677 | if (\is_object($function)) { 678 | // Closures are currently implemented as objects 679 | $function = [ 680 | $function, 681 | '', 682 | ]; 683 | } else { 684 | $function = (array)$function; 685 | } 686 | 687 | if (\is_object($function[0])) { 688 | // Object Class Calling 689 | return \spl_object_hash($function[0]) . $function[1]; 690 | } 691 | 692 | if (\is_string($function[0])) { 693 | // Static Calling 694 | return $function[0] . $function[1]; 695 | } 696 | 697 | return false; 698 | } 699 | 700 | /** 701 | * Call "All" Hook 702 | * 703 | * @param array $args 704 | */ 705 | public function _call_all_hook(array $args) 706 | { 707 | \reset($this->filters['all']); 708 | 709 | do { 710 | foreach ((array)\current($this->filters['all']) as $the_) { 711 | if (null !== $the_['function']) { 712 | 713 | if (null !== $the_['include_path']) { 714 | /** @noinspection PhpIncludeInspection */ 715 | include_once $the_['include_path']; 716 | } 717 | 718 | \call_user_func_array($the_['function'], $args); 719 | } 720 | } 721 | } while (\next($this->filters['all']) !== false); 722 | } 723 | 724 | /** @noinspection MagicMethodsValidityInspection */ 725 | /** 726 | * @param array $args 727 | * 728 | * @deprecated use "this->_call_all_hook()" 729 | */ 730 | public function __call_all_hook(array $args) 731 | { 732 | // <-- refactoring "__call_all_hook()" into "_call_all_hook()" is a breaking change (BC), 733 | // so we will only deprecate the usage 734 | 735 | $this->_call_all_hook($args); 736 | } 737 | 738 | /** 739 | * Add hook for shortcode tag. 740 | * 741 | *
742 | *
743 | * There can only be one hook for each shortcode. Which means that if another
744 | * plugin has a similar shortcode, it will override yours or yours will override
745 | * theirs depending on which order the plugins are included and/or ran.
746 | *
747 | *
748 | *
753 | * // [footag foo="bar"]
754 | * function footag_func($atts) {
755 | * return "foo = {$atts[foo]}";
756 | * }
757 | * add_shortcode('footag', 'footag_func');
758 | *
759 | *
760 | * Example with nice attribute defaults:
761 | *
762 | *
763 | * // [bartag foo="bar"]
764 | * function bartag_func($atts) {
765 | * $args = shortcode_atts(array(
766 | * 'foo' => 'no foo',
767 | * 'baz' => 'default baz',
768 | * ), $atts);
769 | *
770 | * return "foo = {$args['foo']}";
771 | * }
772 | * add_shortcode('bartag', 'bartag_func');
773 | *
774 | *
775 | * Example with enclosed content:
776 | *
777 | *
778 | * // [baztag]content[/baztag]
779 | * function baztag_func($atts, $content='') {
780 | * return "content = $content";
781 | * }
782 | * add_shortcode('baztag', 'baztag_func');
783 | *
784 | *
785 | * @param string $tag Shortcode tag to be searched in post content.
786 | * @param callable $funcHook to run when shortcode is found.
787 | * 788 | * @return bool 789 | */ 790 | public function add_shortcode(string $tag, $func): bool 791 | { 792 | if (\is_callable($func)) { 793 | self::$shortcode_tags[$tag] = $func; 794 | 795 | return true; 796 | } 797 | 798 | return false; 799 | } 800 | 801 | /** 802 | * Removes hook for shortcode. 803 | * 804 | * @param string $tagshortcode tag to remove hook for.
805 | * 806 | * @return bool 807 | */ 808 | public function remove_shortcode(string $tag): bool 809 | { 810 | if (isset(self::$shortcode_tags[$tag])) { 811 | unset(self::$shortcode_tags[$tag]); 812 | 813 | return true; 814 | } 815 | 816 | return false; 817 | } 818 | 819 | /** 820 | * This function is simple, it clears all of the shortcode tags by replacing the 821 | * shortcodes by a empty array. This is actually a very efficient method 822 | * for removing all shortcodes. 823 | * 824 | * @return bool 825 | */ 826 | public function remove_all_shortcodes(): bool 827 | { 828 | self::$shortcode_tags = []; 829 | 830 | return true; 831 | } 832 | 833 | /** 834 | * Whether a registered shortcode exists named $tag 835 | * 836 | * @param string $tag 837 | * 838 | * @return bool 839 | */ 840 | public function shortcode_exists(string $tag): bool 841 | { 842 | return \array_key_exists($tag, self::$shortcode_tags); 843 | } 844 | 845 | /** 846 | * Whether the passed content contains the specified shortcode. 847 | * 848 | * @param string $content 849 | * @param string $tag 850 | * 851 | * @return bool 852 | */ 853 | public function has_shortcode(string $content, string $tag): bool 854 | { 855 | if (false === \strpos($content, '[')) { 856 | return false; 857 | } 858 | 859 | if ($this->shortcode_exists($tag)) { 860 | \preg_match_all('/' . $this->get_shortcode_regex() . '/s', $content, $matches, PREG_SET_ORDER); 861 | if (empty($matches)) { 862 | return false; 863 | } 864 | 865 | foreach ($matches as $shortcode) { 866 | if ($tag === $shortcode[2]) { 867 | return true; 868 | } 869 | 870 | if (!empty($shortcode[5]) && $this->has_shortcode($shortcode[5], $tag)) { 871 | return true; 872 | } 873 | } 874 | } 875 | 876 | return false; 877 | } 878 | 879 | /** 880 | * Search content for shortcodes and filter shortcodes through their hooks. 881 | * 882 | *
883 | *
884 | * If there are no shortcode tags defined, then the content will be returned
885 | * without any filtering. This might cause issues when plugins are disabled but
886 | * the shortcode will still show up in the post or content.
887 | *
Content to search for shortcodes.
890 | * 891 | * @return stringContent with shortcodes filtered out.
892 | */ 893 | public function do_shortcode(string $content): string 894 | { 895 | if (empty(self::$shortcode_tags) || !\is_array(self::$shortcode_tags)) { 896 | return $content; 897 | } 898 | 899 | $pattern = $this->get_shortcode_regex(); 900 | 901 | return \preg_replace_callback( 902 | "/$pattern/s", 903 | [ 904 | $this, 905 | '_do_shortcode_tag', 906 | ], 907 | $content 908 | ); 909 | } 910 | 911 | /** 912 | * Retrieve the shortcode regular expression for searching. 913 | * 914 | *
915 | *
916 | * The regular expression combines the shortcode tags in the regular expression
917 | * in a regex class.
918 | *
919 | *
920 | * The regular expression contains 6 different sub matches to help with parsing.
921 | *
922 | *
923 | * 1 - An extra [ to allow for escaping shortcodes with double [[]]
924 | * 2 - The shortcode name
925 | * 3 - The shortcode argument list
926 | * 4 - The self closing /
927 | * 5 - The content of a shortcode when it wraps some content.
928 | * 6 - An extra ] to allow for escaping shortcodes with double [[]]
929 | *
regular expression match array
977 | * 978 | * @return mixedfalse on failure
979 | */ 980 | private function _do_shortcode_tag(array $m) 981 | { 982 | // allow [[foo]] syntax for escaping a tag 983 | if ($m[1] == '[' && $m[6] == ']') { 984 | return \substr($m[0], 1, -1); 985 | } 986 | 987 | $tag = $m[2]; 988 | $attr = $this->shortcode_parse_atts($m[3]); 989 | 990 | // enclosing tag - extra parameter 991 | if (isset($m[5])) { 992 | return $m[1] . \call_user_func(self::$shortcode_tags[$tag], $attr, $m[5], $tag) . $m[6]; 993 | } 994 | 995 | // self-closing tag 996 | return $m[1] . \call_user_func(self::$shortcode_tags[$tag], $attr, null, $tag) . $m[6]; 997 | } 998 | 999 | /** 1000 | * Retrieve all attributes from the shortcodes tag. 1001 | * 1002 | *
1003 | *
1004 | * The attributes list has the attribute name as the key and the value of the
1005 | * attribute as the value in the key/value pair. This allows for easier
1006 | * retrieval of the attributes, since all attributes have to be known.
1007 | *
List of attributes and their value.
1012 | */ 1013 | public function shortcode_parse_atts(string $text): array 1014 | { 1015 | $atts = []; 1016 | $pattern = '/(\w+)\s*=\s*"([^"]*)"(?:\s|$)|(\w+)\s*=\s*\'([^\']*)\'(?:\s|$)|(\w+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/'; 1017 | $text = \preg_replace("/[\x{00a0}\x{200b}]+/u", ' ', $text); 1018 | $matches = []; 1019 | if (\preg_match_all($pattern, $text, $matches, PREG_SET_ORDER)) { 1020 | foreach ($matches as $m) { 1021 | if (!empty($m[1])) { 1022 | $atts[\strtolower($m[1])] = \stripcslashes($m[2]); 1023 | } elseif (!empty($m[3])) { 1024 | $atts[\strtolower($m[3])] = \stripcslashes($m[4]); 1025 | } elseif (!empty($m[5])) { 1026 | $atts[\strtolower($m[5])] = \stripcslashes($m[6]); 1027 | } elseif (isset($m[7]) && $m[7] !== '') { 1028 | $atts[] = \stripcslashes($m[7]); 1029 | } elseif (isset($m[8])) { 1030 | $atts[] = \stripcslashes($m[8]); 1031 | } 1032 | } 1033 | } else { 1034 | $atts = \ltrim($text); 1035 | } 1036 | 1037 | return $atts; 1038 | } 1039 | 1040 | /** 1041 | * Combine user attributes with known attributes and fill in defaults when needed. 1042 | * 1043 | *
1044 | *
1045 | * The pairs should be considered to be all of the attributes which are
1046 | * supported by the caller and given as a list. The returned attributes will
1047 | * only contain the attributes in the $pairs list.
1048 | *
1049 | *
1050 | * If the $atts list has unsupported attributes, then they will be ignored and
1051 | * removed from the final returned list.
1052 | *
Entire list of supported attributes and their defaults.
1055 | * @param array $attsUser defined attributes in shortcode tag.
1056 | * @param string $shortcode[optional] The name of the shortcode, provided for context to enable filtering.
1057 | * 1058 | * @return arrayCombined and filtered attribute list.
1059 | */ 1060 | public function shortcode_atts($pairs, $atts, $shortcode = ''): array 1061 | { 1062 | $atts = (array)$atts; 1063 | $out = []; 1064 | foreach ($pairs as $name => $default) { 1065 | if (array_key_exists($name, $atts)) { 1066 | $out[$name] = $atts[$name]; 1067 | } else { 1068 | $out[$name] = $default; 1069 | } 1070 | } 1071 | 1072 | /** 1073 | * Filter a shortcode's default attributes. 1074 | * 1075 | *
1076 | *
1077 | * If the third parameter of the shortcode_atts() function is present then this filter is available.
1078 | * The third parameter, $shortcode, is the name of the shortcode.
1079 | *
The output array of shortcode attributes.
1082 | * @param array $pairsThe supported attributes and their defaults.
1083 | * @param array $attsThe user defined shortcode attributes.
1084 | */ 1085 | if ($shortcode) { 1086 | $out = $this->apply_filters( 1087 | "shortcode_atts_{$shortcode}", 1088 | $out, 1089 | $pairs, 1090 | $atts 1091 | ); 1092 | } 1093 | 1094 | return $out; 1095 | } 1096 | 1097 | /** 1098 | * Remove all shortcode tags from the given content. 1099 | * 1100 | * @param string $contentContent to remove shortcode tags.
1101 | * 1102 | * @return stringContent without shortcode tags.
1103 | */ 1104 | public function strip_shortcodes(string $content): string 1105 | { 1106 | 1107 | if (empty(self::$shortcode_tags) || !\is_array(self::$shortcode_tags)) { 1108 | return $content; 1109 | } 1110 | 1111 | $pattern = $this->get_shortcode_regex(); 1112 | 1113 | return preg_replace_callback( 1114 | "/$pattern/s", 1115 | [ 1116 | $this, 1117 | '_strip_shortcode_tag', 1118 | ], 1119 | $content 1120 | ); 1121 | } 1122 | 1123 | /** 1124 | * Strip shortcode by tag. 1125 | * 1126 | * @param array $m 1127 | * 1128 | * @return string 1129 | */ 1130 | private function _strip_shortcode_tag(array $m): string 1131 | { 1132 | // allow [[foo]] syntax for escaping a tag 1133 | if ($m[1] == '[' && $m[6] == ']') { 1134 | return substr($m[0], 1, -1); 1135 | } 1136 | 1137 | return $m[1] . $m[6]; 1138 | } 1139 | 1140 | } 1141 | --------------------------------------------------------------------------------