| 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 | } |