/** * Constructor * * @param EntityCache $entity_cache Entity cache * @param EntityTable $entity_table Entity service */ public function __construct(EntityCache $entity_cache, EntityTable $entity_table) { $this->_callable_cache_checker = function ($guid) use($entity_cache) { return $entity_cache->get($guid); }; $this->_callable_entity_loader = function ($options) use($entity_table) { return $entity_table->getEntities($options); }; }
/** * {@inheritdoc} */ public function updateRow($guid, stdClass $row) { $attributes = array_merge((array) $this->rows[$guid], (array) $row); // Rebuild query specs for the udpated row $this->addQuerySpecs((object) $attributes); return parent::updateRow($guid, $row); }
/** * Deletes all private settings for an entity * * @param int $entity_guid The Entity GUID * @return bool */ function removeAllForEntity($entity_guid) { $entity_guid = (int) $entity_guid; $entity = $this->entities->get($entity_guid); if (!$entity instanceof \ElggEntity) { return false; } return $this->db->deleteData("DELETE FROM {$this->table}\n\t\t\tWHERE entity_guid = {$entity_guid}"); }
/** * Returns entities based upon metadata. Also accepts all * options available to elgg_get_entities(). Supports * the singular option shortcut. * * @note Using metadata_names and metadata_values results in a * "names IN (...) AND values IN (...)" clause. This is subtly * differently than default multiple metadata_name_value_pairs, which use * "(name = value) AND (name = value)" clauses. * * When in doubt, use name_value_pairs. * * To ask for entities that do not have a metadata value, use a custom * where clause like this: * * $options['wheres'][] = "NOT EXISTS ( * SELECT 1 FROM {$dbprefix}metadata md * WHERE md.entity_guid = e.guid * AND md.name_id = $name_metastring_id * AND md.value_id = $value_metastring_id)"; * * Note the metadata name and value has been denormalized in the above example. * * @see elgg_get_entities * * @param array $options Array in format: * * metadata_names => null|ARR metadata names * * metadata_values => null|ARR metadata values * * metadata_name_value_pairs => null|ARR ( * name => 'name', * value => 'value', * 'operand' => '=', * 'case_sensitive' => true * ) * Currently if multiple values are sent via * an array (value => array('value1', 'value2') * the pair's operand will be forced to "IN". * If passing "IN" as the operand and a string as the value, * the value must be a properly quoted and escaped string. * * metadata_name_value_pairs_operator => null|STR The operator to use for combining * (name = value) OPERATOR (name = value); default AND * * metadata_case_sensitive => BOOL Overall Case sensitive * * order_by_metadata => null|ARR array( * 'name' => 'metadata_text1', * 'direction' => ASC|DESC, * 'as' => text|integer * ) * Also supports array('name' => 'metadata_text1') * * metadata_owner_guids => null|ARR guids for metadata owners * * @return \ElggEntity[]|mixed If count, int. If not count, array. false on errors. */ function getEntities(array $options = array()) { $defaults = array('metadata_names' => ELGG_ENTITIES_ANY_VALUE, 'metadata_values' => ELGG_ENTITIES_ANY_VALUE, 'metadata_name_value_pairs' => ELGG_ENTITIES_ANY_VALUE, 'metadata_name_value_pairs_operator' => 'AND', 'metadata_case_sensitive' => true, 'order_by_metadata' => array(), 'metadata_owner_guids' => ELGG_ENTITIES_ANY_VALUE); $options = array_merge($defaults, $options); $singulars = array('metadata_name', 'metadata_value', 'metadata_name_value_pair', 'metadata_owner_guid'); $options = _elgg_normalize_plural_options_array($options, $singulars); if (!($options = _elgg_entities_get_metastrings_options('metadata', $options))) { return false; } return $this->entityTable->getEntities($options); }
/** * Sets a private setting for an entity. * * @param int $entity_guid The entity GUID * @param string $name The name of the setting * @param string $value The value of the setting * @return bool */ public function set($entity_guid, $name, $value) { $this->cache->clear($entity_guid); _elgg_services()->boot->invalidateCache(); if (!$this->entities->exists($entity_guid)) { return false; } $query = "\n\t\t\tINSERT into {$this->table}\n\t\t\t(entity_guid, name, value) VALUES\n\t\t\t(:entity_guid, :name, :value)\n\t\t\tON DUPLICATE KEY UPDATE value = :value\n\t\t"; $params = [':entity_guid' => (int) $entity_guid, ':name' => (string) $name, ':value' => (string) $value]; $result = $this->db->insertData($query, $params); return $result !== false; }
/** * Send a notification to a subscriber * * @param \Elgg\Notifications\Event $event The notification event * @param int $guid The guid of the subscriber * @param string $method The notification method * @return bool * @access private */ protected function sendNotification(\Elgg\Notifications\Event $event, $guid, $method) { $recipient = $this->entities->get($guid, 'user'); /* @var \ElggUser $recipient */ if (!$recipient || $recipient->isBanned()) { return false; } // don't notify the creator of the content if ($recipient->getGUID() == $event->getActorGUID()) { return false; } $actor = $event->getActor(); $object = $event->getObject(); if (!$actor || !$object) { return false; } if ($object instanceof ElggEntity && !has_access_to_entity($object, $recipient)) { return false; } $language = $recipient->language; $params = array('event' => $event, 'method' => $method, 'recipient' => $recipient, 'language' => $language, 'object' => $object); $subject = $this->getNotificationSubject($event, $recipient); $body = $this->getNotificationBody($event, $recipient); $notification = new \Elgg\Notifications\Notification($event->getActor(), $recipient, $language, $subject, $body, '', $params); $type = 'notification:' . $event->getDescription(); if ($this->hooks->hasHandler('prepare', $type)) { $notification = $this->hooks->trigger('prepare', $type, $params, $notification); } else { // pre Elgg 1.9 notification message generation $notification = $this->getDeprecatedNotificationBody($notification, $event, $method); } if ($this->hooks->hasHandler('send', "notification:{$method}")) { // return true to indicate the notification has been sent $params = array('notification' => $notification, 'event' => $event); return $this->hooks->trigger('send', "notification:{$method}", $params, false); } else { // pre Elgg 1.9 notification handler $userGuid = $notification->getRecipientGUID(); $senderGuid = $notification->getSenderGUID(); $subject = $notification->subject; $body = $notification->body; $params = $notification->params; return (bool) _elgg_notify_user($userGuid, $senderGuid, $subject, $body, $params, array($method)); } }
/** * Can the user change this access collection? * * Use the plugin hook of 'access:collections:write', 'user' to change this. * @see get_write_access_array() for details on the hook. * * Respects access control disabling for admin users and {@link elgg_set_ignore_access()} * * @see get_write_access_array() * * @param int $collection_id The collection id * @param mixed $user_guid The user GUID to check for. Defaults to logged in user. * @return bool */ function canEdit($collection_id, $user_guid = null) { try { $user = $this->entities->getUserForPermissionsCheck($user_guid); } catch (UserFetchFailureException $e) { return false; } $collection = $this->get($collection_id); if (!$user || !$collection) { return false; } $write_access = $this->getWriteAccessArray($user->guid, true); // don't ignore access when checking users. if ($user_guid) { return array_key_exists($collection_id, $write_access); } else { return elgg_get_ignore_access() || array_key_exists($collection_id, $write_access); } }
/** * Handle request to /serve-icon handler * * @param bool $allow_removing_headers Alter PHP's global headers to allow caching * @return BinaryFileResponse */ public function handleServeIconRequest($allow_removing_headers = true) { $response = new Response(); $response->setExpires($this->getCurrentTime('-1 day')); $response->prepare($this->request); if ($allow_removing_headers) { // clear cache-boosting headers set by PHP session header_remove('Cache-Control'); header_remove('Pragma'); header_remove('Expires'); } $path = implode('/', $this->request->getUrlSegments()); if (!preg_match('~serve-icon/(\\d+)/(.*+)$~', $path, $m)) { return $response->setStatusCode(400)->setContent('Malformatted request URL'); } list(, $guid, $size) = $m; $entity = $this->entities->get($guid); if (!$entity instanceof \ElggEntity) { return $response->setStatusCode(404)->setContent('Item does not exist'); } $thumbnail = $entity->getIcon($size); if (!$thumbnail->exists()) { return $response->setStatusCode(404)->setContent('Icon does not exist'); } $if_none_match = $this->request->headers->get('if_none_match'); if (!empty($if_none_match)) { // strip mod_deflate suffixes $this->request->headers->set('if_none_match', str_replace('-gzip', '', $if_none_match)); } $filenameonfilestore = $thumbnail->getFilenameOnFilestore(); $last_updated = filemtime($filenameonfilestore); $etag = '"' . $last_updated . '"'; $response->setPrivate()->setEtag($etag)->setExpires($this->getCurrentTime('+1 day'))->setMaxAge(86400); if ($response->isNotModified($this->request)) { return $response; } $headers = ['Content-Type' => (new MimeTypeDetector())->getType($filenameonfilestore)]; $response = new BinaryFileResponse($filenameonfilestore, 200, $headers, false, 'inline'); $response->prepare($this->request); $response->setPrivate()->setEtag($etag)->setExpires($this->getCurrentTime('+1 day'))->setMaxAge(86400); return $response; }
/** * Can a user annotate an entity? * * @tip Can be overridden by registering for the plugin hook [permissions_check:annotate:<name>, * <entity type>] or [permissions_check:annotate, <entity type>]. The hooks are called in that order. * * @tip If you want logged out users to annotate an object, do not call * canAnnotate(). It's easier than using the plugin hook. * * @param ElggEntity $entity Objet entity * @param int $user_guid User guid (default is logged in user) * @param string $annotation_name The name of the annotation (default is unspecified) * * @return bool */ public function canAnnotate(ElggEntity $entity, $user_guid = 0, $annotation_name = '') { if ($annotation_name === null || $annotation_name === false) { // accepting these for BC $annotation_name = ''; } elseif (!is_string($annotation_name)) { throw new InvalidArgumentException(__METHOD__ . ' expects \\$annotation_name to be a string'); } try { $user = $this->entities->getUserForPermissionsCheck($user_guid); } catch (UserFetchFailureException $e) { return false; } $return = (bool) $user; $params = ['entity' => $entity, 'user' => $user, 'annotation_name' => $annotation_name]; if (!empty($annotation_name)) { $return = $this->hooks->trigger("permissions_check:annotate:{$annotation_name}", $entity->getType(), $params, $return); } return $this->hooks->trigger('permissions_check:annotate', $entity->getType(), $params, $return); }
/** * Return entities matching a given query joining against a relationship. * Also accepts all options available to elgg_get_entities() and * elgg_get_entities_from_metadata(). * * To ask for entities that do not have a particular relationship to an entity, * use a custom where clause like the following: * * $options['wheres'][] = "NOT EXISTS ( * SELECT 1 FROM {$db_prefix}entity_relationships * WHERE guid_one = e.guid * AND relationship = '$relationship' * )"; * * @see elgg_get_entities * @see elgg_get_entities_from_metadata * * @param array $options Array in format: * * relationship => null|STR Type of the relationship. E.g. "member" * * relationship_guid => null|INT GUID of the subject of the relationship, unless "inverse_relationship" is set * to true, in which case this will specify the target. * * inverse_relationship => false|BOOL Are we searching for relationship subjects? By default, the query finds * targets of relationships. * * relationship_join_on => null|STR How the entities relate: guid (default), container_guid, or owner_guid * Examples using the relationship 'friend': * 1. use 'guid' if you want the user's friends * 2. use 'owner_guid' if you want the entities the user's friends own * (including in groups) * 3. use 'container_guid' if you want the entities in the user's personal * space (non-group) * * relationship_created_time_lower => null|INT Relationship created time lower boundary in epoch time * * relationship_created_time_upper => null|INT Relationship created time upper boundary in epoch time * * @return \ElggEntity[]|mixed If count, int. If not count, array. false on errors. */ public function getEntities($options) { $defaults = array('relationship' => null, 'relationship_guid' => null, 'inverse_relationship' => false, 'relationship_join_on' => 'guid', 'relationship_created_time_lower' => ELGG_ENTITIES_ANY_VALUE, 'relationship_created_time_upper' => ELGG_ENTITIES_ANY_VALUE); $options = array_merge($defaults, $options); $join_column = "e.{$options['relationship_join_on']}"; $clauses = $this->getEntityRelationshipWhereSql($join_column, $options['relationship'], $options['relationship_guid'], $options['inverse_relationship']); if ($clauses) { // merge wheres to pass to get_entities() if (isset($options['wheres']) && !is_array($options['wheres'])) { $options['wheres'] = array($options['wheres']); } elseif (!isset($options['wheres'])) { $options['wheres'] = array(); } $options['wheres'] = array_merge($options['wheres'], $clauses['wheres']); // limit based on time created $time_wheres = $this->entities->getEntityTimeWhereSql('r', $options['relationship_created_time_upper'], $options['relationship_created_time_lower']); if ($time_wheres) { $options['wheres'] = array_merge($options['wheres'], array($time_wheres)); } // merge joins to pass to get_entities() if (isset($options['joins']) && !is_array($options['joins'])) { $options['joins'] = array($options['joins']); } elseif (!isset($options['joins'])) { $options['joins'] = array(); } $options['joins'] = array_merge($options['joins'], $clauses['joins']); if (isset($options['selects']) && !is_array($options['selects'])) { $options['selects'] = array($options['selects']); } elseif (!isset($options['selects'])) { $options['selects'] = array(); } $select = array('r.id'); $options['selects'] = array_merge($options['selects'], $select); if (!isset($options['group_by'])) { $options['group_by'] = $clauses['group_by']; } } return $this->metadata->getEntities($options); }
/** * Sets the last action time of the given user to right now. * * @see _elgg_session_boot The session boot calls this at the beginning of every request * * @param ElggUser $user User entity * @return void */ public function setLastAction(ElggUser $user) { $time = $this->getCurrentTime()->getTimestamp(); if ($user->last_action == $time) { // no change required return; } $query = "\n\t\t\tUPDATE {$this->table}\n\t\t\tSET\n\t\t\t\tprev_last_action = last_action,\n\t\t\t\tlast_action = :last_action\n\t\t\tWHERE guid = :guid\n\t\t"; $params = [':last_action' => $time, ':guid' => (int) $user->guid]; $user->prev_last_action = $user->last_action; $user->last_action = $time; execute_delayed_write_query($query, null, $params); $this->entity_cache->set($user); // If we save the user to memcache during this request, then we'll end up with the // old (incorrect) attributes cached (notice the above query is delayed). So it's // simplest to just resave the user after all plugin code runs. register_shutdown_function(function () use($user, $time) { $this->entities->updateLastAction($user, $time); // keep entity table in sync $user->storeInPersistedCache(_elgg_get_memcache('new_entity_cache'), $time); }); }
/** * Send a notification to a subscriber * * @param NotificationEvent $event The notification event * @param int $guid The guid of the subscriber * @param string $method The notification method * @param array $params Default notification params * @return bool * @access private */ protected function sendNotification(NotificationEvent $event, $guid, $method, array $params = []) { $actor = $event->getActor(); $object = $event->getObject(); if ($event instanceof InstantNotificationEvent) { $recipient = $this->entities->get($guid); /* @var \ElggEntity $recipient */ $subject = elgg_extract('subject', $params, ''); $body = elgg_extract('body', $params, ''); $summary = elgg_extract('summary', $params, ''); } else { $recipient = $this->entities->get($guid, 'user'); /* @var \ElggUser $recipient */ if (!$recipient || $recipient->isBanned()) { return false; } if ($recipient->getGUID() == $event->getActorGUID()) { // Content creators should not be receiving subscription // notifications about their own content return false; } if (!$actor || !$object) { return false; } if ($object instanceof ElggEntity && !has_access_to_entity($object, $recipient)) { // Recipient does not have access to the notification object // The access level may have changed since the event was enqueued return false; } $subject = $this->getNotificationSubject($event, $recipient); $body = $this->getNotificationBody($event, $recipient); $summary = ''; $params['origin'] = Notification::ORIGIN_SUBSCRIPTIONS; } $language = $recipient->language; $params['event'] = $event; $params['method'] = $method; $params['sender'] = $actor; $params['recipient'] = $recipient; $params['language'] = $language; $params['object'] = $object; $params['action'] = $event->getAction(); $notification = new Notification($actor, $recipient, $language, $subject, $body, $summary, $params); $notification = $this->hooks->trigger('prepare', 'notification', $params, $notification); if (!$notification instanceof Notification) { throw new RuntimeException("'prepare','notification' hook must return an instance of " . Notification::class); } $type = 'notification:' . $event->getDescription(); if ($this->hooks->hasHandler('prepare', $type)) { $notification = $this->hooks->trigger('prepare', $type, $params, $notification); if (!$notification instanceof Notification) { throw new RuntimeException("'prepare','{$type}' hook must return an instance of " . Notification::class); } } else { // pre Elgg 1.9 notification message generation $notification = $this->getDeprecatedNotificationBody($notification, $event, $method); } $notification = $this->hooks->trigger('format', "notification:{$method}", [], $notification); if (!$notification instanceof Notification) { throw new RuntimeException("'format','notification:{$method}' hook must return an instance of " . Notification::class); } if ($this->hooks->hasHandler('send', "notification:{$method}")) { // return true to indicate the notification has been sent $params = array('notification' => $notification, 'event' => $event); $result = $this->hooks->trigger('send', "notification:{$method}", $params, false); if ($this->logger->getLevel() == Logger::INFO) { $logger_data = print_r((array) $notification->toObject(), true); if ($result) { $this->logger->info("Notification sent: " . $logger_data); } else { $this->logger->info("Notification was not sent: " . $logger_data); } } return $result; } else { // pre Elgg 1.9 notification handler $userGuid = $notification->getRecipientGUID(); $senderGuid = $notification->getSenderGUID(); $subject = $notification->subject; $body = $notification->body; $params = $notification->params; return (bool) _elgg_notify_user($userGuid, $senderGuid, $subject, $body, $params, array($method)); } }
/** * Populate the boot data * * @param \stdClass $config Elgg CONFIG object * @param \Elgg\Database $db Elgg database * @param EntityTable $entities Entities service * @param Plugins $plugins Plugins service * * @return void * @throws \InstallationException */ public function populate(\stdClass $config, Database $db, EntityTable $entities, Plugins $plugins) { // get subtypes $rows = $db->getData("\n\t\t\tSELECT *\n\t\t\tFROM {$db->prefix}entity_subtypes\n\t\t"); foreach ($rows as $row) { $this->subtype_data[$row->id] = $row; } // get config $rows = $db->getData("\n\t\t\tSELECT *\n\t\t\tFROM {$db->prefix}config\n\t\t"); foreach ($rows as $row) { $this->config_values[$row->name] = unserialize($row->value); } if (!array_key_exists('installed', $this->config_values)) { // try to fetch from old pre 3.0 datalists table // need to do this to be able to perform an upgrade from 2.x to 3.0 try { $rows = $db->getData("\n\t\t\t\t\tSELECT *\n\t\t\t\t\tFROM {$db->prefix}datalists\n\t\t\t\t"); foreach ($rows as $row) { $value = $row->value; if ($row->name == 'processed_upgrades') { // config table already serializes data so no need to double serialize $value = unserialize($value); } $this->config_values[$row->name] = $value; } } catch (\Exception $e) { } } // get site entity $this->site = $entities->get($this->config_values['default_site'], 'site'); if (!$this->site) { throw new \InstallationException("Unable to handle this request. This site is not configured or the database is down."); } // get plugins $this->active_plugins = $plugins->find('active'); // get plugin settings if (!$this->active_plugins) { return; } // find GUIDs with not too many private settings $guids = array_map(function (\ElggPlugin $plugin) { return $plugin->guid; }, $this->active_plugins); // find plugin GUIDs with not too many settings $limit = 40; $set = implode(',', $guids); $sql = "\n\t\t\tSELECT entity_guid\n\t\t\tFROM {$db->prefix}private_settings\n\t\t\tWHERE entity_guid IN ({$set})\n\t\t\t AND name NOT LIKE 'plugin:user_setting:%'\n\t\t\t AND name NOT LIKE 'elgg:internal:%'\n\t\t\tGROUP BY entity_guid\n\t\t\tHAVING COUNT(*) > {$limit}\n\t\t"; $unsuitable_guids = $db->getData($sql, function ($row) { return (int) $row->entity_guid; }); $guids = array_values($guids); $guids = array_diff($guids, $unsuitable_guids); if ($guids) { // get the settings $set = implode(',', $guids); $rows = $db->getData("\n\t\t\t\tSELECT entity_guid, `name`, `value`\n\t\t\t\tFROM {$db->prefix}private_settings\n\t\t\t\tWHERE entity_guid IN ({$set})\n\t\t\t\t AND name NOT LIKE 'plugin:user_setting:%'\n\t\t\t\t AND name NOT LIKE 'elgg:internal:%'\n\t\t\t\tORDER BY entity_guid\n\t\t\t"); // make sure we show all entities as loaded $this->plugin_settings = array_fill_keys($guids, []); foreach ($rows as $i => $row) { $this->plugin_settings[$row->entity_guid][$row->name] = $row->value; } } }