Elgg  Version 4.3
AccessCollections.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Database;
4 
6 use Elgg\Config;
7 use Elgg\Database;
14 
22 
23  use Loggable;
24 
28  const TABLE_NAME = 'access_collections';
29 
33  const MEMBERSHIP_TABLE_NAME = 'access_collection_membership';
34 
38  protected $config;
39 
43  protected $db;
44 
48  protected $access_cache;
49 
53  protected $hooks;
54 
58  protected $session;
59 
63  protected $entities;
64 
68  protected $capabilities;
69 
73  protected $translator;
74 
78  protected $init_complete = false;
79 
92  public function __construct(
94  Database $db,
97  \ElggCache $cache,
101  $this->config = $config;
102  $this->db = $db;
103  $this->entities = $entities;
104  $this->capabilities = $capabilities;
105  $this->access_cache = $cache;
106  $this->hooks = $hooks;
107  $this->session = $session;
108  $this->translator = $translator;
109  }
110 
116  public function markInitComplete() {
117  $this->init_complete = true;
118  }
119 
144  public function getAccessArray(int $user_guid = 0, bool $flush = false) {
145  $cache = $this->access_cache;
146 
147  if ($flush) {
148  $cache->clear();
149  }
150 
151  if ($user_guid == 0) {
152  $user_guid = $this->session->getLoggedInUserGuid();
153  }
154 
155  $hash = $user_guid . 'get_access_array';
156 
157  if ($cache[$hash]) {
158  $access_array = $cache[$hash];
159  } else {
160  // Public access is always visible
161  $access_array = [ACCESS_PUBLIC];
162 
163  // The following can only return sensible data for a known user.
164  if ($user_guid) {
165  $access_array[] = ACCESS_LOGGED_IN;
166 
167  // Get ACLs that user owns or is a member of
168  $select = Select::fromTable(self::TABLE_NAME);
169 
170  $membership_query = $select->subquery(self::MEMBERSHIP_TABLE_NAME);
171  $membership_query->select('access_collection_id')
172  ->where($select->compare('user_guid', '=', $user_guid, ELGG_VALUE_GUID));
173 
174  $select->select('id')
175  ->where($select->compare('owner_guid', '=', $user_guid, ELGG_VALUE_GUID))
176  ->orWhere($select->compare('id', 'in', $membership_query->getSQL()));
177 
178  $collections = $this->db->getData($select);
179  if (!empty($collections)) {
180  foreach ($collections as $collection) {
181  $access_array[] = (int) $collection->id;
182  }
183  }
184 
185  $ignore_access = $this->capabilities->canBypassPermissionsCheck($user_guid);
186 
187  if ($ignore_access === true) {
188  $access_array[] = ACCESS_PRIVATE;
189  }
190  }
191 
192  if ($this->init_complete) {
193  $cache[$hash] = $access_array;
194  }
195  }
196 
197  $options = [
198  'user_id' => $user_guid,
199  ];
200 
201  // see the warning in the docs for this function about infinite loop potential
202  return $this->hooks->trigger('access:collections:read', 'user', $options, $access_array);
203  }
204 
219  public function hasAccessToEntity(\ElggEntity $entity, int $user_guid = 0): bool {
220  if ($entity->access_id == ACCESS_PUBLIC) {
221  // Public entities are always accessible
222  return true;
223  }
224 
225  try {
226  $user = $this->entities->getUserForPermissionsCheck($user_guid);
227  $user_guid = $user ? $user->guid : 0; // No GUID given and not logged in
228  } catch (UserFetchFailureException $e) {
229  // Not a user GUID
230  $user_guid = 0;
231  }
232 
233  if ($user_guid === $entity->owner_guid) {
234  // Owners have access to their own content
235  return true;
236  }
237 
238  if (!empty($user_guid )&& $entity->access_id === ACCESS_LOGGED_IN) {
239  // Existing users have access to entities with logged in access
240  return true;
241  }
242 
243  // See #7159. Must not allow ignore access to affect query
244  $row = elgg_call(ELGG_ENFORCE_ACCESS, function() use ($entity, $user_guid) {
245  return $this->entities->getRow($entity->guid, $user_guid);
246  });
247 
248  return !empty($row);
249  }
250 
276  public function getWriteAccessArray($user_guid = 0, $flush = false, array $input_params = []) {
277  $cache = $this->access_cache;
278 
279  if ($flush) {
280  $cache->clear();
281  }
282 
283  if ($user_guid == 0) {
284  $user_guid = $this->session->getLoggedInUserGuid();
285  }
286 
287  $user_guid = (int) $user_guid;
288 
289  $hash = $user_guid . 'get_write_access_array';
290 
291  if ($cache[$hash]) {
292  $access_array = $cache[$hash];
293  } else {
294  $access_array = [
298  ];
299 
300  $access_array += $this->getCollectionsForWriteAccess($user_guid);
301 
302  if ($this->init_complete) {
303  $cache[$hash] = $access_array;
304  }
305  }
306 
307  $options = [
308  'user_id' => $user_guid,
309  'input_params' => $input_params,
310  ];
311 
312  $access_array = $this->hooks->trigger('access:collections:write', 'user', $options, $access_array);
313 
314  // move logged in and public to the end of the array
315  foreach ([ACCESS_LOGGED_IN, ACCESS_PUBLIC] as $access) {
316  if (!isset($access_array[$access])) {
317  continue;
318  }
319 
320  $temp = $access_array[$access];
321  unset($access_array[$access]);
322  $access_array[$access] = $temp;
323  }
324 
325 
326  return $access_array;
327  }
328 
338  protected function getCollectionsForWriteAccess(int $owner_guid) {
339  $subtypes = $this->hooks->trigger('access:collections:write:subtypes', 'user', ['owner_guid' => $owner_guid], []);
340 
341  $select = Select::fromTable(self::TABLE_NAME);
342 
343  $ors = [
344  $select->compare('subtype', 'is null'),
345  ];
346  if (!empty($subtypes)) {
347  $ors[] = $select->compare('subtype', 'in', $subtypes, ELGG_VALUE_STRING);
348  }
349 
350  $select->select('*')
351  ->where($select->compare('owner_guid', '=', $owner_guid, ELGG_VALUE_GUID))
352  ->andWhere($select->merge($ors, 'OR'))
353  ->orderBy('name', 'ASC');
354 
355  $collections = $this->db->getData($select, [$this, 'rowToElggAccessCollection']);
356  if (empty($collections)) {
357  return [];
358  }
359 
360  $result = [];
361  foreach ($collections as $collection) {
362  $result[$collection->id] = $collection->getDisplayName();
363  }
364 
365  return $result;
366  }
367 
381  public function canEdit(int $collection_id, int $user_guid = null) {
382  try {
383  $user = $this->entities->getUserForPermissionsCheck($user_guid);
384  } catch (UserFetchFailureException $e) {
385  return false;
386  }
387 
388  if (!$user instanceof \ElggUser) {
389  return false;
390  }
391 
392  $collection = $this->get($collection_id);
393  if (!$collection instanceof \ElggAccessCollection) {
394  return false;
395  }
396 
397  if ($this->capabilities->canBypassPermissionsCheck($user->guid)) {
398  return true;
399  }
400 
401  $write_access = $this->getWriteAccessArray($user->guid, true);
402  return array_key_exists($collection_id, $write_access);
403  }
404 
422  public function create(string $name, int $owner_guid = 0, string $subtype = null): ?int {
423  $name = trim($name);
424  if (empty($name)) {
425  return null;
426  }
427 
428  if (isset($subtype)) {
430  if (strlen($subtype) > 255) {
431  $this->getLogger()->error("The subtype length for access collections cannot be greater than 255");
432  return null;
433  }
434  }
435 
436  if ($owner_guid === 0) {
437  $owner_guid = $this->session->getLoggedInUserGuid();
438  }
439 
440  $insert = Insert::intoTable(self::TABLE_NAME);
441  $insert->values([
442  'name' => $insert->param($name, ELGG_VALUE_STRING),
443  'subtype' => $insert->param($subtype, ELGG_VALUE_STRING),
444  'owner_guid' => $insert->param($owner_guid, ELGG_VALUE_GUID),
445  ]);
446 
447  $id = $this->db->insertData($insert);
448  if (empty($id)) {
449  return null;
450  }
451 
452  $this->access_cache->clear();
453 
454  $hook_params = [
455  'collection_id' => $id,
456  'name' => $name,
457  'subtype' => $subtype,
458  'owner_guid' => $owner_guid,
459  ];
460 
461  if (!$this->hooks->trigger('access:collections:addcollection', 'collection', $hook_params, true)) {
462  $this->delete($id);
463  return null;
464  }
465 
466  return $id;
467  }
468 
477  public function rename(int $collection_id, string $name): bool {
478 
479  $update = Update::table(self::TABLE_NAME);
480  $update->set('name', $update->param($name, ELGG_VALUE_STRING))
481  ->where($update->compare('id', '=', $collection_id, ELGG_VALUE_ID));
482 
483  if ($this->db->updateData($update, true)) {
484  $this->access_cache->clear();
485 
486  return true;
487  }
488 
489  return false;
490  }
491 
505  public function update($collection_id, array $new_members = []) {
506  $acl = $this->get($collection_id);
507 
508  if (!$acl instanceof \ElggAccessCollection) {
509  return false;
510  }
511 
512  $to_guid = function($elem) {
513  if (empty($elem)) {
514  return 0;
515  }
516  if (is_object($elem)) {
517  return (int) $elem->guid;
518  }
519  return (int) $elem;
520  };
521 
522  $current_members = [];
523  $new_members = array_map($to_guid, $new_members);
524 
525  $current_members_batch = $this->getMembers($collection_id, [
526  'batch' => true,
527  'limit' => false,
528  'callback' => false,
529  ]);
530 
531  foreach ($current_members_batch as $row) {
532  $current_members[] = $to_guid($row);
533  }
534 
535  $remove_members = array_diff($current_members, $new_members);
536  $add_members = array_diff($new_members, $current_members);
537 
538  $result = true;
539 
540  foreach ($add_members as $guid) {
541  $result = $result && $this->addUser($guid, $collection_id);
542  }
543 
544  foreach ($remove_members as $guid) {
545  $result = $result && $this->removeUser($guid, $collection_id);
546  }
547 
548  $this->access_cache->clear();
549 
550  return $result;
551  }
552 
560  public function delete(int $collection_id): bool {
561  $params = [
562  'collection_id' => $collection_id,
563  ];
564 
565  if (!$this->hooks->trigger('access:collections:deletecollection', 'collection', $params, true)) {
566  return false;
567  }
568 
569  // Deleting membership doesn't affect result of deleting ACL.
570  $delete_membership = Delete::fromTable(self::MEMBERSHIP_TABLE_NAME);
571  $delete_membership->where($delete_membership->compare('access_collection_id', '=', $collection_id, ELGG_VALUE_ID));
572 
573  $this->db->deleteData($delete_membership);
574 
575  $delete = Delete::fromTable(self::TABLE_NAME);
576  $delete->where($delete->compare('id', '=', $collection_id, ELGG_VALUE_ID));
577 
578  $result = $this->db->deleteData($delete);
579 
580  $this->access_cache->clear();
581 
582  return (bool) $result;
583  }
584 
592  public function rowToElggAccessCollection(\stdClass $row): \ElggAccessCollection {
593  return new \ElggAccessCollection($row);
594  }
595 
603  public function get(int $collection_id): ?\ElggAccessCollection {
604  $query = Select::fromTable(self::TABLE_NAME);
605  $query->select('*')
606  ->where($query->compare('id', '=', $collection_id, ELGG_VALUE_ID));
607 
608  return $this->db->getDataRow($query, [$this, 'rowToElggAccessCollection']) ?: null;
609  }
610 
618  public function hasUser($user_guid, $collection_id) {
619  $options = [
620  'guids' => (int) $user_guid,
621  'count' => true,
622  ];
623  return (bool) $this->getMembers($collection_id, $options);
624  }
625 
636  public function addUser(int $user_guid, int $collection_id): bool {
637 
638  $collection = $this->get($collection_id);
639  if (!$collection instanceof \ElggAccessCollection) {
640  return false;
641  }
642 
643  $user = $this->entities->get($user_guid);
644  if (!$user instanceof \ElggUser) {
645  return false;
646  }
647 
648  $hook_params = [
649  'collection_id' => $collection->id,
650  'user_guid' => $user_guid
651  ];
652 
653  $result = $this->hooks->trigger('access:collections:add_user', 'collection', $hook_params, true);
654  if ($result == false) {
655  return false;
656  }
657 
658  // if someone tries to insert the same data twice, we catch the exception and return true
659  $insert = Insert::intoTable(self::MEMBERSHIP_TABLE_NAME);
660  $insert->values([
661  'access_collection_id' => $insert->param($collection_id, ELGG_VALUE_ID),
662  'user_guid' => $insert->param($user_guid, ELGG_VALUE_GUID),
663  ]);
664 
665  try {
666  $result = $this->db->insertData($insert);
667  } catch (DatabaseException $e) {
668  $prev = $e->getPrevious();
669  if ($prev instanceof UniqueConstraintViolationException) {
670  // duplicate key exception, catched for performance reasons
671  return true;
672  }
673 
674  throw $e;
675  }
676 
677  $this->access_cache->clear();
678 
679  return $result !== false;
680  }
681 
692  public function removeUser(int $user_guid, int $collection_id): bool {
693 
694  $params = [
695  'collection_id' => $collection_id,
696  'user_guid' => $user_guid,
697  ];
698 
699  if (!$this->hooks->trigger('access:collections:remove_user', 'collection', $params, true)) {
700  return false;
701  }
702 
703  $delete = Delete::fromTable(self::MEMBERSHIP_TABLE_NAME);
704  $delete->where($delete->compare('access_collection_id', '=', $collection_id, ELGG_VALUE_ID))
705  ->andWhere($delete->compare('user_guid', '=', $user_guid, ELGG_VALUE_GUID));
706 
707  $this->access_cache->clear();
708 
709  return (bool) $this->db->deleteData($delete);
710  }
711 
720  public function getEntityCollections(array $options = []): array {
721  $supported_options = ['owner_guid', 'subtype'];
722 
723  $select = Select::fromTable(self::TABLE_NAME);
724  $select->select('*')
725  ->orderBy('name', 'ASC');
726 
727  foreach ($supported_options as $option) {
728  $option_value = elgg_extract($option, $options);
729  if (!isset($option_value)) {
730  continue;
731  }
732 
733  switch ($option) {
734  case 'owner_guid':
735  $select->andWhere($select->compare($option, '=', $option_value, ELGG_VALUE_GUID));
736  break;
737  case 'subtype':
738  $select->andWhere($select->compare($option, '=', $option_value, ELGG_VALUE_STRING));
739  break;
740  }
741  }
742 
743  return $this->db->getData($select, [$this, 'rowToElggAccessCollection']);
744  }
745 
754  public function getMembers(int $collection_id, array $options = []) {
755  $options['wheres'][] = function(QueryBuilder $qb, $table_alias) use ($collection_id) {
756  $qb->join($table_alias, self::MEMBERSHIP_TABLE_NAME, 'acm', $qb->compare('acm.user_guid', '=', "{$table_alias}.guid"));
757 
758  return $qb->compare('acm.access_collection_id', '=', $collection_id, ELGG_VALUE_INTEGER);
759  };
760 
761  return Entities::find($options);
762  }
763 
771  public function getCollectionsByMember(int $member_guid): array {
772  $select = Select::fromTable(self::TABLE_NAME, 'ac');
773  $select->join('ac', self::MEMBERSHIP_TABLE_NAME, 'acm', $select->compare('ac.id', '=', 'acm.access_collection_id'));
774 
775  $select->select('ac.*')
776  ->where($select->compare('acm.user_guid', '=', $member_guid, ELGG_VALUE_GUID))
777  ->orderBy('name', 'ASC');
778 
779  return $this->db->getData($select, [$this, 'rowToElggAccessCollection']);
780  }
781 
799  public function getReadableAccessLevel(int $entity_access_id) {
801 
802  // Check if entity access id is a defined global constant
803  $access_array = [
804  ACCESS_PRIVATE => $translator->translate('access:label:private'),
805  ACCESS_FRIENDS => $translator->translate('access:label:friends'),
806  ACCESS_LOGGED_IN => $translator->translate('access:label:logged_in'),
807  ACCESS_PUBLIC => $translator->translate('access:label:public'),
808  ];
809 
810  if (array_key_exists($entity_access_id, $access_array)) {
811  return $access_array[$entity_access_id];
812  }
813 
814  // Entity access id is probably a custom access collection
815  // Check if the user has write access to it and can see it's label
816  // Admins should always be able to see the readable version
817  $collection = $this->get($entity_access_id);
818 
819  if (!$collection instanceof \ElggAccessCollection || !$collection->canEdit()) {
820  // return 'Limited' if the collection can not be loaded or it can not be edited
821  return $translator->translate('access:limited:label');
822  }
823 
824  return $collection->getDisplayName();
825  }
826 }
getReadableAccessLevel(int $entity_access_id)
Return the name of an ACCESS_* constant or an access collection, but only if the logged in user owns ...
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:592
create(string $name, int $owner_guid=0, string $subtype=null)
Creates a new access collection.
removeUser(int $user_guid, int $collection_id)
Removes a user from an access collection.
$user_guid
Definition: login_as.php:10
hasAccessToEntity(\ElggEntity $entity, int $user_guid=0)
Can a user access an entity.
$params
Saves global plugin settings.
Definition: save.php:13
const ELGG_ENFORCE_ACCESS
Definition: constants.php:147
static find(array $options=[])
Build and execute a new query from an array of legacy options.
Definition: Repository.php:86
canEdit($user_guid=null)
Check if user can edit this collection.
rename(int $collection_id, string $name)
Renames an access collection.
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
static table($table, $alias=null)
{}
Definition: Update.php:13
The Elgg database.
Definition: Database.php:25
const ACCESS_FRIENDS
Definition: constants.php:15
if(elgg_view_exists("widgets/{$widget->handler}/edit")) $access
Definition: save.php:19
markInitComplete()
Mark the access system as initialized.
const ELGG_VALUE_INTEGER
Value types.
Definition: constants.php:126
canEdit(int $collection_id, int $user_guid=null)
Can the user change this access collection?
const ELGG_VALUE_GUID
Definition: constants.php:128
Database abstraction query builder.
$input_params
Definition: livesearch.php:14
$delete
__construct(Config $config, Database $db, EntityTable $entities, UserCapabilities $capabilities,\ElggCache $cache, PluginHooksService $hooks,\ElggSession $session, Translator $translator)
Constructor.
const ELGG_VALUE_ID
Definition: constants.php:129
getCollectionsForWriteAccess(int $owner_guid)
Returns an array of access collections to be used in the write access array.
$options
Elgg admin footer.
Definition: footer.php:6
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
$owner_guid
static intoTable($table)
{}
Definition: Insert.php:13
getWriteAccessArray($user_guid=0, $flush=false, array $input_params=[])
Returns an array of access permissions that the user is allowed to save content with.
const ACCESS_PRIVATE
Definition: constants.php:12
$entity
Definition: reset.php:8
const ACCESS_LOGGED_IN
Definition: constants.php:13
getCollectionsByMember(int $member_guid)
Return an array of collections that the entity is member of.
Exception indicating a user could not be looked up for a permissions check.
rowToElggAccessCollection(\stdClass $row)
Transforms a database row to an instance of ElggAccessCollection.
hasUser($user_guid, $collection_id)
Check if user is already in the collection.
join($fromAlias, $join, $alias, $condition=null)
{}
A generic parent class for database exceptions.
$user
Definition: ban.php:7
compare($x, $comparison, $y=null, $type=null, $case_sensitive=null)
Build value comparison clause.
getEntityCollections(array $options=[])
Returns access collections.
elgg_extract($key, $array, $default=null, $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:547
static fromTable($table, $alias=null)
{}
Definition: Select.php:13
getLogger()
Returns logger.
Definition: Loggable.php:37
addUser(int $user_guid, int $collection_id)
Adds a user to an access collection.
const ELGG_VALUE_STRING
Definition: constants.php:127
$subtype
Definition: delete.php:22
$query
$subtypes
getAccessArray(int $user_guid=0, bool $flush=false)
Returns an array of access IDs a user is permitted to see.
const ACCESS_PUBLIC
Definition: constants.php:14
User capabilities service.
Access collections database service.
$id
Generic annotation delete action.
Definition: delete.php:6
$qb
Definition: queue.php:11
static fromTable($table, $alias=null)
{}
Definition: Delete.php:13
Entity table database service.
Definition: EntityTable.php:26
$guid
Reset an ElggUpgrade.
Definition: reset.php:6
getMembers(int $collection_id, array $options=[])
Get members of an access collection.
update($collection_id, array $new_members=[])
Updates the membership in an access collection.