Merge pull request #24117 from civicrm/5.52
[civicrm-core.git] / Civi / Api4 / Utils / CoreUtil.php
1 <?php
2
3 /*
4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
6 | |
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 +--------------------------------------------------------------------+
11 */
12
13 namespace Civi\Api4\Utils;
14
15 use Civi\API\Exception\NotImplementedException;
16 use Civi\API\Exception\UnauthorizedException;
17 use Civi\API\Request;
18 use CRM_Core_DAO_AllCoreTables as AllCoreTables;
19
20 class CoreUtil {
21
22 /**
23 * @param $entityName
24 *
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
28 */
29 public static function getBAOFromApiName($entityName) {
30 if ($entityName === 'CustomValue' || strpos($entityName, 'Custom_') === 0) {
31 return 'CRM_Core_BAO_CustomValue';
32 }
33 $dao = AllCoreTables::getFullName($entityName);
34 return $dao ? AllCoreTables::getBAOClassName($dao) : NULL;
35 }
36
37 /**
38 * @param $entityName
39 * @return string|\Civi\Api4\Generic\AbstractEntity
40 */
41 public static function getApiClass($entityName) {
42 $className = 'Civi\Api4\\' . $entityName;
43 if (class_exists($className)) {
44 return $className;
45 }
46 return self::getInfoItem($entityName, 'class');
47 }
48
49 /**
50 * Get a piece of metadata about an entity
51 *
52 * @param string $entityName
53 * @param string $keyToReturn
54 * @return mixed
55 */
56 public static function getInfoItem(string $entityName, string $keyToReturn) {
57 $provider = \Civi::service('action_object_provider');
58 return $provider->getEntities()[$entityName][$keyToReturn] ?? NULL;
59 }
60
61 /**
62 * Get name of unique identifier, typically "id"
63 * @param string $entityName
64 * @return string
65 */
66 public static function getIdFieldName(string $entityName): string {
67 return self::getInfoItem($entityName, 'primary_key')[0] ?? 'id';
68 }
69
70 /**
71 * Get table name of given entity
72 *
73 * @param string $entityName
74 *
75 * @return string
76 */
77 public static function getTableName($entityName) {
78 return self::getInfoItem($entityName, 'table_name');
79 }
80
81 /**
82 * Given a sql table name, return the name of the api entity.
83 *
84 * @param $tableName
85 * @return string|NULL
86 */
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) {
91 return $entityName;
92 }
93 }
94 return NULL;
95 }
96
97 /**
98 * @return string[]
99 */
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';
107 return $operators;
108 }
109
110 /**
111 * For a given API Entity, return the types of custom fields it supports and the column they join to.
112 *
113 * @param string $entityName
114 * @return array{extends: array, column: string, grouping: mixed}|null
115 */
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) {
119 case 'Contact':
120 return [
121 'extends' => array_merge(['Contact'], array_keys(\CRM_Core_SelectValues::contactType())),
122 'column' => 'id',
123 'grouping' => ['contact_type', 'contact_sub_type'],
124 ];
125
126 case 'RelationshipCache':
127 return [
128 'extends' => ['Relationship'],
129 'column' => 'relationship_id',
130 'grouping' => 'relationship_type_id',
131 ];
132 }
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)) {
136 return [
137 'extends' => [$entityName],
138 'column' => 'id',
139 'grouping' => ($customGroupExtends[$entityName]['grouping'] ?: array_column(\CRM_Utils_Array::findAll($extendsSubGroups, ['extends' => $entityName]), 'grouping', 'id')) ?: NULL,
140 ];
141 }
142 return NULL;
143 }
144
145 /**
146 * Checks if a custom group exists and is multivalued
147 *
148 * @param $customGroupName
149 * @return bool
150 * @throws \CRM_Core_Exception
151 */
152 public static function isCustomEntity($customGroupName) {
153 return $customGroupName && \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroupName, 'is_multiple', 'name');
154 }
155
156 /**
157 * Check if current user is authorized to perform specified action on a given entity.
158 *
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.
163 * @return bool
164 * @throws \API_Exception
165 * @throws \CRM_Core_Exception
166 * @throws \Civi\API\Exception\NotImplementedException
167 * @throws \Civi\API\Exception\UnauthorizedException
168 */
169 public static function checkAccessRecord(\Civi\Api4\Generic\AbstractAction $apiRequest, array $record, int $userID) {
170
171 // Super-admins always have access to everything
172 if (\CRM_Core_Permission::check('all CiviCRM permissions and ACLs', $userID)) {
173 return TRUE;
174 }
175
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();
181 }
182
183 $event = new \Civi\Api4\Event\AuthorizeRecordEvent($apiRequest, $record, $userID);
184 \Civi::dispatcher()->dispatch('civi.api4.authorizeRecord', $event);
185
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);
192 }
193 else {
194 $event->setAuthorized(TRUE);
195 }
196 }
197 return $event->isAuthorized();
198 }
199
200 /**
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.
203 *
204 * @param string $entityName
205 * @param string $actionName
206 * @param array $record
207 * @param int $userID
208 * Contact ID of the user we are testing, or 0 for the anonymous user.
209 *
210 * @return bool
211 * @throws \API_Exception
212 * @throws \CRM_Core_Exception
213 */
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');
218 try {
219 [$actionObjectProvider] = $kernel->resolve($apiRequest);
220 $kernel->authorize($actionObjectProvider, $apiRequest);
221 }
222 catch (UnauthorizedException $e) {
223 return FALSE;
224 }
225 // Gatekeeper permission check passed, now check fine-grained permission
226 return static::checkAccessRecord($apiRequest, $record, $userID);
227 }
228
229 /**
230 * @return \Civi\Api4\Service\Schema\SchemaMap
231 */
232 public static function getSchemaMap() {
233 $cache = \Civi::cache('metadata');
234 $schemaMap = $cache->get('api4.schema.map');
235 if (!$schemaMap) {
236 $schemaMap = \Civi::service('schema_map_builder')->build();
237 $cache->set('api4.schema.map', $schemaMap);
238 }
239 return $schemaMap;
240 }
241
242 /**
243 * Fetches database references + those returned by hook
244 *
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
250 */
251 public static function getRefCount(string $entityName, $entityId) {
252 $daoName = self::getInfoItem($entityName, 'dao');
253 if (!$daoName) {
254 throw new NotImplementedException("Cannot getRefCount for $entityName - dao not found.");
255 }
256 /** @var \CRM_Core_DAO $dao */
257 $dao = new $daoName();
258 $dao->id = $entityId;
259 return $dao->getReferenceCounts();
260 }
261
262 /**
263 * @return array
264 */
265 public static function getSearchableOptions(): array {
266 return [
267 'primary' => ts('Primary'),
268 'secondary' => ts('Secondary'),
269 'bridge' => ts('Bridge'),
270 'none' => ts('None'),
271 ];
272 }
273
274 /**
275 * Collect the 'type' values from every entity.
276 *
277 * @return array
278 */
279 public static function getEntityTypes(): array {
280 $provider = \Civi::service('action_object_provider');
281 $entityTypes = [];
282 foreach ($provider->getEntities() as $entity) {
283 foreach ($entity['type'] ?? [] as $type) {
284 $entityTypes[$type] = $type;
285 }
286 }
287 return $entityTypes;
288 }
289
290 /**
291 * Get the suffixes supported by a given option group
292 *
293 * @param string|int $optionGroup
294 * OptionGroup id or name
295 * @param string $key
296 * Is $optionGroup being passed as "id" or "name"
297 * @return array
298 */
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);
303 }
304 if (!isset($fields)) {
305 return ['name', 'label', 'description'];
306 }
307 return explode(',', $fields);
308 }
309
310 }