| @@ -290,11 +290,11 @@ | ||
| 290 | 290 | |
| 291 | 291 | $out = array( |
| 292 | 292 | 'id' => (int) $post->ID, |
| 293 | 293 | 'agentId' => $agent_id, |
| 294 | - 'agentName' => $agent ? $agent->display_name : __( 'Deleted agent', 'desktop-mode' ), | |
| 294 | + 'agentName' => $agent ? openstation_plain_text_title( $agent->display_name ) : __( 'Deleted agent', 'desktop-mode' ), | |
| 295 | 295 | 'agentDescription' => $agent ? (string) get_user_meta( $agent_id, '_desktop_mode_agent_description', true ) : '', |
| 296 | - 'agentAvatarUrl' => function_exists( 'openstation_agent_avatar_url' ) ? openstation_agent_avatar_url() : '', | |
| 296 | + 'agentAvatarUrl' => function_exists( 'openstation_agent_avatar_url' ) ? openstation_agent_avatar_url( $agent_id ) : '', | |
| 297 | 297 | 'title' => (string) $post->post_title, |
| 298 | 298 | // Second sidebar line + who spoke last, so the list can say |
| 299 | 299 | // where each conversation got to instead of repeating its opener. |
| 300 | 300 | 'preview' => openstation_agent_conversation_preview( $messages ), |
| @@ -402,8 +402,47 @@ | ||
| 402 | 402 | ); |
| 403 | 403 | } |
| 404 | 404 | |
| 405 | 405 | /** |
| 406 | + * Keep the save filters off a conversation's title and transcript, | |
| 407 | + * the way `WP_Customize_Manager` does for changeset JSON. Hooked only | |
| 408 | + * while one of the handlers below saves. | |
| 409 | + * | |
| 410 | + * Both are text the client paints as text, never HTML that WordPress | |
| 411 | + * echoes, yet `title_save_pre` and `content_save_pre` rewrite them as | |
| 412 | + * HTML. For a user without `unfiltered_html`, kses turns `&` into | |
| 413 | + * `&`, drops whatever sits between a `<` and a `>`, and unescapes | |
| 414 | + * an attribute's quotes. On a site that balances tags, `balanceTags` | |
| 415 | + * appends closing tags for every role. Those last two leave JSON that | |
| 416 | + * no longer decodes. | |
| 417 | + * | |
| 418 | + * @param array $data Slashed, sanitized post data. | |
| 419 | + * @param array $postarr Sanitized post data. | |
| 420 | + * @param array $unsanitized_postarr Slashed post data as passed to `wp_insert_post()`. | |
| 421 | + * @return array | |
| 422 | + */ | |
| 423 | +function openstation_agent_conversation_preserve_text( $data, $postarr, $unsanitized_postarr ) { | |
| 424 | + global $wpdb; | |
| 425 | + | |
| 426 | + if ( OPENSTATION_AGENT_CHAT_POST_TYPE !== $data['post_type'] ) { | |
| 427 | + return $data; | |
| 428 | + } | |
| 429 | + foreach ( array( 'post_title', 'post_content' ) as $field ) { | |
| 430 | + if ( ! isset( $unsanitized_postarr[ $field ] ) ) { | |
| 431 | + continue; | |
| 432 | + } | |
| 433 | + $data[ $field ] = $unsanitized_postarr[ $field ]; | |
| 434 | + // `wp_insert_post()` has already swapped emoji for entities | |
| 435 | + // where the column is utf8mb3, which rejects them and fails | |
| 436 | + // the save. Redo it on the restored text. | |
| 437 | + if ( in_array( $wpdb->get_col_charset( $wpdb->posts, $field ), array( 'utf8', 'utf8mb3' ), true ) ) { | |
| 438 | + $data[ $field ] = wp_encode_emoji( $data[ $field ] ); | |
| 439 | + } | |
| 440 | + } | |
| 441 | + return $data; | |
| 442 | +} | |
| 443 | + | |
| 444 | +/** | |
| 406 | 445 | * POST /agents/conversations — create from {agentId, messages}. |
| 407 | 446 | * |
| 408 | 447 | * @param WP_REST_Request $request Request. |
| 409 | 448 | * @return WP_REST_Response|WP_Error |
| @@ -426,19 +465,21 @@ | ||
| 426 | 465 | array( 'status' => 400 ) |
| 427 | 466 | ); |
| 428 | 467 | } |
| 429 | 468 | |
| 469 | + add_filter( 'wp_insert_post_data', 'openstation_agent_conversation_preserve_text', 5, 3 ); | |
| 430 | 470 | $post_id = wp_insert_post( |
| 431 | 471 | array( |
| 432 | 472 | 'post_type' => OPENSTATION_AGENT_CHAT_POST_TYPE, |
| 433 | 473 | 'post_status' => 'publish', |
| 434 | 474 | 'post_author' => get_current_user_id(), |
| 435 | - 'post_title' => openstation_agent_conversation_title( $messages ), | |
| 436 | - // JSON survives the insert-path unslashing only when slashed. | |
| 475 | + // Both survive the insert-path unslashing only when slashed. | |
| 476 | + 'post_title' => wp_slash( openstation_agent_conversation_title( $messages ) ), | |
| 437 | 477 | 'post_content' => wp_slash( (string) wp_json_encode( $messages ) ), |
| 438 | 478 | ), |
| 439 | 479 | true |
| 440 | 480 | ); |
| 481 | + remove_filter( 'wp_insert_post_data', 'openstation_agent_conversation_preserve_text', 5 ); | |
| 441 | 482 | if ( is_wp_error( $post_id ) ) { |
| 442 | 483 | return $post_id; |
| 443 | 484 | } |
| 444 | 485 | update_post_meta( $post_id, '_desktop_mode_agent_chat_agent_id', $agent_id ); |
| @@ -488,16 +529,18 @@ | ||
| 488 | 529 | array( 'status' => 400 ) |
| 489 | 530 | ); |
| 490 | 531 | } |
| 491 | 532 | |
| 533 | + add_filter( 'wp_insert_post_data', 'openstation_agent_conversation_preserve_text', 5, 3 ); | |
| 492 | 534 | $updated = wp_update_post( |
| 493 | 535 | array( |
| 494 | 536 | 'ID' => $post->ID, |
| 495 | - 'post_title' => openstation_agent_conversation_title( $messages ), | |
| 537 | + 'post_title' => wp_slash( openstation_agent_conversation_title( $messages ) ), | |
| 496 | 538 | 'post_content' => wp_slash( (string) wp_json_encode( $messages ) ), |
| 497 | 539 | ), |
| 498 | 540 | true |
| 499 | 541 | ); |
| 542 | + remove_filter( 'wp_insert_post_data', 'openstation_agent_conversation_preserve_text', 5 ); | |
| 500 | 543 | if ( is_wp_error( $updated ) ) { |
| 501 | 544 | return $updated; |
| 502 | 545 | } |
| 503 | 546 | |