From: Tim Otten Date: Wed, 14 Jul 2021 00:21:52 +0000 (-0700) Subject: Add WorkflowMessage.get and WorkflowMessage.render APIs X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=ebd92dafbecd02b9fa3431b403ae04fc7cf88cf6;p=civicrm-core.git Add WorkflowMessage.get and WorkflowMessage.render APIs --- diff --git a/CRM/Core/Permission.php b/CRM/Core/Permission.php index 5166b65466..2ad9e9977e 100644 --- a/CRM/Core/Permission.php +++ b/CRM/Core/Permission.php @@ -841,6 +841,10 @@ class CRM_Core_Permission { $prefix . ts('administer payment processors'), ts('Add, Update, or Disable Payment Processors'), ], + 'render templates' => [ + $prefix . ts('render templates'), + ts('Render open-ended template content. (Additional constraints may apply to autoloaded records and specific notations.)'), + ], 'edit message templates' => [ $prefix . ts('edit message templates'), ], diff --git a/Civi/Api4/Action/WorkflowMessage/Render.php b/Civi/Api4/Action/WorkflowMessage/Render.php new file mode 100644 index 0000000000..86611072b9 --- /dev/null +++ b/Civi/Api4/Action/WorkflowMessage/Render.php @@ -0,0 +1,168 @@ +Hello {contact.first_name}!

`) + */ + protected $messageTemplate; + + /** + * @var \Civi\WorkflowMessage\WorkflowMessageInterface + */ + protected $_model; + + public function _run(\Civi\Api4\Generic\Result $result) { + $this->validateValues(); + + $r = \CRM_Core_BAO_MessageTemplate::renderTemplate([ + 'model' => $this->_model, + 'messageTemplate' => $this->getMessageTemplate(), + 'messageTemplateId' => $this->getMessageTemplateId(), + ]); + + $result[] = \CRM_Utils_Array::subset($r, ['subject', 'html', 'text']); + } + + /** + * The token-processor supports a range of context parameters. We enforce different security rules for each kind of input. + * + * Broadly, this distinguishes between a few values: + * - Autoloaded data (e.g. 'contactId', 'activityId'). We need to ensure that the specific records are visible and extant. + * - Inputted data (e.g. 'contact'). We merely ensure that these are type-correct. + * - Prohibited/extended options, e.g. 'smarty' + */ + protected function validateValues() { + $rows = [$this->getValues()]; + $e = new ValidateValuesEvent($this, $rows, new \CRM_Utils_LazyArray(function () use ($rows) { + return array_map( + function ($row) { + return ['old' => NULL, 'new' => $row]; + }, + $rows + ); + })); + $this->onValidateValues($e); + \Civi::dispatcher()->dispatch('civi.api4.validate', $e); + if (!empty($e->errors)) { + throw $e->toException(); + } + } + + protected function onValidateValues(ValidateValuesEvent $e) { + $errorWeightMap = \CRM_Core_Error_Log::getMap(); + $errorWeight = $errorWeightMap[$this->getErrorLevel()]; + + if (count($e->records) !== 1) { + throw new \CRM_Core_Exception("Expected exactly one record to validate"); + } + foreach ($e->records as $recordKey => $record) { + /** @var \Civi\WorkflowMessage\WorkflowMessageInterface $w */ + $w = $this->_model = WorkflowMessage::create($this->getWorkflow(), [ + 'modelProps' => $record, + ]); + $fields = $w->getFields(); + + $unknowns = array_diff(array_keys($record), array_keys($fields)); + foreach ($unknowns as $fieldName) { + $e->addError($recordKey, $fieldName, 'unknown_field', ts('Unknown field (%1). Templates may only be executed with supported fields.', [ + 1 => $fieldName, + ])); + } + + // Merge intrinsic validations + foreach ($w->validate() as $issue) { + if ($errorWeightMap[$issue['severity']] < $errorWeight) { + $e->addError($recordKey, $issue['fields'], $issue['name'], $issue['message']); + } + } + + // Add checks which don't fit in WFM::validate + foreach ($fields as $fieldName => $fieldSpec) { + $fieldValue = $record[$fieldName] ?? NULL; + if ($fieldSpec->getFkEntity() && !empty($fieldValue)) { + if (!empty($params['check_permissions']) && !\Civi\Api4\Utils\CoreUtil::checkAccessDelegated($fieldSpec->getFkEntity(), 'get', ['id' => $fieldValue], CRM_Core_Session::getLoggedInContactID() ?: 0)) { + $e->addError($recordKey, $fieldName, 'nonexistent_id', ts('Referenced record does not exist or is not visible (%1).', [ + 1 => $this->getWorkflow() . '::' . $fieldName, + ])); + } + } + } + } + } + + public function fields() { + return []; + // We don't currently get the name of the workflow. But if we do... + //$item = \Civi\WorkflowMessage\WorkflowMessage::create($this->workflow); + ///** @var \Civi\WorkflowMessage\FieldSpec[] $fields */ + //$fields = $item->getFields(); + //$array = []; + //foreach ($fields as $name => $field) { + // $array[$name] = $field->toArray(); + //} + //return $array; + } + +} diff --git a/Civi/Api4/WorkflowMessage.php b/Civi/Api4/WorkflowMessage.php new file mode 100644 index 0000000000..565bff7953 --- /dev/null +++ b/Civi/Api4/WorkflowMessage.php @@ -0,0 +1,120 @@ +`MessageTemplate.workflow_name`). + * The WorkflowMessage defines the _contract_ or _processing_ of the + * message, and the MessageTemplate defines the _literal prose_. The prose + * would change frequently (eg for different deployments, locales, timeframes, + * and other whims), but contract would change conservatively (eg with a + * code-update and with some attention to backward-compatibility/change-management). + * + * @searchable none + * @since 5.43 + * @package Civi\Api4 + */ +class WorkflowMessage extends Generic\AbstractEntity { + + /** + * @param bool $checkPermissions + * @return Generic\BasicGetAction + */ + public static function get($checkPermissions = TRUE) { + return (new Generic\BasicGetAction(__CLASS__, __FUNCTION__, function ($get) { + return \Civi\WorkflowMessage\WorkflowMessage::getWorkflowSpecs(); + }))->setCheckPermissions($checkPermissions); + } + + /** + * @param bool $checkPermissions + * + * @return \Civi\Api4\Action\WorkflowMessage\Render + */ + public static function render($checkPermissions = TRUE) { + return (new Action\WorkflowMessage\Render(__CLASS__, __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + + /** + * @param bool $checkPermissions + * @return Generic\BasicGetFieldsAction + */ + public static function getFields($checkPermissions = TRUE) { + return (new Generic\BasicGetFieldsAction(__CLASS__, __FUNCTION__, function() { + return [ + [ + 'name' => 'name', + 'title' => 'Name', + 'data_type' => 'String', + ], + [ + 'name' => 'group', + 'title' => 'Group', + 'data_type' => 'String', + ], + [ + 'name' => 'class', + 'title' => 'Class', + 'data_type' => 'String', + ], + [ + 'name' => 'description', + 'title' => 'Description', + 'data_type' => 'String', + ], + ]; + }))->setCheckPermissions($checkPermissions); + } + + public static function permissions() { + return [ + 'meta' => ['access CiviCRM'], + 'default' => ['administer CiviCRM'], + 'render' => [ + // nested array = OR + [ + 'edit message templates', + 'edit user-driven message templates', + 'edit system workflow message templates', + 'render templates', + ], + ], + ]; + } + + /** + * @inheritDoc + */ + public static function getInfo() { + $info = parent::getInfo(); + $info['primary_key'] = ['name']; + return $info; + } + +} diff --git a/Civi/WorkflowMessage/WorkflowMessage.php b/Civi/WorkflowMessage/WorkflowMessage.php index 3647286ef5..5ad47eaed4 100644 --- a/Civi/WorkflowMessage/WorkflowMessage.php +++ b/Civi/WorkflowMessage/WorkflowMessage.php @@ -12,6 +12,7 @@ namespace Civi\WorkflowMessage; +use Civi\Api4\Utils\ReflectionUtils; use Civi\WorkflowMessage\Exception\WorkflowMessageException; /** @@ -170,4 +171,38 @@ class WorkflowMessage { return $map; } + /** + * Get general description of available workflow-messages. + * + * @return array + * Array(string $workflowName => string $className). + * Ex: ["case_activity" => ["name" => "case_activity", "group" => "msg_workflow_case"] + * @internal + */ + public static function getWorkflowSpecs() { + $compute = function() { + $keys = ['name', 'group', 'class', 'description', 'comment']; + $list = []; + foreach (self::getWorkflowNameClassMap() as $name => $class) { + $specs = [ + 'name' => $name, + 'group' => \CRM_Utils_Constant::value($class . '::GROUP'), + 'class' => $class, + ]; + $list[$name] = \CRM_Utils_Array::subset( + array_merge(ReflectionUtils::getCodeDocs(new \ReflectionClass($class)), $specs), + $keys); + } + return $list; + }; + + $cache = \Civi::cache('long'); + $cacheKey = 'WorkflowMessage-' . __FUNCTION__; + $list = $cache->get($cacheKey); + if ($list === NULL) { + $cache->set($cacheKey, $list = $compute()); + } + return $list; + } + } diff --git a/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php b/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php new file mode 100644 index 0000000000..056dc396bb --- /dev/null +++ b/tests/phpunit/api/v4/Entity/WorkflowMessageTest.php @@ -0,0 +1,94 @@ +addWhere('name', 'LIKE', 'case%') + ->execute() + ->indexBy('name'); + $this->assertTrue(isset($result['case_activity'])); + } + + public function testRenderDefaultTemplate() { + $ex = \Civi\Api4\WorkflowMessageExample::get(0) + ->addWhere('name', '=', 'case_activity.class_1') + ->addSelect('data', 'workflow') + ->addChain('render', WorkflowMessage::render() + ->setWorkflow('$workflow') + ->setValues('$data.modelProps')) + ->execute() + ->single(); + $result = $ex['render'][0]; + $this->assertRegExp('/Case ID : 1234/', $result['text']); + } + + public function testRenderCustomTemplate() { + $ex = \Civi\Api4\WorkflowMessageExample::get(0) + ->addWhere('name', '=', 'case_activity.class_1') + ->addSelect('data') + ->execute() + ->single(); + $result = \Civi\Api4\WorkflowMessage::render(0) + ->setWorkflow('case_activity') + ->setValues($ex['data']['modelProps']) + ->setMessageTemplate([ + 'msg_text' => 'The role is {$contact.role}.', + ]) + ->execute() + ->single(); + $this->assertRegExp('/The role is myrole./', $result['text']); + } + + public function testRenderExamples() { + $examples = \Civi\Api4\WorkflowMessageExample::get(0) + ->addWhere('tags', 'CONTAINS', 'phpunit') + ->addSelect('name', 'workflow', 'data', 'asserts') + ->execute(); + $this->assertTrue($examples->rowCount >= 1); + foreach ($examples as $example) { + $this->assertTrue(!empty($example['data']['modelProps']), sprintf("Example (%s) is tagged phpunit. It should have modelProps.", $example['name'])); + $this->assertTrue(!empty($example['asserts']['default']), sprintf("Example (%s) is tagged phpunit. It should have assertions.", $example['name'])); + $result = \Civi\Api4\WorkflowMessage::render(0) + ->setWorkflow($example['workflow']) + ->setValues($example['data']['modelProps']) + ->execute() + ->single(); + foreach ($example['asserts']['default'] as $num => $assert) { + $msg = sprintf('Check assertion(%s) on example (%s)', $num, $example['name']); + if (isset($assert['regex'])) { + $this->assertRegExp($assert['regex'], $result[$assert['for']], $msg); + } + else { + $this->fail('Unrecognized assertion: ' . json_encode($assert)); + } + } + } + } + +}