Changeset 62198
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php
r61839 r62198 39 39 40 40 /** 41 * Maximum total size (in bytes) of the request body. 42 * 43 * @since 7.0.0 44 * @var int 45 */ 46 const MAX_BODY_SIZE = 16 * MB_IN_BYTES; 47 48 /** 49 * Maximum number of rooms allowed per request. 50 * 51 * @since 7.0.0 52 * @var int 53 */ 54 const MAX_ROOMS_PER_REQUEST = 50; 55 56 /** 57 * Maximum length of a single update data string. 58 * 59 * @since 7.0.0 60 * @var int 61 */ 62 const MAX_UPDATE_DATA_SIZE = MB_IN_BYTES; 63 64 /** 41 65 * Sync update type: compaction. 42 66 * … … 97 121 'properties' => array( 98 122 'data' => array( 99 'type' => 'string', 100 'required' => true, 123 'type' => 'string', 124 'required' => true, 125 'maxLength' => self::MAX_UPDATE_DATA_SIZE, 101 126 ), 102 127 'type' => array( … … 150 175 'callback' => array( $this, 'handle_request' ), 151 176 'permission_callback' => array( $this, 'check_permissions' ), 177 'validate_callback' => array( $this, 'validate_request' ), 152 178 'args' => array( 153 179 'rooms' => array( … … 156 182 'type' => 'object', 157 183 ), 184 'maxItems' => self::MAX_ROOMS_PER_REQUEST, 158 185 'required' => true, 159 186 'type' => 'array', … … 225 252 226 253 /** 254 * Validates that the request body does not exceed the maximum allowed size. 255 * 256 * Runs as the route-level validate_callback, after per-arg schema 257 * validation has already passed. 258 * 259 * @since 7.0.0 260 * 261 * @param WP_REST_Request $request The REST request. 262 * @return true|WP_Error True if valid, WP_Error if the body is too large. 263 */ 264 public function validate_request( WP_REST_Request $request ) { 265 $body = $request->get_body(); 266 if ( is_string( $body ) && strlen( $body ) > self::MAX_BODY_SIZE ) { 267 return new WP_Error( 268 'rest_sync_body_too_large', 269 __( 'Request body is too large.' ), 270 array( 'status' => 413 ) 271 ); 272 } 273 274 return true; 275 } 276 277 /** 227 278 * Handles request: stores sync updates and awareness data, and returns 228 279 * updates the client is missing. … … 279 330 * @param string $entity_kind The entity kind, e.g. 'postType', 'taxonomy', 'root'. 280 331 * @param string $entity_name The entity name, e.g. 'post', 'category', 'site'. 281 * @param string|null $object_id The object ID / entity key for single entities, null for collections.332 * @param string|null $object_id The numeric object ID / entity key for single entities, null for collections. 282 333 * @return bool True if user has permission, otherwise false. 283 334 */ 284 335 private function can_user_sync_entity_type( string $entity_kind, string $entity_name, ?string $object_id ): bool { 285 // Handle single post type entities with a defined object ID. 286 if ( 'postType' === $entity_kind && is_numeric( $object_id ) ) { 287 return current_user_can( 'edit_post', (int) $object_id ); 288 } 289 290 // Handle single taxonomy term entities with a defined object ID. 291 if ( 'taxonomy' === $entity_kind && is_numeric( $object_id ) ) { 292 $taxonomy = get_taxonomy( $entity_name ); 293 return isset( $taxonomy->cap->assign_terms ) && current_user_can( $taxonomy->cap->assign_terms ); 294 } 295 296 // Handle single comment entities with a defined object ID. 297 if ( 'root' === $entity_kind && 'comment' === $entity_name && is_numeric( $object_id ) ) { 298 return current_user_can( 'edit_comment', (int) $object_id ); 336 if ( is_string( $object_id ) ) { 337 if ( ! ctype_digit( $object_id ) ) { 338 return false; 339 } 340 $object_id = (int) $object_id; 341 } 342 if ( null !== $object_id && $object_id <= 0 ) { 343 // Object ID must be numeric if provided. 344 return false; 345 } 346 347 // Validate permissions for the provided object ID. 348 if ( is_int( $object_id ) ) { 349 // Handle single post type entities with a defined object ID. 350 if ( 'postType' === $entity_kind ) { 351 if ( get_post_type( $object_id ) !== $entity_name ) { 352 // Post is not of the specified post type. 353 return false; 354 } 355 return current_user_can( 'edit_post', $object_id ); 356 } 357 358 // Handle single taxonomy term entities with a defined object ID. 359 if ( 'taxonomy' === $entity_kind ) { 360 $term_exists = term_exists( $object_id, $entity_name ); 361 if ( ! is_array( $term_exists ) || ! isset( $term_exists['term_id'] ) ) { 362 // Either term doesn't exist OR term is not in specified taxonomy. 363 return false; 364 } 365 366 return current_user_can( 'edit_term', $object_id ); 367 } 368 369 // Handle single comment entities with a defined object ID. 370 if ( 'root' === $entity_kind && 'comment' === $entity_name ) { 371 return current_user_can( 'edit_comment', $object_id ); 372 } 299 373 } 300 374 -
trunk/tests/phpunit/tests/rest-api/rest-sync-server.php
r62099 r62198 10 10 class WP_Test_REST_Sync_Server extends WP_Test_REST_Controller_Testcase { 11 11 12 protected static $editor_id; 13 protected static $subscriber_id; 14 protected static $post_id; 12 protected static int $editor_id; 13 protected static int $subscriber_id; 14 protected static int $post_id; 15 protected static int $category_id; 16 protected static int $tag_id; 17 protected static int $comment_id; 15 18 16 19 public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { … … 18 21 self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) ); 19 22 self::$post_id = $factory->post->create( array( 'post_author' => self::$editor_id ) ); 23 self::$category_id = $factory->category->create(); 24 self::$tag_id = $factory->tag->create(); 25 self::$comment_id = $factory->comment->create( array( 'comment_post_ID' => self::$post_id ) ); 20 26 21 27 // Enable option in setUpBeforeClass to ensure REST routes are registered. … … 28 34 delete_option( 'wp_collaboration_enabled' ); 29 35 wp_delete_post( self::$post_id, true ); 36 wp_delete_term( self::$category_id, 'category' ); 37 wp_delete_term( self::$tag_id, 'post_tag' ); 38 wp_delete_comment( self::$comment_id, true ); 30 39 } 31 40 … … 278 287 } 279 288 289 /** 290 * @ticket 64890 291 */ 292 public function test_sync_malformed_object_id_rejected() { 293 wp_set_current_user( self::$editor_id ); 294 295 $response = $this->dispatch_sync( array( $this->build_room( 'postType/post:1abc' ) ) ); 296 297 $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); 298 } 299 300 /** 301 * @ticket 64890 302 */ 303 public function test_sync_zero_object_id_rejected(): void { 304 wp_set_current_user( self::$editor_id ); 305 306 $response = $this->dispatch_sync( array( $this->build_room( 'postType/post:0' ) ) ); 307 308 $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); 309 } 310 311 /** 312 * @ticket 64890 313 */ 314 public function test_sync_post_type_mismatch_rejected(): void { 315 wp_set_current_user( self::$editor_id ); 316 317 // The test post is of type 'post', not 'page'. 318 $response = $this->dispatch_sync( array( $this->build_room( 'postType/page:' . self::$post_id ) ) ); 319 320 $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); 321 } 322 323 /** 324 * @ticket 64890 325 */ 326 public function test_sync_taxonomy_term_allowed(): void { 327 wp_set_current_user( self::$editor_id ); 328 329 $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:' . self::$category_id ) ) ); 330 331 $this->assertSame( 200, $response->get_status() ); 332 } 333 334 /** 335 * @ticket 64890 336 */ 337 public function test_sync_nonexistent_taxonomy_term_rejected(): void { 338 wp_set_current_user( self::$editor_id ); 339 340 $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:999999' ) ) ); 341 342 $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); 343 } 344 345 /** 346 * @ticket 64890 347 */ 348 public function test_sync_taxonomy_term_wrong_taxonomy_rejected(): void { 349 wp_set_current_user( self::$editor_id ); 350 351 // The tag term exists in 'post_tag', not 'category'. 352 $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:' . self::$tag_id ) ) ); 353 354 $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); 355 } 356 357 /** 358 * @ticket 64890 359 */ 360 public function test_sync_comment_allowed(): void { 361 wp_set_current_user( self::$editor_id ); 362 363 $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:' . self::$comment_id ) ) ); 364 365 $this->assertSame( 200, $response->get_status() ); 366 } 367 368 /** 369 * @ticket 64890 370 */ 371 public function test_sync_nonexistent_comment_rejected(): void { 372 wp_set_current_user( self::$editor_id ); 373 374 $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:999999' ) ) ); 375 376 $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); 377 } 378 379 /** 380 * @ticket 64890 381 */ 382 public function test_sync_nonexistent_post_type_collection_rejected(): void { 383 wp_set_current_user( self::$editor_id ); 384 385 $response = $this->dispatch_sync( array( $this->build_room( 'postType/nonexistent_type' ) ) ); 386 387 $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); 388 } 389 280 390 /* 281 391 * Validation tests. … … 292 402 293 403 $this->assertSame( 400, $response->get_status() ); 404 } 405 406 /** 407 * Verifies that schema type validation rejects a non-string value for the 408 * update 'data' field, confirming that per-arg schema validation still runs 409 * with a route-level validate_callback registered. 410 * 411 * @ticket 64890 412 */ 413 public function test_sync_rejects_non_string_update_data(): void { 414 wp_set_current_user( self::$editor_id ); 415 416 $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); 417 $request->set_body_params( 418 array( 419 'rooms' => array( 420 array( 421 'after' => 0, 422 'awareness' => array( 'user' => 'test' ), 423 'client_id' => 1, 424 'room' => $this->get_post_room(), 425 'updates' => array( 426 array( 427 'data' => 12345, 428 'type' => 'update', 429 ), 430 ), 431 ), 432 ), 433 ) 434 ); 435 436 $response = rest_get_server()->dispatch( $request ); 437 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 438 } 439 440 /** 441 * Verifies that schema enum validation rejects an invalid update type, 442 * confirming that per-arg schema validation still runs with a route-level 443 * validate_callback registered. 444 * 445 * @ticket 64890 446 */ 447 public function test_sync_rejects_invalid_update_type_enum(): void { 448 wp_set_current_user( self::$editor_id ); 449 450 $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); 451 $request->set_body_params( 452 array( 453 'rooms' => array( 454 array( 455 'after' => 0, 456 'awareness' => array( 'user' => 'test' ), 457 'client_id' => 1, 458 'room' => $this->get_post_room(), 459 'updates' => array( 460 array( 461 'data' => 'dGVzdA==', 462 'type' => 'invalid_type', 463 ), 464 ), 465 ), 466 ), 467 ) 468 ); 469 470 $response = rest_get_server()->dispatch( $request ); 471 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 472 } 473 474 /** 475 * Verifies that schema required-field validation rejects a room missing 476 * the 'client_id' field, confirming that per-arg schema validation still 477 * runs with a route-level validate_callback registered. 478 * 479 * @ticket 64890 480 */ 481 public function test_sync_rejects_missing_required_room_field(): void { 482 wp_set_current_user( self::$editor_id ); 483 484 $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); 485 $request->set_body_params( 486 array( 487 'rooms' => array( 488 array( 489 'after' => 0, 490 'awareness' => array( 'user' => 'test' ), 491 // 'client_id' deliberately omitted. 492 'room' => $this->get_post_room(), 493 'updates' => array(), 494 ), 495 ), 496 ) 497 ); 498 499 $response = rest_get_server()->dispatch( $request ); 500 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 501 } 502 503 /** 504 * Verifies that the maxItems constraint rejects a request with more rooms 505 * than MAX_ROOMS_PER_REQUEST. 506 * 507 * @ticket 64890 508 */ 509 public function test_sync_rejects_rooms_exceeding_max_items(): void { 510 wp_set_current_user( self::$editor_id ); 511 512 $rooms = array(); 513 for ( $i = 0; $i < WP_HTTP_Polling_Sync_Server::MAX_ROOMS_PER_REQUEST + 1; $i++ ) { 514 $rooms[] = $this->build_room( 'root/site', $i + 1 ); 515 } 516 517 $response = $this->dispatch_sync( $rooms ); 518 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 519 } 520 521 /** 522 * Verifies that the maxLength constraint rejects update data exceeding 523 * MAX_UPDATE_DATA_SIZE. 524 * 525 * @ticket 64890 526 */ 527 public function test_sync_rejects_update_data_exceeding_max_length(): void { 528 wp_set_current_user( self::$editor_id ); 529 530 $oversized_data = str_repeat( 'a', WP_HTTP_Polling_Sync_Server::MAX_UPDATE_DATA_SIZE + 1 ); 531 532 $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); 533 $request->set_body_params( 534 array( 535 'rooms' => array( 536 array( 537 'after' => 0, 538 'awareness' => array( 'user' => 'test' ), 539 'client_id' => 1, 540 'room' => $this->get_post_room(), 541 'updates' => array( 542 array( 543 'data' => $oversized_data, 544 'type' => 'update', 545 ), 546 ), 547 ), 548 ), 549 ) 550 ); 551 552 $response = rest_get_server()->dispatch( $request ); 553 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 554 } 555 556 /** 557 * Verifies that the route-level validate_callback rejects a request body 558 * exceeding MAX_BODY_SIZE. 559 * 560 * @ticket 64890 561 */ 562 public function test_sync_rejects_oversized_request_body(): void { 563 wp_set_current_user( self::$editor_id ); 564 565 $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); 566 567 // Set valid parsed params so per-arg schema validation passes first. 568 $request->set_body_params( 569 array( 570 'rooms' => array( 571 $this->build_room( $this->get_post_room() ), 572 ), 573 ) 574 ); 575 576 // Set an oversized raw body to trigger the route-level validate_callback. 577 $request->set_body( str_repeat( 'x', WP_HTTP_Polling_Sync_Server::MAX_BODY_SIZE + 1 ) ); 578 579 $response = rest_get_server()->dispatch( $request ); 580 $this->assertErrorResponse( 'rest_sync_body_too_large', $response, 413 ); 294 581 } 295 582
Note: See TracChangeset
for help on using the changeset viewer.