Merge pull request #21937 from eileenmcnaughton/upit
[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\Request;
16 use Civi\Api4\Entity;
17 use Civi\Api4\Event\CreateApi4RequestEvent;
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 $e = new CreateApi4RequestEvent($entityName);
43 \Civi::dispatcher()->dispatch('civi.api4.createRequest', $e);
44 return $e->className;
45 }
46
47 /**
48 * Get a piece of metadata about an entity
49 *
50 * @param string $entityName
51 * @param string $keyToReturn
52 * @return mixed
53 */
54 public static function getInfoItem(string $entityName, string $keyToReturn) {
55 // Because this function might be called thousands of times per request, read directly
56 // from the cache set by Apiv4 Entity.get to avoid the processing overhead of the API wrapper.
57 $cached = \Civi::cache('metadata')->get('api4.entities.info');
58 if ($cached) {
59 $info = $cached[$entityName] ?? NULL;
60 }
61 // If the cache is empty, calling Entity.get will populate it and we'll use it next time.
62 else {
63 $info = Entity::get(FALSE)
64 ->addWhere('name', '=', $entityName)
65 ->addSelect($keyToReturn)
66 ->execute()->first();
67 }
68 return $info ? $info[$keyToReturn] ?? NULL : NULL;
69 }
70
71 /**
72 * Get name of unique identifier, typically "id"
73 * @param string $entityName
74 * @return string
75 */
76 public static function getIdFieldName(string $entityName): string {
77 return self::getInfoItem($entityName, 'primary_key')[0] ?? 'id';
78 }
79
80 /**
81 * Get table name of given entity
82 *
83 * @param string $entityName
84 *
85 * @return string
86 */
87 public static function getTableName($entityName) {
88 if (strpos($entityName, 'Custom_') === 0) {
89 $customGroup = substr($entityName, 7);
90 return \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroup, 'table_name', 'name');
91 }
92 return AllCoreTables::getTableForEntityName($entityName);
93 }
94
95 /**
96 * Given a sql table name, return the name of the api entity.
97 *
98 * @param $tableName
99 * @return string|NULL
100 */
101 public static function getApiNameFromTableName($tableName) {
102 $entityName = AllCoreTables::getBriefName(AllCoreTables::getClassForTable($tableName));
103 // Real entities
104 if ($entityName) {
105 // Verify class exists
106 return self::getApiClass($entityName) ? $entityName : NULL;
107 }
108 // Multi-value custom group pseudo-entities
109 $customGroup = \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $tableName, 'name', 'table_name');
110 return self::isCustomEntity($customGroup) ? "Custom_$customGroup" : NULL;
111 }
112
113 /**
114 * @return string[]
115 */
116 public static function getOperators() {
117 $operators = \CRM_Core_DAO::acceptedSQLOperators();
118 $operators[] = 'CONTAINS';
119 $operators[] = 'IS EMPTY';
120 $operators[] = 'IS NOT EMPTY';
121 $operators[] = 'REGEXP';
122 $operators[] = 'NOT REGEXP';
123 return $operators;
124 }
125
126 /**
127 * For a given API Entity, return the types of custom fields it supports and the column they join to.
128 *
129 * @param string $entityName
130 * @return array|mixed|null
131 * @throws \API_Exception
132 * @throws \Civi\API\Exception\UnauthorizedException
133 */
134 public static function getCustomGroupExtends(string $entityName) {
135 // Custom_group.extends pretty much maps 1-1 with entity names, except for a couple oddballs (Contact, Participant).
136 switch ($entityName) {
137 case 'Contact':
138 return [
139 'extends' => array_merge(['Contact'], array_keys(\CRM_Core_SelectValues::contactType())),
140 'column' => 'id',
141 ];
142
143 case 'Participant':
144 return [
145 'extends' => ['Participant', 'ParticipantRole', 'ParticipantEventName', 'ParticipantEventType'],
146 'column' => 'id',
147 ];
148
149 case 'RelationshipCache':
150 return [
151 'extends' => ['Relationship'],
152 'column' => 'relationship_id',
153 ];
154 }
155 if (array_key_exists($entityName, \CRM_Core_SelectValues::customGroupExtends())) {
156 return [
157 'extends' => [$entityName],
158 'column' => 'id',
159 ];
160 }
161 return NULL;
162 }
163
164 /**
165 * Checks if a custom group exists and is multivalued
166 *
167 * @param $customGroupName
168 * @return bool
169 * @throws \CRM_Core_Exception
170 */
171 public static function isCustomEntity($customGroupName) {
172 return $customGroupName && \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroupName, 'is_multiple', 'name');
173 }
174
175 /**
176 * Check if current user is authorized to perform specified action on a given entity.
177 *
178 * @param \Civi\Api4\Generic\AbstractAction $apiRequest
179 * @param array $record
180 * @param int|string $userID
181 * Contact ID of the user we are testing,. 0 for the anonymous user.
182 * @return bool
183 * @throws \API_Exception
184 * @throws \CRM_Core_Exception
185 * @throws \Civi\API\Exception\NotImplementedException
186 * @throws \Civi\API\Exception\UnauthorizedException
187 */
188 public static function checkAccessRecord(\Civi\Api4\Generic\AbstractAction $apiRequest, array $record, int $userID) {
189
190 // Super-admins always have access to everything
191 if (\CRM_Core_Permission::check('all CiviCRM permissions and ACLs', $userID)) {
192 return TRUE;
193 }
194
195 // For get actions, just run a get and ACLs will be applied to the query.
196 // It's a cheap trick and not as efficient as not running the query at all,
197 // but BAO::checkAccess doesn't consistently check permissions for the "get" action.
198 if (is_a($apiRequest, '\Civi\Api4\Generic\DAOGetAction')) {
199 return (bool) $apiRequest->addSelect('id')->addWhere('id', '=', $record['id'])->execute()->count();
200 }
201
202 $event = new \Civi\Api4\Event\AuthorizeRecordEvent($apiRequest, $record, $userID);
203 \Civi::dispatcher()->dispatch('civi.api4.authorizeRecord', $event);
204
205 // Note: $bao::_checkAccess() is a quasi-listener. TODO: Convert to straight-up listener.
206 if ($event->isAuthorized() === NULL) {
207 $baoName = self::getBAOFromApiName($apiRequest->getEntityName());
208 if ($baoName && method_exists($baoName, '_checkAccess')) {
209 $authorized = $baoName::_checkAccess($event->getEntityName(), $event->getActionName(), $event->getRecord(), $event->getUserID());
210 $event->setAuthorized($authorized);
211 }
212 else {
213 $event->setAuthorized(TRUE);
214 }
215 }
216 return $event->isAuthorized();
217 }
218
219 /**
220 * If the permissions of record $A are based on record $B, then use `checkAccessDelegated($B...)`
221 * to make see if access to $B is permitted.
222 *
223 * @param string $entityName
224 * @param string $actionName
225 * @param array $record
226 * @param int $userID
227 * Contact ID of the user we are testing, or 0 for the anonymous user.
228 *
229 * @return bool
230 * @throws \API_Exception
231 * @throws \CRM_Core_Exception
232 */
233 public static function checkAccessDelegated(string $entityName, string $actionName, array $record, int $userID) {
234 $apiRequest = Request::create($entityName, $actionName, ['version' => 4]);
235 // TODO: Should probably emit civi.api.authorize for checking guardian permission; but in APIv4 with std cfg, this is de-facto equivalent.
236 if (!$apiRequest->isAuthorized()) {
237 return FALSE;
238 }
239 return static::checkAccessRecord($apiRequest, $record, $userID);
240 }
241
242 /**
243 * @return \Civi\Api4\Service\Schema\SchemaMap
244 */
245 public static function getSchemaMap() {
246 $cache = \Civi::cache('metadata');
247 $schemaMap = $cache->get('api4.schema.map');
248 if (!$schemaMap) {
249 $schemaMap = \Civi::service('schema_map_builder')->build();
250 $cache->set('api4.schema.map', $schemaMap);
251 }
252 return $schemaMap;
253 }
254
255 }