diff --git a/docs/appendix/upgrade-notes/2.x-to-3.0.rst b/docs/appendix/upgrade-notes/2.x-to-3.0.rst index 33d2ec308b4..e085f717003 100644 --- a/docs/appendix/upgrade-notes/2.x-to-3.0.rst +++ b/docs/appendix/upgrade-notes/2.x-to-3.0.rst @@ -190,6 +190,9 @@ Deprecated APIs * ``ban_user``: Use ``ElggUser->ban()`` * ``create_metadata``: Use ``ElggEntity`` setter or ``ElggEntity->setMetadata()`` * ``update_metadata``: Use ``ElggMetadata->save()`` + * ``get_metadata_url`` + * ``create_annotation``: Use ``ElggEntity->annotate()`` + * ``update_metadata``: Use ``ElggAnnotation->save()`` * ``elgg_get_user_validation_status``: Use ``ElggUser->isValidated()`` * ``make_user_admin``: Use ``ElggUser->makeAdmin()`` * ``remove_user_admin``: Use ``ElggUser->removeAdmin()`` @@ -203,6 +206,7 @@ Deprecated APIs * ``elgg_list_entities_from_relationship``: Use ``elgg_list_entities()`` * ``elgg_list_entities_from_private_settings``: Use ``elgg_list_entities()`` * ``elgg_list_entities_from_access_id``: Use ``elgg_list_entities()`` + * ``elgg_batch_delete_callback`` Removed global vars ------------------- @@ -281,6 +285,11 @@ an instance of ``\Elgg\Database\QueryBuilder`` and return a prepared statement. Plugins should not rely on joined and selected table aliases. Closures passed to the options array will receive a second argument that corresponds to the selected table alias. Plugins must perform their own joins and use joined aliases accordingly. +Note that all of the private API around building raw SQL strings has also been removed. If you were relying on them in your plugins, +be advised that anything marked as ``@access private`` and ``@internal`` in core can be modified and removed at any time, and we do not guarantee +any backward compatibility for those functions. DO NOT USE THEM. If you find yourself needing to use them, open an issue +on Github and we will consider adding a public equivalent. + Metadata Changes ---------------- @@ -546,6 +555,7 @@ Miscellaneous API changes * The config value ``entity_types`` is no longer present or used. * Uploaded images are autorotated based on their orientation metadata. * The view ``object/widget/edit/num_display`` now uses an ``input/number`` field instead of ``input/select``; you might need to update your widget edit views accordingly. + * Annotation names are no longer trimmed during save View extension behaviour changed -------------------------------- diff --git a/engine/classes/Elgg/BootData.php b/engine/classes/Elgg/BootData.php index 6b14c23e4b3..31837055d86 100644 --- a/engine/classes/Elgg/BootData.php +++ b/engine/classes/Elgg/BootData.php @@ -23,7 +23,7 @@ class BootData { /** * @var \ElggPlugin[] */ - private $active_plugins = []; + private $active_plugins; /** * @var array diff --git a/engine/classes/Elgg/Cache/MetadataCache.php b/engine/classes/Elgg/Cache/MetadataCache.php index 60679087f3f..72ae97bd739 100644 --- a/engine/classes/Elgg/Cache/MetadataCache.php +++ b/engine/classes/Elgg/Cache/MetadataCache.php @@ -19,9 +19,9 @@ class MetadataCache { protected $values = []; /** - * @var \ElggSession + * @var array */ - protected $session; + protected $ids = []; /** * @var ElggSharedMemoryCache @@ -47,12 +47,14 @@ public function __construct(ElggSharedMemoryCache $cache = null) { * * @param int $entity_guid The GUID of the entity * @param array $values The metadata values to cache + * @param array $ids The metadata ids * @return void * * @access private For testing only */ - public function inject($entity_guid, array $values) { + public function inject($entity_guid, array $values, array $ids = []) { $this->values[$entity_guid] = $values; + $this->ids[$entity_guid] = $ids; } /** @@ -77,6 +79,28 @@ public function getSingle($entity_guid, $name) { } } + /** + * Get the metadata id for a particular name + * + * Warning: You should always call isLoaded() beforehand to verify that this + * function's return value can be trusted. + * + * @see isLoaded + * + * @param int $entity_guid The GUID of the entity + * @param string $name The metadata name + * + * @return int[]|int|null + */ + public function getSingleId($entity_guid, $name) { + if (isset($this->ids[$entity_guid]) + && array_key_exists($name, $this->ids[$entity_guid])) { + return $this->ids[$entity_guid][$name]; + } else { + return null; + } + } + /** * Forget about all metadata for an entity. * @@ -85,6 +109,7 @@ public function getSingle($entity_guid, $name) { */ public function clear($entity_guid) { unset($this->values[$entity_guid]); + unset($this->ids[$entity_guid]); $this->cache->delete($entity_guid); } @@ -95,7 +120,7 @@ public function clear($entity_guid) { * @return bool */ public function isLoaded($entity_guid) { - return array_key_exists($entity_guid, $this->values); + return array_key_exists($entity_guid, $this->values) && array_key_exists($entity_guid, $this->ids); } /** @@ -108,6 +133,7 @@ public function clearAll() { $this->cache->delete($guid); } $this->values = []; + $this->ids = []; } /** @@ -120,8 +146,10 @@ public function clearAll() { public function invalidateByOptions(array $options) { if (empty($options['guid'])) { $this->clearAll(); + _elgg_services()->entityCache->clear(); } else { $this->clear($options['guid']); + _elgg_services()->entityCache->remove($options['guid']); } } @@ -135,17 +163,14 @@ public function populateFromEntities($guids) { if (empty($guids)) { return; } - + $version = (int) _elgg_config()->version; if (!empty($version) && ($version < 2016110900)) { // can't use this during upgrade from 2.x to 3.0 return; } - if (!is_array($guids)) { - $guids = [$guids]; - } - $guids = array_unique($guids); + $guids = array_unique((array) $guids); foreach ($guids as $i => $guid) { $value = $this->cache->load($guid); @@ -167,42 +192,43 @@ public function populateFromEntities($guids) { 'callback' => false, 'distinct' => false, 'order_by' => [ - new OrderByClause('n_table.entity_guid'), - new OrderByClause('n_table.time_created'), - new OrderByClause('n_table.id'), + new OrderByClause('n_table.entity_guid', 'asc'), + new OrderByClause('n_table.time_created', 'asc'), + new OrderByClause('n_table.id', 'asc') ], ]; + + // We already have a loaded entity, so we can ignore entity access clauses + $ia = elgg_set_ignore_access(true); $data = _elgg_services()->metadataTable->getAll($options); + elgg_set_ignore_access($ia); // make sure we show all entities as loaded foreach ($guids as $guid) { $this->values[$guid] = null; + $this->ids[$guid] = null; } // build up metadata for each entity, save when GUID changes (or data ends) - $last_guid = null; - $metadata = []; - $last_row_idx = count($data) - 1; foreach ($data as $i => $row) { + $id = $row->id; $name = $row->name; $value = ($row->value_type === 'text') ? $row->value : (int) $row->value; $guid = $row->entity_guid; - if ($guid !== $last_guid) { - if ($last_guid) { - $this->values[$last_guid] = $metadata; - } - $metadata = []; - } - if (isset($metadata[$name])) { - $metadata[$name] = (array) $metadata[$name]; - $metadata[$name][] = $value; + + if (isset($this->values[$guid][$name])) { + $this->values[$guid][$name] = (array) $this->values[$guid][$name]; + $this->values[$guid][$name][] = $value; } else { - $metadata[$name] = $value; + $this->values[$guid][$name] = $value; } - if (($i == $last_row_idx)) { - $this->values[$guid] = $metadata; + + if (isset($this->ids[$guid][$name])) { + $this->ids[$guid][$name] = (array) $this->ids[$guid][$name]; + $this->ids[$guid][$name][] = $id; + } else { + $this->ids[$guid][$name] = $id; } - $last_guid = $guid; } foreach ($guids as $guid) { @@ -220,7 +246,7 @@ public function populateFromEntities($guids) { * @return array */ public function filterMetadataHeavyEntities(array $guids, $limit = 1024000) { - + $data = _elgg_services()->metadataTable->getAll([ 'guids' => $guids, 'limit' => 0, @@ -229,7 +255,7 @@ public function filterMetadataHeavyEntities(array $guids, $limit = 1024000) { 'order_by' => 'n_table.entity_guid, n_table.time_created ASC', 'group_by' => 'n_table.entity_guid', ]); - + // don't cache if metadata for entity is over 10MB (or rolled INT) foreach ($data as $row) { if ($row->bytes > $limit || $row->bytes < 0) { diff --git a/engine/classes/Elgg/Database/AccessCollections.php b/engine/classes/Elgg/Database/AccessCollections.php index f8ffcddf882..2df06df8adc 100644 --- a/engine/classes/Elgg/Database/AccessCollections.php +++ b/engine/classes/Elgg/Database/AccessCollections.php @@ -236,133 +236,6 @@ public function getAccessArray($user_guid = 0, $flush = false) { return $this->hooks->trigger('access:collections:read', 'user', $options, $access_array); } - /** - * Returns the SQL where clause for enforcing read access to data. - * - * Note that if this code is executed in privileged mode it will return (1=1). - * - * Otherwise it returns a where clause to retrieve the data that a user has - * permission to read. - * - * Plugin authors can hook into the 'get_sql', 'access' plugin hook to modify, - * remove, or add to the where clauses. The plugin hook will pass an array with the current - * ors and ands to the function in the form: - * array( - * 'ors' => array(), - * 'ands' => array() - * ) - * - * The results will be combined into an SQL where clause in the form: - * ((or1 OR or2 OR orN) AND (and1 AND and2 AND andN)) - * - * @param array $options Array in format: - * - * table_alias => STR Optional table alias. This is based on the select and join clauses. - * Default is 'e'. - * - * user_guid => INT Optional GUID for the user that we are retrieving data for. - * Defaults to the logged in user if null. - * Passing 0 will build a query for a logged out user (even if there is a logged in user) - * - * use_enabled_clause => BOOL Optional. Should we append the enabled clause? The default - * is set by access_show_hidden_entities(). - * - * access_column => STR Optional access column name. Default is 'access_id'. - * - * owner_guid_column => STR Optional owner_guid column. Default is 'owner_guid'. - * - * guid_column => STR Optional guid_column. Default is 'guid'. - * - * @return string - * @access private - */ - public function getWhereSql(array $options = []) { - - $defaults = [ - 'table_alias' => 'e', - 'user_guid' => $this->session->getLoggedInUserGuid(), - 'use_enabled_clause' => !access_get_show_hidden_status(), - 'access_column' => 'access_id', - 'owner_guid_column' => 'owner_guid', - 'guid_column' => 'guid', - ]; - - foreach ($options as $key => $value) { - if (is_null($value)) { - // remove null values so we don't loose defaults in array_merge - unset($options[$key]); - } - } - - $options = array_merge($defaults, $options); - - // just in case someone passes a . at the end - $options['table_alias'] = rtrim($options['table_alias'], '.'); - - foreach (['table_alias', 'access_column', 'owner_guid_column', 'guid_column'] as $key) { - $options[$key] = sanitize_string($options[$key]); - } - $options['user_guid'] = sanitize_int($options['user_guid'], false); - - // only add dot if we have an alias or table name - $table_alias = $options['table_alias'] ? $options['table_alias'] . '.' : ''; - - if (!isset($options['ignore_access'])) { - $options['ignore_access'] = $this->capabilities->canBypassPermissionsCheck($options['user_guid']); - } - - $clauses = [ - 'ors' => [], - 'ands' => [] - ]; - - $prefix = $this->db->prefix; - - if ($options['ignore_access']) { - $clauses['ors']['ignore_access'] = '1 = 1'; - } else if ($options['user_guid']) { - // include content of user's friends - $clauses['ors']['friends_access'] = "$table_alias{$options['access_column']} = " . ACCESS_FRIENDS . " - AND $table_alias{$options['owner_guid_column']} IN ( - SELECT guid_one FROM {$prefix}entity_relationships - WHERE relationship = 'friend' AND guid_two = {$options['user_guid']} - )"; - - // include user's content - $clauses['ors']['owner_access'] = "$table_alias{$options['owner_guid_column']} = {$options['user_guid']}"; - } - - // include standard accesses (public, logged in, access collections) - if (!$options['ignore_access']) { - $access_list = $this->getAccessList($options['user_guid']); - $clauses['ors']['acl_access'] = "$table_alias{$options['access_column']} IN {$access_list}"; - } - - if ($options['use_enabled_clause']) { - $clauses['ands']['use_enabled'] = "{$table_alias}enabled = 'yes'"; - } - - $clauses = $this->hooks->trigger('get_sql', 'access', $options, $clauses); - - $clauses_str = ''; - if (is_array($clauses['ors']) && $clauses['ors']) { - $clauses_str = '(' . implode(' OR ', $clauses['ors']) . ')'; - } - - if (is_array($clauses['ands']) && $clauses['ands']) { - if ($clauses_str) { - $clauses_str .= ' AND '; - } - $clauses_str .= '(' . implode(' AND ', $clauses['ands']) . ')'; - } - - if (empty($clauses_str)) { - $clauses_str = '1 = 1'; - } - - return "($clauses_str)"; - } - /** * Can a user access an entity. * diff --git a/engine/classes/Elgg/Database/Annotations.php b/engine/classes/Elgg/Database/Annotations.php index a1178c5f6e6..e8c40758157 100644 --- a/engine/classes/Elgg/Database/Annotations.php +++ b/engine/classes/Elgg/Database/Annotations.php @@ -25,21 +25,6 @@ */ class Annotations extends Repository { - /** - * Build and execute a new query from an array of legacy options - * - * @param array $options Options - * - * @return ElggAnnotation[]|int|mixed - */ - public static function find(array $options = []) { - try { - return static::with($options)->execute(); - } catch (DataFormatException $e) { - return elgg_extract('count', $options) ? 0 : false; - } - } - /** * {@inheritdoc} */ @@ -138,23 +123,7 @@ public function get($limit = null, $offset = null, $callback = null) { $distinct = $this->options->distinct ? "DISTINCT" : ""; $qb->select("$distinct n_table.*"); - foreach ($this->options->selects as $select_clause) { - $select_clause->prepare($qb, 'n_table'); - } - - foreach ($this->options->group_by as $group_by_clause) { - $group_by_clause->prepare($qb, 'n_table'); - } - - foreach ($this->options->having as $having_clause) { - $having_clause->prepare($qb, 'n_table'); - } - - if (!empty($this->options->order_by)) { - foreach ($this->options->order_by as $order_by_clause) { - $order_by_clause->prepare($qb, 'n_table'); - } - } + $this->expandInto($qb, 'n_table'); $qb = $this->buildQuery($qb); @@ -185,28 +154,6 @@ public function get($limit = null, $offset = null, $callback = null) { return $results; } - /** - * {@inheritdoc} - */ - public function batch($limit = null, $offset = null, $callback = null) { - - $options = $this->options->getArrayCopy(); - - $options['limit'] = (int) $limit; - $options['offset'] = (int) $offset; - $options['callback'] = $callback; - unset($options['count'], - $options['batch'], - $options['batch_size'], - $options['batch_inc_offset'] - ); - - $batch_size = $this->options->batch_size; - $batch_inc_offset = $this->options->batch_inc_offset; - - return new \ElggBatch([static::class, 'find'], $options, null, $batch_size, $batch_inc_offset); - } - /** * Execute the query resolving calculation, count and/or batch options * @@ -286,22 +233,8 @@ protected function buildQuery(QueryBuilder $qb) { * @return Closure|CompositeExpression|mixed|null|string */ protected function buildEntityWhereClause(QueryBuilder $qb) { - $where = new EntityWhereClause(); - $where->guids = $this->options->guids; - $where->owner_guids = $this->options->owner_guids; - $where->container_guids = $this->options->container_guids; - $where->type_subtype_pairs = $this->options->type_subtype_pairs; - $where->created_after = $this->options->created_after; - $where->created_before = $this->options->created_before; - $where->updated_after = $this->options->updated_after; - $where->updated_before = $this->options->updated_before; - $where->last_action_after = $this->options->last_action_after; - $where->last_action_before = $this->options->last_action_before; - $where->access_ids = $this->options->access_ids; - $joined_alias = $qb->joinEntitiesTable('n_table', 'entity_guid', 'inner', 'e'); - - return $where->prepare($qb, $joined_alias); + return EntityWhereClause::factory($this->options)->prepare($qb, $joined_alias); } /** diff --git a/engine/classes/Elgg/Database/AnnotationsTable.php b/engine/classes/Elgg/Database/AnnotationsTable.php index de0fb15902e..77fc861926a 100644 --- a/engine/classes/Elgg/Database/AnnotationsTable.php +++ b/engine/classes/Elgg/Database/AnnotationsTable.php @@ -1,59 +1,53 @@ db = $db; - $this->session = $session; $this->events = $events; } /** - * Get a specific annotation by its id. - * If you want multiple annotation objects, use - * {@link elgg_get_annotations()}. + * Get a specific annotation by its id * - * @param int $id The id of the annotation object being retrieved. + * @param int $id The id of the annotation object * * @return ElggAnnotation|false */ - function get($id) { + public function get($id) { $qb = Select::fromTable('annotations'); $qb->select('*'); @@ -68,179 +62,227 @@ function get($id) { return false; } - + /** - * Deletes an annotation using its ID. + * Deletes an annotation using its ID + * + * @param ElggAnnotation $annotation Annotation * - * @param int $id The annotation ID to delete. * @return bool */ - function delete($id) { - $annotation = $this->get($id); - if (!$annotation) { + public function delete(ElggAnnotation $annotation) { + if (!$annotation->canEdit()) { + return false; + } + + if (!$this->events->trigger('delete', 'annotation', $annotation)) { return false; } - return $annotation->delete(); + + $qb = Delete::fromTable('annotations'); + $qb->where($qb->compare('id', '=', $annotation->id, ELGG_VALUE_INTEGER)); + $deleted = $this->db->deleteData($qb); + + if ($deleted) { + elgg_delete_river([ + 'annotation_id' => $annotation->id, + 'limit' => false, + ]); + } + + return $deleted; } - + /** - * Create a new annotation. + * Create a new annotation and return its ID * - * @param int $entity_guid GUID of entity to be annotated - * @param string $name Name of annotation - * @param string $value Value of annotation - * @param string $value_type Type of value (default is auto detection) - * @param int $owner_guid Owner of annotation (default is logged in user) - * @param int $access_id Access level of annotation + * @param ElggAnnotation $annotation Annotation + * @param ElggEntity $entity Entity being annotated * - * @return int|bool id on success or false on failure + * @return int|bool */ - function create($entity_guid, $name, $value, $value_type = '', $owner_guid = 0, $access_id = ACCESS_PRIVATE) { - - $result = false; - - $entity_guid = (int) $entity_guid; - $value_type = \ElggExtender::detectValueType($value, $value_type); - - $owner_guid = (int) $owner_guid; - if ($owner_guid == 0) { - $owner_guid = $this->session->getLoggedInUserGuid(); + public function create(ElggAnnotation $annotation, ElggEntity $entity) { + if ($annotation->id) { + return $this->update($annotation); } - - $access_id = (int) $access_id; - $entity = get_entity($entity_guid); + $annotation->entity_guid = $entity->guid; - if (!$entity) { - _elgg_services()->logger->error("Unable to load en entity with $entity_guid to annotate it"); - $entity_type = null; - } else { - $entity_type = $entity->type; + // @todo It looks like annotations permissions are not being checked anywhere... + // Uncomment once tests have been updated + // See #11418 + //if (!$entity->canAnnotate(0, $annotation->name)) { + // return false; + //} + + if (!$this->events->trigger('annotate', $entity->getType(), $entity)) { + return false; } - if ($this->events->trigger('annotate', $entity_type, $entity)) { - $sql = "INSERT INTO {$this->db->prefix}annotations - (entity_guid, name, value, value_type, owner_guid, time_created, access_id) - VALUES - (:entity_guid, :name, :value, :value_type, :owner_guid, :time_created, :access_id)"; - - $result = $this->db->insertData($sql, [ - ':entity_guid' => $entity_guid, - ':name' => $name, - ':value' => $value, - ':value_type' => $value_type, - ':owner_guid' => $owner_guid, - ':time_created' => $this->getCurrentTime()->getTimestamp(), - ':access_id' => $access_id, - ]); - - if ($result !== false) { - $obj = elgg_get_annotation_from_id($result); - if ($this->events->trigger('create', 'annotation', $obj)) { - return $result; - } else { - // plugin returned false to reject annotation - elgg_delete_annotation_by_id($result); - return false; - } - } + if (!$this->events->triggerBefore('create', 'annotation', $annotation)) { + return false; + } + + $time_created = $this->getCurrentTime()->getTimestamp(); + + $qb = Insert::intoTable('annotations'); + $qb->values([ + 'entity_guid' => $qb->param($annotation->entity_guid, ELGG_VALUE_INTEGER), + 'name' => $qb->param($annotation->name, ELGG_VALUE_STRING), + 'value' => $qb->param($annotation->value, $annotation->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING), + 'value_type' => $qb->param($annotation->value_type, ELGG_VALUE_STRING), + 'owner_guid' => $qb->param($annotation->owner_guid, ELGG_VALUE_INTEGER), + 'time_created' => $qb->param($time_created, ELGG_VALUE_INTEGER), + 'access_id' => $qb->param($annotation->access_id, ELGG_VALUE_INTEGER), + ]); + + $result = $this->db->insertData($qb); + if ($result === false) { + return false; } - + + $annotation->id = $result; + $annotation->time_created = $time_created; + + if (!$this->events->trigger('create', 'annotation', $annotation)) { + elgg_delete_annotation_by_id($result); + + return false; + } + + $entity->updateLastAction($annotation->time_created); + + $this->events->triggerAfter('create', 'annotation', $annotation); + return $result; } - + /** - * Update an annotation. + * Store updated annotation in the database * - * @param int $annotation_id Annotation ID - * @param string $name Name of annotation - * @param string $value Value of annotation - * @param string $value_type Type of value - * @param int $owner_guid Owner of annotation - * @param int $access_id Access level of annotation + * @todo Add canAnnotate check if entity guid changes + * + * @param ElggAnnotation $annotation Annotation to store * * @return bool */ - function update($annotation_id, $name, $value, $value_type, $owner_guid, $access_id) { + public function update(ElggAnnotation $annotation) { + if (!$annotation->canEdit()) { + return false; + } - $annotation_id = (int) $annotation_id; - - $annotation = $this->get($annotation_id); - if (!$annotation) { + if (!$this->events->triggerBefore('update', 'annotation', $annotation)) { return false; } + + $qb = Update::table('annotations'); + $qb->set('name', $qb->param($annotation->name, ELGG_VALUE_STRING)) + ->set('value', $qb->param($annotation->value, $annotation->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING)) + ->set('value_type', $qb->param($annotation->value_type, ELGG_VALUE_STRING)) + ->set('access_id', $qb->param($annotation->access_id, ELGG_VALUE_INTEGER)) + ->set('owner_guid', $qb->param($annotation->owner_guid, ELGG_VALUE_INTEGER)) + ->where($qb->compare('id', '=', $annotation->id, ELGG_VALUE_INTEGER)); + + $result = $this->db->updateData($qb); + + if ($result === false) { + return false; + } + + $this->events->trigger('update', 'annotation', $annotation); + $this->events->triggerAfter('update', 'annotation', $annotation); + + return $result; + } + + /** + * Disable the annotation. + * + * @param ElggAnnotation $annotation Annotation + * + * @return bool + * @since 1.8 + */ + public function disable(ElggAnnotation $annotation) { + if ($annotation->enabled == 'no') { + return true; + } + if (!$annotation->canEdit()) { return false; } - - $name = trim($name); - $value_type = \ElggExtender::detectValueType($value, $value_type); - - $owner_guid = (int) $owner_guid; - if ($owner_guid == 0) { - $owner_guid = $this->session->getLoggedInUserGuid(); + + if (!elgg_trigger_event('disable', $annotation->getType(), $annotation)) { + return false; } - - $access_id = (int) $access_id; - - $sql = "UPDATE {$this->db->prefix}annotations - (name, value, value_type, access_id, owner_guid) - VALUES - (:name, :value, :value_type, :access_id, :owner_guid) - WHERE id = :annotation_id"; - - $result = $this->db->updateData($sql, false, [ - ':name' => $name, - ':value' => $value, - ':value_type' => $value_type, - ':access_id' => $access_id, - ':owner_guid' => $owner_guid, - ':annotation_id' => $annotation_id, - ]); - - if ($result !== false) { - // @todo add plugin hook that sends old and new annotation information before db access - $obj = $this->get($annotation_id); - $this->events->trigger('update', 'annotation', $obj); + + if ($annotation->id) { + $qb = Update::table('annotations'); + $qb->set('enabled', $qb->param('no', ELGG_VALUE_STRING)) + ->where($qb->compare('id', '=', $annotation->id, ELGG_VALUE_INTEGER)); + + if (!$this->db->updateData($qb)) { + return false; + } } - - return $result; + + $annotation->enabled = 'no'; + + return true; } - + /** - * Returns annotations. Accepts all elgg_get_entities() options for entity - * restraints. + * Enable the annotation * - * @see elgg_get_entities() + * @param ElggAnnotation $annotation Annotation * - * @param array $options Array in format: + * @return bool + * @since 1.8 + */ + public function enable(ElggAnnotation $annotation) { + if ($annotation->enabled == 'yes') { + return true; + } + + if (!$annotation->canEdit()) { + return false; + } + + if (!$this->events->trigger('enable', $annotation->getType(), $annotation)) { + return false; + } + + if ($annotation->id) { + $qb = Update::table('annotations'); + $qb->set('enabled', $qb->param('yes', ELGG_VALUE_STRING)) + ->where($qb->compare('id', '=', $annotation->id, ELGG_VALUE_INTEGER)); + + if (!$this->db->updateData($qb)) { + return false; + } + } + + $annotation->enabled = 'yes'; + + return true; + } + + /** + * Returns annotations. Accepts all {@link elgg_get_entities()} options * - * annotation_names => null|ARR Annotation names - * annotation_values => null|ARR Annotation values - * annotation_ids => null|ARR annotation ids - * annotation_case_sensitive => BOOL Overall Case sensitive - * annotation_owner_guids => null|ARR guids for annotation owners - * annotation_created_time_lower => INT Lower limit for created time. - * annotation_created_time_upper => INT Upper limit for created time. - * annotation_calculation => STR Perform the MySQL function on the annotation values returned. - * Do not confuse this "annotation_calculation" option with the - * "calculation" option to elgg_get_entities_from_annotation_calculation(). - * The "annotation_calculation" option causes this function to - * return the result of performing a mathematical calculation on - * all annotations that match the query instead of \ElggAnnotation - * objects. - * See the docs for elgg_get_entities_from_annotation_calculation() - * for the proper use of the "calculation" option. + * @see elgg_get_entities() * + * @param array $options Options * * @return ElggAnnotation[]|mixed */ - function find(array $options = []) { + public function find(array $options = []) { $options['metastring_type'] = 'annotations'; $options = _elgg_normalize_metastrings_options($options); + return Annotations::find($options); } - + /** * Deletes annotations based on $options. * @@ -248,10 +290,14 @@ function find(array $options = []) { * This requires at least one constraint: annotation_owner_guid(s), * annotation_name(s), annotation_value(s), or guid(s) must be set. * - * @param array $options An options array. {@link elgg_get_annotations()} + * @see elgg_get_annotations() + * @see elgg_get_entities() + * + * @param array $options Options + * * @return bool|null true on success, false on failure, null if no annotations to delete. */ - function deleteAll(array $options) { + public function deleteAll(array $options) { if (!_elgg_is_valid_options_for_batch_operation($options, 'annotation')) { return false; } @@ -276,7 +322,7 @@ function deleteAll(array $options) { return $success == $count; } - + /** * Disables annotations based on $options. * @@ -285,11 +331,11 @@ function deleteAll(array $options) { * @param array $options An options array. {@link elgg_get_annotations()} * @return bool|null true on success, false on failure, null if no annotations disabled. */ - function disableAll(array $options) { + public function disableAll(array $options) { if (!_elgg_is_valid_options_for_batch_operation($options, 'annotation')) { return false; } - + // if we can see hidden (disabled) we need to use the offset // otherwise we risk an infinite loop if there are more than 50 $inc_offset = access_get_show_hidden_status(); @@ -314,7 +360,7 @@ function disableAll(array $options) { return $success == $count; } - + /** * Enables annotations based on $options. * @@ -326,7 +372,7 @@ function disableAll(array $options) { * @param array $options An options array. {@link elgg_get_annotations()} * @return bool|null true on success, false on failure, null if no metadata enabled. */ - function enableAll(array $options) { + public function enableAll(array $options) { if (!_elgg_is_valid_options_for_batch_operation($options, 'annotation')) { return false; } @@ -350,33 +396,29 @@ function enableAll(array $options) { return $success == $count; } - + /** * Check to see if a user has already created an annotation on an object * - * @param int $entity_guid Entity guid - * @param string $annotation_type Type of annotation - * @param int $owner_guid Defaults to logged in user. + * @param int $entity_guid Entity guid + * @param string $name Annotation name + * @param int $owner_guid Owner guid * * @return bool */ - function exists($entity_guid, $annotation_type, $owner_guid = null) { - - if (!$owner_guid && !($owner_guid = $this->session->getLoggedInUserGuid())) { + public function exists($entity_guid, $name, $owner_guid) { + if (!$owner_guid) { return false; } - $sql = "SELECT id FROM {$this->db->prefix}annotations - WHERE owner_guid = :owner_guid - AND entity_guid = :entity_guid - AND name = :annotation_type"; + $qb = Select::fromTable('annotations'); + $qb->select('id'); + $qb->where($qb->compare('owner_guid', '=', $owner_guid, ELGG_VALUE_INTEGER)) + ->andWhere($qb->compare('entity_guid', '=', $entity_guid, ELGG_VALUE_INTEGER)) + ->andWhere($qb->compare('name', '=', $name, ELGG_VALUE_STRING)); - $result = $this->db->getDataRow($sql, null, [ - ':owner_guid' => (int) $owner_guid, - ':entity_guid' => (int) $entity_guid, - ':annotation_type' => $annotation_type, - ]); - - return (bool) $result; + $result = $this->db->getDataRow($qb); + + return $result && $result->id; } } diff --git a/engine/classes/Elgg/Database/Clauses/EntityWhereClause.php b/engine/classes/Elgg/Database/Clauses/EntityWhereClause.php index d4f1945821b..de9b8c91e0e 100644 --- a/engine/classes/Elgg/Database/Clauses/EntityWhereClause.php +++ b/engine/classes/Elgg/Database/Clauses/EntityWhereClause.php @@ -3,6 +3,7 @@ namespace Elgg\Database\Clauses; use Elgg\Database\QueryBuilder; +use Elgg\Database\QueryOptions; /** * Builds queries for filtering entities by their properties in the entities table @@ -117,4 +118,27 @@ public function prepare(QueryBuilder $qb, $table_alias = '') { return $qb->merge($wheres); } + + /** + * Build new clause from options + * + * @param QueryOptions $options Options + * @return static + */ + public static function factory(QueryOptions $options) { + $where = new static(); + $where->guids = $options->guids; + $where->owner_guids = $options->owner_guids; + $where->container_guids = $options->container_guids; + $where->type_subtype_pairs = $options->type_subtype_pairs; + $where->created_after = $options->created_after; + $where->created_before = $options->created_before; + $where->updated_after = $options->updated_after; + $where->updated_before = $options->updated_before; + $where->last_action_after = $options->last_action_after; + $where->last_action_before = $options->last_action_before; + $where->access_ids = $options->access_ids; + + return $where; + } } diff --git a/engine/classes/Elgg/Database/Clauses/RiverWhereClause.php b/engine/classes/Elgg/Database/Clauses/RiverWhereClause.php new file mode 100644 index 00000000000..f69b6e57e3e --- /dev/null +++ b/engine/classes/Elgg/Database/Clauses/RiverWhereClause.php @@ -0,0 +1,111 @@ +use_enabled_clause)) { + $this->use_enabled_clause = !access_get_show_hidden_status(); + } + + if (!isset($this->enabled) && $this->use_enabled_clause) { + $this->enabled = 'yes'; + } + + $types = new TypeSubtypeWhereClause(); + $types->type_subtype_pairs = $this->type_subtype_pairs; + $wheres[] = $types->prepare($qb, $table_alias); + + $wheres[] = $qb->compare($alias('id'), '=', $this->ids, ELGG_VALUE_ID); + $wheres[] = $qb->compare($alias('annotation_id'), '=', $this->annotation_ids, ELGG_VALUE_ID); + $wheres[] = $qb->compare($alias('view'), '=', $this->views, ELGG_VALUE_STRING); + $wheres[] = $qb->compare($alias('action_type'), '=', $this->action_types, ELGG_VALUE_STRING); + $wheres[] = $qb->compare($alias('subject_guid'), '=', $this->subject_guids, ELGG_VALUE_GUID); + $wheres[] = $qb->compare($alias('object_guid'), '=', $this->object_guids, ELGG_VALUE_GUID); + $wheres[] = $qb->compare($alias('target_guid'), '=', $this->target_guids, ELGG_VALUE_GUID); + $wheres[] = $qb->compare($alias('enabled'), '=', $this->enabled, ELGG_VALUE_STRING); + $wheres[] = $qb->between($alias('posted'), $this->created_after, $this->created_before, ELGG_VALUE_TIMESTAMP); + + return $qb->merge($wheres); + } + +} diff --git a/engine/classes/Elgg/Database/Entities.php b/engine/classes/Elgg/Database/Entities.php index 7d47b2f228d..3970ab6ef59 100644 --- a/engine/classes/Elgg/Database/Entities.php +++ b/engine/classes/Elgg/Database/Entities.php @@ -33,21 +33,6 @@ */ class Entities extends Repository { - /** - * Build and execute a new query from an array of legacy options - * - * @param array $options Options - * - * @return ElggEntity[]|int|mixed - */ - public static function find(array $options = []) { - try { - return static::with($options)->execute(); - } catch (DataFormatException $e) { - return elgg_extract('count', $options) ? 0 : false; - } - } - /** * {@inheritdoc} */ @@ -138,23 +123,7 @@ public function get($limit = null, $offset = null, $callback = null) { $distinct = $this->options->distinct ? "DISTINCT" : ""; $qb->select("$distinct e.*"); - foreach ($this->options->selects as $select_clause) { - $select_clause->prepare($qb, 'e'); - } - - foreach ($this->options->group_by as $group_by_clause) { - $group_by_clause->prepare($qb, 'e'); - } - - foreach ($this->options->having as $having_clause) { - $having_clause->prepare($qb, 'e'); - } - - if (!empty($this->options->order_by)) { - foreach ($this->options->order_by as $order_by_clause) { - $order_by_clause->prepare($qb, 'e'); - } - } + $this->expandInto($qb, 'e'); $qb = $this->buildQuery($qb); @@ -183,28 +152,6 @@ public function get($limit = null, $offset = null, $callback = null) { return _elgg_services()->entityTable->fetch($qb, $options); } - /** - * {@inheritdoc} - */ - public function batch($limit = null, $offset = null, $callback = null) { - - $options = $this->options->getArrayCopy(); - - $options['limit'] = (int) $limit; - $options['offset'] = (int) $offset; - $options['callback'] = $callback; - unset($options['count'], - $options['batch'], - $options['batch_size'], - $options['batch_inc_offset'] - ); - - $batch_size = $this->options->batch_size; - $batch_inc_offset = $this->options->batch_inc_offset; - - return new \ElggBatch([static::class, 'find'], $options, null, $batch_size, $batch_inc_offset); - } - /** * Execute the query resolving calculation, count and/or batch options * @@ -284,20 +231,7 @@ protected function buildQuery(QueryBuilder $qb) { * @return Closure|CompositeExpression|mixed|null|string */ protected function buildEntityClause(QueryBuilder $qb) { - $where = new EntityWhereClause(); - $where->guids = $this->options->guids; - $where->owner_guids = $this->options->owner_guids; - $where->container_guids = $this->options->container_guids; - $where->type_subtype_pairs = $this->options->type_subtype_pairs; - $where->created_after = $this->options->created_after; - $where->created_before = $this->options->created_before; - $where->updated_after = $this->options->updated_after; - $where->updated_before = $this->options->updated_before; - $where->last_action_after = $this->options->last_action_after; - $where->last_action_before = $this->options->last_action_before; - $where->access_ids = $this->options->access_ids; - - return $where->prepare($qb, 'e'); + return EntityWhereClause::factory($this->options)->prepare($qb, 'e'); } /** @@ -321,9 +255,8 @@ protected function buildPairedMetadataClause(QueryBuilder $qb, $clauses, $boolea } else { $joined_alias = $qb->joinMetadataTable('e', 'guid', $clause->names); } + $parts[] = $clause->prepare($qb, $joined_alias); } - - $parts[] = $clause->prepare($qb, $joined_alias); } return $qb->merge($parts, $boolean); diff --git a/engine/classes/Elgg/Database/EntityTable.php b/engine/classes/Elgg/Database/EntityTable.php index 17814730ed7..274f4eb348b 100644 --- a/engine/classes/Elgg/Database/EntityTable.php +++ b/engine/classes/Elgg/Database/EntityTable.php @@ -7,6 +7,8 @@ use Elgg\Cache\MetadataCache; use Elgg\Config; use Elgg\Database; +use Elgg\Database\Clauses\EntityWhereClause; +use Elgg\Database\Clauses\OrderByClause; use Elgg\Database\EntityTable\UserFetchFailureException; use Elgg\EntityPreloader; use Elgg\EventsService; @@ -183,19 +185,15 @@ public function getRow($guid, $user_guid = null) { return false; } - $access = _elgg_get_access_where_sql([ - 'table_alias' => '', - 'user_guid' => $user_guid, - ]); - - $sql = "SELECT * FROM {$this->db->prefix}entities - WHERE guid = :guid AND $access"; + $where = new EntityWhereClause(); + $where->guids = $guid; + $where->viewer_guid = $user_guid; - $params = [ - ':guid' => (int) $guid, - ]; + $select = Select::fromTable('entities'); + $select->select('*'); + $select->addClause($where); - return $this->db->getDataRow($sql, null, $params); + return $this->db->getDataRow($select); } /** @@ -531,171 +529,11 @@ public function fetchFromSql($sql, \ElggBatch $batch = null) { return $rows; } - /** - * Returns SQL where clause for type and subtype on main entity table - * - * @param string $table Entity table prefix as defined in SELECT...FROM entities $table - * @param null|array $types Array of types or null if none. - * @param null|array $subtypes Array of subtypes or null if none - * @param null|array $pairs Array of pairs of types and subtypes - * - * @return false|string - * @access private - */ - public function getEntityTypeSubtypeWhereSql($table, $types, $subtypes, $pairs) { - // short circuit if nothing is requested - if (!$types && !$subtypes && !$pairs) { - return ''; - } - - $or_clauses = []; - - if (is_array($pairs)) { - foreach ($pairs as $paired_type => $paired_subtypes) { - if (!empty($paired_subtypes)) { - $paired_subtypes = (array) $paired_subtypes; - $paired_subtypes = array_map(function ($el) { - $el = trim((string) $el, "\"\'"); - - return "'$el'"; - }, $paired_subtypes); - - $paired_subtypes_in = implode(',', $paired_subtypes); - - $or_clauses[] = "( - {$table}.type = '{$paired_type}' - AND {$table}.subtype IN ({$paired_subtypes_in}) - )"; - } else { - $or_clauses[] = "({$table}.type = '{$paired_type}')"; - } - } - } else { - $types = (array) $types; - - foreach ($types as $type) { - if (!empty($subtypes)) { - $subtypes = (array) $subtypes; - $subtypes = array_map(function ($el) { - $el = trim((string) $el, "\"\'"); - - return "'$el'"; - }, $subtypes); - - $subtypes_in = implode(',', $subtypes); - - $or_clauses[] = "( - {$table}.type = '{$type}' - AND {$table}.subtype IN ({$subtypes_in}) - )"; - } else { - $or_clauses[] = "({$table}.type = '{$type}')"; - } - } - } - - if (empty($or_clauses)) { - return ''; - } - - return '(' . implode(' OR ', $or_clauses) . ')'; - } - - /** - * Returns SQL where clause for owner and containers. - * - * @param string $column Column name the guids should be checked against. Usually - * best to provide in table.column format. - * @param null|array $guids Array of GUIDs. - * - * @return false|string - * @access private - */ - public function getGuidBasedWhereSql($column, $guids) { - // short circuit if nothing requested - // 0 is a valid guid - if (!$guids && $guids !== 0) { - return ''; - } - - // normalize and sanitise owners - if (!is_array($guids)) { - $guids = [$guids]; - } - - $guids_sanitized = []; - foreach ($guids as $guid) { - if ($guid !== ELGG_ENTITIES_NO_VALUE) { - $guid = sanitise_int($guid); - - if (!$guid) { - return false; - } - } - $guids_sanitized[] = $guid; - } - - $where = ''; - $guid_str = implode(',', $guids_sanitized); - - // implode(',', 0) returns 0. - if ($guid_str !== false && $guid_str !== '') { - $where = "($column IN ($guid_str))"; - } - - return $where; - } - - /** - * Returns SQL where clause for entity time limits. - * - * @param string $table Entity table prefix as defined in - * SELECT...FROM entities $table - * @param null|int $time_created_upper Time created upper limit - * @param null|int $time_created_lower Time created lower limit - * @param null|int $time_updated_upper Time updated upper limit - * @param null|int $time_updated_lower Time updated lower limit - * - * @return false|string false on fail, string on success. - * @access private - */ - public function getEntityTimeWhereSql($table, $time_created_upper = null, - $time_created_lower = null, $time_updated_upper = null, $time_updated_lower = null) { - - $wheres = []; - - // exploit PHP's loose typing (quack) to check that they are INTs and not str cast to 0 - if ($time_created_upper && $time_created_upper == sanitise_int($time_created_upper)) { - $wheres[] = "{$table}.time_created <= $time_created_upper"; - } - - if ($time_created_lower && $time_created_lower == sanitise_int($time_created_lower)) { - $wheres[] = "{$table}.time_created >= $time_created_lower"; - } - - if ($time_updated_upper && $time_updated_upper == sanitise_int($time_updated_upper)) { - $wheres[] = "{$table}.time_updated <= $time_updated_upper"; - } - - if ($time_updated_lower && $time_updated_lower == sanitise_int($time_updated_lower)) { - $wheres[] = "{$table}.time_updated >= $time_updated_lower"; - } - - if (is_array($wheres) && count($wheres) > 0) { - $where_str = implode(' AND ', $wheres); - return "($where_str)"; - } - - return ''; - } - /** * Returns a list of months in which entities were updated or created. * * @tip Use this to generate a list of archives by month for when entities were added or updated. * - * @todo document how to pass in array for $subtype - * * @warning Months are returned in the form YYYYMM. * * @param string $type The type of entity @@ -707,63 +545,30 @@ public function getEntityTimeWhereSql($table, $time_created_upper = null, */ public function getDates($type = '', $subtype = '', $container_guid = 0, $order_by = 'time_created') { - $where = []; - - if ($type) { - $type = sanitise_string($type); - $where[] = "type='$type'"; - } - - if (is_array($subtype)) { - $or_clauses = []; - if (sizeof($subtype)) { - foreach ($subtype as $typekey => $subtypearray) { - foreach ($subtypearray as $subtypeval) { - $subtype_str = sanitize_string($subtypeval); - $type_str = sanitize_string($typekey); - $or_clauses = "(type = '{$type_str}' and subtype = '{$subtype_str}')"; - } - } - } - if (!empty($or_clauses)) { - $where[] = '(' . implode(' OR ', $or_clauses) . ')'; - } - } else { - if ($subtype) { - $where[] = "subtype='$subtype'"; - } - } - - if ($container_guid !== 0) { - if (is_array($container_guid)) { - foreach ($container_guid as $key => $val) { - $container_guid[$key] = (int) $val; - } - $where[] = "container_guid in (" . implode(",", $container_guid) . ")"; - } else { - $container_guid = (int) $container_guid; - $where[] = "container_guid = {$container_guid}"; - } - } + $options = [ + 'types' => $type, + 'subtypes' => $subtype, + 'container_guids' => $container_guid, + 'callback' => false, + 'order_by' => [ + new OrderByClause($order_by), + ], + ]; - $where[] = _elgg_get_access_where_sql(['table_alias' => '']); + $options = new QueryOptions($options); - $sql = "SELECT DISTINCT EXTRACT(YEAR_MONTH FROM FROM_UNIXTIME(time_created)) AS yearmonth - FROM {$this->db->prefix}entities where "; + $qb = Select::fromTable('entities'); + $qb->select("DISTINCT EXTRACT(YEAR_MONTH FROM FROM_UNIXTIME(time_created)) AS yearmonth"); + $qb->addClause(EntityWhereClause::factory($options)); - foreach ($where as $w) { - $sql .= " $w and "; + $results = _elgg_services()->db->getData($qb); + if (empty($results)) { + return false; } - $sql .= "1=1 ORDER BY $order_by"; - if ($result = $this->db->getData($sql)) { - $endresult = []; - foreach ($result as $res) { - $endresult[] = $res->yearmonth; - } - return $endresult; - } - return false; + return array_map(function($e) { + return $e->yearmonth; + }, $results); } /** diff --git a/engine/classes/Elgg/Database/LegacyQueryOptionsAdapter.php b/engine/classes/Elgg/Database/LegacyQueryOptionsAdapter.php index 2b4012f14cc..d2b144f5d7c 100644 --- a/engine/classes/Elgg/Database/LegacyQueryOptionsAdapter.php +++ b/engine/classes/Elgg/Database/LegacyQueryOptionsAdapter.php @@ -607,6 +607,9 @@ protected function normalizePairedOptions($type = 'metadata', array $options = [ $options["{$type}_name_value_pairs"][$key]['case_sensitive'] = elgg_extract("{$type}_case_sensitive", $options, true); } if (!isset($value['type'])) { + if (is_bool($value['value'])) { + $value['value'] = (int) $value['value']; + } if (is_int($value['value'])) { $options["{$type}_name_value_pairs"][$key]['type'] = ELGG_VALUE_INTEGER; } else { @@ -750,6 +753,7 @@ protected function normalizeTimeOptions(array $options = []) { 'annotation_created', 'relationship_created', 'last_action', + 'posted', ]; $bounds = ['time_lower', 'time_upper', 'after', 'before']; @@ -762,6 +766,7 @@ protected function normalizeTimeOptions(array $options = []) { $new_prop_name = $prop_name; $new_prop_name = str_replace('modified', 'updated', $new_prop_name); + $new_prop_name = str_replace('posted', 'created', $new_prop_name); $new_prop_name = str_replace('time_lower', 'after', $new_prop_name); $new_prop_name = str_replace('time_upper', 'before', $new_prop_name); diff --git a/engine/classes/Elgg/Database/Metadata.php b/engine/classes/Elgg/Database/Metadata.php index 3ca82ede06e..f722e395cd6 100644 --- a/engine/classes/Elgg/Database/Metadata.php +++ b/engine/classes/Elgg/Database/Metadata.php @@ -28,21 +28,6 @@ */ class Metadata extends Repository { - /** - * Build and execute a new query from an array of legacy options - * - * @param array $options Options - * - * @return ElggMetadata[]|int|mixed - */ - public static function find(array $options = []) { - try { - return static::with($options)->execute(); - } catch (DataFormatException $e) { - return elgg_extract('count', $options) ? 0 : false; - } - } - /** * {@inheritdoc} */ @@ -144,23 +129,7 @@ public function get($limit = null, $offset = null, $callback = null) { $distinct = $this->options->distinct ? "DISTINCT" : ""; $qb->select("$distinct n_table.*"); - foreach ($this->options->selects as $select_clause) { - $select_clause->prepare($qb, 'n_table'); - } - - foreach ($this->options->group_by as $group_by_clause) { - $group_by_clause->prepare($qb, 'n_table'); - } - - foreach ($this->options->having as $having_clause) { - $having_clause->prepare($qb, 'n_table'); - } - - if (!empty($this->options->order_by)) { - foreach ($this->options->order_by as $order_by_clause) { - $order_by_clause->prepare($qb, 'n_table'); - } - } + $this->expandInto($qb, 'n_table'); $qb = $this->buildQuery($qb); @@ -186,28 +155,6 @@ public function get($limit = null, $offset = null, $callback = null) { return _elgg_services()->db->getData($qb, $callback); } - /** - * {@inheritdoc} - */ - public function batch($limit = null, $offset = null, $callback = null) { - - $options = $this->options->getArrayCopy(); - - $options['limit'] = (int) $limit; - $options['offset'] = (int) $offset; - $options['callback'] = $callback; - unset($options['count'], - $options['batch'], - $options['batch_size'], - $options['batch_inc_offset'] - ); - - $batch_size = $this->options->batch_size; - $batch_inc_offset = $this->options->batch_inc_offset; - - return new ElggBatch([static::class, 'find'], $options, null, $batch_size, $batch_inc_offset); - } - /** * Execute the query resolving calculation, count and/or batch options * @@ -290,22 +237,9 @@ protected function buildEntityWhereClause(QueryBuilder $qb) { // Even if all of these properties are empty, we want to add this clause regardless, // to ensure that entity access clauses are appended to the query - $where = new EntityWhereClause(); - $where->guids = $this->options->guids; - $where->owner_guids = $this->options->owner_guids; - $where->container_guids = $this->options->container_guids; - $where->type_subtype_pairs = $this->options->type_subtype_pairs; - $where->created_after = $this->options->created_after; - $where->created_before = $this->options->created_before; - $where->updated_after = $this->options->updated_after; - $where->updated_before = $this->options->updated_before; - $where->last_action_after = $this->options->last_action_after; - $where->last_action_before = $this->options->last_action_before; - $where->access_ids = $this->options->access_ids; $joined_alias = $qb->joinEntitiesTable('n_table', 'entity_guid', 'inner', 'e'); - - return $where->prepare($qb, $joined_alias); + return EntityWhereClause::factory($this->options)->prepare($qb, $joined_alias); } /** diff --git a/engine/classes/Elgg/Database/MetadataTable.php b/engine/classes/Elgg/Database/MetadataTable.php index f7b393c8582..6b61abde903 100644 --- a/engine/classes/Elgg/Database/MetadataTable.php +++ b/engine/classes/Elgg/Database/MetadataTable.php @@ -1,45 +1,41 @@ cache = $cache; + MetadataCache $metadata_cache, + Database $db, + Events $events + ) { + $this->metadata_cache = $metadata_cache; $this->db = $db; - $this->entityTable = $entityTable; $this->events = $events; - $this->session = $session; - $this->table = $this->db->prefix . "metadata"; } /** @@ -77,7 +67,7 @@ public function __construct( * * @return bool */ - function registerTagName($name) { + public function registerTagName($name) { if (!in_array($name, $this->tag_names)) { $this->tag_names[] = $name; } @@ -85,25 +75,43 @@ function registerTagName($name) { return true; } + /** + * Unregisters a metadata tag name + * + * @param string $name Tag name + * + * @return bool + */ + public function unregisterTagName($name) { + $index = array_search($name, $this->tag_names); + if ($index >= 0) { + unset($this->tag_names[$index]); + + return true; + } + + return false; + } + /** * Returns an array of valid metadata names for tags. * * @return string[] */ - function getTagNames() { + public function getTagNames() { return $this->tag_names; } /** - * Get a specific metadata object by its id. - * If you want multiple metadata objects, use - * {@link elgg_get_metadata()}. + * Get a specific metadata object by its id + * + * @see MetadataTable::getAll() * * @param int $id The id of the metadata object being retrieved. * * @return ElggMetadata|false false if not found */ - function get($id) { + public function get($id) { $qb = Select::fromTable('metadata'); $qb->select('*'); @@ -120,178 +128,169 @@ function get($id) { } /** - * Deletes metadata using its ID. + * Deletes metadata using its ID + * + * @param ElggMetadata $metadata Metadata * - * @param int $id The metadata ID to delete. * @return bool */ - function delete($id) { - $metadata = $this->get($id); + public function delete(ElggMetadata $metadata) { + if (!$metadata->id || !$metadata->canEdit()) { + return false; + } + + if (!elgg_trigger_event('delete', 'metadata', $metadata)) { + return false; + } + + $qb = Delete::fromTable('metadata'); + $qb->where($qb->compare('id', '=', $metadata->id, ELGG_VALUE_INTEGER)); - return $metadata ? $metadata->delete() : false; + $deleted = $this->db->deleteData($qb); + + if ($deleted) { + $this->metadata_cache->clear($metadata->entity_guid); + } + + return $deleted; } /** - * Create a new metadata object, or update an existing one. + * Create a new metadata object, or update an existing one (if multiple is allowed) * * Metadata can be an array by setting allow_multiple to true, but it is an - * indexed array with no control over the indexing. + * indexed array with no control over the indexing * - * @param int $entity_guid The entity to attach the metadata to - * @param string $name Name of the metadata - * @param string $value Value of the metadata - * @param string $value_type 'text', 'integer', or '' for automatic detection - * @param bool $allow_multiple Allow multiple values for one key. Default is false + * @param ElggMetadata $metadata Metadata + * @param bool $allow_multiple Allow multiple values for one key. Default is false * * @return int|false id of metadata or false if failure */ - function create($entity_guid, $name, $value, $value_type = '', $allow_multiple = false) { - - $entity_guid = (int) $entity_guid; - $value_type = \ElggExtender::detectValueType($value, trim($value_type)); - $allow_multiple = (boolean) $allow_multiple; - - if (!isset($value)) { + public function create(ElggMetadata $metadata, $allow_multiple = false) { + if (!isset($metadata->value) || !isset($metadata->entity_guid)) { + elgg_log("Metadata must have a value and entity guid", 'ERROR'); return false; } - if (strlen($value) > self::MYSQL_TEXT_BYTE_LIMIT) { - elgg_log("Metadata '$name' is above the MySQL TEXT size limit and may be truncated.", 'WARNING'); + if (!is_scalar($metadata->value)) { + elgg_log("To set multiple metadata values use ElggEntity::setMetadata", 'ERROR'); + return false; } - $query = "SELECT * FROM {$this->table} - WHERE entity_guid = :entity_guid and name = :name LIMIT 1"; + if ($metadata->id) { + if ($this->update($metadata)) { + return $metadata->id; + } + } - $existing = $this->db->getDataRow($query, null, [ - ':entity_guid' => $entity_guid, - ':name' => $name, - ]); - if ($existing && !$allow_multiple) { - $id = (int) $existing->id; - $result = $this->update($id, $name, $value, $value_type); + if (strlen($metadata->value) > self::MYSQL_TEXT_BYTE_LIMIT) { + elgg_log("Metadata '$metadata->name' is above the MySQL TEXT size limit and may be truncated.", 'WARNING'); + } - if (!$result) { - return false; - } - } else { - // Support boolean types - if (is_bool($value)) { - $value = (int) $value; + if (!$allow_multiple) { + $id = $this->getIdsByName($metadata->entity_guid, $metadata->name); + if (is_array($id)) { + throw new \LogicException("Multiple '{$metadata->name}' metadata values exist for entity [guid: {$metadata->entity_guid}]. Use ElggEntity::setMetadata()"); } - // If ok then add it - $query = "INSERT INTO {$this->table} - (entity_guid, name, value, value_type, time_created) - VALUES (:entity_guid, :name, :value, :value_type, :time_created)"; - - $id = $this->db->insertData($query, [ - ':entity_guid' => $entity_guid, - ':name' => $name, - ':value' => $value, - ':value_type' => $value_type, - ':time_created' => $this->getCurrentTime()->getTimestamp(), - ]); - - if ($id !== false) { - $obj = $this->get($id); - if ($this->events->trigger('create', 'metadata', $obj)) { - $this->cache->clear($entity_guid); - - return $id; - } else { - $this->delete($id); + if ($id) { + $metadata->id = $id; + + if ($this->update($metadata)) { + return $metadata->id; } } } - return $id; + if (!$this->events->triggerBefore('create', 'metadata', $metadata)) { + return false; + } + + $time_created = $this->getCurrentTime()->getTimestamp(); + + $qb = Insert::intoTable('metadata'); + $qb->values([ + 'name' => $qb->param($metadata->name, ELGG_VALUE_STRING), + 'entity_guid' => $qb->param($metadata->entity_guid, ELGG_VALUE_INTEGER), + 'value' => $qb->param($metadata->value, $metadata->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING), + 'value_type' => $qb->param($metadata->value_type, ELGG_VALUE_STRING), + 'time_created' => $qb->param($time_created, ELGG_VALUE_INTEGER), + ]); + + $id = $this->db->insertData($qb); + + if ($id === false) { + return false; + } + + $metadata->id = (int) $id; + $metadata->time_created = $time_created; + + if ($this->events->trigger('create', 'metadata', $metadata)) { + $this->metadata_cache->clear($metadata->entity_guid); + + $this->events->triggerAfter('create', 'metadata', $metadata); + + return $id; + } else { + $this->delete($metadata); + + return false; + } } /** - * Update a specific piece of metadata. + * Update a specific piece of metadata * - * @param int $id ID of the metadata to update - * @param string $name Metadata name - * @param string $value Metadata value - * @param string $value_type Value type + * @param ElggMetadata $metadata Updated metadata * * @return bool */ - function update($id, $name, $value, $value_type) { - $id = (int) $id; - - if (!$md = $this->get($id)) { + public function update(ElggMetadata $metadata) { + if (!$metadata->canEdit()) { return false; } - if (!$md->canEdit()) { + + if (!$this->events->triggerBefore('update', 'metadata', $metadata)) { return false; } - $value_type = \ElggExtender::detectValueType($value, trim($value_type)); - - // Support boolean types (as integers) - if (is_bool($value)) { - $value = (int) $value; - } - if (strlen($value) > self::MYSQL_TEXT_BYTE_LIMIT) { - elgg_log("Metadata '$name' is above the MySQL TEXT size limit and may be truncated.", 'WARNING'); + if (strlen($metadata->value) > self::MYSQL_TEXT_BYTE_LIMIT) { + elgg_log("Metadata '$metadata->name' is above the MySQL TEXT size limit and may be truncated.", 'WARNING'); } - // If ok then add it - $query = "UPDATE {$this->table} - SET name = :name, - value = :value, - value_type = :value_type - WHERE id = :id"; - - $result = $this->db->updateData($query, false, [ - ':name' => $name, - ':value' => $value, - ':value_type' => $value_type, - ':id' => $id, - ]); - if ($result !== false) { - $this->cache->clear($md->entity_guid); + $qb = Update::table('metadata'); + $qb->set('name', $qb->param($metadata->name, ELGG_VALUE_STRING)) + ->set('value', $qb->param($metadata->value, $metadata->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING)) + ->set('value_type', $qb->param($metadata->value_type, ELGG_VALUE_STRING)) + ->where($qb->compare('id', '=', $metadata->id, ELGG_VALUE_INTEGER)); + + $result = $this->db->updateData($qb); - // @todo this event tells you the metadata has been updated, but does not - // let you do anything about it. What is needed is a plugin hook before - // the update that passes old and new values. - $obj = $this->get($id); - $this->events->trigger('update', 'metadata', $obj); + if ($result === false) { + return false; } - return $result; + $this->metadata_cache->clear($metadata->entity_guid); + + $this->events->trigger('update', 'metadata', $metadata); + $this->events->triggerAfter('update', 'metadata', $metadata); + + return true; } /** - * Returns metadata. Accepts all elgg_get_entities() options for entity - * restraints. + * Returns metadata * - * @see elgg_get_entities + * Accepts all {@link elgg_get_entities()} options for entity restraints. * - * @warning 1.7's find_metadata() didn't support limits and returned all metadata. - * This function defaults to a limit of 25. There is probably not a reason - * for you to return all metadata unless you're exporting an entity, - * have other restraints in place, or are doing something horribly - * wrong in your code. + * @see elgg_get_entities() * - * @param array $options Array in format: - * - * metadata_names => null|ARR metadata names - * metadata_values => null|ARR metadata values - * metadata_ids => null|ARR metadata ids - * metadata_case_sensitive => BOOL Overall Case sensitive - * metadata_created_time_lower => INT Lower limit for created time. - * metadata_created_time_upper => INT Upper limit for created time. - * metadata_calculation => STR Perform the MySQL function on the metadata values returned. - * The "metadata_calculation" option causes this function to - * return the result of performing a mathematical calculation on - * all metadata that match the query instead of returning - * \ElggMetadata objects. + * @param array $options Options * * @return ElggMetadata[]|mixed */ - function getAll(array $options = []) { + public function getAll(array $options = []) { $options['metastring_type'] = 'metadata'; $options = _elgg_normalize_metastrings_options($options); @@ -306,17 +305,21 @@ function getAll(array $options = []) { * This requires at least one constraint: * metadata_name(s), metadata_value(s), or guid(s) must be set. * - * @param array $options An options array. {@link elgg_get_metadata()} + * @see elgg_get_metadata() + * @see elgg_get_entities() + * + * @param array $options Options + * * @return bool|null true on success, false on failure, null if no metadata to delete. */ - function deleteAll(array $options) { + public function deleteAll(array $options) { if (!_elgg_is_valid_options_for_batch_operation($options, 'metadata')) { return false; } // This moved last in case an object's constructor sets metadata. Currently the batch // delete process has to create the entity to delete its metadata. See #5214 - $this->cache->invalidateByOptions($options); + $this->metadata_cache->invalidateByOptions($options); $options['batch'] = true; $options['batch_size'] = 50; @@ -340,17 +343,37 @@ function deleteAll(array $options) { } /** - * Get the URL for this metadata - * - * By default this links to the export handler in the current view. + * Returns ID(s) of metadata with a particular name attached to an entity * - * @param int $id Metadata ID + * @param int $entity_guid Entity guid + * @param string $name Metadata name * - * @return mixed + * @return int[]|int|null */ - function getUrl($id) { - $extender = $this->get($id); + public function getIdsByName($entity_guid, $name) { + if ($this->metadata_cache->isLoaded($entity_guid)) { + $ids = $this->metadata_cache->getSingleId($entity_guid, $name); + } else { + $qb = Select::fromTable('metadata'); + $qb->select('id') + ->where($qb->compare('entity_guid', '=', $entity_guid, ELGG_VALUE_INTEGER)) + ->andWhere($qb->compare('name', '=', $name, ELGG_VALUE_STRING)); + + $callback = function (\stdClass $row) { + return (int) $row->id; + }; + + $ids = $this->db->getData($qb, $callback); + } + + if (empty($ids)) { + return null; + } + + if (is_array($ids) && count($ids) === 1) { + return array_shift($ids); + } - return $extender ? $extender->getURL() : false; + return $ids; } } diff --git a/engine/classes/Elgg/Database/Plugins.php b/engine/classes/Elgg/Database/Plugins.php index 1225e8e3bfd..4f1ce0a74ba 100644 --- a/engine/classes/Elgg/Database/Plugins.php +++ b/engine/classes/Elgg/Database/Plugins.php @@ -21,7 +21,7 @@ class Plugins { /** * @var \ElggPlugin[] */ - private $boot_plugins = []; + private $boot_plugins; /** * @var array|null @@ -62,13 +62,15 @@ public function __construct(Pool $pool, PluginSettingsCache $cache) { /** * Set the list of active plugins according to the boot data cache * - * @param \ElggPlugin[] $plugins Set of active plugins + * @param \ElggPlugin[]|null $plugins Set of active plugins * @return void */ - public function setBootPlugins(array $plugins) { + public function setBootPlugins($plugins) { $this->boot_plugins = $plugins; - foreach ($plugins as $plugin) { - $this->plugins_by_id->put($plugin->getID(), $plugin); + if (is_array($plugins)) { + foreach ($plugins as $plugin) { + $this->plugins_by_id->put($plugin->getID(), $plugin); + } } } @@ -390,11 +392,15 @@ function load() { * @param string $status The status of the plugins. active, inactive, or all. * @return \ElggPlugin[] */ - function find($status = 'active') { + public function find($status = 'active') { if (!_elgg_services()->db) { return []; } + if ($status === 'active' && isset($this->boot_plugins)) { + return $this->boot_plugins; + } + $priority = $this->namespacePrivateSetting('internal', 'priority'); $site_guid = 1; diff --git a/engine/classes/Elgg/Database/QueryBuilder.php b/engine/classes/Elgg/Database/QueryBuilder.php index 6f43d79a3f9..1b649003476 100644 --- a/engine/classes/Elgg/Database/QueryBuilder.php +++ b/engine/classes/Elgg/Database/QueryBuilder.php @@ -37,6 +37,11 @@ abstract class QueryBuilder extends DbalQueryBuilder { */ protected $joins = []; + /** + * @var int + */ + protected $join_index = 0; + /** * @var string */ @@ -305,9 +310,9 @@ public function between($x, $lower = null, $upper = null, $type = null) { * @return string */ public function getNextJoinAlias() { - $index = count($this->joins) + 1; + $this->join_index++; - return "qbt{$index}"; + return "qbt{$this->join_index}"; } /** diff --git a/engine/classes/Elgg/Database/RelationshipsTable.php b/engine/classes/Elgg/Database/RelationshipsTable.php index 3cc71fc4775..636b009d443 100644 --- a/engine/classes/Elgg/Database/RelationshipsTable.php +++ b/engine/classes/Elgg/Database/RelationshipsTable.php @@ -284,60 +284,6 @@ public function getAll($guid, $inverse_relationship = false) { return $this->db->getData($query, [$this, 'rowToElggRelationship'], $params); } - /** - * Returns SQL appropriate for relationship joins and wheres - * - * @todo add support for multiple relationships and guids. - * - * @param string $column Column name the GUID should be checked against. - * Provide in table.column format. - * @param string $relationship Type of the relationship - * @param int $relationship_guid Entity GUID to check - * @param bool $inverse_relationship Is $relationship_guid the target of the relationship? - * - * @return mixed - * @access private - */ - public function getEntityRelationshipWhereSql($column, $relationship = null, - $relationship_guid = null, $inverse_relationship = false) { - - if ($relationship === null && $relationship_guid === null) { - return ''; - } - - $wheres = []; - $joins = []; - $group_by = ''; - - if ($inverse_relationship) { - $joins[] = "JOIN {$this->db->prefix}entity_relationships r on r.guid_one = $column"; - } else { - $joins[] = "JOIN {$this->db->prefix}entity_relationships r on r.guid_two = $column"; - } - - if ($relationship) { - $wheres[] = "r.relationship = '" . $this->db->sanitizeString($relationship) . "'"; - } - - if ($relationship_guid) { - if ($inverse_relationship) { - $wheres[] = "r.guid_two = '$relationship_guid'"; - } else { - $wheres[] = "r.guid_one = '$relationship_guid'"; - } - } else { - // See #5775. Queries that do not include a relationship_guid must be grouped by entity table alias, - // otherwise the result set is not unique - $group_by = $column; - } - - if ($where_str = implode(' AND ', $wheres)) { - return ['wheres' => ["($where_str)"], 'joins' => $joins, 'group_by' => $group_by]; - } - - return ''; - } - /** * Gets the number of entities by a the number of entities related to them in a particular way. * This is a good way to get out the users with the most friends, or the groups with the diff --git a/engine/classes/Elgg/Database/Repository.php b/engine/classes/Elgg/Database/Repository.php index a1bb3d16bef..8f886b5a5d4 100644 --- a/engine/classes/Elgg/Database/Repository.php +++ b/engine/classes/Elgg/Database/Repository.php @@ -75,6 +75,43 @@ public static function with(array $options = null) { return $query; } + /** + * Build and execute a new query from an array of legacy options + * + * @param array $options Options + * + * @return ElggData[]|int|mixed + */ + public static function find(array $options = []) { + try { + return static::with($options)->execute(); + } catch (\DataFormatException $e) { + return elgg_extract('count', $options) ? 0 : false; + } + } + + /** + * {@inheritdoc} + */ + public function batch($limit = null, $offset = null, $callback = null) { + + $options = $this->options->getArrayCopy(); + + $options['limit'] = (int) $limit; + $options['offset'] = (int) $offset; + $options['callback'] = $callback; + unset($options['count'], + $options['batch'], + $options['batch_size'], + $options['batch_inc_offset'] + ); + + $batch_size = $this->options->batch_size; + $batch_inc_offset = $this->options->batch_inc_offset; + + return new \ElggBatch([static::class, 'find'], $options, null, $batch_size, $batch_inc_offset); + } + /** * {@inheritdoc} */ @@ -131,4 +168,32 @@ public function orderBy($expression, $direction) { return $this; } + + /** + * Extend query builder with select, group_by, having and order_by clauses from $options + * + * @param QueryBuilder $qb Query builder + * @param $table_alias Table alias + * + * @return void + */ + public function expandInto(QueryBuilder $qb, $table_alias = nul) { + foreach ($this->options->selects as $select_clause) { + $select_clause->prepare($qb, $table_alias); + } + + foreach ($this->options->group_by as $group_by_clause) { + $group_by_clause->prepare($qb, $table_alias); + } + + foreach ($this->options->having as $having_clause) { + $having_clause->prepare($qb, $table_alias); + } + + if (!empty($this->options->order_by)) { + foreach ($this->options->order_by as $order_by_clause) { + $order_by_clause->prepare($qb, $table_alias); + } + } + } } diff --git a/engine/classes/Elgg/Database/River.php b/engine/classes/Elgg/Database/River.php new file mode 100644 index 00000000000..8823b2e66e4 --- /dev/null +++ b/engine/classes/Elgg/Database/River.php @@ -0,0 +1,370 @@ + null, + 'subject_guids' => null, + 'object_guids' => null, + 'target_guids' => null, + 'annotation_ids' => null, + 'views' => null, + 'action_types' => null, + 'posted_time_lower' => null, + 'posted_time_upper' => null, + 'limit' => 20, + 'offset' => 0, + ]; + + $options = array_merge($defaults, $options); + parent::__construct($options); + } + + /** + * Build and execute a new query from an array of legacy options + * + * @param array $options Options + * + * @return ElggRiverItem[]|int|mixed + */ + public static function find(array $options = []) { + return parent::find($options); + } + + /** + * {@inheritdoc} + */ + public function count() { + $qb = Select::fromTable('river', 'rv'); + + $count_expr = $this->options->distinct ? "DISTINCT rv.id" : "*"; + $qb->select("COUNT({$count_expr}) AS total"); + + $qb = $this->buildQuery($qb); + + $result = _elgg_services()->db->getDataRow($qb); + + if (!$result) { + return 0; + } + + return (int) $result->total; + } + + /** + * Performs a mathematical calculation on river annotations + * + * @param string $function Valid numeric function + * @param string $property Property name + * @param string $property_type 'annotation' + * + * @return int|float + * @throws InvalidParameterException + */ + public function calculate($function, $property, $property_type = 'annotation') { + + if (!in_array(strtolower($function), QueryBuilder::$calculations)) { + throw new InvalidArgumentException("'$function' is not a valid numeric function"); + } + + $qb = Select::fromTable('river', 'rv'); + + $alias = 'n_table'; + if (!empty($this->options->annotation_name_value_pairs) && $this->options->annotation_name_value_pairs[0]->names != $property) { + $alias = $qb->getNextJoinAlias(); + + $annotation = new AnnotationWhereClause(); + $annotation->names = $property; + $qb->addClause($annotation, $alias); + } + + $qb->join('rv', 'annotations', $alias, "rv.annotation_id = $alias.id"); + $qb->select("{$function}(n_table.value) AS calculation"); + + $qb = $this->buildQuery($qb); + + $result = _elgg_services()->db->getDataRow($qb); + + if (!$result) { + return 0; + } + + return (int) $result->calculation; + } + + /** + * Fetch river items + * + * @param int $limit Limit + * @param int $offset Offset + * @param callable $callback Custom callback + * + * @return ElggEntity[] + */ + public function get($limit = null, $offset = null, $callback = null) { + + $qb = Select::fromTable('river', 'rv'); + + $distinct = $this->options->distinct ? "DISTINCT" : ""; + $qb->select("$distinct rv.*"); + + $this->expandInto($qb, 'rv'); + + $qb = $this->buildQuery($qb); + + // Keeping things backwards compatible + $original_order = elgg_extract('order_by', $this->options->__original_options); + if (empty($original_order) && $original_order !== false) { + $qb->addOrderBy('rv.posted', 'desc'); + } + + if ($limit) { + $qb->setMaxResults((int) $limit); + $qb->setFirstResult((int) $offset); + } + + $callback = $callback ? : $this->options->callback; + if (!isset($callback)) { + $callback = function ($row) { + return new ElggRiverItem($row); + }; + } + + $items = _elgg_services()->db->getData($qb, $callback); + + if ($items) { + _elgg_prefetch_river_entities($items); + } + + return $items; + } + + /** + * Execute the query resolving calculation, count and/or batch options + * + * @return array|\ElggData[]|ElggEntity[]|false|int + * @throws \LogicException + */ + public function execute() { + + if ($this->options->annotation_calculation) { + $clauses = $this->options->annotation_name_value_pairs; + if (count($clauses) > 1 && $this->options->annotation_name_value_pairs_operator !== 'OR') { + throw new \LogicException("Annotation calculation can not be performed on multiple annotation name value pairs merged with AND"); + } + + $clause = array_shift($clauses); + + return $this->calculate($this->options->annotation_calculation, $clause->names, 'annotation'); + } else if ($this->options->count) { + return $this->count(); + } else if ($this->options->batch) { + return $this->batch($this->options->limit, $this->options->offset, $this->options->callback); + } else { + return $this->get($this->options->limit, $this->options->offset, $this->options->callback); + } + } + + /** + * Build a database query + * + * @param QueryBuilder $qb + * + * @return QueryBuilder + */ + protected function buildQuery(QueryBuilder $qb) { + + $ands = []; + + foreach ($this->options->joins as $join) { + $join->prepare($qb, 'rv'); + } + + foreach ($this->options->wheres as $where) { + $ands[] = $where->prepare($qb, 'rv'); + } + + $ands[] = $this->buildRiverClause($qb); + $ands[] = $this->buildEntityClauses($qb); + $ands[] = $this->buildPairedAnnotationClause($qb, $this->options->annotation_name_value_pairs, $this->options->annotation_name_value_pairs_operator); + $ands[] = $this->buildPairedRelationshipClause($qb, $this->options->relationship_pairs); + + $ands = $qb->merge($ands); + + if (!empty($ands)) { + $qb->andWhere($ands); + } + + return $qb; + } + + /** + * Process river properties + * + * @param QueryBuilder $qb Query builder + * + * @return CompositeExpression|mixed|null|string + */ + protected function buildRiverClause(QueryBuilder $qb) { + $where = new RiverWhereClause(); + $where->ids = $this->options->ids; + $where->views = $this->options->views; + $where->action_types = $this->options->action_types; + $where->subject_guids = $this->options->subject_guids; + $where->object_guids = $this->options->object_guids; + $where->target_guids = $this->options->target_guids; + $where->type_subtype_pairs = $this->options->type_subtype_pairs; + $where->created_after = $this->options->created_after; + $where->created_before = $this->options->created_before; + + return $where->prepare($qb, 'rv'); + } + + /** + * Add subject, object and target clauses + * Make sure all three are accessible by the user + * + * @param QueryBuilder $qb Query builder + * + * @return CompositeExpression|mixed|null|string + */ + public function buildEntityClauses($qb) { + + $use_access_clause = !_elgg_services()->userCapabilities->canBypassPermissionsCheck(); + + $ands = []; + + if ($this->options->subject_guids || $use_access_clause) { + $qb->joinEntitiesTable('rv', 'subject_guid', 'inner', 'se'); + $subject = new EntityWhereClause(); + $subject->guids = $this->options->subject_guids; + $ands[] = $subject->prepare($qb, 'se'); + } + + if ($this->options->object_guids || $use_access_clause) { + $qb->joinEntitiesTable('rv', 'object_guid', 'inner', 'oe'); + $object = new EntityWhereClause(); + $object->guids = $this->options->object_guids; + $ands[] = $object->prepare($qb, 'oe'); + } + + if ($this->options->target_guids || $use_access_clause) { + $target_ors = []; + $qb->joinEntitiesTable('rv', 'target_guid', 'left', 'te'); + $target = new EntityWhereClause(); + $target->guids = $this->options->target_guids; + $target_ors[] = $target->prepare($qb, 'te'); + // Note the LEFT JOIN + $target_ors[] = $qb->compare('te.guid', 'IS NULL'); + $ands[] = $qb->merge($target_ors, 'OR'); + } + + return $qb->merge($ands); + } + + /** + * Process annotation name value pairs + * Joins the annotation table on entity guid in the entities table and applies annotation where clauses + * + * @param QueryBuilder $qb Query builder + * @param AnnotationWhereClause[] $clauses Where clauses + * @param string $boolean Merge boolean + * + * @return CompositeExpression|string + */ + protected function buildPairedAnnotationClause(QueryBuilder $qb, $clauses, $boolean = 'AND') { + $parts = []; + + foreach ($clauses as $clause) { + if (strtoupper($boolean) === 'OR' || count($clauses) === 1) { + $joined_alias = 'n_table'; + } else { + $joined_alias = $qb->getNextJoinAlias(); + } + $joins = $qb->getQueryPart('join'); + $is_joined = false; + if (!empty($joins['rv'])) { + foreach ($joins['rv'] as $join) { + if ($join['joinAlias'] === $joined_alias) { + $is_joined = true; + } + } + } + + if (!$is_joined) { + $qb->join('rv', 'annotations', $joined_alias, "$joined_alias.id = rv.annotation_id"); + } + + $parts[] = $clause->prepare($qb, $joined_alias); + } + + return $qb->merge($parts, $boolean); + } + + /** + * Process relationship pairs + * + * @param QueryBuilder $qb Query builder + * @param RelationshipWhereClause[] $clauses Where clauses + * @param string $boolean Merge boolean + * + * @return CompositeExpression|string + */ + protected function buildPairedRelationshipClause(QueryBuilder $qb, $clauses, $boolean = 'AND') { + $parts = []; + + foreach ($clauses as $clause) { + $join_on = $clause->join_on === 'guid' ? 'subject_guid' : $clause->join_on; + if (strtoupper($boolean) == 'OR' || count($clauses) === 1) { + $joined_alias = $qb->joinRelationshipTable('rv', $join_on, null, $clause->inverse, 'inner', 'r'); + } else { + $joined_alias = $qb->joinRelationshipTable('rv', $join_on, $clause->names, $clause->inverse); + } + $parts[] = $clause->prepare($qb, $joined_alias); + } + + return $qb->merge($parts, $boolean); + } +} diff --git a/engine/classes/Elgg/Di/ServiceProvider.php b/engine/classes/Elgg/Di/ServiceProvider.php index 8919f996fab..f030f03d94c 100644 --- a/engine/classes/Elgg/Di/ServiceProvider.php +++ b/engine/classes/Elgg/Di/ServiceProvider.php @@ -22,7 +22,7 @@ * @property-read \Elgg\Database\AdminNotices $adminNotices * @property-read \Elgg\Ajax\Service $ajax * @property-read \Elgg\Amd\Config $amdConfig - * @property-read \Elgg\Database\AnnotationsTable $annotations + * @property-read \Elgg\Database\AnnotationsTable $annotationsTable * @property-read \ElggAutoP $autoP * @property-read \Elgg\AutoloadManager $autoloadManager * @property-read \Elgg\BatchUpgrader $batchUpgrader @@ -140,8 +140,8 @@ public function __construct(Config $config) { return $obj; }); - $this->setFactory('annotations', function(ServiceProvider $c) { - return new \Elgg\Database\AnnotationsTable($c->db, $c->session, $c->hooks->getEvents()); + $this->setFactory('annotationsTable', function(ServiceProvider $c) { + return new \Elgg\Database\AnnotationsTable($c->db, $c->hooks->getEvents()); }); $this->setClassName('autoP', \ElggAutoP::class); @@ -292,7 +292,7 @@ public function __construct(Config $config) { $imagine = new \Imagine\Gd\Imagine(); break; } - + return new \Elgg\ImageService($imagine, $c->config); }); @@ -330,8 +330,7 @@ public function __construct(Config $config) { $this->setFactory('metadataTable', function(ServiceProvider $c) { // TODO(ewinslow): Use Pool instead of MetadataCache for caching - return new \Elgg\Database\MetadataTable( - $c->metadataCache, $c->db, $c->entityTable, $c->hooks->getEvents(), $c->session); + return new \Elgg\Database\MetadataTable($c->metadataCache, $c->db, $c->hooks->getEvents()); }); $this->setFactory('mutex', function(ServiceProvider $c) { @@ -465,9 +464,9 @@ public function __construct(Config $config) { }); $this->setClassName('table_columns', \Elgg\Views\TableColumn\ColumnFactory::class); - + $this->setClassName('temp_filestore', \ElggTempDiskFilestore::class); - + $this->setClassName('timer', \Elgg\Timer::class); $this->setFactory('translator', function(ServiceProvider $c) { diff --git a/engine/classes/ElggAnnotation.php b/engine/classes/ElggAnnotation.php index d669f19269e..968551b34c7 100644 --- a/engine/classes/ElggAnnotation.php +++ b/engine/classes/ElggAnnotation.php @@ -1,25 +1,15 @@ initializeAttributes(); if ($row) { foreach ((array) $row as $key => $value) { - $this->attributes[$key] = $value; + $this->$key = $value; } } } /** - * Save this instance - * - * @return int an object id + * Save this instance and returns an annotation ID * - * @throws IOException + * @return int|false */ public function save() { - if ($this->id > 0) { - return update_annotation($this->id, $this->name, $this->value, $this->value_type, - $this->owner_guid, $this->access_id); - } else { - $this->id = create_annotation($this->entity_guid, $this->name, $this->value, - $this->value_type, $this->owner_guid, $this->access_id); - - if (!$this->id) { - throw new \IOException("Unable to save new " . get_class()); - } + if (!isset($this->access_id)) { + $this->access_id = ACCESS_PRIVATE; + } + + if (!isset($this->owner_guid)) { + $this->owner_guid = elgg_get_logged_in_user_guid(); + } + + if ($this->id) { + return _elgg_services()->annotationsTable->update($this); + } + + $entity = get_entity($this->entity_guid); + if (!$entity) { + return false; + } + if (_elgg_services()->annotationsTable->create($this, $entity)) { return $this->id; } + + return false; } /** @@ -74,23 +68,7 @@ public function save() { * @return bool */ public function delete() { - if (!$this->canEdit()) { - return false; - } - - if (!elgg_trigger_event('delete', $this->getType(), $this)) { - return false; - } - - $qb = \Elgg\Database\Delete::fromTable('annotations'); - $qb->where($qb->compare('id', '=', $this->id, ELGG_VALUE_INTEGER)); - $deleted = _elgg_services()->db->deleteData($qb); - - if ($deleted) { - elgg_delete_river(['annotation_id' => $this->id, 'limit' => false]); - } - - return $deleted; + return _elgg_services()->annotationsTable->delete($this); } /** @@ -100,29 +78,7 @@ public function delete() { * @since 1.8 */ public function disable() { - if ($this->enabled == 'no') { - return true; - } - - if (!$this->canEdit()) { - return false; - } - - if (!elgg_trigger_event('disable', $this->getType(), $this)) { - return false; - } - - $qb = \Elgg\Database\Update::table('annotations'); - $qb->set('enabled', $qb->param('no', ELGG_VALUE_STRING)) - ->where($qb->compare('id', '=', $this->id, ELGG_VALUE_INTEGER)); - - if ($this->getDatabase()->updateData($qb, true)) { - $this->enabled = 'no'; - - return true; - } - - return false; + return _elgg_services()->annotationsTable->disable($this); } /** @@ -132,29 +88,7 @@ public function disable() { * @since 1.8 */ public function enable() { - if ($this->enabled == 'yes') { - return true; - } - - if (!$this->canEdit()) { - return false; - } - - if (!elgg_trigger_event('enable', $this->getType(), $this)) { - return false; - } - - $qb = \Elgg\Database\Update::table('annotations'); - $qb->set('enabled', $qb->param('yes', ELGG_VALUE_STRING)) - ->where($qb->compare('id', '=', $this->id, ELGG_VALUE_INTEGER)); - - if ($this->getDatabase()->updateData($qb, true)) { - $this->enabled = 'yes'; - - return true; - } - - return false; + return _elgg_services()->annotationsTable->enable($this); } /** @@ -171,16 +105,8 @@ public function canEdit($user_guid = 0) { return _elgg_services()->userCapabilities->canEditAnnotation($entity, $user_guid, $this); } - // SYSTEM LOG INTERFACE - /** - * For a given ID, return the object associated with it. - * This is used by the river functionality primarily. - * This is useful for checking access permissions etc on objects. - * - * @param int $id An annotation ID. - * - * @return \ElggAnnotation + * {@inheritdoc} */ public function getObjectFromID($id) { return elgg_get_annotation_from_id($id); diff --git a/engine/classes/ElggBatch.php b/engine/classes/ElggBatch.php index d8fce5c7ae7..8eb2aa16002 100644 --- a/engine/classes/ElggBatch.php +++ b/engine/classes/ElggBatch.php @@ -191,7 +191,7 @@ class ElggBatch implements BatchResult { * Instead of returning all objects in memory, it goes through $chunk_size * objects, then requests more from the server. This avoids OOM errors. * - * @param string $getter The function used to get objects. Usually + * @param callable $getter The function used to get objects. Usually * an elgg_get_*() function, but can be any valid PHP callback. * @param array $options The options array to pass to the getter function. If limit is * not set, 10 is used as the default. In most cases that is not @@ -206,7 +206,7 @@ class ElggBatch implements BatchResult { * callbacks that delete rows. You can set this after the * object is created with {@link \ElggBatch::setIncrementOffset()}. */ - public function __construct($getter, $options, $callback = null, $chunk_size = 25, + public function __construct(callable $getter, $options, $callback = null, $chunk_size = 25, $inc_offset = true) { $this->getter = $getter; diff --git a/engine/classes/ElggEntity.php b/engine/classes/ElggEntity.php index d0eb6b0184f..f7f46032651 100644 --- a/engine/classes/ElggEntity.php +++ b/engine/classes/ElggEntity.php @@ -399,7 +399,7 @@ public function setMetadata($name, $value, $value_type = '', $multiple = false) // saved entity. persist md to db. if (!$multiple) { $current_metadata = $this->getMetadata($name); - + if ((is_array($current_metadata) || count($value) > 1 || $value === []) && isset($current_metadata)) { // remove current metadata if needed // need to remove access restrictions right now to delete @@ -425,7 +425,12 @@ public function setMetadata($name, $value, $value_type = '', $multiple = false) // create new metadata foreach ($value as $value_tmp) { - $md_id = _elgg_services()->metadataTable->create($this->guid, $name, $value_tmp, $value_type, $multiple); + $metadata = new ElggMetadata(); + $metadata->entity_guid = $this->guid; + $metadata->name = $name; + $metadata->value_type = $value_type; + $metadata->value = $value_tmp; + $md_id = _elgg_services()->metadataTable->create($metadata, $multiple); if (!$md_id) { return false; } @@ -433,7 +438,7 @@ public function setMetadata($name, $value, $value_type = '', $multiple = false) return true; } - + /** * Set temp metadata on this entity. * @@ -460,11 +465,11 @@ protected function setTempMetadata($name, $value, $multiple = false) { } $this->temp_metadata[$name] = array_merge($this->temp_metadata[$name], $value); - + return true; } - - + + /** * Deletes all metadata on this object (metadata.entity_guid = $this->guid). @@ -761,17 +766,30 @@ private function getAnnotationCalculation($name, $calculation) { * @warning Annotating an unsaved entity more than once with the same name * will only save the last annotation. * + * @todo Update temp_annotations to store an instance of ElggAnnotation and simply call ElggAnnotation::save(), + * after entity is saved + * * @param string $name Annotation name * @param mixed $value Annotation value * @param int $access_id Access ID * @param int $owner_guid GUID of the annotation owner - * @param string $vartype The type of annotation value + * @param string $value_type The type of annotation value * * @return bool|int Returns int if an annotation is saved */ - public function annotate($name, $value, $access_id = ACCESS_PRIVATE, $owner_guid = 0, $vartype = "") { - if ((int) $this->guid > 0) { - return create_annotation($this->getGUID(), $name, $value, $vartype, $owner_guid, $access_id); + public function annotate($name, $value, $access_id = ACCESS_PRIVATE, $owner_guid = 0, $value_type = "") { + if ($this->guid) { + if (!$owner_guid) { + $owner_guid = elgg_get_logged_in_user_guid(); + } + $annotation = new ElggAnnotation(); + $annotation->entity_guid = $this->guid; + $annotation->name = $name; + $annotation->value_type = $value_type; + $annotation->value = $value; + $annotation->owner_guid = $owner_guid; + $annotation->access_id = $access_id; + return $annotation->save(); } else { $this->temp_annotations[$name] = $value; } @@ -873,7 +891,7 @@ public function countComments() { if (is_int($num)) { return $num; } - + return elgg_get_entities([ 'type' => 'object', 'subtype' => 'comment', @@ -882,7 +900,7 @@ public function countComments() { 'distinct' => false, ]); } - + /** * Returns the ACLs owned by the entity * diff --git a/engine/classes/ElggExtender.php b/engine/classes/ElggExtender.php index 06d568b97db..7adff1c26fe 100644 --- a/engine/classes/ElggExtender.php +++ b/engine/classes/ElggExtender.php @@ -25,12 +25,16 @@ */ abstract class ElggExtender extends \ElggData { + protected $int_columns = [ + 'id', + 'entity_guid', + 'owner_guid', + 'time_created', + 'access_id', + ]; + /** - * (non-PHPdoc) - * - * @see \ElggData::initializeAttributes() - * - * @return void + * {@inheritdoc} */ protected function initializeAttributes() { parent::initializeAttributes(); @@ -54,8 +58,14 @@ public function __set($name, $value) { if ($name === 'access_id' && $this instanceof ElggMetadata) { $value = ACCESS_PUBLIC; } + if (isset($value) && in_array($name, $this->int_columns)) { + $value = (int) $value; + } $this->attributes[$name] = $value; if ($name == 'value') { + if (is_bool($value)) { + $value = (int) $value; + } $this->attributes['value_type'] = self::detectValueType($value); } } diff --git a/engine/classes/ElggMetadata.php b/engine/classes/ElggMetadata.php index fe9ceb9dcb0..a822e246622 100644 --- a/engine/classes/ElggMetadata.php +++ b/engine/classes/ElggMetadata.php @@ -1,27 +1,17 @@ initializeAttributes(); if ($row) { foreach ((array) $row as $key => $value) { - $this->attributes[$key] = $value; + $this->$key = $value; } } - $this->attributes['access_id'] = ACCESS_PUBLIC; + $this->access_id = ACCESS_PUBLIC; } /** @@ -68,22 +55,16 @@ public function canEdit($user_guid = 0) { /** * Save metadata object * - * @return int|bool the metadata object id or true if updated + * Returns metadata on success, false on failure * - * @throws IOException + * @return int|false */ public function save() { - if ($this->id > 0) { - return _elgg_services()->metadataTable->update($this->id, $this->name, $this->value, $this->value_type); - } - - $this->id = _elgg_services()->metadataTable->create($this->entity_guid, $this->name, $this->value, $this->value_type); - if (!$this->id) { - throw new \IOException("Unable to save new " . get_class()); + return _elgg_services()->metadataTable->create($this); } - return $this->id; + return _elgg_services()->metadataTable->update($this); } /** @@ -92,36 +73,11 @@ public function save() { * @return bool */ public function delete() { - if (!$this->canEdit()) { - return false; - } - - if (!elgg_trigger_event('delete', $this->getType(), $this)) { - return false; - } - - $qb = \Elgg\Database\Delete::fromTable('metadata'); - $qb->where($qb->compare('id', '=', $this->id, ELGG_VALUE_INTEGER)); - - $deleted = _elgg_services()->db->deleteData($qb); - - if ($deleted) { - _elgg_services()->metadataCache->clear($this->entity_guid); - } - - return $deleted; + return _elgg_services()->metadataTable->delete($this); } - // SYSTEM LOG INTERFACE //////////////////////////////////////////////////////////// - /** - * For a given ID, return the object associated with it. - * This is used by the river functionality primarily. - * This is useful for checking access permissions etc on objects. - * - * @param int $id Metadata ID - * - * @return \ElggMetadata + * {@inheritdoc} */ public function getObjectFromID($id) { return elgg_get_metadata_from_id($id); diff --git a/engine/classes/ElggPlugin.php b/engine/classes/ElggPlugin.php index 0f10818796b..52bf14bb020 100644 --- a/engine/classes/ElggPlugin.php +++ b/engine/classes/ElggPlugin.php @@ -750,7 +750,10 @@ public function activate() { _elgg_services()->hooks->getEvents()->trigger('cache:flush', 'system'); + _elgg_services()->plugins->setBootPlugins(null); + _elgg_services()->logger->notice("Plugin {$this->getID()} has been activated"); + return $return; } @@ -841,6 +844,8 @@ public function deactivate() { _elgg_services()->logger->notice("Plugin {$this->getID()} has been deactivated"); + _elgg_services()->plugins->setBootPlugins(null); + return $this->setStatus(false); } diff --git a/engine/classes/Loggable.php b/engine/classes/Loggable.php index 4a29fcc9fe4..bf8eaea36b6 100644 --- a/engine/classes/Loggable.php +++ b/engine/classes/Loggable.php @@ -43,7 +43,7 @@ public function getSubtype(); * * @param int $id GUID of an entity * - * @return \ElggEntity + * @return static|false */ public function getObjectFromID($id); } diff --git a/engine/lib/access.php b/engine/lib/access.php index 97c443a157d..b6102558dee 100644 --- a/engine/lib/access.php +++ b/engine/lib/access.php @@ -88,7 +88,7 @@ function get_access_list($user_guid = 0, $ignored = 0, $flush = false) { * * @note Internal: this is only used in core for creating the SQL where clause when * retrieving content from the database. The friends access level is handled by - * _elgg_get_access_where_sql(). + * {@link \Elgg\Database\Clauses\AccessWhereClause} * * @see get_write_access_array() for the access levels that a user can write to. * @@ -170,49 +170,6 @@ function access_get_show_hidden_status() { return $ENTITY_SHOW_HIDDEN_OVERRIDE; } -/** - * Returns the SQL where clause for enforcing read access to data. - * - * Note that if this code is executed in privileged mode it will return (1=1). - * - * Otherwise it returns a where clause to retrieve the data that a user has - * permission to read. - * - * Plugin authors can hook into the 'get_sql', 'access' plugin hook to modify, - * remove, or add to the where clauses. The plugin hook will pass an array with the current - * ors and ands to the function in the form: - * array( - * 'ors' => array(), - * 'ands' => array() - * ) - * - * The results will be combined into an SQL where clause in the form: - * ((or1 OR or2 OR orN) AND (and1 AND and2 AND andN)) - * - * @param array $options Array in format: - * - * table_alias => STR Optional table alias. This is based on the select and join clauses. - * Default is 'e'. - * - * user_guid => INT Optional GUID for the user that we are retrieving data for. - * Defaults to the logged in user. - * - * use_enabled_clause => BOOL Optional. Should we append the enabled clause? The default - * is set by access_show_hidden_entities(). - * - * access_column => STR Optional access column name. Default is 'access_id'. - * - * owner_guid_column => STR Optional owner_guid column. Default is 'owner_guid'. - * - * guid_column => STR Optional guid_column. Default is 'guid'. - * - * @return string - * @access private - */ -function _elgg_get_access_where_sql(array $options = []) { - return _elgg_services()->accessCollections->getWhereSql($options); -} - /** * Can a user access an entity. * @@ -470,7 +427,6 @@ function access_init() { */ function access_test($hook, $type, $value, $params) { $value[] = ElggCoreAccessCollectionsTest::class; - $value[] = ElggCoreAccessSQLTest::class; return $value; } diff --git a/engine/lib/admin.php b/engine/lib/admin.php index ad2d64d15f2..9db9045804c 100644 --- a/engine/lib/admin.php +++ b/engine/lib/admin.php @@ -32,7 +32,7 @@ function elgg_get_admins(array $options = []) { $options['type'] = 'user'; $options['metadata_name_value_pairs'] = elgg_extract('metadata_name_value_pairs', $options, []); - $options['metadata_name_value_pairs'][] = ['admin' => 'yes']; + $options['metadata_name_value_pairs']['admin'] = 'yes'; return elgg_get_entities($options); } diff --git a/engine/lib/annotations.php b/engine/lib/annotations.php index 430acdd7d33..638f9ea490f 100644 --- a/engine/lib/annotations.php +++ b/engine/lib/annotations.php @@ -14,51 +14,23 @@ * @return \ElggAnnotation|false */ function elgg_get_annotation_from_id($id) { - return _elgg_services()->annotations->get($id); + return _elgg_services()->annotationsTable->get($id); } /** * Deletes an annotation using its ID. * * @param int $id The annotation ID to delete. + * * @return bool */ function elgg_delete_annotation_by_id($id) { - return _elgg_services()->annotations->delete($id); -} - -/** - * Create a new annotation. - * - * @param int $entity_guid GUID of entity to be annotated - * @param string $name Name of annotation - * @param string $value Value of annotation - * @param string $value_type Type of value (default is auto detection) - * @param int $owner_guid Owner of annotation (default is logged in user) - * @param int $access_id Access level of annotation - * - * @return int|bool id on success or false on failure - */ -function create_annotation($entity_guid, $name, $value, $value_type = '', - $owner_guid = 0, $access_id = ACCESS_PRIVATE) { - return _elgg_services()->annotations->create( - $entity_guid, $name, $value, $value_type, $owner_guid, $access_id); -} + $annotation = elgg_get_annotation_from_id($id); + if (!$annotation) { + return false; + } -/** - * Update an annotation. - * - * @param int $annotation_id Annotation ID - * @param string $name Name of annotation - * @param string $value Value of annotation - * @param string $value_type Type of value - * @param int $owner_guid Owner of annotation - * @param int $access_id Access level of annotation - * - * @return bool - */ -function update_annotation($annotation_id, $name, $value, $value_type, $owner_guid, $access_id) { - return _elgg_services()->annotations->update($annotation_id, $name, $value, $value_type, $owner_guid, $access_id); + return $annotation->delete(); } /** @@ -76,14 +48,14 @@ function update_annotation($annotation_id, $name, $value, $value_type, $owner_gu * @since 1.8.0 */ function elgg_get_annotations(array $options = []) { - return _elgg_services()->annotations->find($options); + return _elgg_services()->annotationsTable->find($options); } /** * Returns a rendered list of annotations with pagination. * * @param array $options Annotation getter and display options. - * {@link elgg_get_annotations()} and {@link elgg_list_entities()}. + * {@link elgg_get_annotations()} and {@link elgg_list_entities()}. * * @return string The list of entities * @since 1.8.0 @@ -112,7 +84,7 @@ function elgg_list_annotations($options) { * @since 1.8.0 */ function elgg_delete_annotations(array $options) { - return _elgg_services()->annotations->deleteAll($options); + return _elgg_services()->annotationsTable->deleteAll($options); } /** @@ -125,7 +97,7 @@ function elgg_delete_annotations(array $options) { * @since 1.8.0 */ function elgg_disable_annotations(array $options) { - return _elgg_services()->annotations->disableAll($options); + return _elgg_services()->annotationsTable->disableAll($options); } /** @@ -141,21 +113,25 @@ function elgg_disable_annotations(array $options) { * @since 1.8.0 */ function elgg_enable_annotations(array $options) { - return _elgg_services()->annotations->enableAll($options); + return _elgg_services()->annotationsTable->enableAll($options); } /** * Check to see if a user has already created an annotation on an object * - * @param int $entity_guid Entity guid - * @param string $annotation_type Type of annotation - * @param int $owner_guid Defaults to logged in user. + * @param int $entity_guid Entity guid + * @param string $name Annotation name + * @param int $owner_guid Defaults to logged in user. * * @return bool * @since 1.8.0 */ -function elgg_annotation_exists($entity_guid, $annotation_type, $owner_guid = null) { - return _elgg_services()->annotations->exists($entity_guid, $annotation_type, $owner_guid); +function elgg_annotation_exists($entity_guid, $name, $owner_guid = null) { + if (!$owner_guid) { + $owner_guid = elgg_get_logged_in_user_guid(); + } + + return _elgg_services()->annotationsTable->exists($entity_guid, $name, $owner_guid); } /** diff --git a/engine/lib/deprecated-3.0.php b/engine/lib/deprecated-3.0.php index 5f7bf734f53..37a2746595d 100644 --- a/engine/lib/deprecated-3.0.php +++ b/engine/lib/deprecated-3.0.php @@ -140,7 +140,7 @@ function register_metadata_as_independent($type, $subtype = '*') { */ function is_metadata_independent($type, $subtype) { elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Metadata no longer is access bound.', '3.0'); - + return false; } @@ -174,13 +174,13 @@ function is_metadata_independent($type, $subtype) { */ function elgg_get_entities_from_attributes(array $options = []) { elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use elgg_get_entities.', '3.0'); - + $options['metadata_name_value_pairs'] = elgg_extract('attribute_name_value_pairs', $options, []); $options['metadata_name_value_pairs_operator'] = elgg_extract('attribute_name_value_pairs_operator', $options, []); - + unset($options['attribute_name_value_pairs']); unset($options['attribute_name_value_pairs_operator']); - + return elgg_get_entities($options); } @@ -196,12 +196,12 @@ function elgg_get_entities_from_attributes(array $options = []) { */ function ban_user($user_guid, $reason = "") { elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use \ElggUser::ban()', '3.0'); - + $user = get_user($user_guid); if (!$user) { return false; } - + return $user->ban($reason); } @@ -216,12 +216,12 @@ function ban_user($user_guid, $reason = "") { */ function unban_user($user_guid) { elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use \ElggUser::unban()', '3.0'); - + $user = get_user($user_guid); if (!$user) { return false; } - + return $user->unban(); } @@ -236,12 +236,12 @@ function unban_user($user_guid) { */ function make_user_admin($user_guid) { elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use \ElggUser::makeAdmin()', '3.0'); - + $user = get_user($user_guid); if (!$user) { return false; } - + return $user->makeAdmin(); } @@ -256,12 +256,12 @@ function make_user_admin($user_guid) { */ function remove_user_admin($user_guid) { elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use \ElggUser::removeAdmin()', '3.0'); - + $user = get_user($user_guid); if (!$user) { return false; } - + return $user->removeAdmin(); } @@ -276,12 +276,12 @@ function remove_user_admin($user_guid) { */ function elgg_get_user_validation_status($user_guid) { elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use \ElggUser::isValidated()', '3.0'); - + $user = get_user($user_guid); if (!$user) { return false; } - + return $user->isValidated(); } @@ -298,12 +298,12 @@ function elgg_get_user_validation_status($user_guid) { */ function elgg_set_user_validation_status($user_guid, $status, $method = '') { elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use \ElggUser::setValidationStatus()', '3.0'); - + $user = get_user($user_guid); if (!$user) { return false; } - + $user->setValidationStatus($status, $method); return true; } @@ -318,14 +318,14 @@ function elgg_set_user_validation_status($user_guid, $status, $method = '') { */ function set_last_action($user) { elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use \ElggUser::setLastAction()', '3.0'); - + if (!$user instanceof ElggUser) { $user = get_user($user); } if (!$user) { return; } - + $user->setLastAction(); } @@ -339,12 +339,12 @@ function set_last_action($user) { */ function set_last_login($user_guid) { elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use \ElggUser::setLastLogin()', '3.0'); - + $user = get_user($user_guid); if (!$user) { return; } - + $user->setLastLogin(); } @@ -361,9 +361,18 @@ function set_last_login($user_guid) { * @deprecated Use \ElggMetadata->save() */ function update_metadata($id, $name, $value, $value_type) { - elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use \ElggMetadata->save()', '3.0'); - - return _elgg_services()->metadataTable->update($id, $name, $value, $value_type); + elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use ElggEntity setter or ElggEntity::setMetadata()', '3.0'); + + $metadata = elgg_get_metadata_from_id($id); + if (!$metadata) { + return false; + } + + $metadata->name = $name; + $metadata->value_type = $value_type; + $metadata->value = $value; + + return $metadata->save(); } /** @@ -385,10 +394,27 @@ function update_metadata($id, $name, $value, $value_type) { * @deprecated Use \ElggEntity setter or \Entity->setMetadata() */ function create_metadata($entity_guid, $name, $value, $value_type = '', $ignored1 = null, - $ignored2 = null, $allow_multiple = false) { - elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use \ElggEntity setter or \Entity->setMetadata()', '3.0'); - - return _elgg_services()->metadataTable->create($entity_guid, $name, $value, $value_type, $allow_multiple); + $ignored2 = null, $allow_multiple = false) { + elgg_deprecated_notice( + __FUNCTION__ . ' is deprecated. + Use ElggEntity setter or ElggEntity::setMetadata()', + '3.0'); + + $entity = get_entity($entity_guid); + if (!$entity) { + return false; + } + + if ($allow_multiple) { + return $entity->setMetadata($name, $value, $value_type, $allow_multiple); + } + + $metadata = new ElggMetadata(); + $metadata->entity_guid = $entity_guid; + $metadata->name = $name; + $metadata->value_type = $value_type; + $metadata->value = $value; + return $metadata->save(); } /** @@ -936,3 +962,106 @@ function _elgg_delete_metastring_based_object_by_id($id, $type) { return false; } + +/** + * Get the URL for this metadata + * + * By default this links to the export handler in the current view. + * + * @param int $id Metadata ID + * + * @return mixed + * @deprecated 3.0 + */ +function get_metadata_url($id) { + elgg_deprecated_notice(__FUNCTION__ . ' has been deprecated and will be removed', '3.0'); + $metadata = elgg_get_metadata_from_id($id); + if (!$metadata instanceof ElggMetadata) { + return; + } + + return $metadata->getURL(); +} + + +/** + * Create a new annotation. + * + * @param int $entity_guid GUID of entity to be annotated + * @param string $name Name of annotation + * @param string $value Value of annotation + * @param string $value_type Type of value (default is auto detection) + * @param int $owner_guid Owner of annotation (default is logged in user) + * @param int $access_id Access level of annotation + * + * @return int|bool id on success or false on failure + * @deprecated 3.0 + */ +function create_annotation($entity_guid, $name, $value, $value_type = '', $owner_guid = 0, $access_id = ACCESS_PRIVATE) { + elgg_deprecated_notice( + __FUNCTION__ . ' has been deprecated. + Use ElggAnnotation::save() or ElggEntity::annotate() + ', '3.0' + ); + + $annotation = new ElggAnnotation(); + $annotation->entity_guid = $entity_guid; + $annotation->name = $name; + $annotation->value_type = $value_type; + $annotation->value = $value; + $annotation->owner_guid = $owner_guid; + $annotation->access_id = $access_id; + + return $annotation->save(); +} + +/** + * Update an annotation. + * + * @param int $annotation_id Annotation ID + * @param string $name Name of annotation + * @param string $value Value of annotation + * @param string $value_type Type of value + * @param int $owner_guid Owner of annotation + * @param int $access_id Access level of annotation + * + * @return bool + */ +function update_annotation($annotation_id, $name, $value, $value_type, $owner_guid, $access_id) { + elgg_deprecated_notice( + __FUNCTION__ . ' has been deprecated. + Use ElggAnnotation::save() or ElggEntity::annotate() + ', '3.0' + ); + + $annotation = elgg_get_annotation_from_id($annotation_id); + if (!$annotation) { + return false; + } + + $annotation->name = $name; + $annotation->value_type = $value_type; + $annotation->value = $value; + $annotation->owner_guid = $owner_guid; + $annotation->access_id = $access_id; + + return $annotation->save(); +} + +/** + * Delete objects with a delete() method. + * + * Used as a callback for \ElggBatch. + * + * @param object $object The object to disable + * @return bool + * @access private + * + * @deprecated 3.0 + */ +function elgg_batch_delete_callback($object) { + elgg_deprecated_notice(__FUNCTION__ . ' has been deprecated.', '3.0'); + + // our db functions return the number of rows affected... + return $object->delete() ? true : false; +} \ No newline at end of file diff --git a/engine/lib/elgglib.php b/engine/lib/elgglib.php index 652504258e6..7e0b7edb89c 100644 --- a/engine/lib/elgglib.php +++ b/engine/lib/elgglib.php @@ -1594,20 +1594,6 @@ function elgg_batch_disable_callback($object) { return $object->disable() ? true : false; } -/** - * Delete objects with a delete() method. - * - * Used as a callback for \ElggBatch. - * - * @param object $object The object to disable - * @return bool - * @access private - */ -function elgg_batch_delete_callback($object) { - // our db functions return the number of rows affected... - return $object->delete() ? true : false; -} - /** * Checks if there are some constraints on the options array for * potentially dangerous operations. diff --git a/engine/lib/entities.php b/engine/lib/entities.php index cd44a3df910..14c649dd981 100644 --- a/engine/lib/entities.php +++ b/engine/lib/entities.php @@ -533,41 +533,6 @@ function elgg_get_entities(array $options = []) { return \Elgg\Database\Entities::find($options); } -/** - * Returns SQL where clause for owner and containers. - * - * @param string $column Column name the guids should be checked against. Usually - * best to provide in table.column format. - * @param null|array $guids Array of GUIDs. - * - * @return false|string - * @since 1.8.0 - * @access private - */ -function _elgg_get_guid_based_where_sql($column, $guids) { - return _elgg_services()->entityTable->getGuidBasedWhereSql($column, $guids); -} - -/** - * Returns SQL where clause for entity time limits. - * - * @param string $table Entity table prefix as defined in - * SELECT...FROM entities $table - * @param null|int $time_created_upper Time created upper limit - * @param null|int $time_created_lower Time created lower limit - * @param null|int $time_updated_upper Time updated upper limit - * @param null|int $time_updated_lower Time updated lower limit - * - * @return false|string false on fail, string on success. - * @since 1.7.0 - * @access private - */ -function _elgg_get_entity_time_where_sql($table, $time_created_upper = null, - $time_created_lower = null, $time_updated_upper = null, $time_updated_lower = null) { - return _elgg_services()->entityTable->getEntityTimeWhereSql($table, - $time_created_upper, $time_created_lower, $time_updated_upper, $time_updated_lower); -} - /** * Returns a string of rendered entities. * diff --git a/engine/lib/metadata.php b/engine/lib/metadata.php index 8efaa4846f1..3990fd640a9 100644 --- a/engine/lib/metadata.php +++ b/engine/lib/metadata.php @@ -27,7 +27,11 @@ function elgg_get_metadata_from_id($id) { * @return bool */ function elgg_delete_metadata_by_id($id) { - return _elgg_services()->metadataTable->delete($id); + $metadata = elgg_get_metadata_from_id($id); + if (!$metadata) { + return; + } + return $metadata->delete(); } /** @@ -85,19 +89,6 @@ function metadata_array_to_values($array) { return $valuearray; } -/** - * Get the URL for this metadata - * - * By default this links to the export handler in the current view. - * - * @param int $id Metadata ID - * - * @return mixed - */ -function get_metadata_url($id) { - return _elgg_services()->metadataTable->getUrl($id); -} - /** * Invalidate the metadata cache based on options passed to various *_metadata functions * diff --git a/engine/lib/relationships.php b/engine/lib/relationships.php index f9602139975..e75069fac89 100644 --- a/engine/lib/relationships.php +++ b/engine/lib/relationships.php @@ -104,27 +104,6 @@ function get_entity_relationships($guid, $inverse_relationship = false) { return _elgg_services()->relationshipsTable->getAll($guid, $inverse_relationship); } -/** - * Returns SQL appropriate for relationship joins and wheres - * - * @todo add support for multiple relationships and guids. - * - * @param string $column Column name the GUID should be checked against. - * Provide in table.column format. - * @param string $relationship Type of the relationship - * @param int $relationship_guid Entity GUID to check - * @param bool $inverse_relationship Is $relationship_guid the target of the relationship? - * - * @return mixed - * @since 1.7.0 - * @access private - */ -function elgg_get_entity_relationship_where_sql($column, $relationship = null, - $relationship_guid = null, $inverse_relationship = false) { - return _elgg_services()->relationshipsTable->getEntityRelationshipWhereSql( - $column, $relationship, $relationship_guid, $inverse_relationship); -} - /** * Gets the number of entities by a the number of entities related to them in a particular way. * This is a good way to get out the users with the most friends, or the groups with the diff --git a/engine/lib/river.php b/engine/lib/river.php index 8b1e9e5745a..c209ec1ce3c 100644 --- a/engine/lib/river.php +++ b/engine/lib/river.php @@ -37,7 +37,7 @@ * @since 1.9 */ function elgg_create_river_item(array $options = []) { - + $view = elgg_extract('view', $options, ''); // use default viewtype for when called from web services api if (!empty($view) && !elgg_view_exists($view, 'default')) { @@ -94,16 +94,16 @@ function elgg_create_river_item(array $options = []) { 'posted' => $posted, ]; $col_types = [ - 'type' => 'string', - 'subtype' => 'string', - 'action_type' => 'string', - 'access_id' => 'int', - 'view' => 'string', - 'subject_guid' => 'int', - 'object_guid' => 'int', - 'target_guid' => 'int', - 'annotation_id' => 'int', - 'posted' => 'int', + 'type' => ELGG_VALUE_STRING, + 'subtype' => ELGG_VALUE_STRING, + 'action_type' => ELGG_VALUE_STRING, + 'access_id' => ELGG_VALUE_INTEGER, + 'view' => ELGG_VALUE_STRING, + 'subject_guid' => ELGG_VALUE_INTEGER, + 'object_guid' => ELGG_VALUE_INTEGER, + 'target_guid' => ELGG_VALUE_INTEGER, + 'annotation_id' => ELGG_VALUE_INTEGER, + 'posted' => ELGG_VALUE_INTEGER, ]; // return false to stop insert @@ -113,37 +113,32 @@ function elgg_create_river_item(array $options = []) { return true; } - $dbprefix = _elgg_config()->dbprefix; - + $qb = \Elgg\Database\Insert::intoTable('river'); foreach ($values as $name => $value) { - $sql_columns[] = $name; - $sql_values[] = ":$name"; - $query_params[":$name"] = ($col_types[$name] === 'int') ? (int) $value : $value; + $query_params[$name] = $qb->param($value, $col_types[$name]); } - $sql = " - INSERT INTO {$dbprefix}river (" . implode(',', $sql_columns) . ") - VALUES (" . implode(',', $sql_values) . ") - "; - $id = insert_data($sql, $query_params); + $qb->values($query_params); + + $id = _elgg_services()->db->insertData($qb); if (!$id) { return false; } - // update the entities which had the action carried out on it - // @todo shouldn't this be done elsewhere? Like when an annotation is saved? - $object->updateLastAction($values['posted']); + if (!$return_item) { + return $id; + } $ia = elgg_set_ignore_access(true); - $river_items = elgg_get_river(['id' => $id]); + $item = elgg_get_river_item_from_id($id); elgg_set_ignore_access($ia); - if (!$river_items) { + if (!$item) { return false; } - elgg_trigger_event('created', 'river', $river_items[0]); + elgg_trigger_event('created', 'river', $item); - return $return_item ? $river_items[0] : $id; + return $item; } /** @@ -156,21 +151,25 @@ function elgg_create_river_item(array $options = []) { * subject_guids => INT|ARR Subject guid(s) * object_guids => INT|ARR Object guid(s) * target_guids => INT|ARR Target guid(s) - * annotation_ids => INT|ARR The identifier of the annotation(s) * action_types => STR|ARR The river action type(s) identifier * posted_time_lower => INT The lower bound on the time posted * posted_time_upper => INT The upper bound on the time posted * + * Additionally accepts all "annotation_*" options supported by {@link elgg_get_entities()} + * annotation_ids => INT|ARR The identifier of the annotation(s) + * * types => STR|ARR Entity type string(s) * subtypes => STR|ARR Entity subtype string(s) * type_subtype_pairs => ARR Array of type => subtype pairs where subtype * can be an array of subtype strings * + * Additionally accepts all "relationship_*" options supported by {@link elgg_get_entities()} * relationship => STR Relationship identifier * relationship_guid => INT|ARR Entity guid(s) * inverse_relationship => BOOL Subject or object of the relationship (false) + * relationship_join_on => STR subject_guid|object_guid|target_guid (defaults to subject_guid) * - * limit => INT Number to show per page (20) + * limit => INT Number to show per page (20) * offset => INT Offset in list (0) * count => BOOL Count the river items? (false) * order_by => STR Order by clause (rv.posted desc) @@ -196,179 +195,21 @@ function elgg_create_river_item(array $options = []) { * @since 1.8.0 */ function elgg_get_river(array $options = []) { - $defaults = [ - 'ids' => ELGG_ENTITIES_ANY_VALUE, - - 'subject_guids' => ELGG_ENTITIES_ANY_VALUE, - 'object_guids' => ELGG_ENTITIES_ANY_VALUE, - 'target_guids' => ELGG_ENTITIES_ANY_VALUE, - 'annotation_ids' => ELGG_ENTITIES_ANY_VALUE, - 'action_types' => ELGG_ENTITIES_ANY_VALUE, - - 'relationship' => null, - 'relationship_guid' => null, - 'inverse_relationship' => false, - - 'types' => ELGG_ENTITIES_ANY_VALUE, - 'subtypes' => ELGG_ENTITIES_ANY_VALUE, - 'type_subtype_pairs' => ELGG_ENTITIES_ANY_VALUE, - - 'posted_time_lower' => ELGG_ENTITIES_ANY_VALUE, - 'posted_time_upper' => ELGG_ENTITIES_ANY_VALUE, - - 'limit' => 20, - 'offset' => 0, - 'count' => false, - 'distinct' => true, - - 'batch' => false, - 'batch_inc_offset' => true, - 'batch_size' => 25, - - 'order_by' => 'rv.posted desc', - 'group_by' => ELGG_ENTITIES_ANY_VALUE, - - 'wheres' => [], - 'joins' => [], - ]; - - if (isset($options['view']) || isset($options['views'])) { - $msg = __FUNCTION__ . ' does not support the "views" option, though you may specify values for' - . ' the "rv.view" column using the "wheres" option.'; - elgg_log($msg, 'WARNING'); - } - - $options = array_merge($defaults, $options); - - if ($options['batch'] && !$options['count']) { - $batch_size = $options['batch_size']; - $batch_inc_offset = $options['batch_inc_offset']; - - // clean batch keys from $options. - unset($options['batch'], $options['batch_size'], $options['batch_inc_offset']); - - return new \ElggBatch('elgg_get_river', $options, null, $batch_size, $batch_inc_offset); - } - - $singulars = ['id', 'subject_guid', 'object_guid', 'target_guid', 'annotation_id', 'action_type', 'type', 'subtype']; - $options = _elgg_normalize_plural_options_array($options, $singulars); - - $wheres = $options['wheres']; - - $wheres[] = _elgg_get_guid_based_where_sql('rv.id', $options['ids']); - $wheres[] = _elgg_get_guid_based_where_sql('rv.subject_guid', $options['subject_guids']); - $wheres[] = _elgg_get_guid_based_where_sql('rv.object_guid', $options['object_guids']); - $wheres[] = _elgg_get_guid_based_where_sql('rv.target_guid', $options['target_guids']); - $wheres[] = _elgg_get_guid_based_where_sql('rv.annotation_id', $options['annotation_ids']); - $wheres[] = _elgg_river_get_action_where_sql($options['action_types']); - $wheres[] = _elgg_get_river_type_subtype_where_sql('rv', $options['types'], - $options['subtypes'], $options['type_subtype_pairs']); - - if ($options['posted_time_lower'] && is_int($options['posted_time_lower'])) { - $wheres[] = "rv.posted >= {$options['posted_time_lower']}"; - } - - if ($options['posted_time_upper'] && is_int($options['posted_time_upper'])) { - $wheres[] = "rv.posted <= {$options['posted_time_upper']}"; - } - - if (!access_get_show_hidden_status()) { - $wheres[] = "rv.enabled = 'yes'"; - } - - $dbprefix = _elgg_config()->dbprefix; - - // joins - $joins = []; - $joins[] = "JOIN {$dbprefix}entities oe ON rv.object_guid = oe.guid"; - - // LEFT JOIN is used because all river items do not necessarily have target - $joins[] = "LEFT JOIN {$dbprefix}entities te ON rv.target_guid = te.guid"; - - if ($options['relationship_guid']) { - $clauses = elgg_get_entity_relationship_where_sql( - 'rv.subject_guid', - $options['relationship'], - $options['relationship_guid'], - $options['inverse_relationship']); - if ($clauses) { - $wheres = array_merge($wheres, $clauses['wheres']); - $joins = array_merge($joins, $clauses['joins']); - } - } - - // add optional joins - $joins = array_merge($joins, $options['joins']); - - // see if any functions failed - // remove empty strings on successful functions - foreach ($wheres as $i => $where) { - if ($where === false) { - return false; - } elseif (empty($where)) { - unset($wheres[$i]); - } - } - - // remove identical where clauses - $wheres = array_unique($wheres); - $prefix = _elgg_config()->dbprefix; - - if (!$options['count']) { - $distinct = $options['distinct'] ? "DISTINCT" : ""; - - $query = "SELECT $distinct rv.* FROM {$prefix}river rv "; - } else { - // note: when DISTINCT unneeded, it's slightly faster to compute COUNT(*) than IDs - $count_expr = $options['distinct'] ? "DISTINCT rv.id" : "*"; - - $query = "SELECT COUNT($count_expr) as total FROM {$prefix}river rv "; - } - - // add joins - foreach ($joins as $j) { - $query .= " $j "; - } - - // add wheres - $query .= ' WHERE '; - - foreach ($wheres as $w) { - $query .= " $w AND "; - } - - // Make sure that user has access to all the entities referenced by each river item - $object_access_where = _elgg_get_access_where_sql(['table_alias' => 'oe']); - $target_access_where = _elgg_get_access_where_sql(['table_alias' => 'te']); - - // We use LEFT JOIN with entities table but the WHERE clauses are used - // regardless if a JOIN is successfully made. The "te.guid IS NULL" is - // needed because of this. - $query .= "$object_access_where AND ($target_access_where OR te.guid IS NULL) "; - - if (!$options['count']) { - $options['group_by'] = sanitise_string($options['group_by']); - if ($options['group_by']) { - $query .= " GROUP BY {$options['group_by']}"; - } - - $options['order_by'] = sanitise_string($options['order_by']); - $query .= " ORDER BY {$options['order_by']}"; - - if ($options['limit']) { - $limit = sanitise_int($options['limit']); - $offset = sanitise_int($options['offset'], false); - $query .= " LIMIT $offset, $limit"; - } + return \Elgg\Database\River::find($options); +} - $river_items = get_data($query, '_elgg_row_to_elgg_river_item'); - _elgg_prefetch_river_entities($river_items); +/** + * Get river item from its ID + * + * @param int $id ID + * @return ElggRiverItem|false + */ +function elgg_get_river_item_from_id($id) { + $items = elgg_get_river([ + 'id' => $id, + ]); - return $river_items; - } else { - $total = get_data_row($query); - return (int) $total->total; - } + return $items ? $items[0] : false; } /** @@ -383,18 +224,34 @@ function elgg_get_river(array $options = []) { * * @return bool|null true on success, false on failure, null if no metadata to delete. * - * @since 1.8.0 + * @since 1.8.0 */ function elgg_delete_river(array $options = []) { - + if (!_elgg_is_valid_options_for_batch_operation($options, 'river')) { // requirements not met return false; } - - $batch = new ElggBatch('elgg_get_river', $options, 'elgg_batch_delete_callback', 25, false); - - return $batch->callbackResult; + + $options['batch'] = true; + $options['batch_size'] = 25; + $options['batch_inc_offset'] = false; + + $river = elgg_get_river($options); + $count = $river->count(); + + if (!$count) { + return; + } + + $success = 0; + foreach ($river as $river_item) { + if ($river_item->delete()) { + $success++; + } + } + + return $success == $count; } /** @@ -407,6 +264,10 @@ function elgg_delete_river(array $options = []) { * @access private */ function _elgg_prefetch_river_entities(array $river_items) { + $river_items = array_filter($river_items, function($e) { + return $e instanceof ElggRiverItem; + }); + // prefetch objects, subjects and targets $guids = []; foreach ($river_items as $item) { @@ -500,164 +361,6 @@ function elgg_list_river(array $options = []) { return elgg_view('page/components/list', $options); } -/** - * Convert a database row to a new \ElggRiverItem - * - * @param \stdClass $row Database row from the river table - * - * @return \ElggRiverItem - * @since 1.8.0 - * @access private - */ -function _elgg_row_to_elgg_river_item($row) { - if (!($row instanceof \stdClass)) { - return null; - } - - return new \ElggRiverItem($row); -} - -/** - * Returns SQL where clause for type and subtype on river table - * - * @internal This is a simplified version of elgg_get_entity_type_subtype_where_sql() - * which could be used for all queries once the subtypes have been denormalized. - * - * @param string $table 'rv' - * @param null|array $types Array of types or null if none. - * @param null|array $subtypes Array of subtypes or null if none - * @param null|array $pairs Array of pairs of types and subtypes - * - * @return string - * @since 1.8.0 - * @access private - */ -function _elgg_get_river_type_subtype_where_sql($table, $types, $subtypes, $pairs) { - // short circuit if nothing is requested - if (!$types && !$subtypes && !$pairs) { - return ''; - } - - $wheres = []; - $types_wheres = []; - $subtypes_wheres = []; - - // if no pairs, use types and subtypes - if (!is_array($pairs)) { - if ($types) { - if (!is_array($types)) { - $types = [$types]; - } - foreach ($types as $type) { - $type = sanitise_string($type); - $types_wheres[] = "({$table}.type = '$type')"; - } - } - - if ($subtypes) { - if (!is_array($subtypes)) { - $subtypes = [$subtypes]; - } - foreach ($subtypes as $subtype) { - $subtype = sanitise_string($subtype); - $subtypes_wheres[] = "({$table}.subtype = '$subtype')"; - } - } - - if (is_array($types_wheres) && count($types_wheres)) { - $types_wheres = [implode(' OR ', $types_wheres)]; - } - - if (is_array($subtypes_wheres) && count($subtypes_wheres)) { - $subtypes_wheres = ['(' . implode(' OR ', $subtypes_wheres) . ')']; - } - - $wheres = [implode(' AND ', array_merge($types_wheres, $subtypes_wheres))]; - } else { - // using type/subtype pairs - foreach ($pairs as $paired_type => $paired_subtypes) { - $paired_type = sanitise_string($paired_type); - if (is_array($paired_subtypes)) { - $paired_subtypes = array_map('sanitise_string', $paired_subtypes); - $paired_subtype_str = implode("','", $paired_subtypes); - if ($paired_subtype_str) { - $wheres[] = "({$table}.type = '$paired_type'" - . " AND {$table}.subtype IN ('$paired_subtype_str'))"; - } - } else { - $paired_subtype = sanitise_string($paired_subtypes); - $wheres[] = "({$table}.type = '$paired_type'" - . " AND {$table}.subtype = '$paired_subtype')"; - } - } - } - - if (is_array($wheres) && count($wheres)) { - $where = implode(' OR ', $wheres); - return "($where)"; - } - - return ''; -} - -/** - * Get the where clause based on river action type strings - * - * @param array $types Array of action type strings - * - * @return string - * @since 1.8.0 - * @access private - */ -function _elgg_river_get_action_where_sql($types) { - if (!$types) { - return ''; - } - - if (!is_array($types)) { - $types = sanitise_string($types); - return "(rv.action_type = '$types')"; - } - - // sanitize types array - $types_sanitized = []; - foreach ($types as $type) { - $types_sanitized[] = sanitise_string($type); - } - - $type_str = implode("','", $types_sanitized); - return "(rv.action_type IN ('$type_str'))"; -} - -/** - * Get the where clause based on river view strings - * - * @param array $views Array of view strings - * - * @return string - * @since 1.8.0 - * @access private - */ -function _elgg_river_get_view_where_sql($views) { - if (!$views) { - return ''; - } - - if (!is_array($views)) { - $views = sanitise_string($views); - return "(rv.view = '$views')"; - } - - // sanitize views array - $views_sanitized = []; - foreach ($views as $view) { - $views_sanitized[] = sanitise_string($view); - } - - $view_str = implode("','", $views_sanitized); - return "(rv.view IN ('$view_str'))"; -} - /** * Sets the access ID on river items for a particular object * @@ -667,7 +370,7 @@ function _elgg_river_get_view_where_sql($views) { * @return bool Depending on success */ function update_river_access_by_object($object_guid, $access_id) { - + $dbprefix = _elgg_config()->dbprefix; $query = " UPDATE {$dbprefix}river @@ -775,18 +478,18 @@ function _elgg_river_menu_setup(\Elgg\Hook $hook) { if (!elgg_is_logged_in()) { return; } - + $item = $hook->getParam('item'); if (!($item instanceof ElggRiverItem)) { return; } - + if (!$item->canDelete()) { return; } - + $return = $hook->getValue(); - + $return[] = \ElggMenuItem::factory([ 'name' => 'delete', 'href' => "action/river/delete?id={$item->id}", @@ -812,11 +515,11 @@ function _elgg_river_init() { elgg_register_plugin_hook_handler('unit_test', 'system', '_elgg_river_test'); elgg_register_plugin_hook_handler('register', 'menu:river', '_elgg_river_menu_setup'); - + // For BC, we want required AMD modules to be loaded even if plugins // overwrite these views elgg_extend_view('forms/comment/save', 'forms/comment/save_deps'); - + } /** diff --git a/engine/lib/statistics.php b/engine/lib/statistics.php index 865decfc443..a91548b4bc2 100644 --- a/engine/lib/statistics.php +++ b/engine/lib/statistics.php @@ -55,23 +55,27 @@ function get_entity_statistics($owner_guid = 0) { * @return int */ function get_number_users($show_deactivated = false) { - $access = ""; - if (!$show_deactivated) { - $access = "and " . _elgg_get_access_where_sql(['table_alias' => '']); + $where = new \Elgg\Database\Clauses\EntityWhereClause(); + $where->type_subtype_pairs = [ + 'user' => null, + ]; + + if ($show_deactivated) { + $where->use_enabled_clause = false; } - $prefix = _elgg_config()->dbprefix; - $query = "SELECT count(*) as count - from {$prefix}entities where type='user' $access"; + $select = \Elgg\Database\Select::fromTable('entities', 'e'); + $select->select('COUNT(DISTINCT e.guid) AS count'); + $select->addClause($where, 'e'); - $result = get_data_row($query); + $result = _elgg_services()->db->getDataRow($select); if ($result) { return $result->count; } - return false; + return 0; } /** diff --git a/engine/lib/tags.php b/engine/lib/tags.php index f32c150392c..12911cf1312 100644 --- a/engine/lib/tags.php +++ b/engine/lib/tags.php @@ -31,163 +31,57 @@ function string_to_tag_array($string) { /** * Get popular tags and their frequencies * - * Supports similar arguments as elgg_get_entities() + * Accepts all options supported by {@link elgg_get_entities()} * - * @param array $options Array in format: + * Returns an array of objects that include "tag" and "total" properties * - * threshold => INT minimum tag count + * @todo When updating this function for 3.0, I have noticed that docs explicitly mention + * that tags must be registered, but it was not really checked anywhere in code + * So, either update the docs or decide what the behavior should be * - * tag_names => array() metadata tag names - must be registered tags + * @param array $options Options * - * limit => INT number of tags to return (default from settings) + * @option int $threshold Minimum number of tag occurrences + * @option string[] $tag_names Names of registered tag names to include in search * - * types => null|STR entity type (SQL: type = '$type') - * - * subtypes => null|STR entity subtype (SQL: subtype IN ('subtype1', 'subtype2)) - * Use ELGG_ENTITIES_NO_VALUE to match the default subtype. - * Use ELGG_ENTITIES_ANY_VALUE to match any subtype. - * - * type_subtype_pairs => null|ARR (array('type' => 'subtype')) - * array( - * 'object' => array('blog', 'file'), // All objects with subtype of 'blog' or 'file' - * 'user' => ELGG_ENTITY_ANY_VALUE, // All users irrespective of subtype - * ); - * - * owner_guids => null|INT entity guid - * - * container_guids => null|INT container_guid - * - * created_time_lower => null|INT Created time lower boundary in epoch time - * - * created_time_upper => null|INT Created time upper boundary in epoch time - * - * modified_time_lower => null|INT Modified time lower boundary in epoch time - * - * modified_time_upper => null|INT Modified time upper boundary in epoch time - * - * wheres => array() Additional where clauses to AND together - * - * joins => array() Additional joins - * - * @return object[]|false If no tags or error, false - * otherwise, array of objects with ->tag and ->total values + * @return object[]|false * @since 1.7.1 */ function elgg_get_tags(array $options = []) { - _elgg_check_unsupported_site_guid($options); - $defaults = [ 'threshold' => 1, 'tag_names' => [], - 'limit' => _elgg_config()->default_limit, - - 'types' => ELGG_ENTITIES_ANY_VALUE, - 'subtypes' => ELGG_ENTITIES_ANY_VALUE, - 'type_subtype_pairs' => ELGG_ENTITIES_ANY_VALUE, - - 'owner_guids' => ELGG_ENTITIES_ANY_VALUE, - 'container_guids' => ELGG_ENTITIES_ANY_VALUE, - - 'modified_time_lower' => ELGG_ENTITIES_ANY_VALUE, - 'modified_time_upper' => ELGG_ENTITIES_ANY_VALUE, - 'created_time_lower' => ELGG_ENTITIES_ANY_VALUE, - 'created_time_upper' => ELGG_ENTITIES_ANY_VALUE, - - 'joins' => [], - 'wheres' => [], ]; - $options = array_merge($defaults, $options); - $singulars = ['type', 'subtype', 'owner_guid', 'container_guid', 'tag_name']; + $singulars = ['tag_name']; $options = _elgg_normalize_plural_options_array($options, $singulars); - $registered_tags = elgg_get_registered_tag_metadata_names(); - - if (!is_array($options['tag_names'])) { - return false; - } - - // empty array so use all registered tag names - if (count($options['tag_names']) == 0) { - $options['tag_names'] = $registered_tags; - } - - $wheres = $options['wheres']; - - // catch for tags that were spaces - $wheres[] = "md.value != ''"; - - $sanitised_tags = []; - foreach ($options['tag_names'] as $tag) { - $sanitised_tags[] = '"' . sanitise_string($tag) . '"'; - } - $tags_in = implode(',', $sanitised_tags); - $wheres[] = "(md.name IN ($tags_in))"; - - $wheres[] = _elgg_services()->entityTable->getEntityTypeSubtypeWhereSql('e', $options['types'], - $options['subtypes'], $options['type_subtype_pairs']); - $wheres[] = _elgg_get_guid_based_where_sql('e.owner_guid', $options['owner_guids']); - $wheres[] = _elgg_get_guid_based_where_sql('e.container_guid', $options['container_guids']); - $wheres[] = _elgg_get_entity_time_where_sql('e', $options['created_time_upper'], - $options['created_time_lower'], $options['modified_time_upper'], $options['modified_time_lower']); - - // see if any functions failed - // remove empty strings on successful functions - foreach ($wheres as $i => $where) { - if ($where === false) { - return false; - } elseif (empty($where)) { - unset($wheres[$i]); - } - } - - // remove identical where clauses - $wheres = array_unique($wheres); - - $joins = $options['joins']; - - $prefix = _elgg_config()->dbprefix; - $joins[] = "JOIN {$prefix}metadata md on md.entity_guid = e.guid"; - - // remove identical join clauses - $joins = array_unique($joins); - - foreach ($joins as $i => $join) { - if ($join === false) { - return false; - } elseif (empty($join)) { - unset($joins[$i]); - } + $tag_names = elgg_extract('tag_names', $options); + if (empty($tag_names)) { + $tag_names = elgg_get_registered_tag_metadata_names(); } - $query = "SELECT md.value as tag, count(md.id) as total "; - $query .= "FROM {$prefix}entities e "; + $threshold = elgg_extract('threshold', $options, 1, false); - // add joins - foreach ($joins as $j) { - $query .= " $j "; - } + unset($options['tag_names']); + unset($options['threshold']); - // add wheres - $query .= ' WHERE '; + $qb = \Elgg\Database\Select::fromTable('metadata', 'md'); + $qb->select('md.value AS tag') + ->addSelect('COUNT(md.id) AS total') + ->where($qb->compare('md.name', 'IN', $tag_names, ELGG_VALUE_STRING)) + ->andWhere($qb->compare('md.value', '!=', '', ELGG_VALUE_STRING)) + ->groupBy('md.value') + ->having($qb->compare('total', '>=', $threshold, ELGG_VALUE_INTEGER)) + ->orderBy('total', 'desc'); - foreach ($wheres as $w) { - $query .= " $w AND "; - } + $options = new \Elgg\Database\QueryOptions($options); + $alias = $qb->joinEntitiesTable('md', 'entity_guid', 'inner', 'e'); + $qb->addClause(\Elgg\Database\Clauses\EntityWhereClause::factory($options), $alias); - // Add access controls - $query .= _elgg_get_access_where_sql(); - - $threshold = sanitise_int($options['threshold']); - $query .= " GROUP BY md.value HAVING total >= {$threshold} "; - $query .= " ORDER BY total DESC "; - - $limit = sanitise_int($options['limit']); - $query .= " LIMIT {$limit} "; - - return get_data($query); + return _elgg_services()->db->getData($qb); } /** @@ -208,6 +102,18 @@ function elgg_register_tag_metadata_name($name) { return _elgg_services()->metadataTable->registerTagName($name); } +/** + * Unregister metadata tag name + * + * @param string $name Tag name + * + * @return bool + * @since 3.0 + */ +function elgg_unregister_tag_metadata_name($name) { + return _elgg_services()->metadataTable->unregisterTagName($name); +} + /** * Returns an array of valid metadata names for tags. * diff --git a/engine/tests/classes/Elgg/BaseTestCase.php b/engine/tests/classes/Elgg/BaseTestCase.php index eedf3784e97..e50875dfc4a 100644 --- a/engine/tests/classes/Elgg/BaseTestCase.php +++ b/engine/tests/classes/Elgg/BaseTestCase.php @@ -141,7 +141,7 @@ protected function setUp() { $app->_services->entityTable->setCurrentTime($dt); $app->_services->metadataTable->setCurrentTime($dt); $app->_services->relationshipsTable->setCurrentTime($dt); - $app->_services->annotations->setCurrentTime($dt); + $app->_services->annotationsTable->setCurrentTime($dt); $app->_services->usersTable->setCurrentTime($dt); // Invalidate memcache diff --git a/engine/tests/classes/Elgg/IntegrationTestCase.php b/engine/tests/classes/Elgg/IntegrationTestCase.php index 47e495d63bb..d5cdaa2dbda 100644 --- a/engine/tests/classes/Elgg/IntegrationTestCase.php +++ b/engine/tests/classes/Elgg/IntegrationTestCase.php @@ -28,7 +28,8 @@ public static function createApplication() { $config = self::getTestingConfig(); $sp = new ServiceProvider($config); - + $config->boot_cache_ttl = 10; + // persistentLogin service needs this set to instantiate without calling DB $sp->config->getCookieConfig(); diff --git a/engine/tests/classes/Elgg/Mocks/Database/AnnotationsTable.php b/engine/tests/classes/Elgg/Mocks/Database/AnnotationsTable.php index ff3d00f6862..2b885c49b12 100644 --- a/engine/tests/classes/Elgg/Mocks/Database/AnnotationsTable.php +++ b/engine/tests/classes/Elgg/Mocks/Database/AnnotationsTable.php @@ -3,74 +3,254 @@ namespace Elgg\Mocks\Database; use Elgg\Database\AnnotationsTable as DbAnnotations; -use ElggMetadata; +use Elgg\Database\Clauses\AnnotationWhereClause; +use Elgg\Database\Delete; +use Elgg\Database\Insert; +use Elgg\Database\Select; +use Elgg\Database\Update; +use ElggAnnotation; class AnnotationsTable extends DbAnnotations { /** - * @var ElggMetadata + * @var \stdClass[] */ - public $mocks = []; + public $rows = []; + + /** + * DB query query_specs + * @var array + */ + public $query_specs = []; /** * @var int */ - private $iterator = 100; + public $iterator = 100; /** * {@inheritdoc} */ public function get($id) { - if (empty($this->mocks[$id])) { + if (empty($this->rows[$id])) { return false; } - return $this->mocks[$id]; + + $annotation = new ElggAnnotation($this->rows[$id]); + + if ($annotation->access_id == ACCESS_PUBLIC) { + // Public entities are always accessible + return $annotation; + } + + $user_guid = isset($user_guid) ? (int) $user_guid : elgg_get_logged_in_user_guid(); + + if (_elgg_services()->userCapabilities->canBypassPermissionsCheck($user_guid)) { + return $annotation; + } + + if ($user_guid && $user_guid == $annotation->owner_guid) { + // Owners have access to their own content + return $annotation; + } + + if ($user_guid && $annotation->access_id == ACCESS_LOGGED_IN) { + // Existing users have access to entities with logged in access + return $annotation; + } + + return parent::get($id); } /** * {@inheritdoc} */ - public function delete($id) { - if (!isset($this->mocks[$id])) { + public function create(ElggAnnotation $annotation, \ElggEntity $entity) { + $this->iterator++; + $id = $this->iterator; + + $row = (object) [ + 'type' => 'annotation', + 'id' => $id, + 'entity_guid' => $entity->guid, + 'owner_guid' => $annotation->owner_guid, + 'access_id' => $annotation->access_id, + 'name' => $annotation->name, + 'value' => $annotation->value, + 'value_type' => $annotation->value_type, + 'time_created' => $this->getCurrentTime()->getTimestamp(), + 'enabled' => $annotation->enabled, + ]; + + $this->rows[$id] = $row; + + $this->addQuerySpecs($row); + + return parent::create($annotation, $entity); + } + + /** + * {@inheritdoc} + */ + public function update(ElggAnnotation $annotation) { + $id = $annotation->id; + if (!isset($this->rows[$id])) { return false; } - unset($this->mocks[$id]); - return true; + + $row = $this->rows[$id]; + $row->name = $annotation->name; + $row->value = $annotation->value; + $row->value_type = $annotation->value_type; + + $this->rows[$id] = $row; + + $this->addQuerySpecs($row); + + return parent::update($annotation); + } + + /** + * {@inheritdoc} + */ + public function getAll(array $options = array()) { + $guids = elgg_extract('guids', $options, (array) elgg_extract('guid', $options)); + + $rows = []; + foreach ($this->rows as $id => $row) { + if (empty($guids) || in_array($row->entity_guid, $guids)) { + $rows[] = new ElggAnnotation($row); + } + } + + return $rows; } /** * {@inheritdoc} */ - public function create($entity_guid, $name, $value, $value_type = '', $owner_guid = 0, $access_id = ACCESS_PRIVATE) { - $entity = get_entity($entity_guid); - if (!$entity) { + public function delete(ElggAnnotation $annotation) { + parent::delete($annotation); + + if (!isset($this->rows[$annotation->id])) { return false; } + $row = $this->rows[$annotation->id]; + $this->clearQuerySpecs($row); - $owner_guid = (int) $owner_guid; - if ($owner_guid == 0) { - $owner_guid = $this->session->getLoggedInUserGuid(); + unset($this->rows[$annotation->id]); + + return true; + } + + /** + * Clear query specs + * + * @param \stdClass $row Data row + * @return void + */ + public function clearQuerySpecs(\stdClass $row) { + if (!isset($this->query_specs[$row->id])) { + return; + } + foreach ($this->query_specs[$row->id] as $spec) { + $this->db->removeQuerySpec($spec); } + } - $this->iterator++; - $id = $this->iterator; + /** + * Add query query_specs for a metadata object + * + * @param \stdClass $row Data row + * + * @return void + */ + public function addQuerySpecs(\stdClass $row) { - $row = (object) [ - 'type' => 'annotation', - 'id' => $id, - 'entity_guid' => $entity->guid, - 'owner_guid' => $owner_guid, - 'name' => $name, - 'value' => $value, - 'value_type' => $value_type, - 'time_created' => $this->getCurrentTime()->getTimestamp(), - 'access_id' => (int) $access_id, - ]; + $this->clearQuerySpecs($row); + + $qb = Select::fromTable('annotations'); + $qb->select('*'); + + $where = new AnnotationWhereClause(); + $where->ids = $row->id; + $qb->addClause($where); + + $this->query_specs[$row->id][] = $this->db->addQuerySpec([ + 'sql' => $qb->getSQL(), + 'params' => $qb->getParameters(), + 'results' => function () use ($row) { + if (isset($this->rows[$row->id])) { + return [$this->rows[$row->id]]; + } + + return []; + }, + ]); + + $qb = Insert::intoTable('annotations'); + $qb->values([ + 'entity_guid' => $qb->param($row->entity_guid, ELGG_VALUE_INTEGER), + 'name' => $qb->param($row->name, ELGG_VALUE_STRING), + 'value' => $qb->param($row->value, $row->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING), + 'value_type' => $qb->param($row->value_type, ELGG_VALUE_STRING), + 'owner_guid' => $qb->param($row->owner_guid, ELGG_VALUE_INTEGER), + 'time_created' => $qb->param($row->time_created, ELGG_VALUE_INTEGER), + 'access_id' => $qb->param($row->access_id, ELGG_VALUE_INTEGER), + ]); + + $this->query_specs[$row->id][] = $this->db->addQuerySpec([ + 'sql' => $qb->getSQL(), + 'params' => $qb->getParameters(), + 'insert_id' => $row->id, + ]); + + $qb = Update::table('annotations'); + $qb->set('name', $qb->param($row->name, ELGG_VALUE_STRING)) + ->set('value', $qb->param($row->value, $row->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING)) + ->set('value_type', $qb->param($row->value_type, ELGG_VALUE_STRING)) + ->set('access_id', $qb->param($row->access_id, ELGG_VALUE_INTEGER)) + ->set('owner_guid', $qb->param($row->owner_guid, ELGG_VALUE_INTEGER)) + ->where($qb->compare('id', '=', $row->id, ELGG_VALUE_INTEGER)); - $annotation = new \ElggAnnotation($row); + $this->query_specs[$row->id][] = $this->db->addQuerySpec([ + 'sql' => $qb->getSQL(), + 'params' => $qb->getParameters(), + 'results' => function () use ($row) { + if (isset($this->rows[$row->id])) { + return [$row->id]; + } - $this->mocks[$id] = $annotation; - return $id; + return []; + }, + ]); + + $qb = Delete::fromTable('annotations'); + $qb->where($qb->compare('id', '=', $row->id, ELGG_VALUE_INTEGER)); + + $this->query_specs[$row->id][] = $this->db->addQuerySpec([ + 'sql' => $qb->getSQL(), + 'params' => $qb->getParameters(), + 'results' => function () use ($row) { + if (isset($this->rows[$row->id])) { + unset($this->rows[$row->id]); + $this->clearQuerySpecs($row); + + return [$row->id]; + } + + return []; + } + ]); } + /** + * Iterate ID + * @return int + */ + public function iterate() { + $this->iterator++; + + return $this->iterator; + } } diff --git a/engine/tests/classes/Elgg/Mocks/Database/EntityTable.php b/engine/tests/classes/Elgg/Mocks/Database/EntityTable.php index 0540c83ffbf..4112e27c656 100644 --- a/engine/tests/classes/Elgg/Mocks/Database/EntityTable.php +++ b/engine/tests/classes/Elgg/Mocks/Database/EntityTable.php @@ -2,7 +2,9 @@ namespace Elgg\Mocks\Database; +use Elgg\Database\Clauses\EntityWhereClause; use Elgg\Database\EntityTable as DbEntityTable; +use Elgg\Database\Select; use ElggEntity; use stdClass; @@ -32,12 +34,47 @@ class EntityTable extends DbEntityTable { */ private $query_specs = []; + /** + * {@inheritdoc} + */ + public function getRow($guid, $user_guid = null) { + if (empty($this->rows[$guid])) { + return false; + } + + $entity = $this->rowToElggStar($this->rows[$guid]); + + if ($entity->access_id == ACCESS_PUBLIC) { + // Public entities are always accessible + return $entity; + } + + $user_guid = isset($user_guid) ? (int) $user_guid : elgg_get_logged_in_user_guid(); + + if (_elgg_services()->userCapabilities->canBypassPermissionsCheck($user_guid)) { + return $entity; + } + + if ($user_guid && $user_guid == $entity->owner_guid) { + // Owners have access to their own content + return $entity; + } + + if ($user_guid && $entity->access_id == ACCESS_LOGGED_IN) { + // Existing users have access to entities with logged in access + return $entity; + } + + return parent::getRow($guid, $user_guid); + } + /** * {@inheritdoc} */ public function insertRow(stdClass $row, array $attributes = []) { $subtype = isset($row->subtype) ? $row->subtype : null; $this->setup(null, $row->type, $subtype, array_merge($attributes, (array) $row)); + return parent::insertRow($row); } @@ -60,6 +97,7 @@ public function updateRow($guid, stdClass $row) { * @param string $type Type of the mock entity * @param string $subtype Subtype of the mock entity * @param array $attributes Attributes of the mock entity + * * @return ElggEntity */ public function setup($guid, $type, $subtype, array $attributes = []) { @@ -115,7 +153,7 @@ public function setup($guid, $type, $subtype, array $attributes = []) { // not an attribute, so needs to be set again $entity->$name = $value; } - + return $entity; } @@ -125,6 +163,7 @@ public function setup($guid, $type, $subtype, array $attributes = []) { */ public function iterate() { $this->iterator++; + return $this->iterator; } @@ -132,6 +171,7 @@ public function iterate() { * Clear query specs * * @param int $guid GUID + * * @return void */ public function clearQuerySpecs($guid) { @@ -146,14 +186,19 @@ public function clearQuerySpecs($guid) { * Add query specs * * @param stdClass $row Entity table row + * * @return void */ public function addQuerySpecs(stdClass $row) { // Clear previous added specs, if any $this->clearQuerySpecs($row->guid); - - $this->addSelectQuerySpecs($row); + + // We may have been too paranoid about access + // If there is a need for more robust access controls in unit tests + // uncomment the following line and remove getRow method + //$this->addSelectQuerySpecs($row); + $this->addInsertQuerySpecs($row); $this->addUpdateQuerySpecs($row); $this->addDeleteQuerySpecs($row); @@ -163,12 +208,11 @@ public function addQuerySpecs(stdClass $row) { * Add query specs for SELECT queries * * @param stdClass $row Data row + * * @return void */ public function addSelectQuerySpecs(stdClass $row) { - $dbprefix = _elgg_config()->dbprefix; - // Access SQL for this row might differ based on: // - logged in user // - show hidden entities status @@ -186,7 +230,7 @@ public function addSelectQuerySpecs(stdClass $row) { (int) $row->container_guid, (int) elgg_get_logged_in_user_guid(), ]); - + $access_combinations = []; foreach ($access_user_guids as $access_user_guid) { @@ -212,36 +256,34 @@ public function addSelectQuerySpecs(stdClass $row) { ]; } - $access_queries = []; foreach ($access_combinations as $access_combination) { - $access_combination['table_alias'] = ''; - $access_queries[] = _elgg_get_access_where_sql($access_combination); - } - $access_queries = array_unique($access_queries); + $where = new EntityWhereClause(); + $where->ignore_access = $access_combination['ignore_access']; + $where->use_enabled_clause = $access_combination['use_enabled_clause']; + $where->viewer_guid = $access_combination['user_guid']; + $where->guids = $row->guid; - foreach ($access_queries as $access) { - - $sql = "SELECT * FROM {$dbprefix}entities - WHERE guid = :guid AND $access"; + $select = Select::fromTable('entities'); + $select->select('*'); + $select->addClause($where); $this->query_specs[$row->guid][] = $this->db->addQuerySpec([ - 'sql' => $sql, - 'params' => [ - ':guid' => (int) $row->guid, - ], - 'results' => function() use ($row, $access_combination) { + 'sql' => $select->getSQL(), + 'params' => $select->getParameters(), + 'results' => function () use ($row, $access_combination) { if (!isset($this->rows[$row->guid])) { return []; } $row = $this->rows[$row->guid]; - if ($access_combination['use_enabled_clause'] && !$row->enabled != 'yes') { + if ($access_combination['use_enabled_clause'] && $row->enabled != 'yes') { // The SELECT query would contain ('enabled' = 'yes') return []; } $has_access = $this->validateRowAccess($row); + return $has_access ? [$row] : []; } ]); @@ -253,6 +295,7 @@ public function addSelectQuerySpecs(stdClass $row) { * This is a reverse engineered approach to an SQL query generated by AccessCollections::getWhereSql() * * @param \stdClass $row Data row + * * @return bool */ public function validateRowAccess($row) { @@ -300,12 +343,13 @@ public function validateRowAccess($row) { * Query specs for INSERT operations * * @param stdClass $row Data row + * * @return void */ public function addInsertQuerySpecs(stdClass $row) { $dbprefix = _elgg_config()->dbprefix; - + $sql = " INSERT INTO {$dbprefix}entities (type, subtype, owner_guid, container_guid, @@ -335,6 +379,7 @@ public function addInsertQuerySpecs(stdClass $row) { * Query specs for UPDATE operations * * @param stdClass $row Data row + * * @return void */ public function addUpdateQuerySpecs(stdClass $row) { @@ -361,11 +406,13 @@ public function addUpdateQuerySpecs(stdClass $row) { ':time_updated' => $row->time_updated, ':guid' => $row->guid, ], - 'results' => function() use ($row) { + 'results' => function () use ($row) { if (isset($this->rows[$row->guid])) { $this->rows[$row->guid] = $row; + return [$row->guid]; } + return []; }, ]); @@ -381,13 +428,15 @@ public function addUpdateQuerySpecs(stdClass $row) { 'params' => [ ':guid' => $row->guid, ], - 'results' => function() use ($row) { + 'results' => function () use ($row) { if (isset($this->rows[$row->guid])) { $row->enabled = 'no'; $this->rows[$row->guid] = $row; $this->addQuerySpecs($row); + return [$row->guid]; } + return []; }, 'times' => 1, @@ -404,13 +453,15 @@ public function addUpdateQuerySpecs(stdClass $row) { 'params' => [ ':guid' => $row->guid, ], - 'results' => function() use ($row) { + 'results' => function () use ($row) { if (isset($this->rows[$row->guid])) { $row->enabled = 'yes'; $this->rows[$row->guid] = $row; $this->addQuerySpecs($row); + return [$row->guid]; } + return []; }, 'times' => 1, @@ -431,13 +482,15 @@ public function addUpdateQuerySpecs(stdClass $row) { ':last_action' => $time, ':guid' => $row->guid, ], - 'results' => function() use ($row, $time) { + 'results' => function () use ($row, $time) { if (isset($this->rows[$row->guid])) { $row->last_action = $time; $this->rows[$row->guid] = $row; $this->addQuerySpecs($row); + return [$row->guid]; } + return []; }, ]); @@ -447,6 +500,7 @@ public function addUpdateQuerySpecs(stdClass $row) { * Query specs for DELETE operations * * @param stdClass $row Data row + * * @return void */ public function addDeleteQuerySpecs(\stdClass $row) { @@ -465,12 +519,14 @@ public function addDeleteQuerySpecs(\stdClass $row) { ], // We are using results instead of 'row_count' to give an accurate // count of deleted rows - 'results' => function() use ($row) { + 'results' => function () use ($row) { if (isset($this->rows[$row->guid])) { // Query spec will be cleared after row is deleted from objects table unset($this->rows[$row->guid]); + return [$row->guid]; } + return []; }, 'times' => 1, diff --git a/engine/tests/classes/Elgg/Mocks/Database/MetadataTable.php b/engine/tests/classes/Elgg/Mocks/Database/MetadataTable.php index 10b382513c2..40b2362d7b0 100644 --- a/engine/tests/classes/Elgg/Mocks/Database/MetadataTable.php +++ b/engine/tests/classes/Elgg/Mocks/Database/MetadataTable.php @@ -3,10 +3,13 @@ namespace Elgg\Mocks\Database; use Elgg\Database\Clauses\MetadataWhereClause; +use Elgg\Database\Delete; +use Elgg\Database\Insert; use Elgg\Database\MetadataTable as DbMetadataTabe; use Elgg\Database\Select; +use Elgg\Database\Update; +use ElggEntity; use ElggMetadata; -use stdClass; /** * @group ElggMetadata @@ -14,7 +17,7 @@ class MetadataTable extends DbMetadataTabe { /** - * @var stdClass[] + * @var \stdClass[] */ public $rows = []; @@ -32,53 +35,46 @@ class MetadataTable extends DbMetadataTabe { /** * {@inheritdoc} */ - public function create($entity_guid, $name, $value, $value_type = '', $ignore = null, $allow_multiple = false) { - $entity = get_entity((int) $entity_guid); - if (!$entity) { - return false; - } - - if (!isset($value)) { - return false; - } - + public function create(ElggMetadata $metadata, $allow_multiple = false) { $this->iterator++; $id = $this->iterator; $row = (object) [ 'type' => 'metadata', 'id' => $id, - 'entity_guid' => $entity->guid, - 'name' => $name, - 'value' => $value, + 'entity_guid' => $metadata->entity_guid, + 'name' => $metadata->name, + 'value' => $metadata->value, + 'value_type' => $metadata->value_type, 'time_created' => $this->getCurrentTime()->getTimestamp(), - 'value_type' => \ElggExtender::detectValueType($value, trim($value_type)), ]; $this->rows[$id] = $row; $this->addQuerySpecs($row); - return parent::create($entity_guid, $name, $value, $value_type, null, $allow_multiple); + return parent::create($metadata, $allow_multiple); } /** * {@inheritdoc} */ - public function update($id, $name, $value, $value_type) { + public function update(ElggMetadata $metadata) { + $id = $metadata->id; if (!isset($this->rows[$id])) { return false; } + $row = $this->rows[$id]; - $row->name = $name; - $row->value = $value; - $row->value_type = \ElggExtender::detectValueType($value, trim($value_type)); + $row->name = $metadata->name; + $row->value = $metadata->value; + $row->value_type = $metadata->value_type; $this->rows[$id] = $row; $this->addQuerySpecs($row); - return parent::update($id, $name, $value, $value_type); + return parent::update($metadata); } /** @@ -100,14 +96,16 @@ public function getAll(array $options = array()) { /** * {@inheritdoc} */ - public function delete($id) { - if (!isset($this->rows[$id])) { + public function delete(ElggMetadata $metadata) { + parent::delete($metadata); + + if (!isset($this->rows[$metadata->id])) { return false; } - $row = $this->rows[$id]; + $row = $this->rows[$metadata->id]; $this->clearQuerySpecs($row); - unset($this->rows[$id]); + unset($this->rows[$metadata->id]); return true; } @@ -115,10 +113,10 @@ public function delete($id) { /** * Clear query specs * - * @param stdClass $row Data row + * @param \stdClass $row Data row * @return void */ - public function clearQuerySpecs(stdClass $row) { + public function clearQuerySpecs(\stdClass $row) { if (!isset($this->query_specs[$row->id])) { return; } @@ -130,10 +128,11 @@ public function clearQuerySpecs(stdClass $row) { /** * Add query query_specs for a metadata object * - * @param stdClass $row Data row + * @param \stdClass $row Data row + * * @return void */ - public function addQuerySpecs(stdClass $row) { + public function addQuerySpecs(\stdClass $row) { $this->clearQuerySpecs($row); @@ -155,37 +154,30 @@ public function addQuerySpecs(stdClass $row) { }, ]); - $dbprefix = elgg_get_config('dbprefix'); - $sql = "INSERT INTO {$dbprefix}metadata - (entity_guid, name, value, value_type, time_created) - VALUES (:entity_guid, :name, :value, :value_type, :time_created)"; + $qb = Insert::intoTable('metadata'); + $qb->values([ + 'name' => $qb->param($row->name, ELGG_VALUE_STRING), + 'entity_guid' => $qb->param($row->entity_guid, ELGG_VALUE_INTEGER), + 'value' => $qb->param($row->value, $row->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING), + 'value_type' => $qb->param($row->value_type, ELGG_VALUE_STRING), + 'time_created' => $qb->param($row->time_created, ELGG_VALUE_INTEGER), + ]); $this->query_specs[$row->id][] = $this->db->addQuerySpec([ - 'sql' => $sql, - 'params' => [ - ':entity_guid' => $row->entity_guid, - ':name' => $row->name, - ':value' => $row->value, - ':value_type' => $row->value_type, - ':time_created' => (int) $row->time_created, - ], + 'sql' => $qb->getSQL(), + 'params' => $qb->getParameters(), 'insert_id' => $row->id, ]); - $sql = "UPDATE {$dbprefix}metadata - SET name = :name, - value = :value, - value_type = :value_type - WHERE id = :id"; + $qb = Update::table('metadata'); + $qb->set('name', $qb->param($row->name, ELGG_VALUE_STRING)) + ->set('value', $qb->param($row->value, $row->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING)) + ->set('value_type', $qb->param($row->value_type, ELGG_VALUE_STRING)) + ->where($qb->compare('id', '=', $row->id, ELGG_VALUE_INTEGER)); $this->query_specs[$row->id][] = $this->db->addQuerySpec([ - 'sql' => $sql, - 'params' => [ - ':name' => $row->name, - ':value' => $row->value, - ':value_type' => $row->value_type, - ':id' => $row->id, - ], + 'sql' => $qb->getSQL(), + 'params' => $qb->getParameters(), 'results' => function() use ($row) { if (isset($this->rows[$row->id])) { return [$row->id]; @@ -194,14 +186,12 @@ public function addQuerySpecs(stdClass $row) { }, ]); - // Delete - $sql = "DELETE FROM {$dbprefix}metadata WHERE id = :id"; + $qb = Delete::fromTable('metadata'); + $qb->where($qb->compare('id', '=', $row->id, ELGG_VALUE_INTEGER)); $this->query_specs[$row->id][] = $this->db->addQuerySpec([ - 'sql' => $sql, - 'params' => [ - ':id' => $row->id, - ], + 'sql' => $qb->getSQL(), + 'params' => $qb->getParameters(), 'results' => function() use ($row) { if (isset($this->rows[$row->id])) { unset($this->rows[$row->id]); diff --git a/engine/tests/classes/Elgg/Mocks/Di/MockServiceProvider.php b/engine/tests/classes/Elgg/Mocks/Di/MockServiceProvider.php index b8ef07cf21a..3108e28c26d 100644 --- a/engine/tests/classes/Elgg/Mocks/Di/MockServiceProvider.php +++ b/engine/tests/classes/Elgg/Mocks/Di/MockServiceProvider.php @@ -5,13 +5,13 @@ /** * Mocking service * - * @property-read \Elgg\Mocks\Database $db Database - * @property-read \Elgg\Mocks\Database\EntityTable $entityTable Entity mocks - * @property-read \Elgg\Mocks\Database\MetadataTable $metadataTable Metadata mocks - * @property-read \Elgg\Mocks\Database\AnnotationsTable $annotations Annotation mocks - * @property-read \Elgg\Mocks\Database\RelationshipsTable $relationshipsTable Annotation mocks - * @property-read \Elgg\Mocks\Database\AccessCollections $accessCollections ACL table mock - * @property-read \Elgg\Mocks\Database\PrivateSettingsTable $privateSettings Private settings table mock + * @property-read \Elgg\Mocks\Database $db Database + * @property-read \Elgg\Mocks\Database\EntityTable $entityTable Entity mocks + * @property-read \Elgg\Mocks\Database\MetadataTable $metadataTable Metadata mocks + * @property-read \Elgg\Mocks\Database\AnnotationsTable $annotationsTable Annotation mocks + * @property-read \Elgg\Mocks\Database\RelationshipsTable $relationshipsTable Annotation mocks + * @property-read \Elgg\Mocks\Database\AccessCollections $accessCollections ACL table mock + * @property-read \Elgg\Mocks\Database\PrivateSettingsTable $privateSettings Private settings table mock * * @since 2.3 */ @@ -26,7 +26,7 @@ public function __construct(\Elgg\Config $config) { parent::__construct($config); - $this->setFactory('session', function(MockServiceProvider $sp) { + $this->setFactory('session', function (MockServiceProvider $sp) { return \ElggSession::getMock(); }); @@ -52,11 +52,11 @@ public function __construct(\Elgg\Config $config) { }); $this->setFactory('metadataTable', function (MockServiceProvider $sp) { - return new \Elgg\Mocks\Database\MetadataTable($sp->metadataCache, $sp->db, $sp->entityTable, $sp->hooks->getEvents(), $sp->session); + return new \Elgg\Mocks\Database\MetadataTable($sp->metadataCache, $sp->db, $sp->hooks->getEvents()); }); - $this->setFactory('annotations', function (MockServiceProvider $sp) { - return new \Elgg\Mocks\Database\AnnotationsTable($sp->db, $sp->session, $sp->hooks->getEvents()); + $this->setFactory('annotationsTable', function (MockServiceProvider $sp) { + return new \Elgg\Mocks\Database\AnnotationsTable($sp->db, $sp->hooks->getEvents()); }); $this->setFactory('relationshipsTable', function (MockServiceProvider $sp) { @@ -84,11 +84,11 @@ public function __construct(\Elgg\Config $config) { ); }); - $this->setFactory('configTable', function(MockServiceProvider $sp) { + $this->setFactory('configTable', function (MockServiceProvider $sp) { return new \Elgg\Mocks\Database\ConfigTable($sp->db, $sp->boot, $sp->logger); }); - - $this->setFactory('mailer', function(MockServiceProvider $sp) { + + $this->setFactory('mailer', function (MockServiceProvider $sp) { return new \Zend\Mail\Transport\InMemory(); }); @@ -98,7 +98,7 @@ public function __construct(\Elgg\Config $config) { return new \Elgg\Database\TestingPlugins($pool, $sp->pluginSettingsCache); }); - $this->setFactory('siteSecret', function(MockServiceProvider $sp) { + $this->setFactory('siteSecret', function (MockServiceProvider $sp) { return new \Elgg\Database\SiteSecret('z1234567890123456789012345678901'); }); diff --git a/engine/tests/phpunit/ElggMetadataUnitTest.php b/engine/tests/phpunit/ElggMetadataUnitTest.php index 4ce0de7aecc..093027ca01d 100644 --- a/engine/tests/phpunit/ElggMetadataUnitTest.php +++ b/engine/tests/phpunit/ElggMetadataUnitTest.php @@ -29,7 +29,12 @@ public function testExtenderConstructor() { 'owner_guid' => $owner->guid, ]); - $id = _elgg_services()->metadataTable->create($object->guid, 'foo', 'bar', ''); + $metadata = new ElggMetadata(); + $metadata->entity_guid = $object->guid; + $metadata->name = 'test_metadata_' . rand(); + $metadata->value = 'test_value_' . rand(); + $id = $metadata->save(); + $metadata = elgg_get_metadata_from_id($id); $this->assertInstanceOf(\ElggMetadata::class, $metadata); @@ -57,7 +62,12 @@ public function testCanSetMetadataUrl() { 'owner_guid' => $owner->guid, ]); - $id = _elgg_services()->metadataTable->create($object->guid, 'foo', 'bar', ''); + $metadata = new ElggMetadata(); + $metadata->entity_guid = $object->guid; + $metadata->name = 'test_metadata_' . rand(); + $metadata->value = 'test_value_' . rand(); + $id = $metadata->save(); + $metadata = elgg_get_metadata_from_id($id); $this->assertInstanceOf(\ElggMetadata::class, $metadata); @@ -85,7 +95,12 @@ public function testCanEditMetadata() { 'owner_guid' => $owner->guid, ]); - $id = _elgg_services()->metadataTable->create($object->guid, 'foo', 'bar', ''); + $metadata = new ElggMetadata(); + $metadata->entity_guid = $object->guid; + $metadata->name = 'test_metadata_' . rand(); + $metadata->value = 'test_value_' . rand(); + $id = $metadata->save(); + $metadata = elgg_get_metadata_from_id($id); $this->assertInstanceOf(\ElggMetadata::class, $metadata); @@ -143,7 +158,12 @@ public function testCanDeleteMetadata() { 'owner_guid' => $owner->guid, ]); - $id = _elgg_services()->metadataTable->create($object->guid, 'foo', 'bar', ''); + $metadata = new ElggMetadata(); + $metadata->entity_guid = $object->guid; + $metadata->name = 'test_metadata_' . rand(); + $metadata->value = 'test_value_' . rand(); + $id = $metadata->save(); + $metadata = elgg_get_metadata_from_id($id); $this->assertInstanceOf(\ElggMetadata::class, $metadata); diff --git a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreAccessSQLTest.php b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreAccessSQLTest.php deleted file mode 100644 index aaf2e4c3d04..00000000000 --- a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreAccessSQLTest.php +++ /dev/null @@ -1,308 +0,0 @@ -user = $this->createOne('user'); - _elgg_services()->session->setLoggedInUser($this->user); - _elgg_services()->hooks->backup(); - } - - public function down() { - _elgg_services()->session->removeLoggedInUser(); - if ($this->user) { - $this->user->delete(); - } - _elgg_services()->hooks->restore(); - } - - public function testCanBuildAccessSqlClausesWithIgnoredAccess() { - $sql = _elgg_get_access_where_sql([ - 'ignore_access' => true, - ]); - $ans = "((1 = 1) AND (e.enabled = 'yes'))"; - $this->assertSqlEqual($ans, $sql, "$sql does not match $ans"); - } - - public function testCanBuildAccessSqlClausesWithIgnoredAccessWithoutDisabledEntities() { - $sql = _elgg_get_access_where_sql([ - 'use_enabled_clause' => false, - 'ignore_access' => true, - ]); - $ans = "((1 = 1))"; - $this->assertSqlEqual($ans, $sql, "$sql does not match $ans"); - } - - public function testCanBuildAccessSqlForLoggedInUser() { - - $this->assertFalse(elgg_is_admin_logged_in()); - - $sql = _elgg_get_access_where_sql(); - - $friends_clause = $this->getFriendsClause($this->user->guid, 'e'); - $owner_clause = $this->getOwnerClause($this->user->guid, 'e'); - $access_clause = $this->getLoggedInAccessListClause('e'); - - $ans = "(($friends_clause OR $owner_clause OR $access_clause) AND (e.enabled = 'yes'))"; - - $this->assertSqlEqual($ans, $sql, "$sql does not match $ans"); - } - - public function testCanBuildAccessSqlWithCustomTableAlias() { - $sql = _elgg_get_access_where_sql([ - 'table_alias' => 'foo', - ]); - - $friends_clause = $this->getFriendsClause($this->user->guid, 'foo'); - $owner_clause = $this->getOwnerClause($this->user->guid, 'foo'); - $access_clause = $this->getLoggedInAccessListClause('foo'); - $ans = "(($friends_clause OR $owner_clause OR $access_clause) AND (foo.enabled = 'yes'))"; - - $this->assertSqlEqual($ans, $sql, "$sql does not match $ans"); - - // test with no alias - $sql = _elgg_get_access_where_sql([ - 'user_guid' => $this->user->guid, - 'table_alias' => '', - ]); - - $friends_clause = $this->getFriendsClause($this->user->guid, ''); - $owner_clause = $this->getOwnerClause($this->user->guid, ''); - $access_clause = $this->getLoggedInAccessListClause(''); - $ans = "(($friends_clause OR $owner_clause OR $access_clause) AND (enabled = 'yes'))"; - - $this->assertSqlEqual($ans, $sql, "$sql does not match $ans"); - } - - public function testCanBuildAccessSqlWithCustomGuidColumn() { - $sql = _elgg_get_access_where_sql([ - 'owner_guid_column' => 'unit_test', - ]); - - $friends_clause = $this->getFriendsClause($this->user->guid, 'e', 'unit_test'); - $owner_clause = $this->getOwnerClause($this->user->guid, 'e', 'unit_test'); - $access_clause = $this->getLoggedInAccessListClause('e'); - $ans = "(($friends_clause OR $owner_clause OR $access_clause) AND (e.enabled = 'yes'))"; - - $this->assertSqlEqual($ans, $sql, "$sql does not match $ans"); - } - - public function testCanBuildAccessSqlForLoggedOutUser() { - - $user = _elgg_services()->session->getLoggedInUser(); - _elgg_services()->session->removeLoggedInUser(); - - $sql = _elgg_get_access_where_sql(); - $access_clause = $this->getLoggedOutAccessListClause('e'); - $ans = "(($access_clause) AND (e.enabled = 'yes'))"; - - $this->assertSqlEqual($ans, $sql, "$sql does not match $ans"); - - _elgg_services()->session->setLoggedInUser($user); - } - - public function testAccessPluginHookRemoveEnabled() { - elgg_register_plugin_hook_handler('get_sql', 'access', [ - $this, - 'removeEnabledCallback' - ]); - $sql = _elgg_get_access_where_sql([ - 'ignore_access' => true, - ]); - $ans = "((1 = 1))"; - $this->assertSqlEqual($ans, $sql, "$sql does not match $ans"); - } - - public function removeEnabledCallback($hook, $type, $clauses, $params) { - $clauses['ands'] = []; - - return $clauses; - } - - public function testAccessPluginHookRemoveOrs() { - elgg_register_plugin_hook_handler('get_sql', 'access', [ - $this, - 'removeOrsCallback' - ]); - $sql = _elgg_get_access_where_sql([ - 'ignore_access' => true, - ]); - $ans = "((e.enabled = 'yes'))"; - $this->assertSqlEqual($ans, $sql, "$sql does not match $ans"); - } - - public function removeOrsCallback($hook, $type, $clauses, $params) { - $clauses['ors'] = []; - - return $clauses; - } - - public function testAccessPluginHookAddOr() { - elgg_register_plugin_hook_handler('get_sql', 'access', [ - $this, - 'addOrCallback' - ]); - $sql = _elgg_get_access_where_sql([ - 'ignore_access' => true, - ]); - $ans = "((1 = 1 OR 57 > 32) AND (e.enabled = 'yes'))"; - $this->assertSqlEqual($ans, $sql, "$sql does not match $ans"); - } - - public function addOrCallback($hook, $type, $clauses, $params) { - $clauses['ors'][] = '57 > 32'; - - return $clauses; - } - - public function testAccessPluginHookAddAnd() { - elgg_register_plugin_hook_handler('get_sql', 'access', [ - $this, - 'addAndCallback' - ]); - $sql = _elgg_get_access_where_sql([ - 'ignore_access' => true, - ]); - $ans = "((1 = 1) AND (e.enabled = 'yes' AND 57 > 32))"; - $this->assertSqlEqual($ans, $sql, "$sql does not match $ans"); - } - - public function testHasAccessToEntity() { - - $session = elgg_get_session(); - - $viewer = $session->getLoggedInUser(); - - $ia = elgg_set_ignore_access(true); - - $owner = $this->createOne('user'); - - $object = $this->createOne('object', [ - 'owner_guid' => $owner->guid, - 'access_id' => ACCESS_PRIVATE, - ]); - - elgg_set_ignore_access($ia); - - $session->removeLoggedInUser(); - - $this->assertFalse(has_access_to_entity($object)); - $this->assertFalse(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - - $ia = elgg_set_ignore_access(true); - $object->access_id = ACCESS_PUBLIC; - $object->save(); - elgg_set_ignore_access($ia); - - $this->assertTrue(has_access_to_entity($object)); - $this->assertTrue(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - - $ia = elgg_set_ignore_access(true); - $object->access_id = ACCESS_LOGGED_IN; - $object->save(); - elgg_set_ignore_access($ia); - - $this->assertFalse(has_access_to_entity($object)); - // even though user is logged out, existing users are presumed to have access to an entity - $this->assertTrue(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - - $session->setLoggedInUser($viewer); - $this->assertTrue(has_access_to_entity($object)); - $this->assertTrue(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - $session->removeLoggedInUser(); - - $ia = elgg_set_ignore_access(true); - $owner->addFriend($viewer->guid); - $object->access_id = ACCESS_FRIENDS; - $object->save(); - elgg_set_ignore_access($ia); - - $this->assertFalse(has_access_to_entity($object)); - $this->assertTrue(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - - $session->setLoggedInUser($viewer); - $this->assertTrue(has_access_to_entity($object)); - $this->assertTrue(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - - $ia = elgg_set_ignore_access(true); - $owner->delete(); - $object->delete(); - elgg_set_ignore_access($ia); - - $session->setLoggedInUser($viewer); - } - - public function addAndCallback($hook, $type, $clauses, $params) { - $clauses['ands'][] = '57 > 32'; - - return $clauses; - } - - protected function assertSqlEqual($sql1, $sql2, $message = '') { - $sql1 = $this->normalizeSql($sql1); - $sql2 = $this->normalizeSql($sql2); - - return $this->assertEquals($sql1, $sql2, $message); - } - - protected function getFriendsClause($user_guid, $table_alias, $owner_guid = 'owner_guid') { - $CONFIG = _elgg_config(); - $table_alias = $table_alias ? $table_alias . '.' : ''; - - return "{$table_alias}access_id = " . ACCESS_FRIENDS . " - AND {$table_alias}{$owner_guid} IN ( - SELECT guid_one FROM {$CONFIG->dbprefix}entity_relationships - WHERE relationship = 'friend' AND guid_two = $user_guid - )"; - } - - protected function getOwnerClause($user_guid, $table_alias, $owner_guid = 'owner_guid') { - $table_alias = $table_alias ? $table_alias . '.' : ''; - - return "{$table_alias}{$owner_guid} = $user_guid"; - } - - protected function getLoggedInAccessListClause($table_alias) { - $table_alias = $table_alias ? $table_alias . '.' : ''; - - return "{$table_alias}access_id IN (2,1)"; - } - - protected function getLoggedOutAccessListClause($table_alias) { - $table_alias = $table_alias ? $table_alias . '.' : ''; - - return "{$table_alias}access_id IN (2)"; - } - - /** - * Attempt to normalize whitespace in a query - * - * @param string $query Query - * @return string - */ - private function normalizeSql($query) { - $query = trim($query); - $query = str_replace(' ', ' ', $query); - $query = strtolower($query); - $query = preg_replace('~\\s+~', ' ', $query); - return $query; - } -} diff --git a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreAnnotationAPITest.php b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreAnnotationAPITest.php index bbf2b65a71f..434c77e77e1 100644 --- a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreAnnotationAPITest.php +++ b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreAnnotationAPITest.php @@ -132,7 +132,9 @@ public function testElggAnnotationExists() { $this->assertFalse(elgg_annotation_exists($guid, 'test_annotation')); - $e->annotate('test_annotation', rand(0, 10000)); + $id = $e->annotate('test_annotation', rand(0, 10000)); + $this->assertNotFalse($id); + $this->assertTrue(elgg_annotation_exists($guid, 'test_annotation')); // this metastring should always exist but an annotation of this name should not $this->assertFalse(elgg_annotation_exists($guid, 'email')); diff --git a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetadataAPITest.php b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetadataAPITest.php index 57a4b8c8dc9..0c3ae15e6b0 100644 --- a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetadataAPITest.php +++ b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetadataAPITest.php @@ -35,7 +35,7 @@ public function testElggGetEntitiesFromMetadata() { $this->object->save(); // create_metadata returns id of metadata on success - $this->assertNotEqual(false, _elgg_services()->metadataTable->create($this->object->guid, 'metaUnitTest', 'tested')); + $this->object->setMetadata('metaUnitTest', 'tested'); // check value with improper case $options = [ @@ -151,6 +151,48 @@ public function caseSensitivePairsProvider() { ]; } + /** + * @group Current + * @dataProvider booleanPairsProvider + */ + public function testElggGetEntitiesFromBooleanMetadata($value, $query, $type) { + + $this->object->subtype = $this->getRandomSubtype(); + $this->object->metadata = $value; + $this->object->save(); + + $options = [ + 'type' => 'object', + 'subtype' => $this->object->subtype, + 'metadata_name_value_pairs' => [ + 'name' => 'metadata', + 'value' => $query, + 'operand' => '=', + 'type' => $type, + ], + 'count' => true, + ]; + + $result = elgg_get_entities($options); + + $this->assertEquals(1, $result); + + $this->object->delete(); + } + + public function booleanPairsProvider() { + return [ + [true, true, null], + [true, 1, null], + [true, '1', ELGG_VALUE_INTEGER], + [false, false, null], + [false, 0, null], + [false, '0', ELGG_VALUE_INTEGER], + [1, true, null], + [0, false, null], + ]; + } + public function testElggGetMetadataCount() { $this->object->title = 'Meta Unit Test'; $this->object->save(); diff --git a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetadataCacheTest.php b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetadataCacheTest.php index 897860dc930..cc658a9bcea 100644 --- a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetadataCacheTest.php +++ b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetadataCacheTest.php @@ -139,7 +139,11 @@ public function testWritesInvalidate() { // create_metadata $this->cache->inject($this->guid1, ['foo' => 'bar']); - _elgg_services()->metadataTable->create($this->guid1, 'foo', 'bar', 'text'); + $metadata = new \ElggMetadata(); + $metadata->entity_guid = $this->guid1; + $metadata->name = 'foo'; + $metadata->value = 'bar'; + $metadata->save(); $this->assertFalse($this->cache->isLoaded($this->guid1)); } diff --git a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetastringsTest.php b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetastringsTest.php index 61ee07ebde9..e31cf49c726 100644 --- a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetastringsTest.php +++ b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreMetastringsTest.php @@ -51,10 +51,13 @@ public function createAnnotations($max = 1) { public function createMetadata($max = 1) { $metadata = []; for ($i = 0; $i < $max; $i++) { - $name = 'test_metadata_name' . rand(); - $value = 'test_metadata_value' . rand(); - $id = _elgg_services()->metadataTable->create($this->object->guid, $name, $value); - $metadata[] = $id; + $name = 'test_metadata_name' . $i . rand(); + $value = 'test_metadata_value' . $i . rand(); + $md = new \ElggMetadata(); + $md->entity_guid = $this->object->guid; + $md->name = $name; + $md->value = $value; + $metadata[] = $md->save(); } return $metadata; diff --git a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreObjectTest.php b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreObjectTest.php index 96ca99ffe51..0eb8408cf3f 100644 --- a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreObjectTest.php +++ b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreObjectTest.php @@ -7,6 +7,7 @@ * * @group IntegrationTests * @group ElggObject + * @group Tags */ class ElggCoreObjectTest extends \Elgg\LegacyIntegrationTestCase { @@ -247,6 +248,63 @@ public function testElggRecursiveDelete() { $this->assertFalse($r); } + public function testCanGetTags() { + + $subtype = $this->getRandomSubtype(); + $objects = $this->createMany('object', 3, [ + 'subtype' => $subtype, + ]); + + elgg_register_tag_metadata_name('foo1'); + elgg_register_tag_metadata_name('foo2'); + elgg_register_tag_metadata_name('foo3'); + + $objects[0]->foo1 = 'one'; + $objects[0]->foo2 = 'two'; + $objects[0]->foo4 = 'four'; + + $objects[1]->foo1 = 'one'; + $objects[1]->foo2 = 'two'; + $objects[1]->foo3 = 'three'; + $objects[1]->foo4 = 'four'; + + $objects[2]->foo1 = 'one'; + $objects[2]->foo2 = ''; + $objects[2]->foo3 = ''; + $objects[2]->foo4 = 'four'; + + $expected = [ + (object) [ + 'tag' => 'one', + 'total' => 3, + ], + (object) [ + 'tag' => 'two', + 'total' => 2, + ], + (object) [ + 'tag' => 'three', + 'total' => 1, + ], + ]; + + $actual = elgg_get_tags([ + 'types' => 'object', + 'subtypes' => $subtype, + 'tag_names' => ['foo1', 'foo2', 'foo3'], + ]); + + $this->assertEquals($expected, $actual); + + elgg_unregister_tag_metadata_name('foo1'); + elgg_unregister_tag_metadata_name('foo2'); + elgg_unregister_tag_metadata_name('foo3'); + + foreach ($objects as $object) { + $object->delete(); + } + } + protected function get_entity_row($guid) { $CONFIG = _elgg_config(); diff --git a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreRiverAPITest.php b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreRiverAPITest.php index d56b36d641a..86ab5f59c37 100644 --- a/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreRiverAPITest.php +++ b/engine/tests/phpunit/integration/Elgg/Integration/ElggCoreRiverAPITest.php @@ -9,6 +9,7 @@ * Elgg Test river api * * @group IntegrationTests + * @group River */ class ElggCoreRiverAPITest extends \Elgg\IntegrationTestCase { @@ -113,6 +114,7 @@ public function testRiverCreationEmitsHookAndEvent() { elgg_register_plugin_hook_handler('creating', 'river', $hook_handler); elgg_register_event_handler('created', 'river', $event_handler); $item = elgg_create_river_item($params); + $this->assertInstanceOf(ElggRiverItem::class, $item); elgg_unregister_plugin_hook_handler('creating', 'river', $hook_handler); elgg_unregister_event_handler('created', 'river', $event_handler); @@ -346,21 +348,6 @@ public function testElggCreateRiverItemBadEntity() { $this->assertFalse(elgg_create_river_item($bad_target)); } - public function testElggTypeSubtypeWhereSQL() { - $types = ['object']; - $subtypes = ['blog']; - $result = _elgg_get_river_type_subtype_where_sql('rv', $types, $subtypes, null); - $this->assertSame($result, "((rv.type = 'object') AND ((rv.subtype = 'blog')))"); - - $types = ['object']; - $subtypes = [ - 'blog', - 'file' - ]; - $result = _elgg_get_river_type_subtype_where_sql('rv', $types, $subtypes, null); - $this->assertSame($result, "((rv.type = 'object') AND ((rv.subtype = 'blog') OR (rv.subtype = 'file')))"); - } - public function testElggRiverDisableEnable() { $this->assertTrue(_elgg_services()->hooks->getEvents()->hasHandler('disable:after', 'all', '_elgg_river_disable')); diff --git a/engine/tests/phpunit/integration/Elgg/Integration/ElggEntityMetadataTest.php b/engine/tests/phpunit/integration/Elgg/Integration/ElggEntityMetadataTest.php index 675e89af8be..114b9693861 100644 --- a/engine/tests/phpunit/integration/Elgg/Integration/ElggEntityMetadataTest.php +++ b/engine/tests/phpunit/integration/Elgg/Integration/ElggEntityMetadataTest.php @@ -1,6 +1,8 @@ qb = Select::fromTable('river', 'alias'); + } + + public function down() { + + } + + public function testBuildEmptyQuery() { + + $expected = null; + + $query = new RiverWhereClause(); + $query->use_enabled_clause = false; + + $qb = Select::fromTable('river', 'alias'); + $actual = $query->prepare($qb, 'alias'); + + $this->assertEquals($expected, $actual); + $this->assertEquals($this->qb->getParameters(), $qb->getParameters()); + } + + public function testBuildQueryFromId() { + + $parts = []; + $parts[] = $this->qb->expr()->eq('alias.id', ':qb1'); + $this->qb->param(1, ELGG_VALUE_INTEGER); + $expected = $this->qb->merge($parts); + + $query = new RiverWhereClause(); + $query->use_enabled_clause = false; + $query->ids = 1; + + $qb = Select::fromTable('river', 'alias'); + $actual = $query->prepare($qb, 'alias'); + + $this->assertEquals($expected, $actual); + $this->assertEquals($this->qb->getParameters(), $qb->getParameters()); + } + + public function testBuildQueryFromAnnotationId() { + + $parts = []; + $parts[] = $this->qb->expr()->eq('alias.annotation_id', ':qb1'); + $this->qb->param(1, ELGG_VALUE_INTEGER); + $expected = $this->qb->merge($parts); + + $query = new RiverWhereClause(); + $query->use_enabled_clause = false; + $query->annotation_ids = 1; + + $qb = Select::fromTable('river', 'alias'); + $actual = $query->prepare($qb, 'alias'); + + $this->assertEquals($expected, $actual); + $this->assertEquals($this->qb->getParameters(), $qb->getParameters()); + } + + public function testBuildQueryFromView() { + + $parts = []; + $parts[] = $this->qb->expr()->in('alias.view', ':qb1'); + $this->qb->param(['view1', 'dir/view2'], ELGG_VALUE_STRING); + $expected = $this->qb->merge($parts); + + $query = new RiverWhereClause(); + $query->use_enabled_clause = false; + $query->views = ['view1', 'dir/view2']; + + $qb = Select::fromTable('river', 'alias'); + $actual = $query->prepare($qb, 'alias'); + + $this->assertEquals($expected, $actual); + $this->assertEquals($this->qb->getParameters(), $qb->getParameters()); + } + + public function testBuildQueryFromActionType() { + + $parts = []; + $parts[] = $this->qb->expr()->in('alias.action_type', ':qb1'); + $this->qb->param(['foo1', 'foo2'], ELGG_VALUE_STRING); + $expected = $this->qb->merge($parts); + + $query = new RiverWhereClause(); + $query->use_enabled_clause = false; + $query->action_types = ['foo1', 'foo2']; + + $qb = Select::fromTable('river', 'alias'); + $actual = $query->prepare($qb, 'alias'); + + $this->assertEquals($expected, $actual); + $this->assertEquals($this->qb->getParameters(), $qb->getParameters()); + } + + public function testBuildQueryFromGuids() { + + $parts = []; + $parts[] = $this->qb->expr()->in('alias.subject_guid', ':qb1'); + $parts[] = $this->qb->expr()->in('alias.object_guid', ':qb2'); + $parts[] = $this->qb->expr()->in('alias.target_guid', ':qb3'); + $this->qb->param([1, 2, 3], ELGG_VALUE_INTEGER); + $this->qb->param([4, 5, 6], ELGG_VALUE_INTEGER); + $this->qb->param([7, 8, 9], ELGG_VALUE_INTEGER); + $expected = $this->qb->merge($parts); + + $query = new RiverWhereClause(); + $query->use_enabled_clause = false; + $query->subject_guids = [1, 2, 3]; + $query->object_guids = [4, 5, 6]; + $query->target_guids = [7, 8, 9]; + + $qb = Select::fromTable('river', 'alias'); + $actual = $query->prepare($qb, 'alias'); + + $this->assertEquals($expected, $actual); + $this->assertEquals($this->qb->getParameters(), $qb->getParameters()); + } + + public function testBuildQueryFromTimeCreated() { + + $after = (new \DateTime())->modify('-1 day'); + $before = (new \DateTime())->modify('+1 day'); + + $parts = []; + + $time_parts = []; + $time_parts[] = $this->qb->expr()->gte('alias.posted', ':qb1'); + $time_parts[] = $this->qb->expr()->lte('alias.posted', ':qb2'); + $this->qb->param($after->getTimestamp(), ELGG_VALUE_INTEGER); + $this->qb->param($before->getTimestamp(), ELGG_VALUE_INTEGER); + $parts[] = $this->qb->merge($time_parts); + + $expected = $this->qb->merge($parts); + + $query = new RiverWhereClause(); + $query->use_enabled_clause = false; + $query->created_after = $after; + $query->created_before = $before; + + $qb = Select::fromTable('river', 'alias'); + $actual = $query->prepare($qb, 'alias'); + + $this->assertEquals($expected, $actual); + $this->assertEquals($this->qb->getParameters(), $qb->getParameters()); + + } + + public function testBuildQueryFromEnabled() { + + $parts = []; + $parts[] = $this->qb->expr()->eq('alias.enabled', ':qb1'); + $this->qb->param('no', ELGG_VALUE_STRING); + $expected = $this->qb->merge($parts); + + $query = new RiverWhereClause(); + $query->enabled = 'no'; + + $qb = Select::fromTable('river', 'alias'); + $actual = $query->prepare($qb, 'alias'); + + $this->assertEquals($expected, $actual); + $this->assertEquals($this->qb->getParameters(), $qb->getParameters()); + } + + +} + diff --git a/engine/tests/phpunit/unit/Elgg/Database/RiverRepositoryTest.php b/engine/tests/phpunit/unit/Elgg/Database/RiverRepositoryTest.php new file mode 100644 index 00000000000..5afc3a8fc52 --- /dev/null +++ b/engine/tests/phpunit/unit/Elgg/Database/RiverRepositoryTest.php @@ -0,0 +1,600 @@ +ids = elgg_extract('ids', $options); + $where->views = elgg_extract('views', $options); + $where->action_types = elgg_extract('action_types', $options); + $where->subject_guids = elgg_extract('subject_guids', $options); + $where->object_guids = elgg_extract('object_guids', $options); + $where->target_guids = elgg_extract('target_guids', $options); + $where->type_subtype_pairs = elgg_extract('type_subtype_pairs', $options); + $where->created_after = elgg_extract('created_after', $options); + $where->created_before = elgg_extract('created_before', $options); + + $qb->addClause($where); + + $qb->joinEntitiesTable('rv', 'subject_guid', 'inner', 'se'); + $subject = new EntityWhereClause(); + $subject->guids = elgg_extract('subject_guids', $options); + $ands[] = $subject->prepare($qb, 'se'); + + $qb->joinEntitiesTable('rv', 'object_guid', 'inner', 'oe'); + $object = new EntityWhereClause(); + $object->guids = elgg_extract('object_guids', $options); + $ands[] = $object->prepare($qb, 'oe'); + + $target_ors = []; + $qb->joinEntitiesTable('rv', 'target_guid', 'left', 'te'); + $target = new EntityWhereClause(); + $target->guids = elgg_extract('target_guids', $options); + $target_ors[] = $target->prepare($qb, 'te'); + // Note the LEFT JOIN + $target_ors[] = $qb->compare('te.guid', 'IS NULL'); + $ands[] = $qb->merge($target_ors, 'OR'); + + $qb->andWhere($qb->merge($ands)); + + return $qb; + } + + public function testCanExecuteCount() { + $options = [ + 'count' => true, + 'subject_guids' => [1, 2, 3], + 'object_guids' => [4, 5, 6], + 'target_guids' => [7, 8, 9], + ]; + + $select = Select::fromTable('river', 'rv'); + $select->select('COUNT(DISTINCT rv.id) AS total'); + + $select = $this->buildQuery($select, $options); + + $spec = _elgg_services()->db->addQuerySpec([ + 'sql' => $select->getSQL(), + 'params' => $select->getParameters(), + 'results' => [ + (object) [ + 'total' => 10, + ] + ] + ]); + + $find = River::find($options); + $count = River::with($options)->count(); + + $this->assertEquals(10, $find); + $this->assertEquals(10, $count); + + _elgg_services()->db->removeQuerySpec($spec); + } + + public function testCanExecuteWithIgnoredAccess() { + $select = Select::fromTable('river', 'rv'); + $select->select('COUNT(DISTINCT rv.id) AS total'); + + $select->addClause(new RiverWhereClause()); + + $spec = _elgg_services()->db->addQuerySpec([ + 'sql' => $select->getSQL(), + 'params' => $select->getParameters(), + 'results' => [ + (object) [ + 'total' => 10, + ] + ] + ]); + + $options = [ + 'count' => true, + ]; + + $ia = elgg_set_ignore_access(); + $find = River::find($options); + $count = River::with($options)->count(); + elgg_set_ignore_access($ia); + + $this->assertEquals(10, $find); + $this->assertEquals(10, $count); + + _elgg_services()->db->removeQuerySpec($spec); + } + + public function testCanExecuteCountWithBadDataFormat() { + $options = [ + 'count' => true, + 'subject_guids' => 'abc', + ]; + + $find = River::find($options); + $this->assertEquals(0, $find); + } + + public function testCanExecuteGet() { + $options = [ + 'limit' => 5, + 'offset' => 5, + 'callback' => false, + 'order_by' => [ + new OrderByClause('rv.id', 'ASC'), + ] + ]; + + $select = Select::fromTable('river', 'rv'); + $select->select('DISTINCT rv.*'); + + $select = $this->buildQuery($select, $options); + + $select->setMaxResults(5); + $select->setFirstResult(5); + $select->addOrderBy('rv.id', 'asc'); + + $rows = $this->getRows(5); + + $spec = _elgg_services()->db->addQuerySpec([ + 'sql' => $select->getSQL(), + 'params' => $select->getParameters(), + 'results' => $rows, + ]); + + $find = River::find($options); + $get = River::with($options)->get(5, 5, false); + + $this->assertEquals($rows, $find); + $this->assertEquals($rows, $get); + + _elgg_services()->db->removeQuerySpec($spec); + } + + public function testCanExecuteGetWithClauses() { + $options = [ + 'limit' => 5, + 'offset' => 5, + 'callback' => false, + 'order_by' => [ + new OrderByClause('rv.id', 'ASC'), + ], + 'selects' => [ + 'max(rv.posted) AS newest', + ], + 'group_by' => [ + 'rv.posted', + ], + 'having' => [ + function (QueryBuilder $qb) { + return $qb->compare('rv.posted', 'IS NOT NULL'); + } + ], + 'joins' => [ + new JoinClause('annotations', 'n_table', 'rv.annotation_id = n_table.id'), + ], + 'wheres' => [ + function (QueryBuilder $qb) { + $alias = $qb->joinEntitiesTable('rv', 'object_guid', 'object'); + + return $qb->compare("$alias.access_id", 'IN', [1, 2, 3], ELGG_VALUE_INTEGER); + } + ] + ]; + + $select = Select::fromTable('river', 'rv'); + $select->select('DISTINCT rv.*'); + $select->addSelect('max(rv.posted) AS newest'); + $select->groupBy('rv.posted'); + $select->join('rv', 'annotations', 'n_table', 'rv.annotation_id = n_table.id'); + $alias = $select->joinEntitiesTable('rv', 'object_guid', 'object'); + $select->where($select->compare("$alias.access_id", 'IN', [1, 2, 3], ELGG_VALUE_INTEGER)); + $select->having($select->compare('rv.posted', 'IS NOT NULL')); + + $select = $this->buildQuery($select, $options); + + $select->setMaxResults(5); + $select->setFirstResult(5); + $select->addOrderBy('rv.id', 'asc'); + + $rows = $this->getRows(5); + + $spec = _elgg_services()->db->addQuerySpec([ + 'sql' => $select->getSQL(), + 'params' => $select->getParameters(), + 'results' => $rows, + ]); + + $find = River::find($options); + $get = River::with($options)->get(5, 5, false); + + $this->assertEquals($rows, $find); + $this->assertEquals($rows, $get); + + _elgg_services()->db->removeQuerySpec($spec); + } + + public function testCanExecuteGetWithBadDataFormat() { + $options = [ + 'limit' => 5, + 'offset' => 5, + 'callback' => false, + 'subject_guids' => 'abc', + ]; + + $find = River::find($options); + $this->assertEquals(false, $find); + } + + public function testCanExecuteBatchGet() { + $options = [ + 'limit' => 5, + 'offset' => 5, + 'callback' => false, + 'order_by' => [ + new OrderByClause('rv.id', 'ASC'), + ], + 'batch' => true, + ]; + + $select = Select::fromTable('river', 'rv'); + $select->select('DISTINCT rv.*'); + + $select = $this->buildQuery($select, $options); + + $select->setMaxResults(5); + $select->setFirstResult(5); + $select->addOrderBy('rv.id', 'asc'); + + $rows = $this->getRows(5); + + $spec = _elgg_services()->db->addQuerySpec([ + 'sql' => $select->getSQL(), + 'params' => $select->getParameters(), + 'results' => $rows, + ]); + + $find = River::find($options); + $batch = River::with($options)->batch(5, 5, false); + + $this->assertInstanceOf(\ElggBatch::class, $find); + $this->assertInstanceOf(\ElggBatch::class, $batch); + + foreach ($find as $i => $row) { + $this->assertEquals($rows[$i], $row); + } + + foreach ($batch as $i => $row) { + $this->assertEquals($rows[$i], $row); + } + + _elgg_services()->db->removeQuerySpec($spec); + } + + public function testCanExecuteAnnotationCalculation() { + + $annotation_names = ['foo']; + + $options = [ + 'annotation_calculation' => 'avg', + 'annotation_name_value_pairs' => [ + 'name' => $annotation_names, + 'value' => 10, + 'operand' => '>', + ] + ]; + + $select = Select::fromTable('river', 'rv'); + + $select->join('rv', 'annotations', 'n_table', "rv.annotation_id = n_table.id"); + $select->select("avg(n_table.value) AS calculation"); + + $select = $this->buildQuery($select, $options); + + $annotation = new AnnotationWhereClause(); + $annotation->names = $annotation_names; + $annotation->values = 10; + $annotation->comparison = '>'; + $annotation->value_type = ELGG_VALUE_INTEGER; + + $select->addClause($annotation, 'n_table'); + + $spec = _elgg_services()->db->addQuerySpec([ + 'sql' => $select->getSQL(), + 'params' => $select->getParameters(), + 'results' => [ + (object) [ + 'calculation' => 10, + ] + ] + ]); + + $find = River::find($options); + $calculate = River::with($options)->calculate('avg', $annotation_names, 'annotation'); + + $this->assertEquals(10, $find); + $this->assertEquals(10, $calculate); + + _elgg_services()->db->removeQuerySpec($spec); + } + + /** + * @expectedException \LogicException + */ + public function testThrowsOnAnnotationCalculationWithMultipleAndPairs() { + + $options = [ + 'annotation_calculation' => 'min', + 'annotation_name_value_pairs' => [ + [ + 'name' => 'status', + 'value' => 'draft', + ], + [ + 'name' => 'category', + 'value' => 'blogs', + ] + ] + ]; + + River::find($options); + } + + public function testCanExecuteQueryWithAnnotationNameValuePairs() { + + $options = [ + 'callback' => false, + 'annotation_name_value_pairs' => [ + 'foo1' => 'bar1', + 'foo2' => 'bar2', + ], + 'order_by' => [ + new OrderByClause('rv.id', 'asc'), + ], + 'limit' => 10, + ]; + + $select = Select::fromTable('river', 'rv'); + $select->select('DISTINCT rv.*'); + + $select = $this->buildQuery($select, $options); + + $alias1 = $select->getNextJoinAlias(); + $select->join('rv', 'annotations', $alias1, "$alias1.id = rv.annotation_id"); + $annotation = new AnnotationWhereClause(); + $annotation->names = ['foo1']; + $annotation->values = ['bar1']; + $wheres[] = $annotation->prepare($select, $alias1); + + $alias2 = $select->getNextJoinAlias(); + $select->join('rv', 'annotations', $alias2, "$alias2.id = rv.annotation_id"); + $annotation = new AnnotationWhereClause(); + $annotation->names = ['foo2']; + $annotation->values = ['bar2']; + $wheres[] = $annotation->prepare($select, $alias2); + + $select->andWhere($select->merge($wheres)); + + $select->setMaxResults(10); + $select->setFirstResult(0); + + $select->orderBy('rv.id', 'asc'); + + $rows = $this->getRows(5); + $spec = _elgg_services()->db->addQuerySpec([ + 'sql' => $select->getSQL(), + 'params' => $select->getParameters(), + 'results' => $rows, + ]); + + $find = River::find($options); + + $this->assertEquals($rows, $find); + + _elgg_services()->db->removeQuerySpec($spec); + } + + public function testCanExecuteQueryWithAnnotationNameValuePairsJoinedByOr() { + + $options = [ + 'callback' => false, + 'annotation_name_value_pairs' => [ + 'foo1' => 'bar1', + 'foo2' => 'bar2', + ], + 'annotation_name_value_pairs_operator' => 'OR', + 'order_by' => [ + new OrderByClause('rv.id', 'asc'), + ], + 'limit' => 10, + ]; + + $select = Select::fromTable('river', 'rv'); + $select->select('DISTINCT rv.*'); + + $select = $this->buildQuery($select, $options); + + $select->join('rv', 'annotations', 'n_table', "n_table.id = rv.annotation_id"); + + $annotation = new AnnotationWhereClause(); + $annotation->names = ['foo1']; + $annotation->values = ['bar1']; + $wheres[] = $annotation->prepare($select, 'n_table'); + + $annotation = new AnnotationWhereClause(); + $annotation->names = ['foo2']; + $annotation->values = ['bar2']; + $wheres[] = $annotation->prepare($select, 'n_table'); + + $select->andWhere($select->merge($wheres, 'OR')); + + $select->setMaxResults(10); + $select->setFirstResult(0); + + $select->orderBy('rv.id', 'asc'); + + $rows = $this->getRows(5); + $spec = _elgg_services()->db->addQuerySpec([ + 'sql' => $select->getSQL(), + 'params' => $select->getParameters(), + 'results' => $rows, + ]); + + $find = River::find($options); + + $this->assertEquals($rows, $find); + + _elgg_services()->db->removeQuerySpec($spec); + } + + /** + * @group RepositoryPairs + */ + public function testCanExecuteQueryWithRelationshipPairs() { + + $options = [ + 'callback' => false, + 'relationship_pairs' => [ + [ + 'relationship' => 'foo1', + 'relationship_guid' => [1, 2, 3], + ], + [ + 'relationship' => 'foo2', + 'relationship_guid' => [4, 5, 6], + 'inverse_relationship' => true, + ] + ], + 'order_by' => [ + new OrderByClause('rv.id', 'asc'), + ], + 'limit' => 10, + ]; + + $select = Select::fromTable('river', 'rv'); + $select->select('DISTINCT rv.*'); + + $select = $this->buildQuery($select, $options); + + $alias1 = $select->joinRelationshipTable('rv', 'subject_guid', ['foo1']); + $private_setting = new RelationshipWhereClause(); + $private_setting->names = ['foo1']; + $private_setting->subject_guids = [1, 2, 3]; + $wheres[] = $private_setting->prepare($select, $alias1); + + $alias2 = $select->joinRelationshipTable('rv', 'subject_guid', ['foo2'], true); + $private_setting = new RelationshipWhereClause(); + $private_setting->names = ['foo2']; + $private_setting->object_guids = [4, 5, 6]; + $wheres[] = $private_setting->prepare($select, $alias2); + + $select->andWhere($select->expr()->andX()->addMultiple($wheres)); + + $select->setMaxResults(10); + $select->setFirstResult(0); + + $select->orderBy('rv.id', 'asc'); + + $rows = $this->getRows(5); + $spec = _elgg_services()->db->addQuerySpec([ + 'sql' => $select->getSQL(), + 'params' => $select->getParameters(), + 'results' => $rows, + ]); + + $find = River::find($options); + + $this->assertEquals($rows, $find); + + _elgg_services()->db->removeQuerySpec($spec); + } + + public function testCanExecuteQueryWithRelationship() { + $options = [ + 'callback' => false, + 'relationship_guid' => [1, 2, 3], + 'relationship' => ['foo1'], + 'inverse_relationship' => false, + 'order_by' => [ + new OrderByClause('rv.id', 'asc'), + ], + 'limit' => 10, + ]; + + $select = Select::fromTable('river', 'rv'); + $select->select('DISTINCT rv.*'); + + $select = $this->buildQuery($select, $options); + + $select->joinRelationshipTable('rv', 'subject_guid', null, false, 'inner', 'r'); + + $relationship = new RelationshipWhereClause(); + $relationship->names = ['foo1']; + $relationship->subject_guids = [1, 2, 3]; + $wheres[] = $relationship->prepare($select, 'r'); + + $select->andWhere($select->merge($wheres, 'OR')); + + $select->setMaxResults(10); + $select->setFirstResult(0); + + $select->orderBy('rv.id', 'asc'); + + $rows = $this->getRows(5); + $spec = _elgg_services()->db->addQuerySpec([ + 'sql' => $select->getSQL(), + 'params' => $select->getParameters(), + 'results' => $rows, + ]); + + $find = River::find($options); + + $this->assertEquals($rows, $find); + + _elgg_services()->db->removeQuerySpec($spec); + } + + public function getRows($limit = 10) { + $rows = []; + for ($i = 0; $i < $limit; $i++) { + $row = (object) [ + 'id' => $i, + 'subject_guid' => rand(100, 999), + 'object_guid' => rand(100, 999), + 'target_guid' => rand(100, 999), + 'enabled' => 'yes', + 'type' => 'object', + 'subtype' => 'foo', + 'access_id' => ACCESS_PUBLIC, + 'posted' => time(), + 'view' => 'foo/bar', + 'action_type' => 'foo:bar', + ]; + $rows[] = $row; + } + + return $rows; + } + +} \ No newline at end of file diff --git a/engine/tests/phpunit/unit/Elgg/Notifications/NotificationsServiceUnitTestCase.php b/engine/tests/phpunit/unit/Elgg/Notifications/NotificationsServiceUnitTestCase.php index 828cef4b9be..324bfc7a7f4 100644 --- a/engine/tests/phpunit/unit/Elgg/Notifications/NotificationsServiceUnitTestCase.php +++ b/engine/tests/phpunit/unit/Elgg/Notifications/NotificationsServiceUnitTestCase.php @@ -114,51 +114,59 @@ public function setupServices() { } public function getTestObject() { - $objects = $this->prepareTestObjects(); - foreach ($objects as $object) { - if ($object instanceof $this->test_object_class) { - return $object; - } - } - throw new Exception("Test object not found for $this->test_object_class class"); - } - - public function prepareTestObjects() { - $this->setupServices(); - $object = $this->createObject([ - 'owner_guid' => $this->actor->guid, - 'container_guid' => $this->actor->guid, - 'access_id' => ACCESS_LOGGED_IN, - 'subtype' => 'test_subtype', - ]); - - $group = $this->createGroup([ - 'owner_guid' => $this->actor->guid, - 'container_guid' => $this->actor->guid, - 'access_id' => ACCESS_LOGGED_IN, - ]); - - $user = $this->actor; - - $metadata_id = _elgg_services()->metadataTable->create($object->guid, 'test_metadata_name', 'test_metadata_value', 'text'); - $metadata = elgg_get_metadata_from_id($metadata_id); - - $annotation_id = $object->annotate('test_annotation_name', 'test_annotation_value', ACCESS_PUBLIC, $this->actor->guid, 'text'); - $annotation = elgg_get_annotation_from_id($annotation_id); - - add_entity_relationship($object->guid, 'test_relationship', $user->guid); - $relationship = check_entity_relationship($object->guid, 'test_relationship', $user->guid); + switch ($this->test_object_class) { + case ElggObject::class : + return $this->createObject([ + 'owner_guid' => $this->actor->guid, + 'container_guid' => $this->actor->guid, + 'access_id' => ACCESS_LOGGED_IN, + 'subtype' => 'test_subtype', + ]); + + case \ElggGroup::class : + return $this->createGroup([ + 'owner_guid' => $this->actor->guid, + 'container_guid' => $this->actor->guid, + 'access_id' => ACCESS_LOGGED_IN, + ]); + + case \ElggUser::class : + return $this->actor; + + case \ElggMetadata::class : + $object = $this->createObject(); + $metadata = new \ElggMetadata(); + $metadata->entity_guid = $object->guid; + $metadata->name = 'test_metadata_name'; + $metadata->value = 'test_metadata_value'; + $metadata->save(); + + return $metadata; + + case \ElggAnnotation::class : + $object = $this->createObject(); + + $annotation = new \ElggAnnotation(); + $annotation->entity_guid = $object->guid; + $annotation->name = 'test_annotation_name'; + $annotation->value = 'test_annotation_value'; + $annotation->owner_guid = $this->actor->guid; + $annotation->access_id = ACCESS_PUBLIC; + $annotation->save(); + + return $annotation; + + case \ElggRelationship::class : + $object = $this->createObject(); + $user = $this->actor; + add_entity_relationship($object->guid, 'test_relationship', $user->guid); + + return check_entity_relationship($object->guid, 'test_relationship', $user->guid); + } - return [ - $object, - $group, - $user, - $metadata, - $annotation, - $relationship, - ]; + throw new Exception("Test object not found for $this->test_object_class class"); } public function testRegisterEvent() { @@ -519,7 +527,7 @@ public function testCanAlterSubscriptionNotificationTranslations() { $recipient = $this->createUser([], [ 'language' => 'en', ]); - + $mock = $this->createMock(SubscriptionsService::class, ['getSubscriptions'], [], '', false); $mock->expects($this->exactly(1)) ->method('getSubscriptions') diff --git a/engine/tests/phpunit/integration/Elgg/Views/CommentViewsRenderingTest.php b/engine/tests/phpunit/unit/Elgg/Views/CommentViewsRenderingTest.php similarity index 83% rename from engine/tests/phpunit/integration/Elgg/Views/CommentViewsRenderingTest.php rename to engine/tests/phpunit/unit/Elgg/Views/CommentViewsRenderingTest.php index fbc03e9e789..7d2db6554d6 100644 --- a/engine/tests/phpunit/integration/Elgg/Views/CommentViewsRenderingTest.php +++ b/engine/tests/phpunit/unit/Elgg/Views/CommentViewsRenderingTest.php @@ -2,8 +2,6 @@ namespace Elgg\Views; -use Elgg\ViewRenderingTestCase; - /** * @group ViewRendering * @group ViewsService @@ -19,7 +17,7 @@ public function getViewNames() { } public function getDefaultViewVars() { - $comment = $this->createOne('object', [ + $comment = $this->createObject([ 'subtype' => 'comment', ]); return [ diff --git a/engine/tests/phpunit/integration/Elgg/Views/GroupViewsRenderingTest.php b/engine/tests/phpunit/unit/Elgg/Views/GroupViewsRenderingTest.php similarity index 92% rename from engine/tests/phpunit/integration/Elgg/Views/GroupViewsRenderingTest.php rename to engine/tests/phpunit/unit/Elgg/Views/GroupViewsRenderingTest.php index ed8b4eae870..ac788cf75c7 100644 --- a/engine/tests/phpunit/integration/Elgg/Views/GroupViewsRenderingTest.php +++ b/engine/tests/phpunit/unit/Elgg/Views/GroupViewsRenderingTest.php @@ -2,8 +2,6 @@ namespace Elgg\Views; -use Elgg\ViewRenderingTestCase; - /** * @group ViewRendering * @group ViewsService diff --git a/engine/tests/phpunit/integration/Elgg/Views/ListingViewsRenderingTest.php b/engine/tests/phpunit/unit/Elgg/Views/ListingViewsRenderingTest.php similarity index 93% rename from engine/tests/phpunit/integration/Elgg/Views/ListingViewsRenderingTest.php rename to engine/tests/phpunit/unit/Elgg/Views/ListingViewsRenderingTest.php index 02fae92db46..439e621e133 100644 --- a/engine/tests/phpunit/integration/Elgg/Views/ListingViewsRenderingTest.php +++ b/engine/tests/phpunit/unit/Elgg/Views/ListingViewsRenderingTest.php @@ -2,8 +2,6 @@ namespace Elgg\Views; -use Elgg\ViewRenderingTestCase; - /** * @group ViewRendering * @group ViewsService diff --git a/engine/tests/phpunit/integration/Elgg/Views/ObjectViewsRenderingTest.php b/engine/tests/phpunit/unit/Elgg/Views/ObjectViewsRenderingTest.php similarity index 96% rename from engine/tests/phpunit/integration/Elgg/Views/ObjectViewsRenderingTest.php rename to engine/tests/phpunit/unit/Elgg/Views/ObjectViewsRenderingTest.php index db19efa5163..77cecf07cdf 100644 --- a/engine/tests/phpunit/integration/Elgg/Views/ObjectViewsRenderingTest.php +++ b/engine/tests/phpunit/unit/Elgg/Views/ObjectViewsRenderingTest.php @@ -2,8 +2,6 @@ namespace Elgg\Views; -use Elgg\ViewRenderingTestCase; - /** * @group ViewRendering * @group ViewsService diff --git a/engine/tests/phpunit/integration/Elgg/Views/PageElementViewsRenderingTest.php b/engine/tests/phpunit/unit/Elgg/Views/PageElementViewsRenderingTest.php similarity index 98% rename from engine/tests/phpunit/integration/Elgg/Views/PageElementViewsRenderingTest.php rename to engine/tests/phpunit/unit/Elgg/Views/PageElementViewsRenderingTest.php index dd781ce67e5..631c637bb3a 100644 --- a/engine/tests/phpunit/integration/Elgg/Views/PageElementViewsRenderingTest.php +++ b/engine/tests/phpunit/unit/Elgg/Views/PageElementViewsRenderingTest.php @@ -2,8 +2,6 @@ namespace Elgg\Views; -use Elgg\ViewRenderingTestCase; - /** * @group ViewRendering * @group ViewsService diff --git a/engine/tests/phpunit/integration/Elgg/Views/RiverViewsRenderingTest.php b/engine/tests/phpunit/unit/Elgg/Views/RiverViewsRenderingTest.php similarity index 95% rename from engine/tests/phpunit/integration/Elgg/Views/RiverViewsRenderingTest.php rename to engine/tests/phpunit/unit/Elgg/Views/RiverViewsRenderingTest.php index 5dcec590396..98534b5c4ef 100644 --- a/engine/tests/phpunit/integration/Elgg/Views/RiverViewsRenderingTest.php +++ b/engine/tests/phpunit/unit/Elgg/Views/RiverViewsRenderingTest.php @@ -2,8 +2,6 @@ namespace Elgg\Views; -use Elgg\ViewRenderingTestCase; - /** * @group ViewRendering * @group ViewsService diff --git a/engine/tests/phpunit/integration/Elgg/Views/UrlOutputTest.php b/engine/tests/phpunit/unit/Elgg/Views/UrlOutputTest.php similarity index 95% rename from engine/tests/phpunit/integration/Elgg/Views/UrlOutputTest.php rename to engine/tests/phpunit/unit/Elgg/Views/UrlOutputTest.php index e58f97f715d..4cd53670e98 100644 --- a/engine/tests/phpunit/integration/Elgg/Views/UrlOutputTest.php +++ b/engine/tests/phpunit/unit/Elgg/Views/UrlOutputTest.php @@ -2,8 +2,6 @@ namespace Elgg\Views; -use Elgg\ViewRenderingTestCase; - /** * @group ViewRendering * @group ViewsService diff --git a/engine/tests/phpunit/integration/Elgg/Views/UserViewsRenderingTest.php b/engine/tests/phpunit/unit/Elgg/Views/UserViewsRenderingTest.php similarity index 98% rename from engine/tests/phpunit/integration/Elgg/Views/UserViewsRenderingTest.php rename to engine/tests/phpunit/unit/Elgg/Views/UserViewsRenderingTest.php index a889a8ef280..6b328ce8c75 100644 --- a/engine/tests/phpunit/integration/Elgg/Views/UserViewsRenderingTest.php +++ b/engine/tests/phpunit/unit/Elgg/Views/UserViewsRenderingTest.php @@ -2,8 +2,6 @@ namespace Elgg\Views; -use Elgg\ViewRenderingTestCase; - /** * @group ViewRendering * @group ViewsService diff --git a/engine/tests/classes/Elgg/ViewRenderingTestCase.php b/engine/tests/phpunit/unit/Elgg/Views/ViewRenderingTestCase.php similarity index 88% rename from engine/tests/classes/Elgg/ViewRenderingTestCase.php rename to engine/tests/phpunit/unit/Elgg/Views/ViewRenderingTestCase.php index 408beba54a5..d5d3f83d4d5 100644 --- a/engine/tests/classes/Elgg/ViewRenderingTestCase.php +++ b/engine/tests/phpunit/unit/Elgg/Views/ViewRenderingTestCase.php @@ -1,18 +1,25 @@ logger->disable(); - $user = $this->getRandomUser(); + $user = $this->createUser(); _elgg_services()->session->setLoggedInUser($user); elgg_set_page_owner_guid($user->guid); + + $this->registerViews(); } public function down() { @@ -34,6 +41,15 @@ abstract public function getViewNames(); */ abstract public function getDefaultViewVars(); + public function registerViews() { + elgg_views_boot(); + + $path = $this->getPath(); + if ($path) { + _elgg_services()->views->registerPluginViews($path); + } + } + /** * An array of views to test * @return array @@ -41,6 +57,8 @@ abstract public function getDefaultViewVars(); public function viewListProvider() { $provides = []; + $this->registerViews(); + $data = _elgg_services()->views->getInspectorData(); foreach ($data['locations'] as $viewtype => $views) { diff --git a/engine/tests/phpunit/integration/Elgg/Views/WidgetViewsRenderingTest.php b/engine/tests/phpunit/unit/Elgg/Views/WidgetViewsRenderingTest.php similarity index 96% rename from engine/tests/phpunit/integration/Elgg/Views/WidgetViewsRenderingTest.php rename to engine/tests/phpunit/unit/Elgg/Views/WidgetViewsRenderingTest.php index 4901457d853..fc2f84a4c2b 100644 --- a/engine/tests/phpunit/integration/Elgg/Views/WidgetViewsRenderingTest.php +++ b/engine/tests/phpunit/unit/Elgg/Views/WidgetViewsRenderingTest.php @@ -2,8 +2,6 @@ namespace Elgg\Views; -use Elgg\ViewRenderingTestCase; - /** * @group ViewRendering * @group ViewsService diff --git a/engine/tests/simpletest/ElggCoreAccessSQLTest.php b/engine/tests/simpletest/ElggCoreAccessSQLTest.php deleted file mode 100644 index b3fb86879d2..00000000000 --- a/engine/tests/simpletest/ElggCoreAccessSQLTest.php +++ /dev/null @@ -1,284 +0,0 @@ -user = $this->createUser(); - _elgg_services()->session->setLoggedInUser($this->user); - _elgg_services()->hooks->backup(); - } - - public function down() { - _elgg_services()->session->setLoggedInUser($this->getAdmin()); - $this->user->delete(); - _elgg_services()->hooks->restore(); - } - - public function testCanBuildAccessSqlClausesWithIgnoredAccess() { - $sql = _elgg_get_access_where_sql([ - 'ignore_access' => true, - ]); - $ans = "((1 = 1) AND (e.enabled = 'yes'))"; - $this->assertTrue($this->assertSqlEqual($ans, $sql), "$sql does not match $ans"); - } - - public function testCanBuildAccessSqlClausesWithIgnoredAccessWithoutDisabledEntities() { - $sql = _elgg_get_access_where_sql([ - 'use_enabled_clause' => false, - 'ignore_access' => true, - ]); - $ans = "((1 = 1))"; - $this->assertTrue($this->assertSqlEqual($ans, $sql), "$sql does not match $ans"); - } - - public function testCanBuildAccessSqlForLoggedInUser() { - $sql = _elgg_get_access_where_sql(); - $friends_clause = $this->getFriendsClause($this->user->guid, 'e'); - $owner_clause = $this->getOwnerClause($this->user->guid, 'e'); - $access_clause = $this->getLoggedInAccessListClause('e'); - $ans = "(($friends_clause OR $owner_clause OR $access_clause) AND (e.enabled = 'yes'))"; - - $this->assertTrue($this->assertSqlEqual($ans, $sql), "$sql does not match $ans"); - } - - public function testCanBuildAccessSqlWithCustomTableAlias() { - $sql = _elgg_get_access_where_sql([ - 'table_alias' => 'foo', - ]); - - $friends_clause = $this->getFriendsClause($this->user->guid, 'foo'); - $owner_clause = $this->getOwnerClause($this->user->guid, 'foo'); - $access_clause = $this->getLoggedInAccessListClause('foo'); - $ans = "(($friends_clause OR $owner_clause OR $access_clause) AND (foo.enabled = 'yes'))"; - - $this->assertTrue($this->assertSqlEqual($ans, $sql), "$sql does not match $ans"); - - // test with no alias - $sql = _elgg_get_access_where_sql([ - 'user_guid' => $this->user->guid, - 'table_alias' => '', - ]); - - $friends_clause = $this->getFriendsClause($this->user->guid, ''); - $owner_clause = $this->getOwnerClause($this->user->guid, ''); - $access_clause = $this->getLoggedInAccessListClause(''); - $ans = "(($friends_clause OR $owner_clause OR $access_clause) AND (enabled = 'yes'))"; - - $this->assertTrue($this->assertSqlEqual($ans, $sql), "$sql does not match $ans"); - } - - public function testCanBuildAccessSqlWithCustomGuidColumn() { - $sql = _elgg_get_access_where_sql([ - 'owner_guid_column' => 'unit_test', - ]); - - $friends_clause = $this->getFriendsClause($this->user->guid, 'e', 'unit_test'); - $owner_clause = $this->getOwnerClause($this->user->guid, 'e', 'unit_test'); - $access_clause = $this->getLoggedInAccessListClause('e'); - $ans = "(($friends_clause OR $owner_clause OR $access_clause) AND (e.enabled = 'yes'))"; - - $this->assertTrue($this->assertSqlEqual($ans, $sql), "$sql does not match $ans"); - } - - public function testCanBuildAccessSqlForLoggedOutUser() { - - $user = _elgg_services()->session->getLoggedInUser(); - _elgg_services()->session->removeLoggedInUser(); - - $sql = _elgg_get_access_where_sql(); - $access_clause = $this->getLoggedOutAccessListClause('e'); - $ans = "(($access_clause) AND (e.enabled = 'yes'))"; - - $this->assertTrue($this->assertSqlEqual($ans, $sql), "$sql does not match $ans"); - - _elgg_services()->session->setLoggedInUser($user); - } - - public function testAccessPluginHookRemoveEnabled() { - elgg_register_plugin_hook_handler('get_sql', 'access', [ - $this, - 'removeEnabledCallback' - ]); - $sql = _elgg_get_access_where_sql([ - 'ignore_access' => true, - ]); - $ans = "((1 = 1))"; - $this->assertTrue($this->assertSqlEqual($ans, $sql), "$sql does not match $ans"); - } - - public function removeEnabledCallback($hook, $type, $clauses, $params) { - $clauses['ands'] = []; - - return $clauses; - } - - public function testAccessPluginHookRemoveOrs() { - elgg_register_plugin_hook_handler('get_sql', 'access', [ - $this, - 'removeOrsCallback' - ]); - $sql = _elgg_get_access_where_sql([ - 'ignore_access' => true, - ]); - $ans = "((e.enabled = 'yes'))"; - $this->assertTrue($this->assertSqlEqual($ans, $sql), "$sql does not match $ans"); - } - - public function removeOrsCallback($hook, $type, $clauses, $params) { - $clauses['ors'] = []; - - return $clauses; - } - - public function testAccessPluginHookAddOr() { - elgg_register_plugin_hook_handler('get_sql', 'access', [ - $this, - 'addOrCallback' - ]); - $sql = _elgg_get_access_where_sql([ - 'ignore_access' => true, - ]); - $ans = "((1 = 1 OR 57 > 32) AND (e.enabled = 'yes'))"; - $this->assertTrue($this->assertSqlEqual($ans, $sql), "$sql does not match $ans"); - } - - public function addOrCallback($hook, $type, $clauses, $params) { - $clauses['ors'][] = '57 > 32'; - - return $clauses; - } - - public function testAccessPluginHookAddAnd() { - elgg_register_plugin_hook_handler('get_sql', 'access', [ - $this, - 'addAndCallback' - ]); - $sql = _elgg_get_access_where_sql([ - 'ignore_access' => true, - ]); - $ans = "((1 = 1) AND (e.enabled = 'yes' AND 57 > 32))"; - $this->assertTrue($this->assertSqlEqual($ans, $sql), "$sql does not match $ans"); - } - - public function testHasAccessToEntity() { - - $session = elgg_get_session(); - - $viewer = $session->getLoggedInUser(); - - $ia = elgg_set_ignore_access(true); - - $owner = $this->createUser(); - - $object = $this->createObject([ - 'owner_guid' => $owner->guid, - 'access_id' => ACCESS_PRIVATE, - ]); - - elgg_set_ignore_access($ia); - - $session->removeLoggedInUser(); - - $this->assertFalse(has_access_to_entity($object)); - $this->assertFalse(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - - $ia = elgg_set_ignore_access(true); - $object->access_id = ACCESS_PUBLIC; - $object->save(); - elgg_set_ignore_access($ia); - - $this->assertTrue(has_access_to_entity($object)); - $this->assertTrue(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - - $ia = elgg_set_ignore_access(true); - $object->access_id = ACCESS_LOGGED_IN; - $object->save(); - elgg_set_ignore_access($ia); - - $this->assertFalse(has_access_to_entity($object)); - // even though user is logged out, existing users are presumed to have access to an entity - $this->assertTrue(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - - $session->setLoggedInUser($viewer); - $this->assertTrue(has_access_to_entity($object)); - $this->assertTrue(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - $session->removeLoggedInUser(); - - $ia = elgg_set_ignore_access(true); - $owner->addFriend($viewer->guid); - $object->access_id = ACCESS_FRIENDS; - $object->save(); - elgg_set_ignore_access($ia); - - $this->assertFalse(has_access_to_entity($object)); - $this->assertTrue(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - - $session->setLoggedInUser($viewer); - $this->assertTrue(has_access_to_entity($object)); - $this->assertTrue(has_access_to_entity($object, $viewer)); - $this->assertTrue(has_access_to_entity($object, $owner)); - - $ia = elgg_set_ignore_access(true); - $owner->delete(); - $object->delete(); - elgg_set_ignore_access($ia); - - $session->setLoggedInUser($viewer); - } - - public function addAndCallback($hook, $type, $clauses, $params) { - $clauses['ands'][] = '57 > 32'; - - return $clauses; - } - - protected function assertSqlEqual($sql1, $sql2) { - $sql1 = preg_replace('/\s+/', '', $sql1); - $sql2 = preg_replace('/\s+/', '', $sql2); - - return $sql1 === $sql2; - } - - protected function getFriendsClause($user_guid, $table_alias, $owner_guid = 'owner_guid') { - $CONFIG = _elgg_config(); - $table_alias = $table_alias ? $table_alias . '.' : ''; - - return "{$table_alias}access_id = " . ACCESS_FRIENDS . " - AND {$table_alias}{$owner_guid} IN ( - SELECT guid_one FROM {$CONFIG->dbprefix}entity_relationships - WHERE relationship = 'friend' AND guid_two = $user_guid - )"; - } - - protected function getOwnerClause($user_guid, $table_alias, $owner_guid = 'owner_guid') { - $table_alias = $table_alias ? $table_alias . '.' : ''; - - return "{$table_alias}{$owner_guid} = $user_guid"; - } - - protected function getLoggedInAccessListClause($table_alias) { - $table_alias = $table_alias ? $table_alias . '.' : ''; - - return "{$table_alias}access_id IN (2,1)"; - } - - protected function getLoggedOutAccessListClause($table_alias) { - $table_alias = $table_alias ? $table_alias . '.' : ''; - - return "{$table_alias}access_id IN (2)"; - } -} diff --git a/engine/tests/simpletest/ElggCoreMetadataAPITest.php b/engine/tests/simpletest/ElggCoreMetadataAPITest.php index 2147f70e43b..47b6ec8986e 100644 --- a/engine/tests/simpletest/ElggCoreMetadataAPITest.php +++ b/engine/tests/simpletest/ElggCoreMetadataAPITest.php @@ -28,7 +28,7 @@ public function testElggGetEntitiesFromMetadata() { $this->object->save(); // create_metadata returns id of metadata on success - $this->assertNotEqual(false, _elgg_services()->metadataTable->create($this->object->guid, 'metaUnitTest', 'tested')); + $this->object->setMetadata('metaUnitTest', 'tested'); // check value with improper case $options = array('metadata_names' => 'metaUnitTest', 'metadata_values' => 'Tested', 'limit' => 10, 'metadata_case_sensitive' => true); diff --git a/engine/tests/simpletest/ElggCoreMetadataCacheTest.php b/engine/tests/simpletest/ElggCoreMetadataCacheTest.php index 394c3a52feb..5d472c80257 100644 --- a/engine/tests/simpletest/ElggCoreMetadataCacheTest.php +++ b/engine/tests/simpletest/ElggCoreMetadataCacheTest.php @@ -135,7 +135,11 @@ public function testWritesInvalidate() { // create_metadata $this->cache->inject($this->guid1, ['foo' => 'bar']); - _elgg_services()->metadataTable->create($this->guid1, 'foo', 'bar', 'text'); + $metadata = new \ElggMetadata(); + $metadata->entity_guid = $this->guid1; + $metadata->name = 'foo'; + $metadata->value = 'bar'; + $metadata->save(); $this->assertFalse($this->cache->isLoaded($this->guid1)); } diff --git a/engine/tests/simpletest/ElggCoreMetastringsTest.php b/engine/tests/simpletest/ElggCoreMetastringsTest.php index 84263bf9ed4..edc197c698b 100644 --- a/engine/tests/simpletest/ElggCoreMetastringsTest.php +++ b/engine/tests/simpletest/ElggCoreMetastringsTest.php @@ -47,10 +47,13 @@ public function createAnnotations($max = 1) { public function createMetadata($max = 1) { $metadata = []; for ($i = 0; $i < $max; $i++) { - $name = 'test_metadata_name' . rand(); - $value = 'test_metadata_value' . rand(); - - $metadata[] = _elgg_services()->metadataTable->create($this->object->guid, $name, $value); + $name = 'test_metadata_name' . $i . rand(); + $value = 'test_metadata_value' . $i . rand(); + $md = new \ElggMetadata(); + $md->entity_guid = $this->object->guid; + $md->name = $name; + $md->value = $value; + $metadata[] = $md->save(); } return $metadata; diff --git a/engine/tests/simpletest/ElggEntityTest.php b/engine/tests/simpletest/ElggEntityTest.php index 74721f21e0e..cf6d959c551 100644 --- a/engine/tests/simpletest/ElggEntityTest.php +++ b/engine/tests/simpletest/ElggEntityTest.php @@ -168,7 +168,12 @@ public function testElggEntityDisableAndEnable() { // add annotations and metadata to check if they're disabled. $annotation_id = create_annotation($this->entity->guid, 'test_annotation_' . rand(), 'test_value_' . rand()); - $metadata_id = _elgg_services()->metadataTable->create($this->entity->guid, 'test_metadata_' . rand(), 'test_value_' . rand()); + + $metadata = new ElggMetadata(); + $metadata->entity_guid = $this->entity->guid; + $metadata->name = 'test_metadata_' . rand(); + $metadata->value = 'test_value_' . rand(); + $metadata_id = $metadata->save(); $this->assertTrue($this->entity->disable()); diff --git a/mod/groups/tests/phpunit/integration/Elgg/Groups/ACLTest.php b/mod/groups/tests/phpunit/integration/Elgg/Groups/ACLTest.php index 16d0a58cef7..e6c8e956297 100644 --- a/mod/groups/tests/phpunit/integration/Elgg/Groups/ACLTest.php +++ b/mod/groups/tests/phpunit/integration/Elgg/Groups/ACLTest.php @@ -13,7 +13,7 @@ class ACLTest extends \Elgg\IntegrationTestCase { protected $group; /** - * @var ElggUser + * @var \ElggUser */ protected $user; @@ -29,8 +29,12 @@ public function up() { } public function down() { - $this->group->delete(); - $this->user->delete(); + if ($this->group) { + $this->group->delete(); + } + if ($this->user) { + $this->user->delete(); + } _elgg_services()->session->removeLoggedInUser(); } diff --git a/mod/search/search_hooks.php b/mod/search/search_hooks.php index b0e013452d3..c19b61252b5 100644 --- a/mod/search/search_hooks.php +++ b/mod/search/search_hooks.php @@ -6,6 +6,9 @@ * @subpackage Search */ +use Elgg\Database\Clauses\MetadataWhereClause; +use Elgg\Database\QueryBuilder; + /** * Get objects that match the search parameters. * @@ -21,35 +24,26 @@ function search_objects_hook($hook, $type, $value, $params) { $params['joins'] = (array) elgg_extract('joins', $params, []); $params['wheres'] = (array) elgg_extract('wheres', $params, []); - $query = sanitise_string($params['query']); + $query = elgg_extract('query', $params); $query_parts = explode(' ', $query); - $db_prefix = elgg_get_config('dbprefix'); - - $params['joins'][] = "JOIN {$db_prefix}metadata md ON e.guid = md.entity_guid"; + $metadata_fields = ['title', 'description']; - $fields = ['title', 'description']; - $wheres = []; - foreach ($fields as $field) { - $sublikes = []; - foreach ($query_parts as $query_part) { - $query_part = sanitise_string($query_part); - if (strlen($query_part) == 0) { - continue; - } - $sublikes[] = "(md.value LIKE '%{$query_part}%')"; - } + $params['wheres'][] = function (QueryBuilder $qb, $alias) use ($query_parts, $metadata_fields) { + $subclauses = []; + $md_alias = $qb->joinMetadataTable($alias, 'guid', $metadata_fields); + foreach ($query_parts as $part) { + $where = new MetadataWhereClause(); + $where->values = "%{$part}%"; + $where->comparison = 'LIKE'; + $where->value_type = ELGG_VALUE_STRING; + $where->case_sensitive = false; - if (empty($sublikes)) { - continue; + $subclauses[] = $where->prepare($qb, $md_alias); } - $wheres[] = "(md.name = '{$field}' AND (" . implode(' AND ', $sublikes) . "))"; - } - - if (!empty($wheres)) { - $params['wheres'][] = '(' . implode(' OR ', $wheres) . ')'; - } + return $qb->merge($subclauses, 'AND'); + }; $params['count'] = true; $count = elgg_get_entities($params); @@ -96,35 +90,26 @@ function search_groups_hook($hook, $type, $value, $params) { $params['joins'] = (array) elgg_extract('joins', $params, []); $params['wheres'] = (array) elgg_extract('wheres', $params, []); - $query = sanitise_string($params['query']); + $query = elgg_extract('query', $params); $query_parts = explode(' ', $query); - $db_prefix = elgg_get_config('dbprefix'); - - $params['joins'][] = "JOIN {$db_prefix}metadata md ON e.guid = md.entity_guid"; + $metadata_fields = ['name', 'description']; - $fields = ['name', 'description']; - $wheres = []; - foreach ($fields as $field) { - $sublikes = []; - foreach ($query_parts as $query_part) { - $query_part = sanitise_string($query_part); - if (strlen($query_part) == 0) { - continue; - } - $sublikes[] = "(md.value LIKE '%{$query_part}%')"; - } + $params['wheres'][] = function (QueryBuilder $qb, $alias) use ($query_parts, $metadata_fields) { + $subclauses = []; + $md_alias = $qb->joinMetadataTable($alias, 'guid', $metadata_fields); + foreach ($query_parts as $part) { + $where = new MetadataWhereClause(); + $where->values = "%{$part}%"; + $where->comparison = 'LIKE'; + $where->value_type = ELGG_VALUE_STRING; + $where->case_sensitive = false; - if (empty($sublikes)) { - continue; + $subclauses[] = $where->prepare($qb, $md_alias); } - $wheres[] = "(md.name = '{$field}' AND (" . implode(' AND ', $sublikes) . "))"; - } - - if (!empty($wheres)) { - $params['wheres'][] = '(' . implode(' OR ', $wheres) . ')'; - } + return $qb->merge($subclauses, 'AND'); + }; $params['count'] = true; @@ -179,13 +164,13 @@ function search_users_hook($hook, $type, $value, $params) { $metadata_fields = ['username', 'name']; $profile_fields = array_keys(elgg_get_config('profile_fields')); - $params['wheres'][] = function (\Elgg\Database\QueryBuilder $qb, $alias) use ($query_parts, $profile_fields, $metadata_fields) { + $params['wheres'][] = function (QueryBuilder $qb, $alias) use ($query_parts, $profile_fields, $metadata_fields) { $wheres = []; $subclauses = []; $md_alias = $qb->joinMetadataTable($alias, 'guid', $metadata_fields, 'left'); foreach ($query_parts as $part) { - $where = new \Elgg\Database\Clauses\MetadataWhereClause(); + $where = new MetadataWhereClause(); $where->values = "%{$part}%"; $where->comparison = 'LIKE'; $where->value_type = ELGG_VALUE_STRING; @@ -285,56 +270,29 @@ function search_tags_hook($hook, $type, $value, $params) { $params['joins'] = (array) elgg_extract('joins', $params, []); $params['wheres'] = (array) elgg_extract('wheres', $params, []); - $db_prefix = elgg_get_config('dbprefix'); - $valid_tag_names = elgg_get_registered_tag_metadata_names(); - // @todo will need to split this up to support searching multiple tags at once. - $query = sanitise_string($params['query']); - // if passed a tag metadata name, only search on that tag name. // tag_name isn't included in the params because it's specific to // tag searches. if ($tag_names = get_input('tag_names')) { - if (is_array($tag_names)) { - $search_tag_names = $tag_names; - } else { - $search_tag_names = [$tag_names]; - } - - // check these are valid to avoid arbitrary metadata searches. - foreach ($search_tag_names as $i => $tag_name) { - if (!in_array($tag_name, $valid_tag_names)) { - unset($search_tag_names[$i]); - } - } + $tag_names = (array) $tag_names; + $search_tag_names = array_intersect($tag_names, $valid_tag_names); } else { $search_tag_names = $valid_tag_names; } - if (!$search_tag_names) { - return ['entities' => [], 'count' => $count]; - } - - // don't use elgg_get_entities_from_metadata() here because of - // performance issues. since we don't care what matches at this point - // use an IN clause to grab everything that matches at once and sort - // out the matches later. - $params['joins'][] = "JOIN {$db_prefix}metadata md on e.guid = md.entity_guid"; - - $access = _elgg_get_access_where_sql([ - 'table_alias' => 'md', - 'guid_column' => 'entity_guid', - ]); - $sanitised_tags = []; - - foreach ($search_tag_names as $tag) { - $sanitised_tags[] = '"' . sanitise_string($tag) . '"'; + if (empty($search_tag_names)) { + return ['entities' => [], 'count' => 0]; } - $tags_in = implode(',', $sanitised_tags); + $query = elgg_extract('query', $params); - $params['wheres'][] = "(md.name IN ($tags_in) AND md.value = '$query' AND $access)"; + $params['metadata_name_value_pairs'][] = [ + 'name' => $search_tag_names, + 'value' => $query, + 'case_sensitive' => false, + ]; $params['count'] = true; $count = elgg_get_entities($params); diff --git a/mod/search/start.php b/mod/search/start.php index a04f1d005c0..4cd882a82fb 100644 --- a/mod/search/start.php +++ b/mod/search/start.php @@ -29,7 +29,7 @@ function search_init() { elgg_extend_view('elgg.css', 'search/css'); elgg_register_plugin_hook_handler('robots.txt', 'site', 'search_exclude_robots'); - + elgg_register_plugin_hook_handler('view_vars', 'output/tag', 'search_output_tag'); } @@ -253,10 +253,10 @@ function search_highlight_words($words, $string) { foreach ($words as $word) { // remove any boolean mode operators $word = preg_replace("/([\-\+~])([\w]+)/i", '$2', $word); - + // escape the delimiter and any other regexp special chars $word = preg_quote($word, '/'); - + $search = "/($word)/i"; // Must replace with placeholders in case one of the search terms is in the html string. @@ -294,7 +294,7 @@ function search_remove_ignored_words($query, $format = 'array') { //$query = str_replace(array('"', '-', '+', '~'), '', stripslashes(strip_tags($query))); $query = stripslashes(strip_tags($query)); $query = trim($query); - + $words = preg_split('/\s+/', $query); if ($format == 'string') { @@ -433,7 +433,7 @@ function search_get_order_by_sql($entities_table, $type_table, $sort, $order) { } if ($on) { - $order_by = "$on $order"; + $order_by = new \Elgg\Database\Clauses\OrderByClause($on, $order); } else { $order_by = ''; } @@ -476,22 +476,22 @@ function search_output_tag(\Elgg\Hook $hook) { // leave unaltered return; } - + $query_params = [ 'q' => elgg_extract('value', $vars), 'search_type' => 'tags', 'type' => elgg_extract('type', $vars, null, false), 'subtype' => elgg_extract('subtype', $vars, null, false), ]; - + $url = elgg_extract('base_url', $vars, 'search'); - + unset($vars['base_url']); unset($vars['type']); unset($vars['subtype']); - + $vars['href'] = elgg_http_add_url_query_elements($url, $query_params); - + return $vars; }