From b3d7df5dfc0e83aee7f8e325145b7efc62026c99 Mon Sep 17 00:00:00 2001 From: colemanw Date: Wed, 17 May 2023 16:16:09 -0400 Subject: [PATCH] APIv4 - Fix html encoding of rich-text fields In ece8de2d2 this was fixed but only for APIv3, and with no unit test. The previous fix also did not cover fields using "TextArea" as their input type, even though they are allowed to store HTML. This fixes for APIv4 and v3 and adds a test. and adds a test. --- CRM/Core/BAO/CustomField.php | 3 + CRM/Utils/API/AbstractFieldCoder.php | 2 +- CRM/Utils/API/HTMLInputCoder.php | 20 +++++- .../api/v4/Custom/BasicCustomFieldTest.php | 64 +++++++++++++++++-- 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/CRM/Core/BAO/CustomField.php b/CRM/Core/BAO/CustomField.php index 9c41ec844e..75cd9ca436 100644 --- a/CRM/Core/BAO/CustomField.php +++ b/CRM/Core/BAO/CustomField.php @@ -155,6 +155,7 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField { CRM_Utils_Hook::post(($op === 'add' ? 'create' : 'edit'), 'CustomField', $customField->id, $customField); CRM_Utils_System::flushCache(); + CRM_Utils_API_HTMLInputCoder::singleton()->flushCache(); // Flush caches is not aggressive about clearing the specific cache we know we want to clear // so do it manually. Ideally we wouldn't need to clear others... Civi::cache('metadata')->clear(); @@ -232,6 +233,7 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField { } CRM_Utils_System::flushCache(); + CRM_Utils_API_HTMLInputCoder::singleton()->flushCache(); Civi::cache('metadata')->clear(); foreach ($customFields as $index => $customField) { @@ -2060,6 +2062,7 @@ WHERE id IN ( %1, %2 ) $add->save(); CRM_Utils_System::flushCache(); + CRM_Utils_API_HTMLInputCoder::singleton()->flushCache(); } /** diff --git a/CRM/Utils/API/AbstractFieldCoder.php b/CRM/Utils/API/AbstractFieldCoder.php index b26d3b82de..f99293f9e8 100644 --- a/CRM/Utils/API/AbstractFieldCoder.php +++ b/CRM/Utils/API/AbstractFieldCoder.php @@ -27,7 +27,7 @@ abstract class CRM_Utils_API_AbstractFieldCoder implements API_Wrapper { /** * Get skipped fields. * - * @return array + * @return string[] * List of field names */ public function getSkipFields() { diff --git a/CRM/Utils/API/HTMLInputCoder.php b/CRM/Utils/API/HTMLInputCoder.php index 8e8dca79c1..444de7f3eb 100644 --- a/CRM/Utils/API/HTMLInputCoder.php +++ b/CRM/Utils/API/HTMLInputCoder.php @@ -22,6 +22,9 @@ * @copyright CiviCRM LLC https://civicrm.org/licensing */ class CRM_Utils_API_HTMLInputCoder extends CRM_Utils_API_AbstractFieldCoder { + /** + * @var string[] + */ private $skipFields = NULL; /** @@ -39,14 +42,21 @@ class CRM_Utils_API_HTMLInputCoder extends CRM_Utils_API_AbstractFieldCoder { return self::$_singleton; } + /** + * @return void + */ + public function flushCache(): void { + $this->skipFields = NULL; + } + /** * Get skipped fields. * - * @return array + * @return string[] * list of field names */ public function getSkipFields() { - if ($this->skipFields === NULL) { + if (!isset($this->skipFields)) { $this->skipFields = [ 'widget_code', 'html_message', @@ -118,9 +128,13 @@ class CRM_Utils_API_HTMLInputCoder extends CRM_Utils_API_AbstractFieldCoder { // Survey entity 'instructions', ]; - $custom = CRM_Core_DAO::executeQuery('SELECT id FROM civicrm_custom_field WHERE html_type = "RichTextEditor"'); + $custom = CRM_Core_DAO::executeQuery(' + SELECT cf.id, cf.name AS field_name, cg.name AS group_name + FROM civicrm_custom_field cf, civicrm_custom_group cg + WHERE cf.custom_group_id = cg.id AND cf.data_type = "Memo"'); while ($custom->fetch()) { $this->skipFields[] = 'custom_' . $custom->id; + $this->skipFields[] = $custom->group_name . '.' . $custom->field_name; } } return $this->skipFields; diff --git a/tests/phpunit/api/v4/Custom/BasicCustomFieldTest.php b/tests/phpunit/api/v4/Custom/BasicCustomFieldTest.php index 503923fb5b..6885e58e87 100644 --- a/tests/phpunit/api/v4/Custom/BasicCustomFieldTest.php +++ b/tests/phpunit/api/v4/Custom/BasicCustomFieldTest.php @@ -58,22 +58,22 @@ class BasicCustomFieldTest extends CustomTestBase { 'first_name' => 'Johann', 'last_name' => 'Tester', 'contact_type' => 'Individual', - 'MyIndividualFields.FavColor' => 'Red', + 'MyIndividualFields.FavColor' => '', ])['id']; $contact = Contact::get(FALSE) ->addSelect('first_name') ->addSelect('MyIndividualFields.FavColor') ->addWhere('id', '=', $contactId) - ->addWhere('MyIndividualFields.FavColor', '=', 'Red') + ->addWhere('MyIndividualFields.FavColor', '=', '') ->execute() ->first(); - $this->assertEquals('Red', $contact['MyIndividualFields.FavColor']); + $this->assertEquals('', $contact['MyIndividualFields.FavColor']); Contact::update() ->addWhere('id', '=', $contactId) - ->addValue('MyIndividualFields.FavColor', 'Blue') + ->addValue('MyIndividualFields.FavColor', 'Blue&Pink') ->execute(); $contact = Contact::get(FALSE) @@ -82,7 +82,7 @@ class BasicCustomFieldTest extends CustomTestBase { ->execute() ->first(); - $this->assertEquals('Blue', $contact['MyIndividualFields.FavColor']); + $this->assertEquals('Blue&Pink', $contact['MyIndividualFields.FavColor']); // Try setting to null Contact::update() @@ -680,4 +680,58 @@ class BasicCustomFieldTest extends CustomTestBase { $this->assertContains('Attendee', $roleOptions); } + /** + * Ensure rich-text html fields store html correctly + */ + public function testRichTextHTML(): void { + $cgName = uniqid('My'); + + $custom = CustomGroup::create(FALSE) + ->addValue('title', $cgName) + ->addValue('extends', 'Contact') + ->addChain('field1', CustomField::create() + ->addValue('label', 'RichText') + ->addValue('custom_group_id', '$id') + ->addValue('html_type', 'RichTextEditor') + ->addValue('data_type', 'Memo'), + 0) + ->addChain('field2', CustomField::create() + ->addValue('label', 'TextArea') + ->addValue('custom_group_id', '$id') + ->addValue('html_type', 'TextArea') + ->addValue('data_type', 'Memo'), + 0) + ->execute()->first(); + + $cid = $this->createTestRecord('Contact', [ + 'first_name' => 'One', + 'last_name' => 'Tester', + "$cgName.RichText" => 'Hello
APIv4 & RichText!', + "$cgName.TextArea" => 'Hello
APIv4 & TextArea!', + ])['id']; + $contact = Contact::get(FALSE) + ->addSelect('custom.*') + ->addWhere('id', '=', $cid) + ->execute()->first(); + $this->assertEquals('Hello
APIv4 & RichText!', $contact["$cgName.RichText"]); + $this->assertEquals('Hello
APIv4 & TextArea!', $contact["$cgName.TextArea"]); + + // The html should have been stored unescaped + $dbVal = \CRM_Core_DAO::singleValueQuery("SELECT {$custom['field1']['column_name']} FROM {$custom['table_name']}"); + $this->assertEquals('Hello
APIv4 & RichText!', $dbVal); + $dbVal = \CRM_Core_DAO::singleValueQuery("SELECT {$custom['field2']['column_name']} FROM {$custom['table_name']}"); + $this->assertEquals('Hello
APIv4 & TextArea!', $dbVal); + + // APIv3 should work the same way + civicrm_api3('Contact', 'create', [ + 'id' => $cid, + "custom_{$custom['field1']['id']}" => 'Hello
APIv3 & RichText!', + "custom_{$custom['field2']['id']}" => 'Hello
APIv3 & TextArea!', + ]); + $dbVal = \CRM_Core_DAO::singleValueQuery("SELECT {$custom['field1']['column_name']} FROM {$custom['table_name']}"); + $this->assertEquals('Hello
APIv3 & RichText!', $dbVal); + $dbVal = \CRM_Core_DAO::singleValueQuery("SELECT {$custom['field2']['column_name']} FROM {$custom['table_name']}"); + $this->assertEquals('Hello
APIv3 & TextArea!', $dbVal); + } + } -- 2.25.1