Changeset 62397
- Timestamp:
- 05/21/2026 07:56:31 AM (45 hours ago)
- Location:
- trunk
- Files:
-
- 1 added
- 5 edited
-
src/wp-includes/abilities-api/class-wp-ability.php (modified) (7 diffs)
-
src/wp-includes/class-wp-filter-sentinel.php (added)
-
src/wp-includes/plugin.php (modified) (1 diff)
-
src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php (modified) (1 diff)
-
tests/phpunit/tests/abilities-api/wpAbility.php (modified) (1 diff)
-
tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/abilities-api/class-wp-ability.php
r62238 r62397 437 437 * this method returns null. If input is provided, it is returned as-is. 438 438 * 439 * @since 6.9.0 439 * The {@see 'wp_ability_normalize_input'} filter fires after the built-in default-value handling, 440 * allowing plugins to transform the result. 441 * 442 * @since 6.9.0 443 * @since 7.1.0 Added the `wp_ability_normalize_input` filter. 440 444 * 441 445 * @param mixed $input Optional. The raw input provided for the ability. Default `null`. 442 * @return mixed The same input, or the default from schema, or `null` if default not set.446 * @return mixed The normalized input, or a `WP_Error` if a filter returned one. 443 447 */ 444 448 public function normalize_input( $input = null ) { 445 if ( null !== $input ) { 446 return $input; 447 } 448 449 $input_schema = $this->get_input_schema(); 450 if ( ! empty( $input_schema ) && array_key_exists( 'default', $input_schema ) ) { 451 return $input_schema['default']; 452 } 453 454 return null; 449 if ( null === $input ) { 450 $input_schema = $this->get_input_schema(); 451 if ( array_key_exists( 'default', $input_schema ) ) { 452 $input = $input_schema['default']; 453 } 454 } 455 456 /** 457 * Filters the normalized input for an ability. 458 * 459 * Fires after `normalize_input()` has applied any default value declared in the input schema, 460 * giving plugins a chance to adjust the input before it is consumed downstream. Common uses 461 * include defaulting beyond what JSON Schema can express, prompt enrichment, and injecting 462 * caller metadata. 463 * 464 * Returning a `WP_Error` causes callers that propagate it (such as `execute()`) to halt 465 * before validation, permission checks, and the registered execute callback. 466 * 467 * @since 7.1.0 468 * 469 * @param mixed $input The normalized input data. 470 * @param string $ability_name The name of the ability. 471 * @param WP_Ability $ability The ability instance. 472 */ 473 return apply_filters( 'wp_ability_normalize_input', $input, $this->name, $this ); 455 474 } 456 475 … … 532 551 * Use `validate_input()` method to validate input before calling this method if needed. 533 552 * 534 * @since 6.9.0 553 * The {@see 'wp_ability_permission_result'} filter fires after the registered 554 * `permission_callback` returns, allowing plugins to override the result. 555 * 556 * @since 6.9.0 557 * @since 7.1.0 Added the `wp_ability_permission_result` filter. 535 558 * 536 559 * @see validate_input() … … 548 571 } 549 572 550 return $this->invoke_callback( $this->permission_callback, $input ); 573 $permission = $this->invoke_callback( $this->permission_callback, $input ); 574 575 /** 576 * Filters the result of an ability's permission check. 577 * 578 * Fires after the registered `permission_callback` returns. Plugins can use this to layer 579 * additional authorization rules on top of the ability's own permission logic — for example, 580 * multi-factor authorization gates or temporary permission elevation for trusted contexts. 581 * 582 * Filters can return `true` to grant, `false` to deny, or a `WP_Error` to deny with a specific 583 * error code and message. The filter receives whatever the `permission_callback` produced. 584 * Any other return value is coerced to `false`. 585 * 586 * @since 7.1.0 587 * 588 * @param bool|WP_Error $permission The permission result returned by `permission_callback`. 589 * @param string $ability_name The name of the ability. 590 * @param mixed $input The input data for the permission check. 591 * @param WP_Ability $ability The ability instance. 592 */ 593 $result = apply_filters( 'wp_ability_permission_result', $permission, $this->name, $input, $this ); 594 if ( ! is_bool( $result ) && ! is_wp_error( $result ) ) { 595 $result = false; 596 } 597 return $result; 551 598 } 552 599 … … 554 601 * Executes the ability callback. 555 602 * 556 * @since 6.9.0 603 * The {@see 'wp_ability_execute_result'} filter fires before this method returns, allowing 604 * plugins to transform the result produced by the registered `execute_callback`. 605 * 606 * @since 6.9.0 607 * @since 7.1.0 Added the `wp_ability_execute_result` filter. 557 608 * 558 609 * @param mixed $input Optional. The input data for the ability. Default `null`. … … 561 612 protected function do_execute( $input = null ) { 562 613 if ( ! is_callable( $this->execute_callback ) ) { 563 returnnew WP_Error(614 $result = new WP_Error( 564 615 'ability_invalid_execute_callback', 565 616 /* translators: %s ability name. */ 566 617 sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), esc_html( $this->name ) ) 567 618 ); 568 } 569 570 return $this->invoke_callback( $this->execute_callback, $input ); 619 } else { 620 $result = $this->invoke_callback( $this->execute_callback, $input ); 621 } 622 623 /** 624 * Filters the result returned by an ability's execute callback. 625 * 626 * Fires after the registered execute callback runs. Plugins can use this to transform the 627 * result — response formatting, stripping internal metadata, content safety filtering, 628 * response enrichment, or recovering from a failure by returning a successful value. 629 * 630 * The filter receives whatever the registered callback produced, including a `WP_Error` 631 * if execution failed. Filters may pass the `WP_Error` through unchanged, override it with 632 * a recovered result, or convert a successful result into a `WP_Error`. 633 * 634 * @since 7.1.0 635 * 636 * @param mixed $result The result returned by the registered `execute_callback`, 637 * or a `WP_Error` if execution failed. 638 * @param string $ability_name The name of the ability. 639 * @param mixed $input The normalized input data. 640 * @param WP_Ability $ability The ability instance. 641 */ 642 return apply_filters( 'wp_ability_execute_result', $result, $this->name, $input, $this ); 571 643 } 572 644 … … 606 678 * 607 679 * @since 6.9.0 680 * @since 7.1.0 Added the `wp_pre_execute_ability` filter. 608 681 * 609 682 * @param mixed $input Optional. The input data for the ability. Default `null`. … … 611 684 */ 612 685 public function execute( $input = null ) { 613 $input = $this->normalize_input( $input ); 686 /** 687 * Filters whether to short-circuit ability execution. 688 * 689 * Returning a value other than the received default bypasses the rest of `execute()` — 690 * input normalization, input validation, permission checks, the registered execute callback, 691 * output validation, and the surrounding actions — and the value is returned to the caller 692 * as-is. Useful for cached responses, rate limiting, maintenance mode, and test mocking. 693 * 694 * To continue with normal execution, return `$pre` unchanged. This preserves any value 695 * (including `null`, `false`, or arbitrary objects) as a valid short-circuit result. 696 * 697 * Because validation is bypassed, callers that short-circuit are responsible for the 698 * integrity of any value they consume from `$input`. 699 * 700 * @since 7.1.0 701 * 702 * @param mixed $pre The pre-computed result. Return this value unchanged to continue execution. 703 * Default `WP_Filter_Sentinel` instance unique to this invocation. 704 * @param string $ability_name The name of the ability. 705 * @param mixed $input The raw input passed to `execute()`. 706 * @param WP_Ability $ability The ability instance. 707 */ 708 $pre_execute_sentinel = new WP_Filter_Sentinel(); 709 $pre = apply_filters( 'wp_pre_execute_ability', $pre_execute_sentinel, $this->name, $input, $this ); 710 if ( $pre !== $pre_execute_sentinel ) { 711 return $pre; 712 } 713 714 $input = $this->normalize_input( $input ); 715 if ( is_wp_error( $input ) ) { 716 return $input; 717 } 718 614 719 $is_valid = $this->validate_input( $input ); 615 720 if ( is_wp_error( $is_valid ) ) { -
trunk/src/wp-includes/plugin.php
r61118 r62397 24 24 // Initialize the filter globals. 25 25 require __DIR__ . '/class-wp-hook.php'; 26 require __DIR__ . '/class-wp-filter-sentinel.php'; 26 27 27 28 /** @var WP_Hook[] $wp_filter */ -
trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php
r61047 r62397 159 159 } 160 160 161 $input = $this->get_input_from_request( $request ); 162 $input = $ability->normalize_input( $input ); 161 $input = $this->get_input_from_request( $request ); 162 $input = $ability->normalize_input( $input ); 163 if ( is_wp_error( $input ) ) { 164 $error_data = $input->get_error_data(); 165 if ( ! is_array( $error_data ) || ! isset( $error_data['status'] ) ) { 166 $input->add_data( array( 'status' => 400 ) ); 167 } 168 169 return $input; 170 } 171 163 172 $is_valid = $ability->validate_input( $input ); 164 173 if ( is_wp_error( $is_valid ) ) { -
trunk/tests/phpunit/tests/abilities-api/wpAbility.php
r62238 r62397 827 827 $this->assertInstanceOf( WP_Error::class, $result, 'Should return WP_Error for output validation failure' ); 828 828 } 829 830 /** 831 * Tests that the wp_ability_normalize_input filter can transform input and receives the 832 * expected ability name and instance (verified via args-as-guards on the transformation). 833 * 834 * @ticket 64989 835 */ 836 public function test_normalize_input_filter_can_transform_input() { 837 $args = array_merge( 838 self::$test_ability_properties, 839 array( 840 'input_schema' => array( 841 'type' => 'string', 842 'description' => 'Test input string.', 843 'required' => true, 844 ), 845 'output_schema' => array( 846 'type' => 'integer', 847 'description' => 'Result integer.', 848 'required' => true, 849 ), 850 'execute_callback' => static function ( string $input ): int { 851 return strlen( $input ); 852 }, 853 ) 854 ); 855 856 $callback = static function ( $input, $ability_name, $ability ) { 857 if ( self::$test_ability_name !== $ability_name || ! $ability instanceof WP_Ability ) { 858 return $input; 859 } 860 return $input . '-transformed'; 861 }; 862 863 add_filter( 'wp_ability_normalize_input', $callback, 10, 3 ); 864 865 $ability = new WP_Ability( self::$test_ability_name, $args ); 866 $result = $ability->execute( 'hello' ); 867 868 remove_filter( 'wp_ability_normalize_input', $callback, 10 ); 869 870 $this->assertSame( strlen( 'hello-transformed' ), $result, 'Result should reflect the transformed input flowing through the execute callback.' ); 871 } 872 873 /** 874 * Tests that returning a WP_Error from wp_ability_normalize_input halts execution. 875 * 876 * @ticket 64989 877 */ 878 public function test_normalize_input_filter_wp_error_halts_execution() { 879 $args = array_merge( 880 self::$test_ability_properties, 881 array( 882 'input_schema' => array( 883 'type' => 'string', 884 'description' => 'Test input string.', 885 'required' => true, 886 ), 887 'execute_callback' => static function ( string $input ) { 888 return strlen( $input ); 889 }, 890 ) 891 ); 892 893 $filter = static function () { 894 return new WP_Error( 'normalize_halt', 'Halted from filter.' ); 895 }; 896 897 add_filter( 'wp_ability_normalize_input', $filter ); 898 899 $ability = new WP_Ability( self::$test_ability_name, $args ); 900 $result = $ability->execute( 'hello' ); 901 902 remove_filter( 'wp_ability_normalize_input', $filter ); 903 904 $this->assertInstanceOf( WP_Error::class, $result, 'Filter returning WP_Error should propagate as the execute() result.' ); 905 $this->assertSame( 'normalize_halt', $result->get_error_code(), 'WP_Error code should be preserved.' ); 906 } 907 908 /** 909 * Tests that the wp_ability_permission_result filter can grant permission that the 910 * callback denied, with the filter's args verified via args-as-guards. 911 * 912 * @ticket 64989 913 */ 914 public function test_permission_result_filter_can_grant_permission() { 915 $args = array_merge( 916 self::$test_ability_properties, 917 array( 918 'input_schema' => array( 919 'type' => 'integer', 920 'description' => 'Test input integer.', 921 'required' => true, 922 ), 923 'output_schema' => array( 924 'type' => 'integer', 925 'description' => 'Result integer.', 926 'required' => true, 927 ), 928 'execute_callback' => static function ( int $input ): int { 929 return $input; 930 }, 931 'permission_callback' => static function (): bool { 932 return false; 933 }, 934 ) 935 ); 936 937 $filter = static function ( $permission, $ability_name, $input, $ability ) { 938 if ( false !== $permission ) { 939 return $permission; 940 } 941 if ( self::$test_ability_name !== $ability_name || 7 !== $input || ! $ability instanceof WP_Ability ) { 942 return $permission; 943 } 944 return true; 945 }; 946 947 add_filter( 'wp_ability_permission_result', $filter, 10, 4 ); 948 949 $ability = new WP_Ability( self::$test_ability_name, $args ); 950 $result = $ability->execute( 7 ); 951 952 remove_filter( 'wp_ability_permission_result', $filter, 10 ); 953 954 $this->assertSame( 7, $result, 'Filter should override the permission denial; reaching the execute callback proves all args matched expectations.' ); 955 } 956 957 /** 958 * Tests that the wp_ability_permission_result filter can deny permission granted by the callback. 959 * 960 * @ticket 64989 961 */ 962 public function test_permission_result_filter_can_deny_permission() { 963 $args = array_merge( 964 self::$test_ability_properties, 965 array( 966 'execute_callback' => static function (): int { 967 return 1; 968 }, 969 'permission_callback' => static function (): bool { 970 return true; 971 }, 972 ) 973 ); 974 975 $filter = static function () { 976 return false; 977 }; 978 979 add_filter( 'wp_ability_permission_result', $filter ); 980 981 $ability = new WP_Ability( self::$test_ability_name, $args ); 982 $result = $ability->execute(); 983 984 remove_filter( 'wp_ability_permission_result', $filter ); 985 986 $this->assertInstanceOf( WP_Error::class, $result, 'Denied permission should produce a WP_Error.' ); 987 $this->assertSame( 'ability_invalid_permissions', $result->get_error_code() ); 988 } 989 990 /** 991 * Tests that the wp_ability_permission_result filter can convert a WP_Error denial from the 992 * callback into a grant, proving the filter receives the WP_Error verbatim. 993 * 994 * @ticket 64989 995 */ 996 public function test_permission_result_filter_can_convert_wp_error_to_grant() { 997 $args = array_merge( 998 self::$test_ability_properties, 999 array( 1000 'execute_callback' => static function (): int { 1001 return 1; 1002 }, 1003 'permission_callback' => static function (): WP_Error { 1004 return new WP_Error( 'callback_denied', 'Denied by callback.' ); 1005 }, 1006 ) 1007 ); 1008 1009 $filter = static function ( $permission ) { 1010 if ( ! is_wp_error( $permission ) || 'callback_denied' !== $permission->get_error_code() ) { 1011 return $permission; 1012 } 1013 return true; 1014 }; 1015 1016 add_filter( 'wp_ability_permission_result', $filter ); 1017 1018 $ability = new WP_Ability( self::$test_ability_name, $args ); 1019 $result = $ability->execute(); 1020 1021 remove_filter( 'wp_ability_permission_result', $filter ); 1022 1023 $this->assertSame( 1, $result, 'Filter received the WP_Error denial and converted it to a grant; execute callback returned its value.' ); 1024 } 1025 1026 /** 1027 * Tests that the wp_ability_permission_result filter fires when check_permissions() is 1028 * called directly (not via execute()). 1029 * 1030 * @ticket 64989 1031 */ 1032 public function test_permission_result_filter_fires_on_direct_check_permissions_call() { 1033 $args = array_merge( 1034 self::$test_ability_properties, 1035 array( 1036 'permission_callback' => static function (): bool { 1037 return true; 1038 }, 1039 ) 1040 ); 1041 1042 $filter = static function () { 1043 return false; 1044 }; 1045 1046 add_filter( 'wp_ability_permission_result', $filter ); 1047 1048 $ability = new WP_Ability( self::$test_ability_name, $args ); 1049 $result = $ability->check_permissions(); 1050 1051 remove_filter( 'wp_ability_permission_result', $filter ); 1052 1053 $this->assertFalse( $result, 'check_permissions() should return the filtered value when called directly.' ); 1054 } 1055 1056 /** 1057 * Tests that a non-bool, non-WP_Error return from the wp_ability_permission_result filter is 1058 * coerced to false so check_permissions() honors its documented bool|WP_Error return type. 1059 * 1060 * @ticket 64989 1061 */ 1062 public function test_permission_result_filter_invalid_value_coerced_to_false() { 1063 $filter = static function () { 1064 return 'not-a-bool'; 1065 }; 1066 1067 add_filter( 'wp_ability_permission_result', $filter ); 1068 1069 $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); 1070 $result = $ability->check_permissions(); 1071 1072 remove_filter( 'wp_ability_permission_result', $filter ); 1073 1074 $this->assertFalse( $result, 'Non-bool, non-WP_Error filter return is coerced to false.' ); 1075 } 1076 1077 /** 1078 * Tests that returning a custom value from wp_pre_execute_ability short-circuits the 1079 * pipeline. The pipeline is configured to fail (permission denial) so a real run would 1080 * surface a WP_Error; receiving the short-circuit value proves the bypass. Filter args 1081 * are verified via args-as-guards on the short-circuit value. 1082 * 1083 * @ticket 64989 1084 */ 1085 public function test_pre_execute_ability_filter_short_circuits_pipeline() { 1086 $args = array_merge( 1087 self::$test_ability_properties, 1088 array( 1089 'input_schema' => array( 1090 'type' => 'integer', 1091 'description' => 'Test input integer.', 1092 'required' => true, 1093 ), 1094 'execute_callback' => static function (): int { 1095 return 1; 1096 }, 1097 'permission_callback' => static function (): bool { 1098 return false; 1099 }, 1100 ) 1101 ); 1102 1103 $filter = static function ( $pre, $ability_name, $input, $ability ) { 1104 if ( self::$test_ability_name !== $ability_name || 99 !== $input || ! $ability instanceof WP_Ability ) { 1105 return $pre; 1106 } 1107 return 'short-circuited'; 1108 }; 1109 1110 add_filter( 'wp_pre_execute_ability', $filter, 10, 4 ); 1111 1112 $ability = new WP_Ability( self::$test_ability_name, $args ); 1113 $result = $ability->execute( 99 ); 1114 1115 remove_filter( 'wp_pre_execute_ability', $filter, 10 ); 1116 1117 $this->assertSame( 'short-circuited', $result, 'Short-circuit value bypasses the pipeline; matching args allowed the filter to set it.' ); 1118 } 1119 1120 /** 1121 * Tests that returning the default value from wp_pre_execute_ability lets the pipeline run. 1122 * 1123 * @ticket 64989 1124 */ 1125 public function test_pre_execute_ability_filter_default_value_runs_pipeline() { 1126 $args = array_merge( 1127 self::$test_ability_properties, 1128 array( 1129 'execute_callback' => static function (): int { 1130 return 5; 1131 }, 1132 ) 1133 ); 1134 1135 $filter = static function ( $pre ) { 1136 return $pre; 1137 }; 1138 1139 add_filter( 'wp_pre_execute_ability', $filter ); 1140 1141 $ability = new WP_Ability( self::$test_ability_name, $args ); 1142 $result = $ability->execute(); 1143 1144 remove_filter( 'wp_pre_execute_ability', $filter ); 1145 1146 $this->assertSame( 5, $result, 'Pipeline should run and return the execute_callback value when filter returns the default value.' ); 1147 } 1148 1149 /** 1150 * Tests that returning null explicitly from wp_pre_execute_ability short-circuits with null. 1151 * 1152 * @ticket 64989 1153 */ 1154 public function test_pre_execute_ability_filter_null_short_circuits() { 1155 $args = array_merge( 1156 self::$test_ability_properties, 1157 array( 1158 'execute_callback' => static function (): int { 1159 return 1; 1160 }, 1161 'permission_callback' => static function (): bool { 1162 return false; 1163 }, 1164 ) 1165 ); 1166 1167 $filter = static function () { 1168 return null; 1169 }; 1170 1171 add_filter( 'wp_pre_execute_ability', $filter ); 1172 1173 $ability = new WP_Ability( self::$test_ability_name, $args ); 1174 $result = $ability->execute(); 1175 1176 remove_filter( 'wp_pre_execute_ability', $filter ); 1177 1178 $this->assertNull( $result, 'Null from filter should be returned as-is and bypass the pipeline.' ); 1179 } 1180 1181 /** 1182 * Tests that returning a freshly constructed object from wp_pre_execute_ability is treated as a 1183 * short-circuit value, not confused with the WP_Filter_Sentinel default. This proves the 1184 * sentinel disambiguates arbitrary object returns. 1185 * 1186 * @ticket 64989 1187 */ 1188 public function test_pre_execute_ability_filter_object_short_circuits() { 1189 $args = array_merge( 1190 self::$test_ability_properties, 1191 array( 1192 'execute_callback' => static function (): int { 1193 return 1; 1194 }, 1195 'permission_callback' => static function (): bool { 1196 return false; 1197 }, 1198 ) 1199 ); 1200 1201 $envelope = (object) array( 'status' => 'approval_pending' ); 1202 $filter = static function () use ( $envelope ) { 1203 return $envelope; 1204 }; 1205 1206 add_filter( 'wp_pre_execute_ability', $filter ); 1207 1208 $ability = new WP_Ability( self::$test_ability_name, $args ); 1209 $result = $ability->execute(); 1210 1211 remove_filter( 'wp_pre_execute_ability', $filter ); 1212 1213 $this->assertSame( $envelope, $result, 'Object from filter is returned as-is; WP_Filter_Sentinel keeps it distinct from the default.' ); 1214 } 1215 1216 /** 1217 * Tests that returning a WP_Error from wp_pre_execute_ability short-circuits with the error. 1218 * 1219 * @ticket 64989 1220 */ 1221 public function test_pre_execute_ability_filter_wp_error_short_circuits() { 1222 $args = array_merge( 1223 self::$test_ability_properties, 1224 array( 1225 'execute_callback' => static function (): int { 1226 return 1; 1227 }, 1228 ) 1229 ); 1230 1231 $filter = static function () { 1232 return new WP_Error( 'pre_short_circuit', 'Cached error.' ); 1233 }; 1234 1235 add_filter( 'wp_pre_execute_ability', $filter ); 1236 1237 $ability = new WP_Ability( self::$test_ability_name, $args ); 1238 $result = $ability->execute(); 1239 1240 remove_filter( 'wp_pre_execute_ability', $filter ); 1241 1242 $this->assertInstanceOf( WP_Error::class, $result, 'WP_Error from filter should be returned as-is.' ); 1243 $this->assertSame( 'pre_short_circuit', $result->get_error_code() ); 1244 } 1245 1246 /** 1247 * Tests that the wp_ability_execute_result filter can transform the result, with all 1248 * filter args verified via args-as-guards. 1249 * 1250 * @ticket 64989 1251 */ 1252 public function test_execute_result_filter_can_transform_result() { 1253 $args = array_merge( 1254 self::$test_ability_properties, 1255 array( 1256 'input_schema' => array( 1257 'type' => 'integer', 1258 'description' => 'Test input integer.', 1259 'required' => true, 1260 ), 1261 'execute_callback' => static function ( int $input ): int { 1262 return $input * 2; 1263 }, 1264 ) 1265 ); 1266 1267 $filter = static function ( $result, $ability_name, $input, $ability ) { 1268 if ( 10 !== $result ) { 1269 return $result; 1270 } 1271 if ( self::$test_ability_name !== $ability_name || 5 !== $input || ! $ability instanceof WP_Ability ) { 1272 return $result; 1273 } 1274 return 99; 1275 }; 1276 1277 add_filter( 'wp_ability_execute_result', $filter, 10, 4 ); 1278 1279 $ability = new WP_Ability( self::$test_ability_name, $args ); 1280 $result = $ability->execute( 5 ); 1281 1282 remove_filter( 'wp_ability_execute_result', $filter, 10 ); 1283 1284 $this->assertSame( 99, $result, 'Filter received expected args and transformed the result.' ); 1285 } 1286 1287 /** 1288 * Tests that the wp_ability_execute_result filter can repair an invalid execute result so 1289 * output validation passes — also proves the filter runs before output validation. 1290 * 1291 * @ticket 64989 1292 */ 1293 public function test_execute_result_filter_can_fix_invalid_output() { 1294 $args = array_merge( 1295 self::$test_ability_properties, 1296 array( 1297 'execute_callback' => static function (): string { 1298 return 'not-a-number'; 1299 }, 1300 ) 1301 ); 1302 1303 $filter = static function () { 1304 return 42; 1305 }; 1306 1307 add_filter( 'wp_ability_execute_result', $filter ); 1308 1309 $ability = new WP_Ability( self::$test_ability_name, $args ); 1310 $result = $ability->execute(); 1311 1312 remove_filter( 'wp_ability_execute_result', $filter ); 1313 1314 $this->assertSame( 42, $result, 'Filter should repair invalid output before validation runs.' ); 1315 } 1316 1317 /** 1318 * Tests that the wp_ability_execute_result filter runs before the wp_after_execute_ability action. 1319 * 1320 * @ticket 64989 1321 */ 1322 public function test_execute_result_filter_runs_before_after_execute_action() { 1323 $order = array(); 1324 1325 $args = array_merge( 1326 self::$test_ability_properties, 1327 array( 1328 'execute_callback' => static function (): int { 1329 return 1; 1330 }, 1331 ) 1332 ); 1333 1334 $filter = static function ( $result ) use ( &$order ) { 1335 $order[] = 'filter'; 1336 return $result; 1337 }; 1338 1339 $action = static function () use ( &$order ) { 1340 $order[] = 'action'; 1341 }; 1342 1343 add_filter( 'wp_ability_execute_result', $filter ); 1344 add_action( 'wp_after_execute_ability', $action ); 1345 1346 $ability = new WP_Ability( self::$test_ability_name, $args ); 1347 $ability->execute(); 1348 1349 remove_filter( 'wp_ability_execute_result', $filter ); 1350 remove_action( 'wp_after_execute_ability', $action ); 1351 1352 $this->assertSame( array( 'filter', 'action' ), $order, 'execute_result filter must run before wp_after_execute_ability action.' ); 1353 } 1354 1355 /** 1356 * Tests that the wp_ability_execute_result filter receives a WP_Error from the execute 1357 * callback and can pass it through (verified via args-as-guards on the WP_Error code). 1358 * 1359 * @ticket 64989 1360 */ 1361 public function test_execute_result_filter_receives_wp_error_from_do_execute() { 1362 $args = array_merge( 1363 self::$test_ability_properties, 1364 array( 1365 'execute_callback' => static function () { 1366 return new WP_Error( 'execute_failed', 'Something went wrong.' ); 1367 }, 1368 ) 1369 ); 1370 1371 $filter = static function ( $result ) { 1372 if ( ! is_wp_error( $result ) || 'execute_failed' !== $result->get_error_code() ) { 1373 return new WP_Error( 'unexpected_input' ); 1374 } 1375 return $result; 1376 }; 1377 1378 add_filter( 'wp_ability_execute_result', $filter ); 1379 1380 $ability = new WP_Ability( self::$test_ability_name, $args ); 1381 $result = $ability->execute(); 1382 1383 remove_filter( 'wp_ability_execute_result', $filter ); 1384 1385 $this->assertInstanceOf( WP_Error::class, $result ); 1386 $this->assertSame( 'execute_failed', $result->get_error_code(), 'Filter saw the expected WP_Error and passed it through.' ); 1387 } 829 1388 } -
trunk/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php
r62094 r62397 904 904 905 905 /** 906 * Tests that a normalization filter error defaults to a 400 REST response. 907 * 908 * @ticket 64989 909 */ 910 public function test_normalize_input_filter_error_defaults_to_bad_request_status(): void { 911 $filter = static function ( $input ) { 912 return new WP_Error( 'normalize_rejected', 'Rejected input.' ); 913 }; 914 915 add_filter( 'wp_ability_normalize_input', $filter ); 916 917 $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/calculator/run' ); 918 $request->set_header( 'Content-Type', 'application/json' ); 919 $request->set_body( 920 wp_json_encode( 921 array( 922 'input' => array( 923 'a' => 5, 924 'b' => 3, 925 ), 926 ) 927 ) 928 ); 929 930 $response = $this->server->dispatch( $request ); 931 932 remove_filter( 'wp_ability_normalize_input', $filter ); 933 934 $this->assertSame( 400, $response->get_status() ); 935 $data = $response->get_data(); 936 $this->assertSame( 'normalize_rejected', $data['code'] ); 937 $this->assertSame( 'Rejected input.', $data['message'] ); 938 } 939 940 /** 941 * Tests that a normalization filter error with custom status keeps that status. 942 * 943 * @ticket 64989 944 */ 945 public function test_normalize_input_filter_error_preserves_custom_status(): void { 946 $filter = static function ( $input ) { 947 return new WP_Error( 948 'normalize_unprocessable', 949 'Input cannot be normalized.', 950 array( 'status' => 422 ) 951 ); 952 }; 953 954 add_filter( 'wp_ability_normalize_input', $filter ); 955 956 $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/calculator/run' ); 957 $request->set_header( 'Content-Type', 'application/json' ); 958 $request->set_body( 959 wp_json_encode( 960 array( 961 'input' => array( 962 'a' => 5, 963 'b' => 3, 964 ), 965 ) 966 ) 967 ); 968 969 $response = $this->server->dispatch( $request ); 970 971 remove_filter( 'wp_ability_normalize_input', $filter ); 972 973 $this->assertSame( 422, $response->get_status() ); 974 $data = $response->get_data(); 975 $this->assertSame( 'normalize_unprocessable', $data['code'] ); 976 $this->assertSame( 'Input cannot be normalized.', $data['message'] ); 977 } 978 979 /** 906 980 * Test ability without annotations defaults to POST method. 907 981 *
Note: See TracChangeset
for help on using the changeset viewer.