Skip to content

Commit

Permalink
APIv4 - Fix html encoding of rich-text fields
Browse files Browse the repository at this point in the history
In ece8de2 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.
  • Loading branch information
colemanw committed May 17, 2023
1 parent b1bd2b1 commit b3d7df5
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 9 deletions.
3 changes: 3 additions & 0 deletions CRM/Core/BAO/CustomField.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ public static function create($params) {
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();
Expand Down Expand Up @@ -232,6 +233,7 @@ public static function writeRecords(array $records): array {
}

CRM_Utils_System::flushCache();
CRM_Utils_API_HTMLInputCoder::singleton()->flushCache();
Civi::cache('metadata')->clear();

foreach ($customFields as $index => $customField) {
Expand Down Expand Up @@ -2060,6 +2062,7 @@ public static function moveField($fieldID, $newGroupID) {
$add->save();

CRM_Utils_System::flushCache();
CRM_Utils_API_HTMLInputCoder::singleton()->flushCache();
}

/**
Expand Down
2 changes: 1 addition & 1 deletion CRM/Utils/API/AbstractFieldCoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ abstract class CRM_Utils_API_AbstractFieldCoder implements API_Wrapper {
/**
* Get skipped fields.
*
* @return array<string>
* @return string[]
* List of field names
*/
public function getSkipFields() {
Expand Down
20 changes: 17 additions & 3 deletions CRM/Utils/API/HTMLInputCoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -39,14 +42,21 @@ public static function singleton() {
return self::$_singleton;
}

/**
* @return void
*/
public function flushCache(): void {
$this->skipFields = NULL;
}

/**
* Get skipped fields.
*
* @return array<string>
* @return string[]
* list of field names
*/
public function getSkipFields() {
if ($this->skipFields === NULL) {
if (!isset($this->skipFields)) {
$this->skipFields = [
'widget_code',
'html_message',
Expand Down Expand Up @@ -118,9 +128,13 @@ public function getSkipFields() {
// 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;
Expand Down
64 changes: 59 additions & 5 deletions tests/phpunit/api/v4/Custom/BasicCustomFieldTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,22 @@ public function testWithSingleField(): void {
'first_name' => 'Johann',
'last_name' => 'Tester',
'contact_type' => 'Individual',
'MyIndividualFields.FavColor' => 'Red',
'MyIndividualFields.FavColor' => '<Red>',
])['id'];

$contact = Contact::get(FALSE)
->addSelect('first_name')
->addSelect('MyIndividualFields.FavColor')
->addWhere('id', '=', $contactId)
->addWhere('MyIndividualFields.FavColor', '=', 'Red')
->addWhere('MyIndividualFields.FavColor', '=', '<Red>')
->execute()
->first();

$this->assertEquals('Red', $contact['MyIndividualFields.FavColor']);
$this->assertEquals('<Red>', $contact['MyIndividualFields.FavColor']);

Contact::update()
->addWhere('id', '=', $contactId)
->addValue('MyIndividualFields.FavColor', 'Blue')
->addValue('MyIndividualFields.FavColor', 'Blue&Pink')
->execute();

$contact = Contact::get(FALSE)
Expand All @@ -82,7 +82,7 @@ public function testWithSingleField(): void {
->execute()
->first();

$this->assertEquals('Blue', $contact['MyIndividualFields.FavColor']);
$this->assertEquals('Blue&Pink', $contact['MyIndividualFields.FavColor']);

// Try setting to null
Contact::update()
Expand Down Expand Up @@ -680,4 +680,58 @@ public function testExtendsParticipantMetadata() {
$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" => '<em>Hello</em><br />APIv4 & RichText!',
"$cgName.TextArea" => '<em>Hello</em><br />APIv4 & TextArea!',
])['id'];
$contact = Contact::get(FALSE)
->addSelect('custom.*')
->addWhere('id', '=', $cid)
->execute()->first();
$this->assertEquals('<em>Hello</em><br />APIv4 & RichText!', $contact["$cgName.RichText"]);
$this->assertEquals('<em>Hello</em><br />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('<em>Hello</em><br />APIv4 & RichText!', $dbVal);
$dbVal = \CRM_Core_DAO::singleValueQuery("SELECT {$custom['field2']['column_name']} FROM {$custom['table_name']}");
$this->assertEquals('<em>Hello</em><br />APIv4 & TextArea!', $dbVal);

// APIv3 should work the same way
civicrm_api3('Contact', 'create', [
'id' => $cid,
"custom_{$custom['field1']['id']}" => '<em>Hello</em><br />APIv3 & RichText!',
"custom_{$custom['field2']['id']}" => '<em>Hello</em><br />APIv3 & TextArea!',
]);
$dbVal = \CRM_Core_DAO::singleValueQuery("SELECT {$custom['field1']['column_name']} FROM {$custom['table_name']}");
$this->assertEquals('<em>Hello</em><br />APIv3 & RichText!', $dbVal);
$dbVal = \CRM_Core_DAO::singleValueQuery("SELECT {$custom['field2']['column_name']} FROM {$custom['table_name']}");
$this->assertEquals('<em>Hello</em><br />APIv3 & TextArea!', $dbVal);
}

}

0 comments on commit b3d7df5

Please sign in to comment.