Merge pull request #23461 from eileenmcnaughton/import_woohoo
[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 */
84 public static function create(array $params) {
85 $params['title'] = $params['title'] ?? self::getTitle($params['entity_type'], $params['entity_id']);
86 $params['view_url'] = $params['view_url'] ?? self::getUrl($params['entity_type'], $params['entity_id'], 'view');
87 $params['edit_url'] = $params['edit_url'] ?? self::getUrl($params['entity_type'], $params['entity_id'], 'update');
88 $params['delete_url'] = $params['delete_url'] ?? (empty($params['is_deleted']) ? self::getUrl($params['entity_type'], $params['entity_id'], 'delete') : NULL);
89 self::add($params['title'], $params['view_url'], $params['entity_id'], $params['entity_type'], $params['contact_id'] ?? NULL, NULL, $params);
90 return $params;
91 }
92
93 /**
94 * Add an item to the recent stack.
95 *
96 * @param string $title
97 * The title to display.
98 * @param string $url
99 * The link for the above title.
100 * @param string $entityId
101 * Object id.
102 * @param string $entityType
103 * @param int $contactId
104 * Deprecated, probably unused param
105 * @param string $contactName
106 * Deprecated, probably unused param
107 * @param array $others
108 */
109 public static function add(
110 $title,
111 $url,
112 $entityId,
113 $entityType,
114 $contactId,
115 $contactName,
116 $others = []
117 ) {
118 $entityType = self::normalizeEntityType($entityType);
119
120 // Abort if this entity type is not supported
121 if (!self::isProviderEnabled($entityType)) {
122 return;
123 }
124
125 // Ensure item is not already present in list
126 self::removeItems(['entity_id' => $entityId, 'entity_type' => $entityType]);
127
128 if (!is_array($others)) {
129 $others = [];
130 }
131
132 array_unshift(self::$_recent,
133 [
134 'title' => $title,
135 // TODO: deprecate & remove "url" in favor of "view_url"
136 'url' => $url,
137 'view_url' => $url,
138 // TODO: deprecate & remove "id" in favor of "entity_id"
139 'id' => $entityId,
140 'entity_id' => (int) $entityId,
141 // TODO: deprecate & remove "type" in favor of "entity_type"
142 'type' => $entityType,
143 'entity_type' => $entityType,
144 // Deprecated param
145 'contact_id' => $contactId,
146 // Param appears to be unused
147 'contactName' => $contactName,
148 'subtype' => $others['subtype'] ?? NULL,
149 // TODO: deprecate & remove "isDeleted" in favor of "is_deleted"
150 'isDeleted' => $others['is_deleted'] ?? $others['isDeleted'] ?? FALSE,
151 'is_deleted' => (bool) ($others['is_deleted'] ?? $others['isDeleted'] ?? FALSE),
152 // imageUrl is deprecated
153 'image_url' => $others['imageUrl'] ?? NULL,
154 'edit_url' => $others['edit_url'] ?? $others['editUrl'] ?? NULL,
155 'delete_url' => $others['delete_url'] ?? $others['deleteUrl'] ?? NULL,
156 'icon' => $others['icon'] ?? self::getIcon($entityType, $entityId),
157 ]
158 );
159
160 // Keep the list trimmed to max length
161 while (count(self::$_recent) > self::$_maxItems) {
162 array_pop(self::$_recent);
163 }
164
165 CRM_Utils_Hook::recent(self::$_recent);
166
167 $session = CRM_Core_Session::singleton();
168 $session->set(self::STORE_NAME, self::$_recent);
169 }
170
171 /**
172 * Get default title for this item, based on the entity's `label_field`
173 *
174 * @param string $entityType
175 * @param int $entityId
176 * @return string|null
177 */
178 private static function getTitle($entityType, $entityId) {
179 $labelField = CoreUtil::getInfoItem($entityType, 'label_field');
180 $title = NULL;
181 if ($labelField) {
182 $record = civicrm_api4($entityType, 'get', [
183 'where' => [['id', '=', $entityId]],
184 'select' => [$labelField],
185 ], 0);
186 $title = $record[$labelField] ?? NULL;
187 }
188 return $title ?? (CoreUtil::getInfoItem($entityType, 'label_field'));
189 }
190
191 /**
192 * Get a link to view/update/delete a given entity.
193 *
194 * @param string $entityType
195 * @param int $entityId
196 * @param string $action
197 * Either 'view', 'update', or 'delete'
198 * @return string|null
199 */
200 private static function getUrl($entityType, $entityId, $action) {
201 if ($action !== 'view') {
202 $check = civicrm_api4($entityType, 'checkAccess', [
203 'action' => $action,
204 'values' => ['id' => $entityId],
205 ], 0);
206 if (empty($check['access'])) {
207 return NULL;
208 }
209 }
210 $paths = (array) CoreUtil::getInfoItem($entityType, 'paths');
211 if (!empty($paths[$action])) {
212 return CRM_Utils_System::url(str_replace('[id]', $entityId, $paths[$action]));
213 }
214 return NULL;
215 }
216
217 /**
218 * @param $entityType
219 * @param $entityId
220 * @return string|null
221 */
222 private static function getIcon($entityType, $entityId) {
223 $icon = NULL;
224 $daoClass = CRM_Core_DAO_AllCoreTables::getFullName($entityType);
225 if ($daoClass) {
226 $icon = CRM_Core_DAO_AllCoreTables::getBAOClassName($daoClass)::getEntityIcon($entityType, $entityId);
227 }
228 return $icon ?: 'fa-gear';
229 }
230
231 /**
232 * Callback for hook_civicrm_post().
233 * @param \Civi\Core\Event\PostEvent $event
234 */
235 public static function on_hook_civicrm_post(\Civi\Core\Event\PostEvent $event) {
236 if ($event->id && CRM_Core_Session::getLoggedInContactID()) {
237 $entityType = self::normalizeEntityType($event->entity);
238 if ($event->action === 'delete') {
239 // Is this an entity that might be in the recent items list?
240 $providersPermitted = Civi::settings()->get('recentItemsProviders') ?: array_keys(self::getProviders());
241 if (in_array($entityType, $providersPermitted)) {
242 self::del(['entity_id' => $event->id, 'entity_type' => $entityType]);
243 }
244 }
245 elseif ($event->action === 'edit') {
246 if (isset($event->object->is_deleted)) {
247 \Civi\Api4\RecentItem::update()
248 ->addWhere('entity_type', '=', $entityType)
249 ->addWhere('entity_id', '=', $event->id)
250 ->addValue('is_deleted', (bool) $event->object->is_deleted)
251 ->execute();
252 }
253 }
254 }
255 }
256
257 /**
258 * Remove items from the array that match given props
259 * @param array $props
260 */
261 private static function removeItems(array $props) {
262 self::initialize();
263
264 self::$_recent = array_filter(self::$_recent, function($item) use ($props) {
265 foreach ($props as $key => $val) {
266 if (($item[$key] ?? NULL) != $val) {
267 return TRUE;
268 }
269 }
270 return FALSE;
271 });
272 }
273
274 /**
275 * Delete item(s) from the recently-viewed list.
276 *
277 * @param array $removeItem
278 * Item to be removed.
279 */
280 public static function del($removeItem) {
281 self::removeItems($removeItem);
282 CRM_Utils_Hook::recent(self::$_recent);
283 $session = CRM_Core_Session::singleton();
284 $session->set(self::STORE_NAME, self::$_recent);
285 }
286
287 /**
288 * Delete an item from the recent stack.
289 *
290 * @param string $id
291 * @deprecated
292 */
293 public static function delContact($id) {
294 CRM_Core_Error::deprecatedFunctionWarning('del');
295 self::del(['contact_id' => $id]);
296 }
297
298 /**
299 * Check if a provider is allowed to add stuff.
300 * If corresponding setting is empty, all are allowed
301 *
302 * @param string $providerName
303 * @return bool
304 */
305 public static function isProviderEnabled($providerName) {
306 $allowed = TRUE;
307
308 // Use core setting recentItemsProviders if configured
309 $providersPermitted = Civi::settings()->get('recentItemsProviders');
310 if ($providersPermitted) {
311 $allowed = in_array($providerName, $providersPermitted);
312 }
313 // Else allow
314 return $allowed;
315 }
316
317 /**
318 * @param string $entityType
319 * @return string
320 */
321 private static function normalizeEntityType($entityType) {
322 // Change Individual/Organization/Household to 'Contact'
323 if (in_array($entityType, CRM_Contact_BAO_ContactType::basicTypes(TRUE), TRUE)) {
324 return 'Contact';
325 }
326 return $entityType;
327 }
328
329 /**
330 * Gets the list of available providers to civi's recent items stack
331 *
332 * TODO: Make this an option group so extensions can extend it.
333 *
334 * @return array
335 */
336 public static function getProviders() {
337 $providers = [
338 'Contact' => ts('Contacts'),
339 'Relationship' => ts('Relationships'),
340 'Activity' => ts('Activities'),
341 'Note' => ts('Notes'),
342 'Group' => ts('Groups'),
343 'Case' => ts('Cases'),
344 'Contribution' => ts('Contributions'),
345 'Participant' => ts('Participants'),
346 'Grant' => ts('Grants'),
347 'Membership' => ts('Memberships'),
348 'Pledge' => ts('Pledges'),
349 'Event' => ts('Events'),
350 'Campaign' => ts('Campaigns'),
351 ];
352
353 return $providers;
354 }
355
356 }