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 +--------------------------------------------------------------------+
13 namespace Civi\Api4\Utils
;
15 use Civi\API\Exception\NotImplementedException
;
16 use Civi\API\Exception\UnauthorizedException
;
18 use CRM_Core_DAO_AllCoreTables
as AllCoreTables
;
25 * @return \CRM_Core_DAO|string
26 * The BAO name for use in static calls. Return doc block is hacked to allow
27 * auto-completion of static methods
29 public static function getBAOFromApiName($entityName) {
30 if ($entityName === 'CustomValue' ||
strpos($entityName, 'Custom_') === 0) {
31 return 'CRM_Core_BAO_CustomValue';
33 $dao = AllCoreTables
::getFullName($entityName);
34 return $dao ? AllCoreTables
::getBAOClassName($dao) : NULL;
39 * @return string|\Civi\Api4\Generic\AbstractEntity
41 public static function getApiClass($entityName) {
42 $className = 'Civi\Api4\\' . $entityName;
43 if (class_exists($className)) {
46 return self
::getInfoItem($entityName, 'class');
50 * Get a piece of metadata about an entity
52 * @param string $entityName
53 * @param string $keyToReturn
56 public static function getInfoItem(string $entityName, string $keyToReturn) {
57 $provider = \Civi
::service('action_object_provider');
58 return $provider->getEntities()[$entityName][$keyToReturn] ??
NULL;
62 * Get name of unique identifier, typically "id"
63 * @param string $entityName
66 public static function getIdFieldName(string $entityName): string {
67 return self
::getInfoItem($entityName, 'primary_key')[0] ??
'id';
71 * Get table name of given entity
73 * @param string $entityName
77 public static function getTableName($entityName) {
78 return self
::getInfoItem($entityName, 'table_name');
82 * Given a sql table name, return the name of the api entity.
87 public static function getApiNameFromTableName($tableName) {
88 $provider = \Civi
::service('action_object_provider');
89 foreach ($provider->getEntities() as $entityName => $info) {
90 if (($info['table_name'] ??
NULL) === $tableName) {
100 public static function getOperators() {
101 $operators = \CRM_Core_DAO
::acceptedSQLOperators();
102 $operators[] = 'CONTAINS';
103 $operators[] = 'IS EMPTY';
104 $operators[] = 'IS NOT EMPTY';
105 $operators[] = 'REGEXP';
106 $operators[] = 'NOT REGEXP';
111 * For a given API Entity, return the types of custom fields it supports and the column they join to.
113 * @param string $entityName
114 * @return array{extends: array, column: string, grouping: mixed}|null
116 public static function getCustomGroupExtends(string $entityName) {
117 // Custom_group.extends pretty much maps 1-1 with entity names, except for Contact.
118 switch ($entityName) {
121 'extends' => array_merge(['Contact'], array_keys(\CRM_Core_SelectValues
::contactType())),
123 'grouping' => ['contact_type', 'contact_sub_type'],
126 case 'RelationshipCache':
128 'extends' => ['Relationship'],
129 'column' => 'relationship_id',
130 'grouping' => 'relationship_type_id',
133 $customGroupExtends = array_column(\CRM_Core_BAO_CustomGroup
::getCustomGroupExtendsOptions(), NULL, 'id');
134 $extendsSubGroups = \CRM_Core_BAO_CustomGroup
::getExtendsEntityColumnIdOptions();
135 if (array_key_exists($entityName, $customGroupExtends)) {
137 'extends' => [$entityName],
139 'grouping' => ($customGroupExtends[$entityName]['grouping'] ?
: array_column(\CRM_Utils_Array
::findAll($extendsSubGroups, ['extends' => $entityName]), 'grouping', 'id')) ?
: NULL,
146 * Checks if a custom group exists and is multivalued
148 * @param $customGroupName
150 * @throws \CRM_Core_Exception
152 public static function isCustomEntity($customGroupName) {
153 return $customGroupName && \CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroupName, 'is_multiple', 'name');
157 * Check if current user is authorized to perform specified action on a given entity.
159 * @param \Civi\Api4\Generic\AbstractAction $apiRequest
160 * @param array $record
161 * @param int|string $userID
162 * Contact ID of the user we are testing,. 0 for the anonymous user.
164 * @throws \API_Exception
165 * @throws \CRM_Core_Exception
166 * @throws \Civi\API\Exception\NotImplementedException
167 * @throws \Civi\API\Exception\UnauthorizedException
169 public static function checkAccessRecord(\Civi\Api4\Generic\AbstractAction
$apiRequest, array $record, int $userID) {
171 // Super-admins always have access to everything
172 if (\CRM_Core_Permission
::check('all CiviCRM permissions and ACLs', $userID)) {
176 // For get actions, just run a get and ACLs will be applied to the query.
177 // It's a cheap trick and not as efficient as not running the query at all,
178 // but BAO::checkAccess doesn't consistently check permissions for the "get" action.
179 if (is_a($apiRequest, '\Civi\Api4\Generic\DAOGetAction')) {
180 return (bool) $apiRequest->addSelect('id')->addWhere('id', '=', $record['id'])->execute()->count();
183 $event = new \Civi\Api4\Event\
AuthorizeRecordEvent($apiRequest, $record, $userID);
184 \Civi
::dispatcher()->dispatch('civi.api4.authorizeRecord', $event);
186 // Note: $bao::_checkAccess() is a quasi-listener. TODO: Convert to straight-up listener.
187 if ($event->isAuthorized() === NULL) {
188 $baoName = self
::getBAOFromApiName($apiRequest->getEntityName());
189 if ($baoName && method_exists($baoName, '_checkAccess')) {
190 $authorized = $baoName::_checkAccess($event->getEntityName(), $event->getActionName(), $event->getRecord(), $event->getUserID());
191 $event->setAuthorized($authorized);
194 $event->setAuthorized(TRUE);
197 return $event->isAuthorized();
201 * If the permissions of record $A are based on record $B, then use `checkAccessDelegated($B...)`
202 * to make see if access to $B is permitted.
204 * @param string $entityName
205 * @param string $actionName
206 * @param array $record
208 * Contact ID of the user we are testing, or 0 for the anonymous user.
211 * @throws \API_Exception
212 * @throws \CRM_Core_Exception
214 public static function checkAccessDelegated(string $entityName, string $actionName, array $record, int $userID) {
215 $apiRequest = Request
::create($entityName, $actionName, ['version' => 4]);
216 // First check gatekeeper permissions via the kernel
217 $kernel = \Civi
::service('civi_api_kernel');
219 [$actionObjectProvider] = $kernel->resolve($apiRequest);
220 $kernel->authorize($actionObjectProvider, $apiRequest);
222 catch (UnauthorizedException
$e) {
225 // Gatekeeper permission check passed, now check fine-grained permission
226 return static::checkAccessRecord($apiRequest, $record, $userID);
230 * @return \Civi\Api4\Service\Schema\SchemaMap
232 public static function getSchemaMap() {
233 $cache = \Civi
::cache('metadata');
234 $schemaMap = $cache->get('api4.schema.map');
236 $schemaMap = \Civi
::service('schema_map_builder')->build();
237 $cache->set('api4.schema.map', $schemaMap);
243 * Fetches database references + those returned by hook
245 * @see \CRM_Utils_Hook::referenceCounts()
246 * @param string $entityName
247 * @param int $entityId
248 * @return array{name: string, type: string, count: int, table: string|null, key: string|null}[]
249 * @throws NotImplementedException
251 public static function getRefCount(string $entityName, $entityId) {
252 $daoName = self
::getInfoItem($entityName, 'dao');
254 throw new NotImplementedException("Cannot getRefCount for $entityName - dao not found.");
256 /** @var \CRM_Core_DAO $dao */
257 $dao = new $daoName();
258 $dao->id
= $entityId;
259 return $dao->getReferenceCounts();
265 public static function getSearchableOptions(): array {
267 'primary' => ts('Primary'),
268 'secondary' => ts('Secondary'),
269 'bridge' => ts('Bridge'),
270 'none' => ts('None'),
275 * Collect the 'type' values from every entity.
279 public static function getEntityTypes(): array {
280 $provider = \Civi
::service('action_object_provider');
282 foreach ($provider->getEntities() as $entity) {
283 foreach ($entity['type'] ??
[] as $type) {
284 $entityTypes[$type] = $type;
291 * Get the suffixes supported by a given option group
293 * @param string|int $optionGroup
294 * OptionGroup id or name
296 * Is $optionGroup being passed as "id" or "name"
299 public static function getOptionValueFields($optionGroup, $key = 'name'): array {
300 // Prevent crash during upgrade
301 if (array_key_exists('option_value_fields', \CRM_Core_DAO_OptionGroup
::getSupportedFields())) {
302 $fields = \CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_OptionGroup', $optionGroup, 'option_value_fields', $key);
304 if (!isset($fields)) {
305 return ['name', 'label', 'description'];
307 return explode(',', $fields);