Skip to content

Commit

Permalink
DomainEmailAddress - Add legacy API adapter
Browse files Browse the repository at this point in the history
Wraps the OptionValue api (v3 and v4) to provide backward compatibility
for accessing DomainEmailAddress via the OptionValue api.
  • Loading branch information
colemanw committed Jan 31, 2025
1 parent c740266 commit 01efb32
Show file tree
Hide file tree
Showing 11 changed files with 469 additions and 86 deletions.
18 changes: 14 additions & 4 deletions CRM/Core/OptionGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class CRM_Core_OptionGroup {
* @var array
*/
public static $_domainIDGroups = [
'from_email_address',
'grant_type',
];

Expand Down Expand Up @@ -109,15 +108,26 @@ public static function &valuesCommon(
* @return array
* The values as specified by the params
*/
public static function &values(
public static function values(
string $name, $flip = FALSE, $grouping = FALSE,
$localize = FALSE, $condition = NULL,
$labelColumnName = 'label', $onlyActive = TRUE, $fresh = FALSE, $keyColumnName = 'value',
$orderBy = 'weight'
) {

if (self::isDomainOptionGroup($name)) {
$cacheKey = self::createCacheKey($name, CRM_Core_I18n::getLocale(), $flip, $grouping, $localize, $condition, $labelColumnName, $onlyActive, $keyColumnName, $orderBy, CRM_Core_Config::domainID());
// Legacy shim for option group that's been moved to its own table
if ($name === 'from_email_address') {
// This gets converted: @see DomainEmailLegacyOptionValueAdapter
$where = [
['option_group_id:name', '=', 'from_email_address'],
['domain_id', '=', 'current_domain'],
];
if ($onlyActive) {
$where[] = ['is_active', '=', TRUE];
}
return civicrm_api4('OptionValue', 'get', [
'where' => $where,
])->column($labelColumnName, $keyColumnName);
}
else {
$cacheKey = self::createCacheKey($name, CRM_Core_I18n::getLocale(), $flip, $grouping, $localize, $condition, $labelColumnName, $onlyActive, $keyColumnName, $orderBy);
Expand Down
230 changes: 230 additions & 0 deletions Civi/API/Subscriber/DomainEmailLegacyOptionValueAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

namespace Civi\API\Subscriber;

use Civi\Api4\Generic\AbstractAction;
use Civi\Core\Service\AutoSubscriber;
use CRM_Core_Config;

/**
* Wraps the OptionValue api (v3 and v4) to provide backward compatibility
* with the DomainEmail entity (formerly the `from_email_address` option group).
*/
class DomainEmailLegacyOptionValueAdapter extends AutoSubscriber {

private static $apiIds = [];

const CONCAT_LABEL = 'CONCAT(\'"\', name, \'" <\', email, \'>\')';

/**
* @return array
*/
public static function getSubscribedEvents() {
return [
'civi.api.prepare' => [
['onApiPrepare', 2000],
],
'civi.api.respond' => [
['onApiRespond', 2000],
],
];
}

public function onApiPrepare(\Civi\API\Event\PrepareEvent $event) {
$apiRequest = $event->getApiRequest();
if ($apiRequest['entity'] !== 'OptionValue') {
return;
}
if ($apiRequest['version'] == 3 && $this->isApi3FromEmailOptionValueRequest($apiRequest)) {
$this->preprocessApi3DomainEmailOptionValues($apiRequest);
$event->setApiRequest($apiRequest);
}
elseif ($apiRequest['version'] == 4 && $this->isApi4FromEmailOptionValueRequest($apiRequest)) {
$this->preprocessApi4DomainEmailOptionValues($apiRequest);
}
}

public function onApiRespond(\Civi\API\Event\RespondEvent $event) {
$apiRequest = $event->getApiRequest();
// If ID was previously stashed by preprocessApi3DomainEmailOptionValues
if (isset(self::$apiIds[$apiRequest['id']])) {
unset(self::$apiIds[$apiRequest['id']]);
$apiResponse = $event->getResponse();
$this->postprocessApi3DomainEmailOptionValues($apiRequest, $apiResponse);
$event->setResponse($apiResponse);
}
}

/**
* @param array $apiRequest
* @return bool
*/
private function isApi3FromEmailOptionValueRequest($apiRequest): bool {
// In api3, 'create' also means 'update'; this supports both.
// This also effectively supports `getsingle` and `getvalue` because they wrap `get`.
// However, 'delete' is not supported because the params don't include option_group_id so we have no way to target them.
$supportedActions = ['create', 'get'];
if (!in_array($apiRequest['action'], $supportedActions, TRUE)) {
return FALSE;
}
$apiParams = $apiRequest['params'];
if (($apiParams['option_group_id'] ?? NULL) === 'from_email_address') {
return TRUE;
}
// Update actions are tricky because they probably won't pass option_group_id in the params
// So check if the label looks like a well-formed email, and if so, see if the id matches a record in civicrm_domain_email_address
if ($apiRequest['action'] === 'create' && !empty($apiParams['id']) && !empty($apiParams['label'])) {
$pattern = '/^"[^"]+" <[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+>$/';
if (preg_match($pattern, $apiParams['label'])) {
return (bool) \CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM civicrm_domain_email_address WHERE id = %1", [
1 => [$apiParams['id'], 'Integer'],
]);
}
}
return FALSE;
}

private function preprocessApi3DomainEmailOptionValues(&$apiRequest) {
// Register request id for postprocessing
self::$apiIds[$apiRequest['id']] = TRUE;
// Modify internal variables of the api request... don't try this at home
$apiRequest['function'] = str_replace('option_value', 'domain_email_address', $apiRequest['function']);
// Switch request to use DomainEmailAddress entity
$apiRequest['entity'] = 'DomainEmailAddress';
$apiParams = &$apiRequest['params'];
unset($apiParams['option_group_id']);
if (!empty($apiParams['return'])) {
if (!is_array($apiParams['return'])) {
$apiParams['return'] = array_map('trim', explode(',', $apiParams['return']));
}
$apiParams['return'][] = 'id';
$apiParams['return'][] = 'name';
$apiParams['return'][] = 'email';
}
if (!empty($apiParams['value'])) {
$apiParams['id'] = $apiParams['value'];
}
if (isset($apiParams['name']) && !isset($apiParams['label'])) {
$apiParams['label'] = $apiParams['name'];
}
unset($apiParams['option_group_id'], $apiParams['name'], $apiParams['value']);
if (isset($apiParams['label'])) {
$apiParams['email'] = \CRM_Utils_Mail::pluckEmailFromHeader(rtrim($apiParams['label']));
$apiParams['name'] = trim(explode('"', $apiParams['label'])[1]);
}
if ($apiRequest['action'] === 'create' && !isset($apiParams['id']) && !isset($apiParams['domain_id'])) {
$apiParams['domain_id'] = CRM_Core_Config::domainID();
}
// Convert chains
foreach (array_keys($apiParams) as $key) {
if (str_starts_with(strtolower($key), 'api.option_value.') || str_starts_with(strtolower($key), 'api.optionvalue.')) {
$action = explode('.', $key)[2];
$apiParams["api.domain_email_address.$action"] = $apiParams[$key];
unset($apiParams[$key]);
}
}
}

private function postprocessApi3DomainEmailOptionValues(array $apiRequest, array &$apiResult) {
if (isset($apiResult['values']) && is_array($apiResult['values'])) {
foreach ($apiResult['values'] as &$value) {
if (isset($value['id'])) {
$value['value'] = $value['id'];
}
if (isset($value['name']) && isset($value['email'])) {
$value['label'] = \CRM_Utils_Mail::formatFromAddress($value);
$value['name'] = $value['label'];
}
}
}
}

/**
* @param \Civi\Api4\Generic\AbstractAction $apiRequest
* @return bool
*/
private function isApi4FromEmailOptionValueRequest($apiRequest): bool {
$supportedActions = ['create', 'get', 'update', 'delete'];
$action = $apiRequest->getActionName();
if (!in_array($action, $supportedActions, TRUE)) {
return FALSE;
}
if ($apiRequest->reflect()->hasProperty('where')) {
foreach ($apiRequest->getWhere() as $clause) {
if ($clause[0] === 'option_group_id:name' || $clause[0] === 'option_group_id.name') {
if ($clause[1] === '=' && $clause[2] === 'from_email_address') {
return TRUE;
}
}
}
}
if ($action === 'create' || $action === 'update') {
$values = $apiRequest->getValues();
if (($values['option_group_id:name'] ?? $values['option_group_id.name'] ?? NULL) === 'from_email_address') {
return TRUE;
}
}
return FALSE;
}

private function preprocessApi4DomainEmailOptionValues(AbstractAction $apiRequest) {
$action = $apiRequest->getActionName();
// Modify internal variables of the api request... don't try this at home
$reflection = $apiRequest->reflect();
$entityNameProperty = $reflection->getProperty('_entityName');
$entityNameProperty->setAccessible(TRUE);
$entityNameProperty->setValue($apiRequest, 'DomainEmailAddress');
// Also reset $_entityFields
$entityFieldsProperty = $reflection->getProperty('_entityFields');
$entityFieldsProperty->setAccessible(TRUE);
$entityFieldsProperty->setValue($apiRequest, NULL);
if ($reflection->hasProperty('where')) {
$where = $apiRequest->getWhere();
foreach ($where as $index => $clause) {
if ($clause[0] === 'option_group_id:name' || $clause[0] === 'option_group_id.name') {
unset($where[$index]);
}
if ($clause[0] === 'value') {
$where[$index][0] = 'id';
}
if ($clause[0] === 'label') {
$where[$index][0] = self::CONCAT_LABEL;
}
}
$apiRequest->setWhere(array_values($where));
}
if ($action === 'get') {
$select = $apiRequest->getSelect() ?: ['*'];
// Remove all non-supported fields from select clause
$allowedFields = array_keys(\Civi::entity('DomainEmailAddress')->getFields());
$allowedFields[] = '*';
$select = array_intersect($select, $allowedFields);
$select[] = self::CONCAT_LABEL . ' AS label';
$select[] = '(id) AS value';
$apiRequest->setSelect($select);
}
if (in_array($action, ['create', 'update'], TRUE)) {
$values = $apiRequest->getValues();
if (isset($values['value']) && $action === 'update' && !isset($values['id'])) {
$values['id'] = $values['value'];
}
$values['label'] ??= $values['name'] ?? NULL;
if (isset($values['label'])) {
$values['email'] = \CRM_Utils_Mail::pluckEmailFromHeader(rtrim($values['label']));
$values['name'] = trim(explode('"', $values['label'])[1]);
}
unset($values['label'], $values['value'], $values['option_group_id:name'], $values['option_group_id.name']);
$apiRequest->setValues($values);
}
}

}
2 changes: 1 addition & 1 deletion Civi/Api4/Generic/AbstractAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ abstract class AbstractAction implements \ArrayAccess {
/**
* @var array
*/
private $_entityFields;
protected $_entityFields;

/**
* @var array
Expand Down
51 changes: 51 additions & 0 deletions api/v3/DomainEmailAddress.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

/**
* APIv3 for CiviCRM DomainEmailAddress, mostly for the sake of backward-compatability.
*
* @see \Civi\API\Subscriber\DomainEmailLegacyOptionValueAdapter
*
* @package CiviCRM_APIv3
*/

/**
* Add or update a DomainEmailAddress.
*
* @param array $params
*
* @return array
*/
function civicrm_api3_domain_email_address_create($params) {
return _civicrm_api3_basic_create(_civicrm_api3_get_BAO(__FUNCTION__), $params);
}

/**
* Deletes an existing DomainEmailAddress.
*
* @param array $params
*
* @return array
*/
function civicrm_api3_domain_email_address_delete($params) {
return _civicrm_api3_basic_delete(_civicrm_api3_get_BAO(__FUNCTION__), $params);
}

/**
* Retrieve one or more DomainEmailAddress.
*
* @param array $params
*
* @return array
*/
function civicrm_api3_domain_email_address_get($params) {
return _civicrm_api3_basic_get(_civicrm_api3_get_BAO(__FUNCTION__), $params);
}
16 changes: 6 additions & 10 deletions tests/phpunit/CRM/Contact/Form/Task/EmailTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

use Civi\Api4\Activity;
use Civi\Api4\OptionValue;
use Civi\Api4\DomainEmailAddress;

/**
* Test class for CRM_Contact_Form_Task_Email.
Expand All @@ -27,9 +27,9 @@ protected function setUp(): void {
$this->individualCreate(['first_name' => 'Antonia', 'last_name' => 'D`souza']);
$this->individualCreate(['first_name' => 'Anthony', 'last_name' => 'Collins']);

$this->createTestEntity('OptionValue', [
'label' => '"Seamus Lee" <[email protected]>',
'option_group_id:name' => 'from_email_address',
$this->createTestEntity('DomainEmailAddress', [
'name' => 'Seamus Lee',
'email' => '[email protected]',
], 'aussie');
}

Expand All @@ -40,8 +40,8 @@ protected function setUp(): void {
*/
public function tearDown(): void {
Civi::settings()->set('allow_mail_from_logged_in_contact', 0);
if (!empty($this->ids['OptionValue'])) {
OptionValue::delete(FALSE)->addWhere('id', 'IN', $this->ids['OptionValue'])->execute();
if (!empty($this->ids['DomainEmailAddress'])) {
DomainEmailAddress::delete(FALSE)->addWhere('id', 'IN', $this->ids['DomainEmailAddress'])->execute();
}
parent::tearDown();
}
Expand All @@ -52,11 +52,7 @@ public function tearDown(): void {
public function testDomainEmailGeneration(): void {
$emails = CRM_Core_BAO_Email::domainEmails();
$this->assertNotEmpty($emails);
$optionValue = $this->callAPISuccess('OptionValue', 'Get', [
'id' => $this->ids['OptionValue']['aussie'],
]);
$this->assertArrayHasKey('"Seamus Lee" <[email protected]>', $emails);
$this->assertEquals('"Seamus Lee" <[email protected]>', $optionValue['values'][$this->ids['OptionValue']['aussie']]['label']);
}

/**
Expand Down
6 changes: 4 additions & 2 deletions tests/phpunit/CRM/Core/BAO/OptionValueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ public function testHandlingForMultiDefaultOptions(): void {
*
* The from_email_address supports a single default per domain.
*
* Note: This accesses the DomainEmailAddress entity through the OptionValue api, using the legacy adapter:
* @see \Civi\API\Subscriber\DomainEmailLegacyOptionValueAdapter
*
* @throws \CRM_Core_Exception
*/
public function testDefaultHandlingForFromEmailAddress(): void {
Expand All @@ -63,8 +66,7 @@ public function testDefaultHandlingForFromEmailAddress(): void {
->setValues([
'option_group_id:name' => 'from_email_address',
'is_default' => TRUE,
'label' => '[email protected]',
'name' => '[email protected]',
'label' => '"Test Email" <[email protected]>',
'value' => 3,
])
->execute();
Expand Down
Loading

0 comments on commit 01efb32

Please sign in to comment.