| @@ -16,12 +16,19 @@ | ||
| 16 | 16 | use JFB_Components\Module\Base_Module_Handle_Trait; |
| 17 | 17 | use JFB_Components\Module\Base_Module_It; |
| 18 | 18 | use JFB_Components\Module\Base_Module_Url_It; |
| 19 | 19 | use JFB_Components\Module\Base_Module_Url_Trait; |
| 20 | +use Jet_Form_Builder\Admin\Pages\Pages_Manager; | |
| 21 | +use Jet_Form_Builder\Admin\Tabs_Handlers\Ssr_Callbacks_Handler; | |
| 22 | +use Jet_Form_Builder\Admin\Tabs_Handlers\Tab_Handler_Manager; | |
| 20 | 23 | use JFB_Modules\Block_Parsers\Field_Data_Parser; |
| 24 | +use JFB_Modules\Validation\Advanced_Rules\Server_Side_Rule; | |
| 25 | +use JFB_Modules\Validation\Advanced_Rules\Ssr_Callback_Registry; | |
| 21 | 26 | use JFB_Modules\Validation\Class_Validation_Handlers; |
| 22 | 27 | use JFB_Modules\Validation\Handlers\Validation_Handler; |
| 23 | 28 | use JFB_Modules\Validation\Rest_Api\Rest_Validation_Endpoint; |
| 29 | +use JFB_Modules\Validation\Ssr\Ssr_Blocked_Callback_Usages; | |
| 30 | +use JFB_Modules\Validation\Ssr\Ssr_Registry_Migration_Notice; | |
| 24 | 31 | |
| 25 | 32 | // If this file is called directly, abort. |
| 26 | 33 | if ( ! defined( 'WPINC' ) ) { |
| 27 | 34 | die; |
| @@ -48,8 +55,12 @@ | ||
| 48 | 55 | */ |
| 49 | 56 | private $rules; |
| 50 | 57 | private $settings; |
| 51 | 58 | private $inline_messages = array(); |
| 59 | + /** | |
| 60 | + * @var Ssr_Registry_Migration_Notice | |
| 61 | + */ | |
| 62 | + private $ssr_registry_migration_notice; | |
| 52 | 63 | |
| 53 | 64 | public function rep_item_id() { |
| 54 | 65 | return 'validation'; |
| 55 | 66 | } |
| @@ -76,8 +87,10 @@ | ||
| 76 | 87 | |
| 77 | 88 | /** @var \JFB_Modules\Rest_Api\Module $rest_api */ |
| 78 | 89 | $rest_api = jet_form_builder()->module( 'rest-api' ); |
| 79 | 90 | $rest_api->get_controller()->install( new Rest_Api\Rest_Validation_Endpoint() ); |
| 91 | + | |
| 92 | + Tab_Handler_Manager::instance()->install( new Ssr_Callbacks_Handler() ); | |
| 80 | 93 | } |
| 81 | 94 | |
| 82 | 95 | /** |
| 83 | 96 | * @throws Repository_Exception |
| @@ -92,8 +105,10 @@ | ||
| 92 | 105 | |
| 93 | 106 | /** @var \JFB_Modules\Rest_Api\Module $rest_api */ |
| 94 | 107 | $rest_api = jet_form_builder()->module( 'rest-api' ); |
| 95 | 108 | $rest_api->get_controller()->uninstall( new Rest_Api\Rest_Validation_Endpoint() ); |
| 109 | + | |
| 110 | + Tab_Handler_Manager::instance()->uninstall( 'ssr-callbacks-tab' ); | |
| 96 | 111 | } |
| 97 | 112 | |
| 98 | 113 | public function init_hooks() { |
| 99 | 114 | add_filter( |
| @@ -125,8 +140,27 @@ | ||
| 125 | 140 | add_action( |
| 126 | 141 | 'jet-form-builder/editor-assets/before', |
| 127 | 142 | array( $this, 'localize_editor_config' ) |
| 128 | 143 | ); |
| 144 | + add_action( | |
| 145 | + 'save_post_jet-form-builder', | |
| 146 | + array( $this, 'refresh_blocked_callback_usages' ) | |
| 147 | + ); | |
| 148 | + add_action( | |
| 149 | + 'delete_post', | |
| 150 | + array( $this, 'remove_blocked_callback_usages_for_deleted_form' ) | |
| 151 | + ); | |
| 152 | + add_action( | |
| 153 | + 'save_post_wp_block', | |
| 154 | + array( $this, 'refresh_blocked_callback_usages_for_referencing_forms' ) | |
| 155 | + ); | |
| 156 | + add_action( | |
| 157 | + 'deleted_post', | |
| 158 | + array( $this, 'refresh_blocked_callback_usages_for_referencing_forms_on_delete' ) | |
| 159 | + ); | |
| 160 | + | |
| 161 | + $this->ssr_registry_migration_notice = new Ssr_Registry_Migration_Notice(); | |
| 162 | + $this->ssr_registry_migration_notice->init_hooks(); | |
| 129 | 163 | } |
| 130 | 164 | |
| 131 | 165 | public function remove_hooks() { |
| 132 | 166 | remove_filter( |
| @@ -157,8 +191,28 @@ | ||
| 157 | 191 | remove_action( |
| 158 | 192 | 'jet-form-builder/editor-assets/before', |
| 159 | 193 | array( $this, 'localize_editor_config' ) |
| 160 | 194 | ); |
| 195 | + remove_action( | |
| 196 | + 'save_post_jet-form-builder', | |
| 197 | + array( $this, 'refresh_blocked_callback_usages' ) | |
| 198 | + ); | |
| 199 | + remove_action( | |
| 200 | + 'delete_post', | |
| 201 | + array( $this, 'remove_blocked_callback_usages_for_deleted_form' ) | |
| 202 | + ); | |
| 203 | + remove_action( | |
| 204 | + 'save_post_wp_block', | |
| 205 | + array( $this, 'refresh_blocked_callback_usages_for_referencing_forms' ) | |
| 206 | + ); | |
| 207 | + remove_action( | |
| 208 | + 'deleted_post', | |
| 209 | + array( $this, 'refresh_blocked_callback_usages_for_referencing_forms_on_delete' ) | |
| 210 | + ); | |
| 211 | + | |
| 212 | + if ( $this->ssr_registry_migration_notice ) { | |
| 213 | + $this->ssr_registry_migration_notice->remove_hooks(); | |
| 214 | + } | |
| 161 | 215 | } |
| 162 | 216 | |
| 163 | 217 | public function register_scripts() { |
| 164 | 218 | $script_asset = require_once jet_form_builder()->plugin_dir( |
| @@ -224,8 +278,301 @@ | ||
| 224 | 278 | |
| 225 | 279 | return $markup; |
| 226 | 280 | } |
| 227 | 281 | |
| 282 | + /** | |
| 283 | + * Keeps `Ssr_Blocked_Callback_Usages` in sync with a form's current content: an admin | |
| 284 | + * who edits a form to fix a denylisted "Server-Side callback" rule (issues-tracker | |
| 285 | + * #20361 follow-up) should see it drop off the "Forms Using Blocked Functions" list | |
| 286 | + * immediately, not have it linger as if nothing changed. Only ever removes/replaces | |
| 287 | + * entries for the saved form's own ID — it never grants trust to anything (a denylisted | |
| 288 | + * name can never be approved regardless of what this records), so it carries none of the | |
| 289 | + * self-service allowlist risk the retired `Ssr_Callback_Allowlist` save-post hooks had. | |
| 290 | + * | |
| 291 | + * @since 3.6.5.3 | |
| 292 | + * | |
| 293 | + * @param int $post_id | |
| 294 | + */ | |
| 295 | + public function refresh_blocked_callback_usages( int $post_id ) { | |
| 296 | + if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) { | |
| 297 | + return; | |
| 298 | + } | |
| 299 | + | |
| 300 | + $post = get_post( $post_id ); | |
| 301 | + | |
| 302 | + if ( ! $post instanceof \WP_Post ) { | |
| 303 | + return; | |
| 304 | + } | |
| 305 | + | |
| 306 | + Ssr_Blocked_Callback_Usages::replace_for_form( | |
| 307 | + $post_id, | |
| 308 | + Ssr_Blocked_Callback_Usages::collect_from_content( $post->post_content ) | |
| 309 | + ); | |
| 310 | + } | |
| 311 | + | |
| 312 | + /** | |
| 313 | + * `refresh_blocked_callback_usages()` only ever runs on `save_post_jet-form-builder`, so | |
| 314 | + * a form that is permanently deleted (not just trashed — `Ssr_Blocked_Callback_Usages` | |
| 315 | + * should still list a trashed form, since it can still be restored) never gets its entry | |
| 316 | + * removed: the settings tab would otherwise keep showing a phantom "Forms Using Blocked | |
| 317 | + * Functions" row with a dead edit link forever (review finding, issues-tracker #20361 | |
| 318 | + * follow-up). `delete_post` fires for every post type, so this checks `post_type` itself | |
| 319 | + * rather than relying on a `delete_post_{post_type}`-style hook, which WordPress does not | |
| 320 | + * provide. | |
| 321 | + * | |
| 322 | + * @since 3.6.5.3 | |
| 323 | + * | |
| 324 | + * @param int $post_id | |
| 325 | + */ | |
| 326 | + public function remove_blocked_callback_usages_for_deleted_form( int $post_id ) { | |
| 327 | + if ( 'jet-form-builder' !== get_post_type( $post_id ) ) { | |
| 328 | + return; | |
| 329 | + } | |
| 330 | + | |
| 331 | + Ssr_Blocked_Callback_Usages::clear_for_form( $post_id ); | |
| 332 | + } | |
| 333 | + | |
| 334 | + /** | |
| 335 | + * `Ssr_Blocked_Callback_Usages::collect_from_content()` expands a form's `core/block` | |
| 336 | + * (reusable block) references to the reusable block's *current* content every time it | |
| 337 | + * runs — but `refresh_blocked_callback_usages()` only runs on `save_post_jet-form- | |
| 338 | + * builder`. Editing a reusable block to add or remove a blocked "Server-Side callback" | |
| 339 | + * rule therefore left every form referencing it stale indefinitely: an admin removing | |
| 340 | + * the blocked rule from a shared reusable block would not see the referencing forms drop | |
| 341 | + * off "Forms Using Blocked Functions" (and, conversely, adding one would not surface a | |
| 342 | + * newly-broken form) until each was individually resaved (review finding, issues-tracker | |
| 343 | + * #20361 follow-up). Hooked to `save_post_wp_block`; re-runs `refresh_blocked_callback_ | |
| 344 | + * usages()` for every `jet-form-builder` form whose `post_content` references this | |
| 345 | + * reusable block's ID, so its current state is reflected without waiting on those forms | |
| 346 | + * to be resaved themselves. | |
| 347 | + * | |
| 348 | + * @since 3.6.5.3 | |
| 349 | + * | |
| 350 | + * @param int $reusable_block_id | |
| 351 | + */ | |
| 352 | + public function refresh_blocked_callback_usages_for_referencing_forms( int $reusable_block_id ) { | |
| 353 | + if ( wp_is_post_autosave( $reusable_block_id ) || wp_is_post_revision( $reusable_block_id ) ) { | |
| 354 | + return; | |
| 355 | + } | |
| 356 | + | |
| 357 | + foreach ( $this->find_forms_referencing_reusable_block( $reusable_block_id ) as $form_id ) { | |
| 358 | + $this->refresh_blocked_callback_usages( $form_id ); | |
| 359 | + } | |
| 360 | + } | |
| 361 | + | |
| 362 | + /** | |
| 363 | + * Same gap as `refresh_blocked_callback_usages_for_referencing_forms()`, for the other | |
| 364 | + * way a reusable block's contribution can change: being permanently deleted. | |
| 365 | + * | |
| 366 | + * Hooked to `deleted_post` (fires immediately *after* the row is actually removed from | |
| 367 | + * `wp_posts`), not `delete_post` (fires while the row still exists, before the DB | |
| 368 | + * `DELETE`): re-running the refresh on `delete_post` would still resolve the reusable | |
| 369 | + * block being deleted via `get_post()` to its old, still-present content, writing back | |
| 370 | + * the exact same blocked usages that are about to become stale — and since nothing else | |
| 371 | + * refreshes after the row is actually gone, the referencing forms would then be stuck | |
| 372 | + * listing usages from a reusable block that no longer exists, indefinitely (review | |
| 373 | + * finding, issues-tracker #20361 follow-up). | |
| 374 | + * | |
| 375 | + * `get_post_type( $post_id )` cannot be used to filter by type here the way | |
| 376 | + * `remove_blocked_callback_usages_for_deleted_form()` does on `delete_post` above — by | |
| 377 | + * `deleted_post` the row (and its cache entry) is already gone, so that lookup would | |
| 378 | + * always return `false`. `deleted_post` passes the now-deleted `WP_Post` object itself as | |
| 379 | + * its second argument, which is the only remaining way to recover its `post_type`. | |
| 380 | + * | |
| 381 | + * @since 3.6.5.3 | |
| 382 | + * | |
| 383 | + * @param int $post_id | |
| 384 | + * @param \WP_Post|null $post | |
| 385 | + */ | |
| 386 | + public function refresh_blocked_callback_usages_for_referencing_forms_on_delete( int $post_id, $post = null ) { | |
| 387 | + if ( ! $post instanceof \WP_Post || 'wp_block' !== $post->post_type ) { | |
| 388 | + return; | |
| 389 | + } | |
| 390 | + | |
| 391 | + $this->refresh_blocked_callback_usages_for_referencing_forms( $post_id ); | |
| 392 | + } | |
| 393 | + | |
| 394 | + /** | |
| 395 | + * Finds every `jet-form-builder` form whose `post_content` references the given reusable | |
| 396 | + * block ID, directly or through a chain of nested reusable blocks (form → reusable block | |
| 397 | + * A → reusable block B, where B is `$reusable_block_id`). | |
| 398 | + * | |
| 399 | + * A single `LIKE '%"ref":<id>%'` scan of *forms* only would miss that chain: B's ref | |
| 400 | + * never appears in the form's own `post_content` at all — only inside A's, which is a | |
| 401 | + * separate `wp_block` post the form's content has no reason to mention (review finding, | |
| 402 | + * issues-tracker #20361 follow-up). This therefore first transitively finds every | |
| 403 | + * `wp_block` post that references `$reusable_block_id`, directly or through further | |
| 404 | + * nesting, then scans forms for a reference to `$reusable_block_id` itself OR to any | |
| 405 | + * reusable block found in that chain. | |
| 406 | + * | |
| 407 | + * Each SQL pass is a `LIKE` scan for the ref ID as a literal string — cheap compared to | |
| 408 | + * running the full block parser/`Block_Helper::get_blocks_from_content()` walk (which | |
| 409 | + * resolves every nested reusable block via a `get_post()` call each) against every row | |
| 410 | + * just to check for a reference. It can return false positives (e.g. the digits appear | |
| 411 | + * inside an unrelated attribute value) but never false negatives, since a real | |
| 412 | + * `core/block` reference always serializes its `ref` as `"ref":<id>` in the block | |
| 413 | + * comment; each candidate form is confirmed with the real parser before being included, | |
| 414 | + * so a false positive only costs one extra, already-necessary parse rather than a wrong | |
| 415 | + * result. | |
| 416 | + * | |
| 417 | + * @since 3.6.5.3 | |
| 418 | + * | |
| 419 | + * @param int $reusable_block_id | |
| 420 | + * | |
| 421 | + * @return int[] | |
| 422 | + */ | |
| 423 | + private function find_forms_referencing_reusable_block( int $reusable_block_id ): array { | |
| 424 | + if ( $reusable_block_id <= 0 ) { | |
| 425 | + return array(); | |
| 426 | + } | |
| 427 | + | |
| 428 | + $relevant_block_ids = $this->find_reusable_blocks_referencing( array( $reusable_block_id ) ); | |
| 429 | + $relevant_block_ids[ $reusable_block_id ] = true; | |
| 430 | + | |
| 431 | + $candidate_ids = $this->find_posts_with_any_ref( 'jet-form-builder', array_keys( $relevant_block_ids ) ); | |
| 432 | + | |
| 433 | + $referencing = array(); | |
| 434 | + | |
| 435 | + foreach ( $candidate_ids as $candidate_id ) { | |
| 436 | + $post = get_post( $candidate_id ); | |
| 437 | + | |
| 438 | + if ( ! $post instanceof \WP_Post ) { | |
| 439 | + continue; | |
| 440 | + } | |
| 441 | + | |
| 442 | + if ( $this->content_references_any_reusable_block( $post->post_content, $relevant_block_ids ) ) { | |
| 443 | + $referencing[] = $candidate_id; | |
| 444 | + } | |
| 445 | + } | |
| 446 | + | |
| 447 | + return $referencing; | |
| 448 | + } | |
| 449 | + | |
| 450 | + /** | |
| 451 | + * Transitively finds every `wp_block` post that references any ID in `$target_ids`, | |
| 452 | + * directly or through further nesting (C → B → A, where A is one of `$target_ids`, also | |
| 453 | + * returns C). Breadth-first over `LIKE` scans, one level of nesting per iteration; a | |
| 454 | + * `$visited` set prevents infinite recursion on a reference cycle (which `Block_Helper:: | |
| 455 | + * walk_by_reusable()` also guards against when actually expanding content, for the same | |
| 456 | + * reason). | |
| 457 | + * | |
| 458 | + * @since 3.6.5.3 | |
| 459 | + * | |
| 460 | + * @param int[] $target_ids | |
| 461 | + * | |
| 462 | + * @return array<int, true> Keyed by post ID for fast membership checks. | |
| 463 | + */ | |
| 464 | + private function find_reusable_blocks_referencing( array $target_ids ): array { | |
| 465 | + $visited = array(); | |
| 466 | + $frontier = $target_ids; | |
| 467 | + | |
| 468 | + while ( ! empty( $frontier ) ) { | |
| 469 | + $found = $this->find_posts_with_any_ref( 'wp_block', $frontier ); | |
| 470 | + | |
| 471 | + $frontier = array(); | |
| 472 | + | |
| 473 | + foreach ( $found as $post_id ) { | |
| 474 | + if ( isset( $visited[ $post_id ] ) ) { | |
| 475 | + continue; | |
| 476 | + } | |
| 477 | + | |
| 478 | + $visited[ $post_id ] = true; | |
| 479 | + $frontier[] = $post_id; | |
| 480 | + } | |
| 481 | + } | |
| 482 | + | |
| 483 | + return $visited; | |
| 484 | + } | |
| 485 | + | |
| 486 | + /** | |
| 487 | + * `LIKE`-scans `$post_type` posts for a `core/block` ref to any ID in `$ref_ids`, | |
| 488 | + * returning post IDs (not yet parser-confirmed — see `find_forms_referencing_reusable_ | |
| 489 | + * block()`'s docblock on why the `LIKE` scan alone is sufficient to build the candidate | |
| 490 | + * set without false negatives). | |
| 491 | + * | |
| 492 | + * @since 3.6.5.3 | |
| 493 | + * | |
| 494 | + * @param string $post_type | |
| 495 | + * @param int[] $ref_ids | |
| 496 | + * | |
| 497 | + * @return int[] | |
| 498 | + */ | |
| 499 | + private function find_posts_with_any_ref( string $post_type, array $ref_ids ): array { | |
| 500 | + global $wpdb; | |
| 501 | + | |
| 502 | + $ref_ids = array_values( array_unique( array_filter( array_map( 'intval', $ref_ids ) ) ) ); | |
| 503 | + | |
| 504 | + if ( empty( $ref_ids ) ) { | |
| 505 | + return array(); | |
| 506 | + } | |
| 507 | + | |
| 508 | + // Placeholder count varies with count( $ref_ids ), so the query string cannot be a | |
| 509 | + // single literal passed straight to $wpdb->prepare() the way PHPCS's WordPress.DB | |
| 510 | + // sniffs expect; both are still fully parameterized (every value — post type and | |
| 511 | + // every LIKE pattern — goes through prepare()'s own %s placeholders, none of them | |
| 512 | + // concatenated directly into the SQL string), so this is safe despite the sniffs | |
| 513 | + // not being able to verify a dynamically-built placeholder list. | |
| 514 | + $like_clauses = array_fill( 0, count( $ref_ids ), 'post_content LIKE %s' ); | |
| 515 | + $like_values = array(); | |
| 516 | + | |
| 517 | + foreach ( $ref_ids as $ref_id ) { | |
| 518 | + $like_values[] = '%' . $wpdb->esc_like( '"ref":' . $ref_id ) . '%'; | |
| 519 | + } | |
| 520 | + | |
| 521 | + $sql = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare | |
| 522 | + "SELECT ID FROM {$wpdb->posts} | |
| 523 | + WHERE post_type = %s | |
| 524 | + AND post_status IN ( 'publish', 'draft', 'pending', 'private', 'future', 'trash' ) | |
| 525 | + AND ( " . implode( ' OR ', $like_clauses ) . ' )', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared | |
| 526 | + array_merge( array( $post_type ), $like_values ) | |
| 527 | + ); | |
| 528 | + | |
| 529 | + $ids = $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching | |
| 530 | + | |
| 531 | + return array_map( 'intval', $ids ); | |
| 532 | + } | |
| 533 | + | |
| 534 | + /** | |
| 535 | + * Confirms a `LIKE`-matched candidate actually references any ID in `$reusable_block_ids` | |
| 536 | + * via a real `core/block` block, by walking the real parsed block tree rather than | |
| 537 | + * trusting the SQL `LIKE` match's text position. Reuses `Block_Helper:: | |
| 538 | + * get_blocks_from_content()`'s existing reusable-block-expanding walk (the same one | |
| 539 | + * `Ssr_Blocked_Callback_Usages::collect_from_content()` relies on) rather than | |
| 540 | + * re-implementing block-tree traversal here, so the two can never drift on what counts | |
| 541 | + * as "references." | |
| 542 | + * | |
| 543 | + * @since 3.6.5.3 | |
| 544 | + * | |
| 545 | + * @param array<int, true> $reusable_block_ids | |
| 546 | + */ | |
| 547 | + private function content_references_any_reusable_block( string $post_content, array $reusable_block_ids ): bool { | |
| 548 | + foreach ( $this->flatten_blocks( \Jet_Form_Builder\Blocks\Block_Helper::get_blocks_from_content( $post_content ) ) as $block ) { | |
| 549 | + if ( | |
| 550 | + 'core/block' === ( $block['blockName'] ?? '' ) && | |
| 551 | + isset( $reusable_block_ids[ (int) ( $block['attrs']['ref'] ?? 0 ) ] ) | |
| 552 | + ) { | |
| 553 | + return true; | |
| 554 | + } | |
| 555 | + } | |
| 556 | + | |
| 557 | + return false; | |
| 558 | + } | |
| 559 | + | |
| 560 | + /** | |
| 561 | + * @param array[] $blocks | |
| 562 | + * | |
| 563 | + * @return \Generator<array> | |
| 564 | + */ | |
| 565 | + private function flatten_blocks( array $blocks ): \Generator { | |
| 566 | + foreach ( $blocks as $block ) { | |
| 567 | + yield $block; | |
| 568 | + | |
| 569 | + if ( ! empty( $block['innerBlocks'] ) ) { | |
| 570 | + yield from $this->flatten_blocks( $block['innerBlocks'] ); | |
| 571 | + } | |
| 572 | + } | |
| 573 | + } | |
| 574 | + | |
| 228 | 575 | public function add_validation_block( Base $block ) { |
| 229 | 576 | /** |
| 230 | 577 | * If in post meta enable Advanced validation |
| 231 | 578 | * or right in block settings |
| @@ -258,20 +605,48 @@ | ||
| 258 | 605 | : $field_name; |
| 259 | 606 | |
| 260 | 607 | foreach ( $rules as $index => &$rule ) { |
| 261 | 608 | if ( 'ssr' === ( $rule['type'] ?? '' ) ) { |
| 262 | - $rule['_sig'] = Rest_Validation_Endpoint::generate_signature( | |
| 609 | + $signature = Rest_Validation_Endpoint::generate_signature( | |
| 263 | 610 | (int) $form_id, |
| 264 | 611 | $signature_path, |
| 265 | 612 | (int) $index |
| 266 | 613 | ); |
| 614 | + $signature_key = Validation_Handler::get_signature_key( $signature_path, (int) $index ); | |
| 267 | 615 | |
| 268 | 616 | printf( |
| 269 | 617 | '<input type="hidden" name="%1$s[%2$s]" value="%3$s" />', |
| 270 | 618 | esc_attr( Validation_Handler::MAIN_SIGNATURES_KEY ), |
| 271 | - esc_attr( Validation_Handler::get_signature_key( $signature_path, (int) $index ) ), | |
| 272 | - esc_attr( $rule['_sig'] ) | |
| 619 | + esc_attr( $signature_key ), | |
| 620 | + esc_attr( $signature ) | |
| 273 | 621 | ); |
| 622 | + | |
| 623 | + // Security: only the lookup key is exposed here, never the signature | |
| 624 | + // itself — the signature already lives in the hidden input above, | |
| 625 | + // and JS reads it from there instead of duplicating it in this | |
| 626 | + // public JSON blob (https://github.com/Crocoblock/issues-tracker/issues/20361). | |
| 627 | + $rule['_sig_key'] = $signature_key; | |
| 628 | + | |
| 629 | + // Security: replace a custom callback's clean name with a stable | |
| 630 | + // opaque ID in this public JSON blob only. The form itself (and | |
| 631 | + // $block->block_attrs) still stores the clean name — the server | |
| 632 | + // always resolves the callback from there, never from this ID or | |
| 633 | + // from the request. Built-ins and the fixed-safe WP Core list are | |
| 634 | + // safe by construction and are left in the clear. | |
| 635 | + $callback_name = (string) ( $rule['value'] ?? '' ); | |
| 636 | + | |
| 637 | + // PHP function names are case-insensitive, and is_builtin_callback() | |
| 638 | + // compares against lowercase IDs — lowercase here too (matching the | |
| 639 | + // FIXED_SAFE check just below) so a builtin saved with non-canonical | |
| 640 | + // casing is still recognized as built-in and left in the clear, rather | |
| 641 | + // than being needlessly masked behind an opaque registry ID. | |
| 642 | + if ( | |
| 643 | + '' !== $callback_name | |
| 644 | + && ! Server_Side_Rule::is_builtin_callback( strtolower( $callback_name ) ) | |
| 645 | + && ! in_array( strtolower( $callback_name ), Server_Side_Rule::FIXED_SAFE, true ) | |
| 646 | + ) { | |
| 647 | + $rule['value'] = Ssr_Callback_Registry::id_for_callback( $callback_name ); | |
| 648 | + } | |
| 274 | 649 | } |
| 275 | 650 | } |
| 276 | 651 | unset( $rule ); |
| 277 | 652 | |
| @@ -319,11 +694,17 @@ | ||
| 319 | 694 | |
| 320 | 695 | public function is_advanced( array $block_attrs ): bool { |
| 321 | 696 | $type = $block_attrs['validation']['type'] ?? ''; |
| 322 | 697 | |
| 323 | - return $type | |
| 324 | - ? self::FORMAT_ADVANCED === $type | |
| 325 | - : $this->is_advanced_form(); | |
| 698 | + if ( self::FORMAT_ADVANCED === $type ) { | |
| 699 | + return true; | |
| 700 | + } | |
| 701 | + | |
| 702 | + if ( '' === $type || 'inherit' === $type ) { | |
| 703 | + return $this->is_advanced_form(); | |
| 704 | + } | |
| 705 | + | |
| 706 | + return false; | |
| 326 | 707 | } |
| 327 | 708 | |
| 328 | 709 | public function is_advanced_form(): bool { |
| 329 | 710 | if ( is_null( $this->settings ) ) { |
| @@ -350,9 +731,9 @@ | ||
| 350 | 731 | |
| 351 | 732 | public function validate_block( Field_Data_Parser $parser ) { |
| 352 | 733 | if ( |
| 353 | 734 | ! $this->is_advanced( $parser->get_settings() ) || |
| 354 | - ! $parser->get_value() || | |
| 735 | + Tools::is_empty( $parser->get_value() ) || | |
| 355 | 736 | $parser->is_inside_conditional() |
| 356 | 737 | ) { |
| 357 | 738 | return; |
| 358 | 739 | } |
| @@ -364,12 +745,13 @@ | ||
| 364 | 745 | wp_localize_script( |
| 365 | 746 | Editor::EDITOR_PACKAGE_HANDLE, |
| 366 | 747 | 'jetFormValidation', |
| 367 | 748 | array( |
| 368 | - 'messages' => Array_Tools::to_array( $this->get_messages() ), | |
| 369 | - 'ssr_callbacks' => Array_Tools::to_array( $this->get_rules()->get_ssr()->get_callbacks() ), | |
| 370 | - 'formats' => $this->formats(), | |
| 371 | - 'rule_types' => Array_Tools::to_array( $this->get_rules()->rep_get_values() ), | |
| 749 | + 'messages' => Array_Tools::to_array( $this->get_messages() ), | |
| 750 | + 'ssr_callbacks' => Array_Tools::to_array( $this->get_rules()->get_ssr()->get_callbacks() ), | |
| 751 | + 'formats' => $this->formats(), | |
| 752 | + 'rule_types' => Array_Tools::to_array( $this->get_rules()->rep_get_values() ), | |
| 753 | + 'ssr_callbacks_settings_url' => Pages_Manager::instance()->get_stable_url( 'jfb-settings' ) . '#ssr-callbacks-tab', | |
| 372 | 754 | ) |
| 373 | 755 | ); |
| 374 | 756 | } |
| 375 | 757 | |