Elgg  Version 7.1
AccessCollections.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Database;
4 
5 use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
7 use Elgg\Config;
8 use Elgg\Database;
14 use Elgg\Traits\Loggable;
16 
24 
25  use Loggable;
26 
30  public const TABLE_NAME = 'access_collections';
31 
35  public const MEMBERSHIP_TABLE_NAME = 'access_collection_membership';
36 
37  protected bool $init_complete = false;
38 
51  public function __construct(
52  protected Config $config,
53  protected Database $db,
54  protected EntityTable $entities,
55  protected UserCapabilities $capabilities,
56  protected AccessCache $access_cache,
57  protected EventsService $events,
58  protected SessionManagerService $session_manager,
59  protected Translator $translator) {
60  }
61 
67  public function markInitComplete(): void {
68  $this->init_complete = true;
69  }
70 
95  public function getAccessArray(int $user_guid = 0, bool $flush = false): array {
96  $cache = $this->access_cache;
97 
98  if ($flush) {
99  $cache->clear();
100  }
101 
102  if ($user_guid === 0) {
103  $user_guid = $this->session_manager->getLoggedInUserGuid();
104  }
105 
106  $hash = $user_guid . 'get_access_array';
107 
108  $access_array = $cache->load($hash);
109  if ($access_array === null) {
110  // Public access is always visible
111  $access_array = [ACCESS_PUBLIC];
112 
113  // The following can only return sensible data for a known user.
114  if ($user_guid) {
115  $access_array[] = ACCESS_LOGGED_IN;
116 
117  // Get ACLs that user owns or is a member of
118  $select = Select::fromTable(self::TABLE_NAME);
119 
120  $membership_query = $select->subquery(self::MEMBERSHIP_TABLE_NAME);
121  $membership_query->select('access_collection_id')
122  ->where($select->compare('user_guid', '=', $user_guid, ELGG_VALUE_GUID));
123 
124  $select->select('id')
125  ->where($select->compare('owner_guid', '=', $user_guid, ELGG_VALUE_GUID))
126  ->orWhere($select->compare('id', 'in', $membership_query->getSQL()));
127 
128  $collections = $this->db->getData($select);
129  if (!empty($collections)) {
130  foreach ($collections as $collection) {
131  $access_array[] = (int) $collection->id;
132  }
133  }
134 
135  $ignore_access = $this->capabilities->canBypassPermissionsCheck($user_guid);
136 
137  if ($ignore_access === true) {
138  $access_array[] = ACCESS_PRIVATE;
139  }
140  }
141 
142  if ($this->init_complete) {
143  $cache->save($hash, $access_array);
144  }
145  }
146 
147  $options = [
148  'user_id' => $user_guid,
149  ];
150 
151  // see the warning in the docs for this function about infinite loop potential
152  return $this->events->triggerResults('access:collections:read', 'user', $options, $access_array);
153  }
154 
169  public function hasAccessToEntity(\ElggEntity $entity, int $user_guid = 0): bool {
171  // Public entities are always accessible
172  return true;
173  }
174 
175  try {
176  $user = $this->entities->getUserForPermissionsCheck($user_guid);
177  $user_guid = $user ? $user->guid : 0; // No GUID given and not logged in
178  } catch (UserFetchFailureException $e) {
179  // Not a user GUID
180  $user_guid = 0;
181  }
182 
183  if ($user_guid === $entity->owner_guid) {
184  // Owners have access to their own content
185  return true;
186  }
187 
188  if (!empty($user_guid) && $entity->access_id === ACCESS_LOGGED_IN) {
189  // Existing users have access to entities with logged in access
190  return true;
191  }
192 
193  // See #7159. Must not allow ignore access to affect query
194  $row = elgg_call(ELGG_ENFORCE_ACCESS, function() use ($entity, $user_guid) {
195  return $this->entities->getRow($entity->guid, $user_guid);
196  });
197 
198  return !empty($row);
199  }
200 
224  public function getWriteAccessArray(int $user_guid = 0, bool $flush = false, array $input_params = []): array {
225  $cache = $this->access_cache;
226 
227  if ($flush) {
228  $cache->clear();
229  }
230 
231  if ($user_guid === 0) {
232  $user_guid = $this->session_manager->getLoggedInUserGuid();
233  }
234 
235  $hash = $user_guid . 'get_write_access_array';
236 
237  $access_array = $cache->load($hash);
238  if ($access_array === null) {
239  $access_array = [
240  ACCESS_PRIVATE => $this->getReadableAccessLevel(ACCESS_PRIVATE),
241  ACCESS_LOGGED_IN => $this->getReadableAccessLevel(ACCESS_LOGGED_IN),
242  ACCESS_PUBLIC => $this->getReadableAccessLevel(ACCESS_PUBLIC),
243  ];
244 
245  $access_array += $this->getCollectionsForWriteAccess($user_guid);
246 
247  if ($this->init_complete) {
248  $cache->save($hash, $access_array);
249  }
250  }
251 
252  $options = [
253  'user_id' => $user_guid,
254  'input_params' => $input_params,
255  ];
256 
257  $access_array = $this->events->triggerResults('access:collections:write', 'user', $options, $access_array);
258 
259  // move logged in and public to the end of the array
260  foreach ([ACCESS_LOGGED_IN, ACCESS_PUBLIC] as $access) {
261  if (!isset($access_array[$access])) {
262  continue;
263  }
264 
265  $temp = $access_array[$access];
266  unset($access_array[$access]);
267  $access_array[$access] = $temp;
268  }
269 
270 
271  return $access_array;
272  }
273 
282  protected function getCollectionsForWriteAccess(int $owner_guid): array {
283  $subtypes = $this->events->triggerResults('access:collections:write:subtypes', 'user', ['owner_guid' => $owner_guid], []);
284 
285  $select = Select::fromTable(self::TABLE_NAME);
286 
287  $ors = [
288  $select->compare('subtype', 'is null'),
289  ];
290  if (!empty($subtypes)) {
291  $ors[] = $select->compare('subtype', 'in', $subtypes, ELGG_VALUE_STRING);
292  }
293 
294  $select->select('*')
295  ->where($select->compare('owner_guid', '=', $owner_guid, ELGG_VALUE_GUID))
296  ->andWhere($select->merge($ors, 'OR'))
297  ->orderBy('name', 'ASC');
298 
299  $collections = $this->db->getData($select, [$this, 'rowToElggAccessCollection']);
300  if (empty($collections)) {
301  return [];
302  }
303 
304  $result = [];
305  foreach ($collections as $collection) {
306  $result[$collection->id] = $collection->getDisplayName();
307  }
308 
309  return $result;
310  }
311 
325  public function canEdit(int $collection_id, int $user_guid = 0): bool {
326  if ($this->capabilities->canBypassPermissionsCheck($user_guid)) {
327  return true;
328  }
329 
330  try {
331  $user = $this->entities->getUserForPermissionsCheck($user_guid);
332  } catch (UserFetchFailureException $e) {
333  return false;
334  }
335 
336  if (!$user instanceof \ElggUser) {
337  return false;
338  }
339 
340  $write_access = $this->getWriteAccessArray($user->guid, true);
341  return array_key_exists($collection_id, $write_access);
342  }
343 
359  public function create(\ElggAccessCollection $acl): bool {
360  if (!empty($acl->id)) {
361  return $this->update($acl);
362  }
363 
364  return $this->events->triggerSequence('create', $acl->getType(), $acl, function (\ElggAccessCollection $acl) {
365  $insert = Insert::intoTable(self::TABLE_NAME);
366  $insert->values([
367  'name' => $insert->param($acl->name, ELGG_VALUE_STRING),
368  'subtype' => $insert->param($acl->subtype, ELGG_VALUE_STRING),
369  'owner_guid' => $insert->param($acl->owner_guid, ELGG_VALUE_GUID),
370  ]);
371 
372  $id = $this->db->insertData($insert);
373  if (empty($id)) {
374  return false;
375  }
376 
377  $acl->id = $id;
378 
379  $this->access_cache->clear();
380 
381  return true;
382  });
383  }
384 
392  public function update(\ElggAccessCollection $acl): bool {
393  if (empty($acl->id)) {
394  return $this->create($acl);
395  }
396 
397  return $this->events->triggerSequence('update', $acl->getType(), $acl, function (\ElggAccessCollection $acl) {
398  $update = Update::table(self::TABLE_NAME);
399  $update->set('name', $update->param($acl->name, ELGG_VALUE_STRING))
400  ->set('subtype', $update->param($acl->subtype, ELGG_VALUE_STRING))
401  ->set('owner_guid', $update->param($acl->owner_guid, ELGG_VALUE_GUID))
402  ->where($update->compare('id', '=', $acl->id, ELGG_VALUE_ID));
403 
404  if (!$this->db->updateData($update)) {
405  return false;
406  }
407 
408  $this->access_cache->clear();
409 
410  return true;
411  });
412  }
413 
421  public function delete(\ElggAccessCollection $acl): bool {
422  if (empty($acl->id)) {
423  return false;
424  }
425 
426  return $this->events->triggerSequence('delete', $acl->getType(), $acl, function (\ElggAccessCollection $acl) {
427  $delete = Delete::fromTable(self::TABLE_NAME);
428  $delete->where($delete->compare('id', '=', $acl->id, ELGG_VALUE_ID));
429 
430  if (!$this->db->deleteData($delete)) {
431  return false;
432  }
433 
434  // cleanup access collection membership (doesn't affect the result of deleting the ACL)
435  $delete_membership = Delete::fromTable(self::MEMBERSHIP_TABLE_NAME);
436  $delete_membership->where($delete_membership->compare('access_collection_id', '=', $acl->id, ELGG_VALUE_ID));
437 
438  $this->db->deleteData($delete_membership);
439 
440  // clear cache
441  $this->access_cache->clear();
442 
443  return true;
444  });
445  }
446 
454  public function rowToElggAccessCollection(\stdClass $row): \ElggAccessCollection {
455  return new \ElggAccessCollection($row);
456  }
457 
465  public function get(int $collection_id): ?\ElggAccessCollection {
466  $query = Select::fromTable(self::TABLE_NAME);
467  $query->select('*')
468  ->where($query->compare('id', '=', $collection_id, ELGG_VALUE_ID));
469 
470  return $this->db->getDataRow($query, [$this, 'rowToElggAccessCollection']) ?: null;
471  }
472 
481  public function hasUser(int $user_guid, int $collection_id): bool {
482  $options = [
483  'guids' => $user_guid,
484  'count' => true,
485  ];
486  return (bool) $this->getMembers($collection_id, $options);
487  }
488 
499  public function addUser(int $user_guid, int $collection_id): bool {
500  $collection = $this->get($collection_id);
501  if (!$collection instanceof \ElggAccessCollection) {
502  return false;
503  }
504 
505  $user = $this->entities->get($user_guid);
506  if (!$user instanceof \ElggUser) {
507  return false;
508  }
509 
510  $event_params = [
511  'collection_id' => $collection->id,
512  'user_guid' => $user_guid
513  ];
514 
515  // @todo https://github.com/Elgg/Elgg/issues/10823
516  $result = $this->events->triggerResults('access:collections:add_user', 'collection', $event_params, true);
517  if ($result === false) {
518  return false;
519  }
520 
521  // if someone tries to insert the same data twice, we catch the exception and return true
522  $insert = Insert::intoTable(self::MEMBERSHIP_TABLE_NAME);
523  $insert->values([
524  'access_collection_id' => $insert->param($collection_id, ELGG_VALUE_ID),
525  'user_guid' => $insert->param($user_guid, ELGG_VALUE_GUID),
526  ]);
527 
528  try {
529  $result = $this->db->insertData($insert);
530  } catch (DatabaseException $e) {
531  $prev = $e->getPrevious();
532  if ($prev instanceof UniqueConstraintViolationException) {
533  // duplicate key exception, catched for performance reasons
534  return true;
535  }
536 
537  throw $e;
538  }
539 
540  $this->access_cache->clear();
541 
542  return $result !== false;
543  }
544 
555  public function removeUser(int $user_guid, int $collection_id): bool {
556 
557  $params = [
558  'collection_id' => $collection_id,
559  'user_guid' => $user_guid,
560  ];
561 
562  // @todo https://github.com/Elgg/Elgg/issues/10823
563  if (!$this->events->triggerResults('access:collections:remove_user', 'collection', $params, true)) {
564  return false;
565  }
566 
567  $delete = Delete::fromTable(self::MEMBERSHIP_TABLE_NAME);
568  $delete->where($delete->compare('access_collection_id', '=', $collection_id, ELGG_VALUE_ID))
569  ->andWhere($delete->compare('user_guid', '=', $user_guid, ELGG_VALUE_GUID));
570 
571  $this->access_cache->clear();
572 
573  return (bool) $this->db->deleteData($delete);
574  }
575 
584  public function getEntityCollections(array $options = []): array {
585  $supported_options = ['owner_guid', 'subtype'];
586 
587  $select = Select::fromTable(self::TABLE_NAME);
588  $select->select('*')
589  ->orderBy('name', 'ASC');
590 
591  foreach ($supported_options as $option) {
592  $option_value = elgg_extract($option, $options);
593  if (!isset($option_value)) {
594  continue;
595  }
596 
597  switch ($option) {
598  case 'owner_guid':
599  $select->andWhere($select->compare($option, '=', $option_value, ELGG_VALUE_GUID));
600  break;
601  case 'subtype':
602  $select->andWhere($select->compare($option, '=', $option_value, ELGG_VALUE_STRING));
603  break;
604  }
605  }
606 
607  return $this->db->getData($select, [$this, 'rowToElggAccessCollection']);
608  }
609 
618  public function getMembers(int $collection_id, array $options = []) {
619  $options['wheres'][] = function(QueryBuilder $qb, $table_alias) use ($collection_id) {
620  $qb->join($table_alias, self::MEMBERSHIP_TABLE_NAME, 'acm', $qb->compare('acm.user_guid', '=', "{$table_alias}.guid"));
621 
622  return $qb->compare('acm.access_collection_id', '=', $collection_id, ELGG_VALUE_INTEGER);
623  };
624 
625  return Entities::find($options);
626  }
627 
635  public function getCollectionsByMember(int $member_guid): array {
636  $select = Select::fromTable(self::TABLE_NAME, 'ac');
637  $select->join($select->getTableAlias(), self::MEMBERSHIP_TABLE_NAME, 'acm', $select->compare("{$select->getTableAlias()}.id", '=', 'acm.access_collection_id'));
638 
639  $select->select("{$select->getTableAlias()}.*")
640  ->where($select->compare('acm.user_guid', '=', $member_guid, ELGG_VALUE_GUID))
641  ->orderBy('name', 'ASC');
642 
643  return $this->db->getData($select, [$this, 'rowToElggAccessCollection']);
644  }
645 
663  public function getReadableAccessLevel(int $entity_access_id): string {
664  $translator = $this->translator;
665 
666  // Check if entity access id is a defined global constant
667  $access_array = [
668  ACCESS_PRIVATE => $translator->translate('access:label:private'),
669  ACCESS_FRIENDS => $translator->translate('access:label:friends'),
670  ACCESS_LOGGED_IN => $translator->translate('access:label:logged_in'),
671  ACCESS_PUBLIC => $translator->translate('access:label:public'),
672  ];
673 
674  if (array_key_exists($entity_access_id, $access_array)) {
675  return $access_array[$entity_access_id];
676  }
677 
678  // Entity access id is probably a custom access collection
679  // Check if the user has write access to it and can see it's label
680  // Admins should always be able to see the readable version
681  $collection = $this->get($entity_access_id);
682 
683  if (!$collection instanceof \ElggAccessCollection || !$collection->canEdit()) {
684  // return 'Limited' if the collection can not be loaded or it can not be edited
685  return $translator->translate('access:limited:label');
686  }
687 
688  return $collection->getDisplayName();
689  }
690 }
$entity
Definition: reset.php:8
$id
Generic annotation delete action.
Definition: delete.php:6
$comment access_id
Definition: save.php:58
$params
Saves global plugin settings.
Definition: save.php:13
$event_params
Definition: save.php:31
return[ 'admin/delete_admin_notices'=>['access'=> 'admin'], 'admin/menu/save'=>['access'=> 'admin'], 'admin/plugins/activate'=>['access'=> 'admin'], 'admin/plugins/activate_all'=>['access'=> 'admin'], 'admin/plugins/deactivate'=>['access'=> 'admin'], 'admin/plugins/deactivate_all'=>['access'=> 'admin'], 'admin/plugins/set_priority'=>['access'=> 'admin'], 'admin/security/security_txt'=>['access'=> 'admin'], 'admin/security/settings'=>['access'=> 'admin'], 'admin/security/regenerate_site_secret'=>['access'=> 'admin'], 'admin/site/cache/clear'=>['access'=> 'admin'], 'admin/site/cache/invalidate'=>['access'=> 'admin'], 'admin/site/icons'=>['access'=> 'admin'], 'admin/site/set_maintenance_mode'=>['access'=> 'admin'], 'admin/site/set_robots'=>['access'=> 'admin'], 'admin/site/theme'=>['access'=> 'admin'], 'admin/site/unlock_upgrade'=>['access'=> 'admin'], 'admin/site/settings'=>['access'=> 'admin'], 'admin/upgrade'=>['access'=> 'admin'], 'admin/upgrade/reset'=>['access'=> 'admin'], 'admin/user/ban'=>['access'=> 'admin'], 'admin/user/bulk/ban'=>['access'=> 'admin'], 'admin/user/bulk/delete'=>['access'=> 'admin'], 'admin/user/bulk/unban'=>['access'=> 'admin'], 'admin/user/bulk/validate'=>['access'=> 'admin'], 'admin/user/change_email'=>['access'=> 'admin'], 'admin/user/delete'=>['access'=> 'admin'], 'admin/user/login_as'=>['access'=> 'admin'], 'admin/user/logout_as'=>[], 'admin/user/makeadmin'=>['access'=> 'admin'], 'admin/user/resetpassword'=>['access'=> 'admin'], 'admin/user/removeadmin'=>['access'=> 'admin'], 'admin/user/unban'=>['access'=> 'admin'], 'admin/user/validate'=>['access'=> 'admin'], 'annotation/delete'=>[], 'avatar/upload'=>[], 'comment/save'=>[], 'diagnostics/download'=>['access'=> 'admin', 'controller'=> \Elgg\Diagnostics\DownloadController::class,], 'entity/chooserestoredestination'=>[], 'entity/delete'=>[], 'entity/mute'=>[], 'entity/restore'=>[], 'entity/subscribe'=>[], 'entity/trash'=>[], 'entity/unmute'=>[], 'entity/unsubscribe'=>[], 'login'=>['access'=> 'logged_out'], 'logout'=>[], 'notifications/mute'=>['access'=> 'public'], 'plugins/settings/remove'=>['access'=> 'admin'], 'plugins/settings/save'=>['access'=> 'admin'], 'plugins/usersettings/save'=>[], 'register'=>['access'=> 'logged_out', 'middleware'=>[\Elgg\Router\Middleware\RegistrationAllowedGatekeeper::class,],], 'river/delete'=>[], 'settings/notifications'=>[], 'settings/notifications/subscriptions'=>[], 'user/changepassword'=>['access'=> 'public'], 'user/requestnewpassword'=>['access'=> 'public'], 'useradd'=>['access'=> 'admin'], 'usersettings/save'=>[], 'widgets/add'=>[], 'widgets/delete'=>[], 'widgets/move'=>[], 'widgets/save'=>[],]
Definition: actions.php:76
$delete
$user
Definition: ban.php:7
$query
getType()
{Return the type of the object - eg.object, group, user, relationship, metadata, annotation etcstring...
canEdit(?int $user_guid=null)
Check if user can edit this collection.
Access collections database service.
removeUser(int $user_guid, int $collection_id)
Removes a user from an access collection.
canEdit(int $collection_id, int $user_guid=0)
Can the user change this access collection?
getCollectionsByMember(int $member_guid)
Return an array of collections that the entity is member of.
getEntityCollections(array $options=[])
Returns access collections.
getWriteAccessArray(int $user_guid=0, bool $flush=false, array $input_params=[])
Returns an array of access permissions that the user is allowed to save content with.
__construct(protected Config $config, protected Database $db, protected EntityTable $entities, protected UserCapabilities $capabilities, protected AccessCache $access_cache, protected EventsService $events, protected SessionManagerService $session_manager, protected Translator $translator)
Constructor.
create(\ElggAccessCollection $acl)
Creates a new access collection.
getCollectionsForWriteAccess(int $owner_guid)
Returns an array of access collections to be used in the write access array.
getMembers(int $collection_id, array $options=[])
Get members of an access collection.
hasUser(int $user_guid, int $collection_id)
Check if user is already in the collection.
addUser(int $user_guid, int $collection_id)
Adds a user to an access collection.
hasAccessToEntity(\ElggEntity $entity, int $user_guid=0)
Can a user access an entity.
rowToElggAccessCollection(\stdClass $row)
Transforms a database row to an instance of ElggAccessCollection.
getAccessArray(int $user_guid=0, bool $flush=false)
Returns an array of access IDs a user is permitted to see.
update(\ElggAccessCollection $acl)
Update an existing access collection.
getReadableAccessLevel(int $entity_access_id)
Return the name of an ACCESS_* constant or an access collection, but only if the logged in user owns ...
markInitComplete()
Mark the access system as initialized.
Entity table database service.
Definition: EntityTable.php:24
Database abstraction query builder.
join(string $fromAlias, string $join, string $alias, ?string $condition=null)
{}
Query builder for fetching data from the database.
Definition: Select.php:8
static fromTable(string $table, ?string $alias=null)
Returns a QueryBuilder for selecting data from a given table.
Definition: Select.php:18
The Elgg database.
Definition: Database.php:26
Events service.
A generic parent class for database exceptions.
Exception indicating a user could not be looked up for a permissions check.
User capabilities service.
if(count($css_vars)< 2) $options
$owner_guid
$subtypes
const ELGG_VALUE_STRING
Definition: constants.php:112
const ELGG_VALUE_ID
Definition: constants.php:114
const ELGG_VALUE_GUID
Definition: constants.php:113
const ACCESS_PRIVATE
Definition: constants.php:10
const ACCESS_FRIENDS
Definition: constants.php:13
const ACCESS_LOGGED_IN
Definition: constants.php:11
const ELGG_VALUE_INTEGER
Value types.
Definition: constants.php:111
const ELGG_ENFORCE_ACCESS
Definition: constants.php:122
const ACCESS_PUBLIC
Definition: constants.php:12
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
foreach($recommendedExtensions as $extension) if(empty(ini_get('session.gc_probability'))||empty(ini_get('session.gc_divisor'))) $db
elgg_call(int $flags, Closure $closure)
Calls a callable autowiring the arguments using public DI services and applying logic based on flags.
Definition: elgglib.php:300
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:246
$input_params
Definition: livesearch.php:15
$user_guid
Definition: login_as.php:10
$qb
Definition: queue.php:14
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.
Definition: robots.php:10
if(elgg_view_exists("widgets/{$widget->handler}/edit")) $access
Definition: save.php:19