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 * An api Action object stores the parameters of the api call, and defines a _run function to execute the action.
31 * Every `protected` class var is considered a parameter (unless it starts with an underscore).
33 * Adding a `protected` var to your Action named e.g. `$thing` will automatically:
34 * - Provide a getter/setter (via `__call` MagicMethod) named `getThing()` and `setThing()`.
35 * - Expose the param in the Api Explorer (be sure to add a doc-block as it displays in the help panel).
36 * - Require a value for the param if you add the "@required" annotation.
38 * @method $this setCheckPermissions(bool $value) Enable/disable permission checks
39 * @method bool getCheckPermissions()
40 * @method $this setDebug(bool $value) Enable/disable debug output
41 * @method bool getDebug()
42 * @method $this setChain(array $chain)
43 * @method array getChain()
45 abstract class AbstractAction
implements \ArrayAccess
{
48 * Api version number; cannot be changed.
52 protected $version = 4;
55 * Additional api requests - will be called once per result.
57 * Keys can be any string - this will be the name given to the output.
59 * You can reference other values in the api results in this call by prefixing them with `$`.
61 * For example, you could create a contact and place them in a group by chaining the
62 * `GroupContact` api to the `Contact` api:
66 * ->setValue('first_name', 'Hello')
67 * ->addChain('add_a_group', GroupContact::create()
68 * ->setValue('contact_id', '$id')
69 * ->setValue('group_id', 123)
73 * This will substitute the id of the newly created contact with `$id`.
77 protected $chain = [];
80 * Whether to enforce acl permissions based on the current user.
82 * Setting to FALSE will disable permission checks and override ACLs.
83 * In REST/javascript this cannot be disabled.
87 protected $checkPermissions = TRUE;
90 * Add debugging info to the api result.
92 * When enabled, `$result->debug` will be populated with information about the api call,
93 * including sql queries executed.
95 * **Note:** with checkPermissions enabled, debug info will only be returned if the user has "view debug output" permission.
99 protected $debug = FALSE;
104 protected $_entityName;
109 protected $_actionName;
112 * @var \ReflectionClass
114 private $_reflection;
124 private $_entityFields;
129 private $_arrayStorage = [];
133 * Used to identify api calls for transactions
134 * @see \Civi\Core\Transaction\Manager
138 public $_debugOutput = [];
141 * Action constructor.
143 * @param string $entityName
144 * @param string $actionName
145 * @throws \API_Exception
147 public function __construct($entityName, $actionName) {
148 // If a namespaced class name is passed in
149 if (strpos($entityName, '\\') !== FALSE) {
150 $entityName = substr($entityName, strrpos($entityName, '\\') +
1);
152 $this->_entityName
= $entityName;
153 $this->_actionName
= $actionName;
154 $this->_id
= \Civi\API\Request
::getNextId();
158 * Strictly enforce api parameters
163 public function __set($name, $value) {
164 throw new \
API_Exception('Unknown api parameter');
170 * @throws \API_Exception
172 public function setVersion($val) {
173 if ($val !== 4 && $val !== '4') {
174 throw new \
API_Exception('Cannot modify api version');
180 * @param string $name
181 * Unique name for this chained request
182 * @param \Civi\Api4\Generic\AbstractAction $apiRequest
183 * @param string|int|array $index
184 * See `civicrm_api4()` for documentation of `$index` param
187 public function addChain($name, AbstractAction
$apiRequest, $index = NULL) {
188 $this->chain
[$name] = [$apiRequest->getEntityName(), $apiRequest->getActionName(), $apiRequest->getParams(), $index];
193 * Magic function to provide automatic getter/setter for params.
197 * @return static|mixed
198 * @throws \API_Exception
200 public function __call($name, $arguments) {
201 $param = lcfirst(substr($name, 3));
202 if (!$param ||
$param[0] == '_') {
203 throw new \
API_Exception('Unknown api parameter: ' . $name);
205 $mode = substr($name, 0, 3);
206 if ($this->paramExists($param)) {
209 return $this->$param;
212 $this->$param = $arguments[0];
216 throw new \
API_Exception('Unknown api parameter: ' . $name);
222 * At this point all the params have been sent in and we initiate the api call & return the result.
223 * This is basically the outer wrapper for api v4.
225 * @return \Civi\Api4\Generic\Result
226 * @throws \API_Exception
227 * @throws \Civi\API\Exception\UnauthorizedException
229 public function execute() {
230 /** @var \Civi\API\Kernel $kernel */
231 $kernel = \Civi
::service('civi_api_kernel');
232 $result = $kernel->runRequest($this);
233 if ($this->debug
&& (!$this->checkPermissions || \CRM_Core_Permission
::check('view debug output'))) {
234 $result->debug
['actionClass'] = get_class($this);
235 $result->debug
= array_merge($result->debug
, $this->_debugOutput
);
238 $result->debug
= NULL;
244 * @param \Civi\Api4\Generic\Result $result
246 abstract public function _run(Result
$result);
249 * Serialize this object's params into an array
252 public function getParams() {
254 foreach ($this->reflect()->getProperties(\ReflectionProperty
::IS_PROTECTED
) as $property) {
255 $name = $property->getName();
256 // Skip variables starting with an underscore
257 if ($name[0] != '_') {
258 $params[$name] = $this->$name;
265 * Get documentation for one or all params
267 * @param string $param
268 * @return array of arrays [description, type, default, (comment)]
270 public function getParamInfo($param = NULL) {
271 if (!isset($this->_paramInfo
)) {
272 $defaults = $this->getParamDefaults();
273 foreach ($this->reflect()->getProperties(\ReflectionProperty
::IS_PROTECTED
) as $property) {
274 $name = $property->getName();
275 if ($name != 'version' && $name[0] != '_') {
276 $this->_paramInfo
[$name] = ReflectionUtils
::getCodeDocs($property, 'Property');
277 $this->_paramInfo
[$name]['default'] = $defaults[$name];
281 return $param ?
$this->_paramInfo
[$param] : $this->_paramInfo
;
287 public function getEntityName() {
288 return $this->_entityName
;
295 public function getActionName() {
296 return $this->_actionName
;
300 * @param string $param
303 public function paramExists($param) {
304 return array_key_exists($param, $this->getParams());
310 protected function getParamDefaults() {
311 return array_intersect_key($this->reflect()->getDefaultProperties(), $this->getParams());
317 public function offsetExists($offset) {
318 return in_array($offset, ['entity', 'action', 'params', 'version', 'check_permissions', 'id']) ||
isset($this->_arrayStorage
[$offset]);
324 public function &offsetGet($offset) {
326 if (in_array($offset, ['entity', 'action'])) {
329 if (in_array($offset, ['entityName', 'actionName', 'params', 'version'])) {
330 $getter = 'get' . ucfirst($offset);
331 $val = $this->$getter();
334 if ($offset == 'check_permissions') {
335 return $this->checkPermissions
;
337 if ($offset == 'id') {
340 if (isset($this->_arrayStorage
[$offset])) {
341 return $this->_arrayStorage
[$offset];
349 public function offsetSet($offset, $value) {
350 if (in_array($offset, ['entity', 'action', 'entityName', 'actionName', 'params', 'version', 'id'])) {
351 throw new \
API_Exception('Cannot modify api4 state via array access');
353 if ($offset == 'check_permissions') {
354 $this->setCheckPermissions($value);
357 $this->_arrayStorage
[$offset] = $value;
364 public function offsetUnset($offset) {
365 if (in_array($offset, ['entity', 'action', 'entityName', 'actionName', 'params', 'check_permissions', 'version', 'id'])) {
366 throw new \
API_Exception('Cannot modify api4 state via array access');
368 unset($this->_arrayStorage
[$offset]);
372 * Is this api call permitted?
374 * This function is called if checkPermissions is set to true.
378 public function isAuthorized() {
379 $permissions = $this->getPermissions();
380 return \CRM_Core_Permission
::check($permissions);
386 public function getPermissions() {
387 $permissions = call_user_func(["\\Civi\\Api4\\" . $this->_entityName
, 'permissions']);
389 // applies to getFields, getActions, etc.
390 'meta' => ['access CiviCRM'],
391 // catch-all, applies to create, get, delete, etc.
392 'default' => ['administer CiviCRM'],
394 $action = $this->getActionName();
395 if (isset($permissions[$action])) {
396 return $permissions[$action];
398 elseif (in_array($action, ['getActions', 'getFields'])) {
399 return $permissions['meta'];
401 return $permissions['default'];
405 * Returns schema fields for this entity & action.
407 * Here we bypass the api wrapper and run the getFields action directly.
408 * This is because we DON'T want the wrapper to check permissions as this is an internal op,
409 * but we DO want permissions to be checked inside the getFields request so e.g. the api_key
410 * field can be conditionally included.
411 * @see \Civi\Api4\Action\Contact\GetFields
413 * @throws \API_Exception
416 public function entityFields() {
417 if (!$this->_entityFields
) {
418 $getFields = ActionUtil
::getAction($this->getEntityName(), 'getFields');
419 $result = new Result();
420 if (method_exists($this, 'getBaoName')) {
421 $getFields->setIncludeCustom(FALSE);
424 ->setCheckPermissions($this->checkPermissions
)
425 ->setAction($this->getActionName())
427 $this->_entityFields
= (array) $result->indexBy('name');
429 return $this->_entityFields
;
433 * @return \ReflectionClass
435 public function reflect() {
436 if (!$this->_reflection
) {
437 $this->_reflection
= new \
ReflectionClass($this);
439 return $this->_reflection
;
443 * Validates required fields for actions which create a new object.
447 * @throws \API_Exception
449 protected function checkRequiredFields($values) {
451 foreach ($this->entityFields() as $fieldName => $fieldInfo) {
452 if (!isset($values[$fieldName]) ||
$values[$fieldName] === '') {
453 if (!empty($fieldInfo['required']) && !isset($fieldInfo['default_value'])) {
454 $unmatched[] = $fieldName;
456 elseif (!empty($fieldInfo['required_if'])) {
457 if ($this->evaluateCondition($fieldInfo['required_if'], ['values' => $values])) {
458 $unmatched[] = $fieldName;
467 * This function is used internally for evaluating field annotations.
469 * It should never be passed raw user input.
471 * @param string $expr
472 * Conditional in php format e.g. $foo > $bar
474 * Variable name => value
476 * @throws \API_Exception
479 protected function evaluateCondition($expr, $vars) {
480 if (strpos($expr, '}') !== FALSE ||
strpos($expr, '{') !== FALSE) {
481 throw new \
API_Exception('Illegal character in expression');
483 $tpl = "{if $expr}1{else}0{/if}";
484 return (bool) trim(\CRM_Core_Smarty
::singleton()->fetchWith('string:' . $tpl, $vars));
488 * When in debug mode, this logs the callback function being used by a Basic*Action class.
490 * @param callable $callable
492 protected function addCallbackToDebugOutput($callable) {
493 if ($this->debug
&& empty($this->_debugOutput
['callback'])) {
494 if (is_scalar($callable)) {
495 $this->_debugOutput
['callback'] = (string) $callable;
497 elseif (is_array($callable)) {
498 foreach ($callable as $key => $unit) {
499 $this->_debugOutput
['callback'][$key] = is_object($unit) ?
get_class($unit) : (string) $unit;
502 elseif (is_object($callable)) {
503 $this->_debugOutput
['callback'] = get_class($callable);