4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
7 | This work is published under the GNU AGPLv3 license with some |
8 | permitted exceptions and without any warranty. For full license |
9 | and copyright information, see https://civicrm.org/licensing |
10 +--------------------------------------------------------------------+
16 * @copyright CiviCRM LLC https://civicrm.org/licensing
21 namespace Civi\Api4\Generic
;
23 use Civi\Api4\Utils\ReflectionUtils
;
24 use Civi\Api4\Utils\ActionUtil
;
27 * Base class for all api actions.
29 * @method $this setCheckPermissions(bool $value) Enable/disable permission checks
30 * @method bool getCheckPermissions()
31 * @method $this setDebug(bool $value) Enable/disable debug output
32 * @method bool getDebug()
33 * @method $this setChain(array $chain)
34 * @method array getChain()
36 abstract class AbstractAction
implements \ArrayAccess
{
39 * Api version number; cannot be changed.
43 protected $version = 4;
46 * Additional api requests - will be called once per result.
48 * Keys can be any string - this will be the name given to the output.
50 * You can reference other values in the api results in this call by prefixing them with $
52 * For example, you could create a contact and place them in a group by chaining the
53 * GroupContact api to the Contact api:
56 * ->setValue('first_name', 'Hello')
57 * ->addChain('add_to_a_group', GroupContact::create()->setValue('contact_id', '$id')->setValue('group_id', 123))
59 * This will substitute the id of the newly created contact with $id.
63 protected $chain = [];
66 * Whether to enforce acl permissions based on the current user.
68 * Setting to FALSE will disable permission checks and override ACLs.
69 * In REST/javascript this cannot be disabled.
73 protected $checkPermissions = TRUE;
76 * Add debugging info to the api result.
80 protected $debug = FALSE;
85 protected $_entityName;
90 protected $_actionName;
93 * @var \ReflectionClass
105 private $_entityFields;
110 private $_arrayStorage = [];
114 * Used to identify api calls for transactions
115 * @see \Civi\Core\Transaction\Manager
119 protected $_debugOutput = [];
122 * Action constructor.
124 * @param string $entityName
125 * @param string $actionName
126 * @throws \API_Exception
128 public function __construct($entityName, $actionName) {
129 // If a namespaced class name is passed in
130 if (strpos($entityName, '\\') !== FALSE) {
131 $entityName = substr($entityName, strrpos($entityName, '\\') +
1);
133 $this->_entityName
= $entityName;
134 $this->_actionName
= $actionName;
135 $this->_id
= \Civi\API\Request
::getNextId();
139 * Strictly enforce api parameters
144 public function __set($name, $value) {
145 throw new \
API_Exception('Unknown api parameter');
151 * @throws \API_Exception
153 public function setVersion($val) {
154 if ($val !== 4 && $val !== '4') {
155 throw new \
API_Exception('Cannot modify api version');
161 * @param string $name
162 * Unique name for this chained request
163 * @param \Civi\Api4\Generic\AbstractAction $apiRequest
164 * @param string|int $index
165 * Either a string for how the results should be indexed e.g. 'name'
166 * or the index of a single result to return e.g. 0 for the first result.
169 public function addChain($name, AbstractAction
$apiRequest, $index = NULL) {
170 $this->chain
[$name] = [$apiRequest->getEntityName(), $apiRequest->getActionName(), $apiRequest->getParams(), $index];
175 * Magic function to provide automatic getter/setter for params.
179 * @return static|mixed
180 * @throws \API_Exception
182 public function __call($name, $arguments) {
183 $param = lcfirst(substr($name, 3));
184 if (!$param ||
$param[0] == '_') {
185 throw new \
API_Exception('Unknown api parameter: ' . $name);
187 $mode = substr($name, 0, 3);
188 if ($this->paramExists($param)) {
191 return $this->$param;
194 $this->$param = $arguments[0];
198 throw new \
API_Exception('Unknown api parameter: ' . $name);
204 * At this point all the params have been sent in and we initiate the api call & return the result.
205 * This is basically the outer wrapper for api v4.
207 * @return \Civi\Api4\Generic\Result
208 * @throws \API_Exception
209 * @throws \Civi\API\Exception\UnauthorizedException
211 public function execute() {
212 /** @var \Civi\API\Kernel $kernel */
213 $kernel = \Civi
::service('civi_api_kernel');
214 $result = $kernel->runRequest($this);
215 if ($this->debug
&& (!$this->checkPermissions || \CRM_Core_Permission
::check('view debug output'))) {
216 $result->debug
= array_merge($result->debug
, $this->_debugOutput
);
219 $result->debug
= NULL;
225 * @param \Civi\Api4\Generic\Result $result
227 abstract public function _run(Result
$result);
230 * Serialize this object's params into an array
233 public function getParams() {
235 foreach ($this->reflect()->getProperties(\ReflectionProperty
::IS_PROTECTED
) as $property) {
236 $name = $property->getName();
237 // Skip variables starting with an underscore
238 if ($name[0] != '_') {
239 $params[$name] = $this->$name;
246 * Get documentation for one or all params
248 * @param string $param
249 * @return array of arrays [description, type, default, (comment)]
251 public function getParamInfo($param = NULL) {
252 if (!isset($this->_paramInfo
)) {
253 $defaults = $this->getParamDefaults();
254 foreach ($this->reflect()->getProperties(\ReflectionProperty
::IS_PROTECTED
) as $property) {
255 $name = $property->getName();
256 if ($name != 'version' && $name[0] != '_') {
257 $this->_paramInfo
[$name] = ReflectionUtils
::getCodeDocs($property, 'Property');
258 $this->_paramInfo
[$name]['default'] = $defaults[$name];
262 return $param ?
$this->_paramInfo
[$param] : $this->_paramInfo
;
268 public function getEntityName() {
269 return $this->_entityName
;
276 public function getActionName() {
277 return $this->_actionName
;
281 * @param string $param
284 public function paramExists($param) {
285 return array_key_exists($param, $this->getParams());
291 protected function getParamDefaults() {
292 return array_intersect_key($this->reflect()->getDefaultProperties(), $this->getParams());
298 public function offsetExists($offset) {
299 return in_array($offset, ['entity', 'action', 'params', 'version', 'check_permissions', 'id']) ||
isset($this->_arrayStorage
[$offset]);
305 public function &offsetGet($offset) {
307 if (in_array($offset, ['entity', 'action'])) {
310 if (in_array($offset, ['entityName', 'actionName', 'params', 'version'])) {
311 $getter = 'get' . ucfirst($offset);
312 $val = $this->$getter();
315 if ($offset == 'check_permissions') {
316 return $this->checkPermissions
;
318 if ($offset == 'id') {
321 if (isset($this->_arrayStorage
[$offset])) {
322 return $this->_arrayStorage
[$offset];
330 public function offsetSet($offset, $value) {
331 if (in_array($offset, ['entity', 'action', 'entityName', 'actionName', 'params', 'version', 'id'])) {
332 throw new \
API_Exception('Cannot modify api4 state via array access');
334 if ($offset == 'check_permissions') {
335 $this->setCheckPermissions($value);
338 $this->_arrayStorage
[$offset] = $value;
345 public function offsetUnset($offset) {
346 if (in_array($offset, ['entity', 'action', 'entityName', 'actionName', 'params', 'check_permissions', 'version', 'id'])) {
347 throw new \
API_Exception('Cannot modify api4 state via array access');
349 unset($this->_arrayStorage
[$offset]);
353 * Is this api call permitted?
355 * This function is called if checkPermissions is set to true.
359 public function isAuthorized() {
360 $permissions = $this->getPermissions();
361 return \CRM_Core_Permission
::check($permissions);
367 public function getPermissions() {
368 $permissions = call_user_func(["\\Civi\\Api4\\" . $this->_entityName
, 'permissions']);
370 // applies to getFields, getActions, etc.
371 'meta' => ['access CiviCRM'],
372 // catch-all, applies to create, get, delete, etc.
373 'default' => ['administer CiviCRM'],
375 $action = $this->getActionName();
376 if (isset($permissions[$action])) {
377 return $permissions[$action];
379 elseif (in_array($action, ['getActions', 'getFields'])) {
380 return $permissions['meta'];
382 return $permissions['default'];
386 * Returns schema fields for this entity & action.
388 * Here we bypass the api wrapper and run the getFields action directly.
389 * This is because we DON'T want the wrapper to check permissions as this is an internal op,
390 * but we DO want permissions to be checked inside the getFields request so e.g. the api_key
391 * field can be conditionally included.
392 * @see \Civi\Api4\Action\Contact\GetFields
394 * @throws \API_Exception
397 public function entityFields() {
398 if (!$this->_entityFields
) {
399 $getFields = ActionUtil
::getAction($this->getEntityName(), 'getFields');
400 $result = new Result();
401 if (method_exists($this, 'getBaoName')) {
402 $getFields->setIncludeCustom(FALSE);
405 ->setCheckPermissions($this->checkPermissions
)
406 ->setAction($this->getActionName())
408 $this->_entityFields
= (array) $result->indexBy('name');
410 return $this->_entityFields
;
414 * @return \ReflectionClass
416 public function reflect() {
417 if (!$this->_reflection
) {
418 $this->_reflection
= new \
ReflectionClass($this);
420 return $this->_reflection
;
424 * Validates required fields for actions which create a new object.
428 * @throws \API_Exception
430 protected function checkRequiredFields($values) {
432 foreach ($this->entityFields() as $fieldName => $fieldInfo) {
433 if (!isset($values[$fieldName]) ||
$values[$fieldName] === '') {
434 if (!empty($fieldInfo['required']) && !isset($fieldInfo['default_value'])) {
435 $unmatched[] = $fieldName;
437 elseif (!empty($fieldInfo['required_if'])) {
438 if ($this->evaluateCondition($fieldInfo['required_if'], ['values' => $values])) {
439 $unmatched[] = $fieldName;
448 * This function is used internally for evaluating field annotations.
450 * It should never be passed raw user input.
452 * @param string $expr
453 * Conditional in php format e.g. $foo > $bar
455 * Variable name => value
457 * @throws \API_Exception
460 protected function evaluateCondition($expr, $vars) {
461 if (strpos($expr, '}') !== FALSE ||
strpos($expr, '{') !== FALSE) {
462 throw new \
API_Exception('Illegal character in expression');
464 $tpl = "{if $expr}1{else}0{/if}";
465 return (bool) trim(\CRM_Core_Smarty
::singleton()->fetchWith('string:' . $tpl, $vars));