(NFC) (dev/core#878) Simplify copyright header template
[civicrm-core.git] / Civi / Api4 / Generic / AbstractAction.php
CommitLineData
19b53e5b 1<?php
380f3545
TO
2
3/*
4 +--------------------------------------------------------------------+
5 | CiviCRM version 5 |
6 +--------------------------------------------------------------------+
f299f7db 7 | Copyright CiviCRM LLC (c) 2004-2020 |
380f3545
TO
8 +--------------------------------------------------------------------+
9 | This file is a part of CiviCRM. |
10 | |
11 | CiviCRM is free software; you can copy, modify, and distribute it |
12 | under the terms of the GNU Affero General Public License |
13 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | |
15 | CiviCRM is distributed in the hope that it will be useful, but |
16 | WITHOUT ANY WARRANTY; without even the implied warranty of |
17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
18 | See the GNU Affero General Public License for more details. |
19 | |
20 | You should have received a copy of the GNU Affero General Public |
21 | License and the CiviCRM Licensing Exception along |
22 | with this program; if not, contact CiviCRM LLC |
23 | at info[AT]civicrm[DOT]org. If you have questions about the |
24 | GNU Affero General Public License or the licensing of CiviCRM, |
25 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
26 +--------------------------------------------------------------------+
27 */
28
29/**
30 *
31 * @package CRM
ca5cec67 32 * @copyright CiviCRM LLC https://civicrm.org/licensing
380f3545
TO
33 * $Id$
34 *
35 */
36
19b53e5b
C
37namespace Civi\Api4\Generic;
38
39use Civi\Api4\Utils\ReflectionUtils;
40use Civi\Api4\Utils\ActionUtil;
41
42/**
43 * Base class for all api actions.
44 *
45 * @method $this setCheckPermissions(bool $value)
46 * @method bool getCheckPermissions()
47 * @method $this setChain(array $chain)
48 * @method array getChain()
49 */
50abstract class AbstractAction implements \ArrayAccess {
51
52 /**
53 * Api version number; cannot be changed.
54 *
55 * @var int
56 */
57 protected $version = 4;
58
59 /**
60 * Additional api requests - will be called once per result.
61 *
62 * Keys can be any string - this will be the name given to the output.
63 *
64 * You can reference other values in the api results in this call by prefixing them with $
65 *
66 * For example, you could create a contact and place them in a group by chaining the
67 * GroupContact api to the Contact api:
68 *
69 * Contact::create()
70 * ->setValue('first_name', 'Hello')
71 * ->addChain('add_to_a_group', GroupContact::create()->setValue('contact_id', '$id')->setValue('group_id', 123))
72 *
73 * This will substitute the id of the newly created contact with $id.
74 *
75 * @var array
76 */
77 protected $chain = [];
78
79 /**
80 * Whether to enforce acl permissions based on the current user.
81 *
82 * Setting to FALSE will disable permission checks and override ACLs.
83 * In REST/javascript this cannot be disabled.
84 *
85 * @var bool
86 */
87 protected $checkPermissions = TRUE;
88
89 /**
90 * @var string
91 */
92 protected $_entityName;
93
94 /**
95 * @var string
96 */
97 protected $_actionName;
98
99 /**
100 * @var \ReflectionClass
101 */
102 private $_reflection;
103
104 /**
105 * @var array
106 */
107 private $_paramInfo;
108
109 /**
110 * @var array
111 */
112 private $_entityFields;
113
114 /**
115 * @var array
116 */
117 private $_arrayStorage = [];
118
119 /**
120 * @var int
121 * Used to identify api calls for transactions
122 * @see \Civi\Core\Transaction\Manager
123 */
124 private $_id;
125
126 /**
127 * Action constructor.
128 *
129 * @param string $entityName
130 * @param string $actionName
131 * @throws \API_Exception
132 */
133 public function __construct($entityName, $actionName) {
134 // If a namespaced class name is passed in
135 if (strpos($entityName, '\\') !== FALSE) {
136 $entityName = substr($entityName, strrpos($entityName, '\\') + 1);
137 }
138 $this->_entityName = $entityName;
139 $this->_actionName = $actionName;
140 $this->_id = \Civi\API\Request::getNextId();
141 }
142
143 /**
144 * Strictly enforce api parameters
145 * @param $name
146 * @param $value
147 * @throws \Exception
148 */
149 public function __set($name, $value) {
150 throw new \API_Exception('Unknown api parameter');
151 }
152
153 /**
154 * @param int $val
155 * @return $this
156 * @throws \API_Exception
157 */
158 public function setVersion($val) {
159 if ($val != 4) {
160 throw new \API_Exception('Cannot modify api version');
161 }
162 return $this;
163 }
164
165 /**
166 * @param string $name
167 * Unique name for this chained request
168 * @param \Civi\Api4\Generic\AbstractAction $apiRequest
169 * @param string|int $index
170 * Either a string for how the results should be indexed e.g. 'name'
171 * or the index of a single result to return e.g. 0 for the first result.
172 * @return $this
173 */
174 public function addChain($name, AbstractAction $apiRequest, $index = NULL) {
175 $this->chain[$name] = [$apiRequest->getEntityName(), $apiRequest->getActionName(), $apiRequest->getParams(), $index];
176 return $this;
177 }
178
179 /**
180 * Magic function to provide addFoo, getFoo and setFoo for params.
181 *
182 * @param $name
183 * @param $arguments
184 * @return static|mixed
185 * @throws \API_Exception
186 */
187 public function __call($name, $arguments) {
188 $param = lcfirst(substr($name, 3));
189 if (!$param || $param[0] == '_') {
190 throw new \API_Exception('Unknown api parameter: ' . $name);
191 }
192 $mode = substr($name, 0, 3);
193 // Handle plural when adding to e.g. $values with "addValue" method.
194 if ($mode == 'add' && $this->paramExists($param . 's')) {
195 $param .= 's';
196 }
197 if ($this->paramExists($param)) {
198 switch ($mode) {
199 case 'get':
200 return $this->$param;
201
202 case 'set':
203 $this->$param = $arguments[0];
204 return $this;
205
206 case 'add':
207 if (!is_array($this->$param)) {
208 throw new \API_Exception('Cannot add to non-array param');
209 }
210 if (array_key_exists(1, $arguments)) {
211 $this->{$param}[$arguments[0]] = $arguments[1];
212 }
213 else {
214 $this->{$param}[] = $arguments[0];
215 }
216 return $this;
217 }
218 }
219 throw new \API_Exception('Unknown api parameter: ' . $name);
220 }
221
222 /**
223 * Invoke api call.
224 *
225 * At this point all the params have been sent in and we initiate the api call & return the result.
226 * This is basically the outer wrapper for api v4.
227 *
228 * @return \Civi\Api4\Generic\Result
229 * @throws \Civi\API\Exception\UnauthorizedException
230 */
231 public function execute() {
232 /** @var \Civi\API\Kernel $kernel */
233 $kernel = \Civi::service('civi_api_kernel');
234
235 return $kernel->runRequest($this);
236 }
237
238 /**
239 * @param \Civi\Api4\Generic\Result $result
240 */
241 abstract public function _run(Result $result);
242
243 /**
244 * Serialize this object's params into an array
245 * @return array
246 */
247 public function getParams() {
248 $params = [];
249 foreach ($this->reflect()->getProperties(\ReflectionProperty::IS_PROTECTED) as $property) {
250 $name = $property->getName();
251 // Skip variables starting with an underscore
252 if ($name[0] != '_') {
253 $params[$name] = $this->$name;
254 }
255 }
256 return $params;
257 }
258
259 /**
260 * Get documentation for one or all params
261 *
262 * @param string $param
263 * @return array of arrays [description, type, default, (comment)]
264 */
265 public function getParamInfo($param = NULL) {
266 if (!isset($this->_paramInfo)) {
267 $defaults = $this->getParamDefaults();
268 foreach ($this->reflect()->getProperties(\ReflectionProperty::IS_PROTECTED) as $property) {
269 $name = $property->getName();
270 if ($name != 'version' && $name[0] != '_') {
271 $this->_paramInfo[$name] = ReflectionUtils::getCodeDocs($property, 'Property');
272 $this->_paramInfo[$name]['default'] = $defaults[$name];
273 }
274 }
275 }
276 return $param ? $this->_paramInfo[$param] : $this->_paramInfo;
277 }
278
279 /**
280 * @return string
281 */
282 public function getEntityName() {
283 return $this->_entityName;
284 }
285
286 /**
287 *
288 * @return string
289 */
290 public function getActionName() {
291 return $this->_actionName;
292 }
293
294 /**
295 * @param string $param
296 * @return bool
297 */
298 public function paramExists($param) {
299 return array_key_exists($param, $this->getParams());
300 }
301
302 /**
303 * @return array
304 */
305 protected function getParamDefaults() {
306 return array_intersect_key($this->reflect()->getDefaultProperties(), $this->getParams());
307 }
308
309 /**
310 * @inheritDoc
311 */
312 public function offsetExists($offset) {
313 return in_array($offset, ['entity', 'action', 'params', 'version', 'check_permissions', 'id']) || isset($this->_arrayStorage[$offset]);
314 }
315
316 /**
317 * @inheritDoc
318 */
319 public function &offsetGet($offset) {
320 $val = NULL;
321 if (in_array($offset, ['entity', 'action'])) {
322 $offset .= 'Name';
323 }
324 if (in_array($offset, ['entityName', 'actionName', 'params', 'version'])) {
325 $getter = 'get' . ucfirst($offset);
326 $val = $this->$getter();
327 return $val;
328 }
329 if ($offset == 'check_permissions') {
330 return $this->checkPermissions;
331 }
332 if ($offset == 'id') {
333 return $this->_id;
334 }
335 if (isset($this->_arrayStorage[$offset])) {
336 return $this->_arrayStorage[$offset];
337 }
338 return $val;
339 }
340
341 /**
342 * @inheritDoc
343 */
344 public function offsetSet($offset, $value) {
345 if (in_array($offset, ['entity', 'action', 'entityName', 'actionName', 'params', 'version', 'id'])) {
346 throw new \API_Exception('Cannot modify api4 state via array access');
347 }
348 if ($offset == 'check_permissions') {
349 $this->setCheckPermissions($value);
350 }
351 else {
352 $this->_arrayStorage[$offset] = $value;
353 }
354 }
355
356 /**
357 * @inheritDoc
358 */
359 public function offsetUnset($offset) {
360 if (in_array($offset, ['entity', 'action', 'entityName', 'actionName', 'params', 'check_permissions', 'version', 'id'])) {
361 throw new \API_Exception('Cannot modify api4 state via array access');
362 }
363 unset($this->_arrayStorage[$offset]);
364 }
365
366 /**
367 * Is this api call permitted?
368 *
369 * This function is called if checkPermissions is set to true.
370 *
371 * @return bool
372 */
373 public function isAuthorized() {
374 $permissions = $this->getPermissions();
375 return \CRM_Core_Permission::check($permissions);
376 }
377
378 /**
379 * @return array
380 */
381 public function getPermissions() {
382 $permissions = call_user_func(["\\Civi\\Api4\\" . $this->_entityName, 'permissions']);
383 $permissions += [
384 // applies to getFields, getActions, etc.
385 'meta' => ['access CiviCRM'],
386 // catch-all, applies to create, get, delete, etc.
387 'default' => ['administer CiviCRM'],
388 ];
389 $action = $this->getActionName();
390 if (isset($permissions[$action])) {
391 return $permissions[$action];
392 }
393 elseif (in_array($action, ['getActions', 'getFields'])) {
394 return $permissions['meta'];
395 }
396 return $permissions['default'];
397 }
398
399 /**
400 * Returns schema fields for this entity & action.
401 *
402 * Here we bypass the api wrapper and execute the getFields action directly.
403 * This is because we DON'T want the wrapper to check permissions as this is an internal op,
404 * but we DO want permissions to be checked inside the getFields request so e.g. the api_key
405 * field can be conditionally included.
406 * @see \Civi\Api4\Action\Contact\GetFields
407 *
408 * @return array
409 */
410 public function entityFields() {
411 if (!$this->_entityFields) {
412 $getFields = ActionUtil::getAction($this->getEntityName(), 'getFields');
413 $result = new Result();
414 if (method_exists($this, 'getBaoName')) {
415 $getFields->setIncludeCustom(FALSE);
416 }
417 $getFields
418 ->setCheckPermissions($this->checkPermissions)
419 ->setAction($this->getActionName())
420 ->_run($result);
421 $this->_entityFields = (array) $result->indexBy('name');
422 }
423 return $this->_entityFields;
424 }
425
426 /**
427 * @return \ReflectionClass
428 */
429 public function reflect() {
430 if (!$this->_reflection) {
431 $this->_reflection = new \ReflectionClass($this);
432 }
433 return $this->_reflection;
434 }
435
436 /**
437 * Validates required fields for actions which create a new object.
438 *
439 * @param $values
440 * @return array
441 * @throws \API_Exception
442 */
443 protected function checkRequiredFields($values) {
444 $unmatched = [];
445 foreach ($this->entityFields() as $fieldName => $fieldInfo) {
446 if (!isset($values[$fieldName]) || $values[$fieldName] === '') {
447 if (!empty($fieldInfo['required']) && !isset($fieldInfo['default_value'])) {
448 $unmatched[] = $fieldName;
449 }
450 elseif (!empty($fieldInfo['required_if'])) {
451 if ($this->evaluateCondition($fieldInfo['required_if'], ['values' => $values])) {
452 $unmatched[] = $fieldName;
453 }
454 }
455 }
456 }
457 return $unmatched;
458 }
459
460 /**
461 * This function is used internally for evaluating field annotations.
462 *
463 * It should never be passed raw user input.
464 *
465 * @param string $expr
466 * Conditional in php format e.g. $foo > $bar
467 * @param array $vars
468 * Variable name => value
469 * @return bool
470 * @throws \API_Exception
471 * @throws \Exception
472 */
473 protected function evaluateCondition($expr, $vars) {
474 if (strpos($expr, '}') !== FALSE || strpos($expr, '{') !== FALSE) {
475 throw new \API_Exception('Illegal character in expression');
476 }
477 $tpl = "{if $expr}1{else}0{/if}";
478 return (bool) trim(\CRM_Core_Smarty::singleton()->fetchWith('string:' . $tpl, $vars));
479 }
480
481}