Commit | Line | Data |
---|---|---|
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 |
13 | namespace Civi\Api4\Utils; |
14 | ||
04309075 | 15 | use Civi\API\Exception\NotImplementedException; |
929a9585 | 16 | use Civi\API\Request; |
0b2471a8 | 17 | use Civi\Api4\Entity; |
8badd1d8 | 18 | use Civi\Api4\Event\CreateApi4RequestEvent; |
19b53e5b C |
19 | use CRM_Core_DAO_AllCoreTables as AllCoreTables; |
20 | ||
19b53e5b C |
21 | class 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 | } |