Merge pull request #22022 from jaapjansma/issue2948
[civicrm-core.git] / Civi / Api4 / Utils / CoreUtil.php
CommitLineData
19b53e5b
C
1<?php
2
380f3545
TO
3/*
4 +--------------------------------------------------------------------+
41498ac5 5 | Copyright CiviCRM LLC. All rights reserved. |
380f3545 6 | |
41498ac5
TO
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 |
380f3545
TO
10 +--------------------------------------------------------------------+
11 */
12
19b53e5b
C
13namespace Civi\Api4\Utils;
14
04309075 15use Civi\API\Exception\NotImplementedException;
929a9585 16use Civi\API\Request;
0b2471a8 17use Civi\Api4\Entity;
8badd1d8 18use Civi\Api4\Event\CreateApi4RequestEvent;
19b53e5b
C
19use CRM_Core_DAO_AllCoreTables as AllCoreTables;
20
19b53e5b
C
21class CoreUtil {
22
23 /**
19b53e5b
C
24 * @param $entityName
25 *
26 * @return \CRM_Core_DAO|string
27 * The BAO name for use in static calls. Return doc block is hacked to allow
28 * auto-completion of static methods
29 */
30 public static function getBAOFromApiName($entityName) {
31 if ($entityName === 'CustomValue' || strpos($entityName, 'Custom_') === 0) {
5e327f37 32 return 'CRM_Core_BAO_CustomValue';
19b53e5b 33 }
0b2471a8
CW
34 $dao = AllCoreTables::getFullName($entityName);
35 return $dao ? AllCoreTables::getBAOClassName($dao) : NULL;
eb378b8a
CW
36 }
37
38 /**
39 * @param $entityName
40 * @return string|\Civi\Api4\Generic\AbstractEntity
41 */
42 public static function getApiClass($entityName) {
8badd1d8
CW
43 $e = new CreateApi4RequestEvent($entityName);
44 \Civi::dispatcher()->dispatch('civi.api4.createRequest', $e);
45 return $e->className;
19b53e5b
C
46 }
47
974c7140 48 /**
b74d4d5c 49 * Get a piece of metadata about an entity
974c7140
CW
50 *
51 * @param string $entityName
52 * @param string $keyToReturn
53 * @return mixed
54 */
55 public static function getInfoItem(string $entityName, string $keyToReturn) {
b74d4d5c
CW
56 // Because this function might be called thousands of times per request, read directly
57 // from the cache set by Apiv4 Entity.get to avoid the processing overhead of the API wrapper.
58 $cached = \Civi::cache('metadata')->get('api4.entities.info');
59 if ($cached) {
60 $info = $cached[$entityName] ?? NULL;
61 }
62 // If the cache is empty, calling Entity.get will populate it and we'll use it next time.
63 else {
64 $info = Entity::get(FALSE)
65 ->addWhere('name', '=', $entityName)
66 ->addSelect($keyToReturn)
67 ->execute()->first();
68 }
0b2471a8 69 return $info ? $info[$keyToReturn] ?? NULL : NULL;
974c7140
CW
70 }
71
12e4505a
CW
72 /**
73 * Get name of unique identifier, typically "id"
74 * @param string $entityName
75 * @return string
76 */
77 public static function getIdFieldName(string $entityName): string {
78 return self::getInfoItem($entityName, 'primary_key')[0] ?? 'id';
79 }
80
19b53e5b 81 /**
5e327f37 82 * Get table name of given entity
19b53e5b 83 *
5e327f37 84 * @param string $entityName
19b53e5b
C
85 *
86 * @return string
87 */
5e327f37
CW
88 public static function getTableName($entityName) {
89 if (strpos($entityName, 'Custom_') === 0) {
90 $customGroup = substr($entityName, 7);
91 return \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroup, 'table_name', 'name');
92 }
93 return AllCoreTables::getTableForEntityName($entityName);
19b53e5b
C
94 }
95
96 /**
97 * Given a sql table name, return the name of the api entity.
98 *
99 * @param $tableName
5e327f37 100 * @return string|NULL
19b53e5b
C
101 */
102 public static function getApiNameFromTableName($tableName) {
5e327f37 103 $entityName = AllCoreTables::getBriefName(AllCoreTables::getClassForTable($tableName));
aa680b8d
CW
104 // Real entities
105 if ($entityName) {
106 // Verify class exists
107 return self::getApiClass($entityName) ? $entityName : NULL;
5e327f37 108 }
aa680b8d
CW
109 // Multi-value custom group pseudo-entities
110 $customGroup = \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $tableName, 'name', 'table_name');
111 return self::isCustomEntity($customGroup) ? "Custom_$customGroup" : NULL;
19b53e5b
C
112 }
113
9d2afe25
CW
114 /**
115 * @return string[]
116 */
117 public static function getOperators() {
39deabd6
CW
118 $operators = \CRM_Core_DAO::acceptedSQLOperators();
119 $operators[] = 'CONTAINS';
c0e68893 120 $operators[] = 'IS EMPTY';
121 $operators[] = 'IS NOT EMPTY';
b5599ad6
PF
122 $operators[] = 'REGEXP';
123 $operators[] = 'NOT REGEXP';
39deabd6 124 return $operators;
9d2afe25
CW
125 }
126
cbda9790
CW
127 /**
128 * For a given API Entity, return the types of custom fields it supports and the column they join to.
129 *
130 * @param string $entityName
131 * @return array|mixed|null
132 * @throws \API_Exception
133 * @throws \Civi\API\Exception\UnauthorizedException
134 */
135 public static function getCustomGroupExtends(string $entityName) {
136 // Custom_group.extends pretty much maps 1-1 with entity names, except for a couple oddballs (Contact, Participant).
137 switch ($entityName) {
138 case 'Contact':
139 return [
140 'extends' => array_merge(['Contact'], array_keys(\CRM_Core_SelectValues::contactType())),
141 'column' => 'id',
142 ];
143
144 case 'Participant':
145 return [
146 'extends' => ['Participant', 'ParticipantRole', 'ParticipantEventName', 'ParticipantEventType'],
147 'column' => 'id',
148 ];
149
150 case 'RelationshipCache':
151 return [
152 'extends' => ['Relationship'],
153 'column' => 'relationship_id',
154 ];
155 }
156 if (array_key_exists($entityName, \CRM_Core_SelectValues::customGroupExtends())) {
157 return [
158 'extends' => [$entityName],
159 'column' => 'id',
160 ];
161 }
162 return NULL;
163 }
164
aa680b8d
CW
165 /**
166 * Checks if a custom group exists and is multivalued
167 *
168 * @param $customGroupName
169 * @return bool
170 * @throws \CRM_Core_Exception
171 */
8badd1d8 172 public static function isCustomEntity($customGroupName) {
aa680b8d
CW
173 return $customGroupName && \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroupName, 'is_multiple', 'name');
174 }
175
929a9585
CW
176 /**
177 * Check if current user is authorized to perform specified action on a given entity.
178 *
849354a5 179 * @param \Civi\Api4\Generic\AbstractAction $apiRequest
929a9585 180 * @param array $record
70da3927
TO
181 * @param int|string $userID
182 * Contact ID of the user we are testing,. 0 for the anonymous user.
929a9585
CW
183 * @return bool
184 * @throws \API_Exception
185 * @throws \CRM_Core_Exception
186 * @throws \Civi\API\Exception\NotImplementedException
187 * @throws \Civi\API\Exception\UnauthorizedException
188 */
70da3927 189 public static function checkAccessRecord(\Civi\Api4\Generic\AbstractAction $apiRequest, array $record, int $userID) {
5623bf2a
CW
190
191 // Super-admins always have access to everything
192 if (\CRM_Core_Permission::check('all CiviCRM permissions and ACLs', $userID)) {
193 return TRUE;
194 }
195
929a9585
CW
196 // For get actions, just run a get and ACLs will be applied to the query.
197 // It's a cheap trick and not as efficient as not running the query at all,
198 // but BAO::checkAccess doesn't consistently check permissions for the "get" action.
849354a5 199 if (is_a($apiRequest, '\Civi\Api4\Generic\DAOGetAction')) {
e294cebf 200 return (bool) $apiRequest->addSelect('id')->addWhere('id', '=', $record['id'])->execute()->count();
929a9585 201 }
e294cebf 202
af4cccf7
TO
203 $event = new \Civi\Api4\Event\AuthorizeRecordEvent($apiRequest, $record, $userID);
204 \Civi::dispatcher()->dispatch('civi.api4.authorizeRecord', $event);
205
206 // Note: $bao::_checkAccess() is a quasi-listener. TODO: Convert to straight-up listener.
207 if ($event->isAuthorized() === NULL) {
208 $baoName = self::getBAOFromApiName($apiRequest->getEntityName());
209 if ($baoName && method_exists($baoName, '_checkAccess')) {
210 $authorized = $baoName::_checkAccess($event->getEntityName(), $event->getActionName(), $event->getRecord(), $event->getUserID());
211 $event->setAuthorized($authorized);
212 }
213 else {
214 $event->setAuthorized(TRUE);
215 }
929a9585 216 }
af4cccf7 217 return $event->isAuthorized();
929a9585
CW
218 }
219
6ea81ac6
TO
220 /**
221 * If the permissions of record $A are based on record $B, then use `checkAccessDelegated($B...)`
222 * to make see if access to $B is permitted.
223 *
224 * @param string $entityName
225 * @param string $actionName
226 * @param array $record
70da3927
TO
227 * @param int $userID
228 * Contact ID of the user we are testing, or 0 for the anonymous user.
6ea81ac6
TO
229 *
230 * @return bool
231 * @throws \API_Exception
232 * @throws \CRM_Core_Exception
233 */
70da3927 234 public static function checkAccessDelegated(string $entityName, string $actionName, array $record, int $userID) {
849354a5
TO
235 $apiRequest = Request::create($entityName, $actionName, ['version' => 4]);
236 // TODO: Should probably emit civi.api.authorize for checking guardian permission; but in APIv4 with std cfg, this is de-facto equivalent.
70da3927 237 if (!$apiRequest->isAuthorized()) {
849354a5
TO
238 return FALSE;
239 }
240 return static::checkAccessRecord($apiRequest, $record, $userID);
6ea81ac6
TO
241 }
242
b1b4a32c
CW
243 /**
244 * @return \Civi\Api4\Service\Schema\SchemaMap
245 */
246 public static function getSchemaMap() {
247 $cache = \Civi::cache('metadata');
248 $schemaMap = $cache->get('api4.schema.map');
249 if (!$schemaMap) {
250 $schemaMap = \Civi::service('schema_map_builder')->build();
251 $cache->set('api4.schema.map', $schemaMap);
252 }
253 return $schemaMap;
254 }
255
04309075
CW
256 /**
257 * Fetches database references + those returned by hook
258 *
259 * @see \CRM_Utils_Hook::referenceCounts()
260 * @param string $entityName
261 * @param int $entityId
262 * @return array{name: string, type: string, count: int, table: string|null, key: string|null}[]
263 * @throws NotImplementedException
264 */
265 public static function getRefCount(string $entityName, $entityId) {
266 $daoName = self::getInfoItem($entityName, 'dao');
267 if (!$daoName) {
268 throw new NotImplementedException("Cannot getRefCount for $entityName - dao not found.");
269 }
270 /** @var \CRM_Core_DAO $dao */
271 $dao = new $daoName();
272 $dao->id = $entityId;
273 return $dao->getReferenceCounts();
274 }
275
19b53e5b 276}