$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'),
],
--- /dev/null
+<?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\Api4\Action\WorkflowMessage;
+
+use Civi\Api4\Event\ValidateValuesEvent;
+use Civi\WorkflowMessage\WorkflowMessage;
+
+/**
+ * Render a message.
+ *
+ * @method $this setValues(array $rows) Set the list of records to be rendered.
+ * @method array getValues()
+ * @method $this setMessageTemplate(array|null $fragments) Set of messages to be rendered.
+ * @method array|null getMessageTemplate()
+ * @method $this setMessageTemplateId(int|null $id) Set of messages to be rendered.
+ * @method int|null getMessageTemplateId()
+ * @method $this setWorkflow(string $workflow)
+ * @method string getWorkflow()
+ * @method $this setErrorLevel(string $workflow)
+ * @method string getErrorLevel()
+ */
+class Render extends \Civi\Api4\Generic\AbstractAction {
+
+ /**
+ * Abort if the validator finds any issues at this error level.
+ *
+ * @var string
+ * @options error,warning,info
+ */
+ protected $errorLevel = 'error';
+
+ /**
+ * Symbolic name of the workflow step for which we need a message.
+ * @var string
+ * @required
+ */
+ protected $workflow;
+
+ /**
+ * @var array
+ */
+ protected $values = [];
+
+ /**
+ * Load and render a specific message template (by ID).
+ *
+ * @var int|null
+ */
+ protected $messageTemplateId;
+
+ /**
+ * Use a draft message template.
+ *
+ * @var array|null
+ * - `subject`: Message template (eg `Hello {contact.first_name}!`)
+ * - `text`: Message template (eg `Hello {contact.first_name}!`)
+ * - `html`: Message template (eg `<p>Hello {contact.first_name}!</p>`)
+ */
+ 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;
+ }
+
+}
--- /dev/null
+<?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 |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+
+namespace Civi\Api4;
+
+/**
+ * A WorkflowMessage describes the inputs to an automated email messages, and it
+ * allows you to render or preview the content fo automated email messages.
+ *
+ * For example, when a constituent donates online, CiviContribute uses the
+ * `contribution_online_receipt` workflow message. This expects certain inputs
+ * (eg `contactId` and `contributionId`) and supports certain tokens
+ * (eg `{contribution.total_amount}`).
+ *
+ * WorkflowMessages are related to MessageTemplates (by way of
+ * `WorkflowMessage.name`<=>`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;
+ }
+
+}
namespace Civi\WorkflowMessage;
+use Civi\Api4\Utils\ReflectionUtils;
use Civi\WorkflowMessage\Exception\WorkflowMessageException;
/**
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;
+ }
+
}
--- /dev/null
+<?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 |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+
+
+namespace api\v4\Entity;
+
+use api\v4\UnitTestCase;
+use Civi\Api4\WorkflowMessage;
+
+/**
+ * @group headless
+ */
+class WorkflowMessageTest extends UnitTestCase {
+
+ public function testGet() {
+ $result = \Civi\Api4\WorkflowMessage::get(0)
+ ->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));
+ }
+ }
+ }
+ }
+
+}