Merge pull request #23876 from colemanw/checkRecentItemsPerms
[civicrm-core.git] / CRM / Utils / Recent.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * @package CRM
14 * @copyright CiviCRM LLC https://civicrm.org/licensing
15 */
16
17 use Civi\Api4\Utils\CoreUtil;
18
19 /**
20 * Recent items utility class.
21 */
22 class CRM_Utils_Recent {
23
24 /**
25 * Store name
26 *
27 * @var string
28 */
29 const STORE_NAME = 'CRM_Utils_Recent';
30
31 /**
32 * Max number of recent items to store
33 *
34 * @var int
35 */
36 const MAX_ITEMS = 30;
37
38 /**
39 * The list of recently viewed items.
40 *
41 * @var array
42 */
43 static private $_recent = NULL;
44
45 /**
46 * Maximum stack size
47 * @var int
48 */
49 static private $_maxItems = 10;
50
51 /**
52 * Initialize this class and set the static variables.
53 */
54 public static function initialize() {
55 $maxItemsSetting = Civi::settings()->get('recentItemsMaxCount');
56 if (isset($maxItemsSetting) && $maxItemsSetting > 0 && $maxItemsSetting < self::MAX_ITEMS) {
57 self::$_maxItems = $maxItemsSetting;
58 }
59 if (!self::$_recent) {
60 $session = CRM_Core_Session::singleton();
61 self::$_recent = $session->get(self::STORE_NAME);
62 if (!self::$_recent) {
63 self::$_recent = [];
64 }
65 }
66 }
67
68 /**
69 * Return the recently viewed array.
70 *
71 * @return array
72 * the recently viewed array
73 */
74 public static function &get() {
75 self::initialize();
76 return self::$_recent;
77 }
78
79 /**
80 * Create function used by the API - supplies defaults
81 *
82 * @param array $params
83 * @param Civi\Api4\Generic\AbstractAction $action
84 */
85 public static function create(array $params, Civi\Api4\Generic\AbstractAction $action) {
86 if ($action->getCheckPermissions()) {
87 $allowed = civicrm_api4($params['entity_type'], 'checkAccess', [
88 'action' => 'get',
89 'values' => ['id' => $params['entity_id']],
90 ], 0);
91 if (empty($allowed['access'])) {
92 return [];
93 }
94 }
95 $params['title'] = $params['title'] ?? self::getTitle($params['entity_type'], $params['entity_id']);
96 $params['view_url'] = $params['view_url'] ?? self::getUrl($params['entity_type'], $params['entity_id'], 'view');
97 $params['edit_url'] = $params['edit_url'] ?? self::getUrl($params['entity_type'], $params['entity_id'], 'update');
98 $params['delete_url'] = $params['delete_url'] ?? (empty($params['is_deleted']) ? self::getUrl($params['entity_type'], $params['entity_id'], 'delete') : NULL);
99 self::add($params['title'], $params['view_url'], $params['entity_id'], $params['entity_type'], $params['contact_id'] ?? NULL, NULL, $params);
100 return $params;
101 }
102
103 /**
104 * Add an item to the recent stack.
105 *
106 * @param string $title
107 * The title to display.
108 * @param string $url
109 * The link for the above title.
110 * @param string $entityId
111 * Object id.
112 * @param string $entityType
113 * @param int $contactId
114 * Deprecated, probably unused param
115 * @param string $contactName
116 * Deprecated, probably unused param
117 * @param array $others
118 */
119 public static function add(
120 $title,
121 $url,
122 $entityId,
123 $entityType,
124 $contactId,
125 $contactName,
126 $others = []
127 ) {
128 $entityType = self::normalizeEntityType($entityType);
129
130 // Abort if this entity type is not supported
131 if (!self::isProviderEnabled($entityType)) {
132 return;
133 }
134
135 // Ensure item is not already present in list
136 self::removeItems(['entity_id' => $entityId, 'entity_type' => $entityType]);
137
138 if (!is_array($others)) {
139 $others = [];
140 }
141
142 array_unshift(self::$_recent,
143 [
144 'title' => $title,
145 // TODO: deprecate & remove "url" in favor of "view_url"
146 'url' => $url,
147 'view_url' => $url,
148 // TODO: deprecate & remove "id" in favor of "entity_id"
149 'id' => $entityId,
150 'entity_id' => (int) $entityId,
151 // TODO: deprecate & remove "type" in favor of "entity_type"
152 'type' => $entityType,
153 'entity_type' => $entityType,
154 // Deprecated param
155 'contact_id' => $contactId,
156 // Param appears to be unused
157 'contactName' => $contactName,
158 'subtype' => $others['subtype'] ?? NULL,
159 // TODO: deprecate & remove "isDeleted" in favor of "is_deleted"
160 'isDeleted' => $others['is_deleted'] ?? $others['isDeleted'] ?? FALSE,
161 'is_deleted' => (bool) ($others['is_deleted'] ?? $others['isDeleted'] ?? FALSE),
162 // imageUrl is deprecated
163 'image_url' => $others['imageUrl'] ?? NULL,
164 'edit_url' => $others['edit_url'] ?? $others['editUrl'] ?? NULL,
165 'delete_url' => $others['delete_url'] ?? $others['deleteUrl'] ?? NULL,
166 'icon' => $others['icon'] ?? self::getIcon($entityType, $entityId),
167 ]
168 );
169
170 // Keep the list trimmed to max length
171 while (count(self::$_recent) > self::$_maxItems) {
172 array_pop(self::$_recent);
173 }
174
175 CRM_Utils_Hook::recent(self::$_recent);
176
177 $session = CRM_Core_Session::singleton();
178 $session->set(self::STORE_NAME, self::$_recent);
179 }
180
181 /**
182 * Get default title for this item, based on the entity's `label_field`
183 *
184 * @param string $entityType
185 * @param int $entityId
186 * @return string|null
187 */
188 private static function getTitle($entityType, $entityId) {
189 $labelField = CoreUtil::getInfoItem($entityType, 'label_field');
190 $title = NULL;
191 if ($labelField) {
192 $record = civicrm_api4($entityType, 'get', [
193 'where' => [['id', '=', $entityId]],
194 'select' => [$labelField],
195 'checkPermissions' => FALSE,
196 ], 0);
197 $title = $record[$labelField] ?? NULL;
198 }
199 return $title ?? (CoreUtil::getInfoItem($entityType, 'title'));
200 }
201
202 /**
203 * Get a link to view/update/delete a given entity.
204 *
205 * @param string $entityType
206 * @param int $entityId
207 * @param string $action
208 * Either 'view', 'update', or 'delete'
209 * @return string|null
210 */
211 private static function getUrl($entityType, $entityId, $action) {
212 if ($action !== 'view') {
213 $check = civicrm_api4($entityType, 'checkAccess', [
214 'action' => $action,
215 'values' => ['id' => $entityId],
216 ], 0);
217 if (empty($check['access'])) {
218 return NULL;
219 }
220 }
221 $paths = (array) CoreUtil::getInfoItem($entityType, 'paths');
222 if (!empty($paths[$action])) {
223 return CRM_Utils_System::url(str_replace('[id]', $entityId, $paths[$action]));
224 }
225 return NULL;
226 }
227
228 /**
229 * @param $entityType
230 * @param $entityId
231 * @return string|null
232 */
233 private static function getIcon($entityType, $entityId) {
234 $icon = NULL;
235 $daoClass = CRM_Core_DAO_AllCoreTables::getFullName($entityType);
236 if ($daoClass) {
237 $icon = CRM_Core_DAO_AllCoreTables::getBAOClassName($daoClass)::getEntityIcon($entityType, $entityId);
238 }
239 return $icon ?: 'fa-gear';
240 }
241
242 /**
243 * Callback for hook_civicrm_post().
244 * @param \Civi\Core\Event\PostEvent $event
245 */
246 public static function on_hook_civicrm_post(\Civi\Core\Event\PostEvent $event) {
247 if ($event->id && CRM_Core_Session::getLoggedInContactID()) {
248 $entityType = self::normalizeEntityType($event->entity);
249 if ($event->action === 'delete') {
250 // Is this an entity that might be in the recent items list?
251 $providersPermitted = Civi::settings()->get('recentItemsProviders') ?: array_keys(self::getProviders());
252 if (in_array($entityType, $providersPermitted)) {
253 self::del(['entity_id' => $event->id, 'entity_type' => $entityType]);
254 }
255 }
256 elseif ($event->action === 'edit') {
257 if (isset($event->object->is_deleted)) {
258 \Civi\Api4\RecentItem::update(FALSE)
259 ->addWhere('entity_type', '=', $entityType)
260 ->addWhere('entity_id', '=', $event->id)
261 ->addValue('is_deleted', (bool) $event->object->is_deleted)
262 ->execute();
263 }
264 }
265 }
266 }
267
268 /**
269 * Remove items from the array that match given props
270 * @param array $props
271 */
272 private static function removeItems(array $props) {
273 self::initialize();
274
275 self::$_recent = array_filter(self::$_recent, function($item) use ($props) {
276 foreach ($props as $key => $val) {
277 if (($item[$key] ?? NULL) != $val) {
278 return TRUE;
279 }
280 }
281 return FALSE;
282 });
283 }
284
285 /**
286 * Delete item(s) from the recently-viewed list.
287 *
288 * @param array $removeItem
289 * Item to be removed.
290 */
291 public static function del($removeItem) {
292 self::removeItems($removeItem);
293 CRM_Utils_Hook::recent(self::$_recent);
294 $session = CRM_Core_Session::singleton();
295 $session->set(self::STORE_NAME, self::$_recent);
296 }
297
298 /**
299 * Delete an item from the recent stack.
300 *
301 * @param string $id
302 * @deprecated
303 */
304 public static function delContact($id) {
305 CRM_Core_Error::deprecatedFunctionWarning('del');
306 self::del(['contact_id' => $id]);
307 }
308
309 /**
310 * Check if a provider is allowed to add stuff.
311 * If corresponding setting is empty, all are allowed
312 *
313 * @param string $providerName
314 * @return bool
315 */
316 public static function isProviderEnabled($providerName) {
317 $allowed = TRUE;
318
319 // Use core setting recentItemsProviders if configured
320 $providersPermitted = Civi::settings()->get('recentItemsProviders');
321 if ($providersPermitted) {
322 $allowed = in_array($providerName, $providersPermitted);
323 }
324 // Else allow
325 return $allowed;
326 }
327
328 /**
329 * @param string $entityType
330 * @return string
331 */
332 private static function normalizeEntityType($entityType) {
333 // Change Individual/Organization/Household to 'Contact'
334 if (in_array($entityType, CRM_Contact_BAO_ContactType::basicTypes(TRUE), TRUE)) {
335 return 'Contact';
336 }
337 return $entityType;
338 }
339
340 /**
341 * Gets the list of available providers to civi's recent items stack
342 *
343 * TODO: Make this an option group so extensions can extend it.
344 *
345 * @return array
346 */
347 public static function getProviders() {
348 $providers = [
349 'Contact' => ts('Contacts'),
350 'Relationship' => ts('Relationships'),
351 'Activity' => ts('Activities'),
352 'Note' => ts('Notes'),
353 'Group' => ts('Groups'),
354 'Case' => ts('Cases'),
355 'Contribution' => ts('Contributions'),
356 'Participant' => ts('Participants'),
357 'Grant' => ts('Grants'),
358 'Membership' => ts('Memberships'),
359 'Pledge' => ts('Pledges'),
360 'Event' => ts('Events'),
361 'Campaign' => ts('Campaigns'),
362 ];
363
364 return $providers;
365 }
366
367 }