123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753 |
- <?php
- /**
- * Elgg metastrngs
- * Functions to manage object metastrings.
- *
- * @package Elgg.Core
- * @subpackage DataModel.MetaStrings
- */
- /**
- * Gets the metastring identifier for a value.
- *
- * Elgg normalizes the names and values of annotations and metadata. This function
- * provides the identifier used as the index in the metastrings table. Plugin
- * developers should only use this if denormalizing names/values for performance
- * reasons (to avoid multiple joins on the metastrings table).
- *
- * @param string $string The value
- * @param bool $case_sensitive Should the retrieval be case sensitive?
- * If not, there may be more than one result
- *
- * @return int|array metastring id or array of ids
- * @since 1.9.0
- */
- function elgg_get_metastring_id($string, $case_sensitive = true) {
- return _elgg_services()->metastringsTable->getId($string, $case_sensitive);
- }
- /**
- * Add a metastring.
- *
- * @warning You should not call this directly. Use elgg_get_metastring_id().
- *
- * @param string $string The value to be normalized
- * @return int The identifier for this string
- */
- function _elgg_add_metastring($string) {
- return _elgg_services()->metastringsTable->add($string);
- }
- /**
- * Returns an array of either \ElggAnnotation or \ElggMetadata objects.
- * Accepts all elgg_get_entities() options for entity restraints.
- *
- * @see elgg_get_entities
- *
- * @param array $options Array in format:
- *
- * metastring_names => null|ARR metastring names
- *
- * metastring_values => null|ARR metastring values
- *
- * metastring_ids => null|ARR metastring ids
- *
- * metastring_case_sensitive => BOOL Overall Case sensitive
- *
- * metastring_owner_guids => null|ARR Guids for metadata owners
- *
- * metastring_created_time_lower => INT Lower limit for created time.
- *
- * metastring_created_time_upper => INT Upper limit for created time.
- *
- * metastring_calculation => STR Perform the MySQL function on the metastring values
- * returned.
- * This differs from egef_annotation_calculation in that
- * it returns only the calculation of all annotation values.
- * You can sum, avg, count, etc. egef_annotation_calculation()
- * returns \ElggEntities ordered by a calculation on their
- * annotation values.
- *
- * metastring_type => STR metadata or annotation(s)
- *
- * @return \ElggExtender[]|int An array or count of metastring based objects
- * @access private
- */
- function _elgg_get_metastring_based_objects($options) {
- $options = _elgg_normalize_metastrings_options($options);
- switch ($options['metastring_type']) {
- case 'metadata':
- $type = 'metadata';
- $callback = 'row_to_elggmetadata';
- break;
- case 'annotations':
- case 'annotation':
- $type = 'annotations';
- $callback = 'row_to_elggannotation';
- break;
- default:
- return false;
- }
- $defaults = array(
- // entities
- 'types' => ELGG_ENTITIES_ANY_VALUE,
- 'subtypes' => ELGG_ENTITIES_ANY_VALUE,
- 'type_subtype_pairs' => ELGG_ENTITIES_ANY_VALUE,
- 'guids' => ELGG_ENTITIES_ANY_VALUE,
- 'owner_guids' => ELGG_ENTITIES_ANY_VALUE,
- 'container_guids' => ELGG_ENTITIES_ANY_VALUE,
- 'site_guids' => get_config('site_guid'),
- '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,
- // options are normalized to the plural in case we ever add support for them.
- 'metastring_names' => ELGG_ENTITIES_ANY_VALUE,
- 'metastring_values' => ELGG_ENTITIES_ANY_VALUE,
- //'metastring_name_value_pairs' => ELGG_ENTITIES_ANY_VALUE,
- //'metastring_name_value_pairs_operator' => 'AND',
- 'metastring_case_sensitive' => true,
- //'order_by_metastring' => array(),
- 'metastring_calculation' => ELGG_ENTITIES_NO_VALUE,
- 'metastring_created_time_lower' => ELGG_ENTITIES_ANY_VALUE,
- 'metastring_created_time_upper' => ELGG_ENTITIES_ANY_VALUE,
- 'metastring_owner_guids' => ELGG_ENTITIES_ANY_VALUE,
- 'metastring_ids' => ELGG_ENTITIES_ANY_VALUE,
- // sql
- 'order_by' => 'n_table.time_created ASC, n_table.id ASC',
- 'limit' => elgg_get_config('default_limit'),
- 'offset' => 0,
- 'count' => false,
- 'selects' => array(),
- 'wheres' => array(),
- 'joins' => array(),
- 'distinct' => true,
- 'preload_owners' => false,
- 'callback' => $callback,
- );
- // @todo Ignore site_guid right now because of #2910
- $options['site_guid'] = ELGG_ENTITIES_ANY_VALUE;
- $options = array_merge($defaults, $options);
- // can't use helper function with type_subtype_pair because
- // it's already an array...just need to merge it
- if (isset($options['type_subtype_pair'])) {
- if (isset($options['type_subtype_pairs'])) {
- $options['type_subtype_pairs'] = array_merge($options['type_subtype_pairs'],
- $options['type_subtype_pair']);
- } else {
- $options['type_subtype_pairs'] = $options['type_subtype_pair'];
- }
- }
- $singulars = array(
- 'type', 'subtype', 'type_subtype_pair',
- 'guid', 'owner_guid', 'container_guid', 'site_guid',
- 'metastring_name', 'metastring_value',
- 'metastring_owner_guid', 'metastring_id',
- 'select', 'where', 'join'
- );
- $options = _elgg_normalize_plural_options_array($options, $singulars);
- if (!$options) {
- return false;
- }
- $db_prefix = elgg_get_config('dbprefix');
- // evaluate where clauses
- if (!is_array($options['wheres'])) {
- $options['wheres'] = array($options['wheres']);
- }
- $wheres = $options['wheres'];
- // entities
- $wheres[] = _elgg_get_entity_type_subtype_where_sql('e', $options['types'],
- $options['subtypes'], $options['type_subtype_pairs']);
- $wheres[] = _elgg_get_guid_based_where_sql('e.guid', $options['guids']);
- $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_guid_based_where_sql('e.site_guid', $options['site_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']);
- $wheres[] = _elgg_get_entity_time_where_sql('n_table', $options['metastring_created_time_upper'],
- $options['metastring_created_time_lower'], null, null);
- $wheres[] = _elgg_get_guid_based_where_sql('n_table.owner_guid',
- $options['metastring_owner_guids']);
- // 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);
- // evaluate join clauses
- if (!is_array($options['joins'])) {
- $options['joins'] = array($options['joins']);
- }
- $joins = $options['joins'];
- $joins[] = "JOIN {$db_prefix}entities e ON n_table.entity_guid = e.guid";
- // evaluate selects
- if (!is_array($options['selects'])) {
- $options['selects'] = array($options['selects']);
- }
- $selects = $options['selects'];
- // For performance reasons we don't want the joins required for metadata / annotations
- // unless we're going through one of their callbacks.
- // this means we expect the functions passing different callbacks to pass their required joins.
- // If we're doing a calculation
- $custom_callback = ($options['callback'] == 'row_to_elggmetadata'
- || $options['callback'] == 'row_to_elggannotation');
- $is_calculation = $options['metastring_calculation'] ? true : false;
-
- if ($custom_callback || $is_calculation) {
- $joins[] = "JOIN {$db_prefix}metastrings n on n_table.name_id = n.id";
- $joins[] = "JOIN {$db_prefix}metastrings v on n_table.value_id = v.id";
- $selects[] = 'n.string as name';
- $selects[] = 'v.string as value';
- }
- foreach ($joins as $i => $join) {
- if ($join === false) {
- return false;
- } elseif (empty($join)) {
- unset($joins[$i]);
- }
- }
- // metastrings
- $metastring_clauses = _elgg_get_metastring_sql('n_table', $options['metastring_names'],
- $options['metastring_values'], null, $options['metastring_ids'],
- $options['metastring_case_sensitive']);
- if ($metastring_clauses) {
- $wheres = array_merge($wheres, $metastring_clauses['wheres']);
- $joins = array_merge($joins, $metastring_clauses['joins']);
- } else {
- $wheres[] = _elgg_get_access_where_sql(array(
- 'table_alias' => 'n_table',
- 'guid_column' => 'entity_guid',
- ));
- }
- $distinct = $options['distinct'] ? "DISTINCT " : "";
- if ($options['metastring_calculation'] === ELGG_ENTITIES_NO_VALUE && !$options['count']) {
- $selects = array_unique($selects);
- // evalutate selects
- $select_str = '';
- if ($selects) {
- foreach ($selects as $select) {
- $select_str .= ", $select";
- }
- }
- $query = "SELECT $distinct n_table.*{$select_str} FROM {$db_prefix}$type n_table";
- } elseif ($options['count']) {
- // count is over the entities
- $query = "SELECT count($distinct e.guid) as calculation FROM {$db_prefix}$type n_table";
- } else {
- $query = "SELECT {$options['metastring_calculation']}(v.string) as calculation FROM {$db_prefix}$type n_table";
- }
- // remove identical join clauses
- $joins = array_unique($joins);
- // add joins
- foreach ($joins as $j) {
- $query .= " $j ";
- }
- // add wheres
- $query .= ' WHERE ';
- foreach ($wheres as $w) {
- $query .= " $w AND ";
- }
- // Add access controls
- $query .= _elgg_get_access_where_sql(array('table_alias' => 'e'));
- // reverse order by
- if (isset($options['reverse_order_by']) && $options['reverse_order_by']) {
- $options['order_by'] = _elgg_sql_reverse_order_by_clause($options['order_by']);
- }
- if ($options['metastring_calculation'] === ELGG_ENTITIES_NO_VALUE && !$options['count']) {
- if (isset($options['group_by'])) {
- $options['group_by'] = sanitise_string($options['group_by']);
- $query .= " GROUP BY {$options['group_by']}";
- }
- if (isset($options['order_by']) && $options['order_by']) {
- $options['order_by'] = sanitise_string($options['order_by']);
- $query .= " ORDER BY {$options['order_by']}, n_table.id";
- }
- if ($options['limit']) {
- $limit = sanitise_int($options['limit']);
- $offset = sanitise_int($options['offset'], false);
- $query .= " LIMIT $offset, $limit";
- }
- $dt = get_data($query, $options['callback']);
- if ($options['preload_owners'] && is_array($dt) && count($dt) > 1) {
- _elgg_services()->entityPreloader->preload($dt, ['owner_guid']);
- }
- return $dt;
- } else {
- $result = get_data_row($query);
- return $result->calculation;
- }
- }
- /**
- * Returns an array of joins and wheres for use in metastrings.
- *
- * @note The $pairs is reserved for name/value pairs if we want to implement those.
- *
- * @param string $table The annotation or metadata table name or alias
- * @param array $names An array of names
- * @param array $values An array of values
- * @param array $pairs Name / value pairs. Not currently used.
- * @param array $ids Metastring IDs
- * @param bool $case_sensitive Should name and values be case sensitive?
- *
- * @return array
- * @access private
- */
- function _elgg_get_metastring_sql($table, $names = null, $values = null,
- $pairs = null, $ids = null, $case_sensitive = false) {
- if ((!$names && $names !== 0)
- && (!$values && $values !== 0)
- && !$ids
- && (!$pairs && $pairs !== 0)) {
- return array();
- }
- $db_prefix = elgg_get_config('dbprefix');
- // binary forces byte-to-byte comparision of strings, making
- // it case- and diacritical-mark- sensitive.
- // only supported on values.
- $binary = ($case_sensitive) ? ' BINARY ' : '';
- $return = array (
- 'joins' => array (),
- 'wheres' => array()
- );
- $wheres = array();
- // get names wheres and joins
- $names_where = '';
- if ($names !== null) {
- if (!is_array($names)) {
- $names = array($names);
- }
- $sanitised_names = array();
- foreach ($names as $name) {
- // normalise to 0.
- if (!$name) {
- $name = '0';
- }
- $sanitised_names[] = '\'' . sanitise_string($name) . '\'';
- }
- if ($names_str = implode(',', $sanitised_names)) {
- $return['joins'][] = "JOIN {$db_prefix}metastrings msn on $table.name_id = msn.id";
- $names_where = "(msn.string IN ($names_str))";
- }
- }
- // get values wheres and joins
- $values_where = '';
- if ($values !== null) {
- if (!is_array($values)) {
- $values = array($values);
- }
- $sanitised_values = array();
- foreach ($values as $value) {
- // normalize to 0
- if (!$value) {
- $value = 0;
- }
- $sanitised_values[] = '\'' . sanitise_string($value) . '\'';
- }
- if ($values_str = implode(',', $sanitised_values)) {
- $return['joins'][] = "JOIN {$db_prefix}metastrings msv on $table.value_id = msv.id";
- $values_where = "({$binary}msv.string IN ($values_str))";
- }
- }
- if ($ids !== null) {
- if (!is_array($ids)) {
- $ids = array($ids);
- }
- $ids_str = implode(',', $ids);
- if ($ids_str) {
- $wheres[] = "n_table.id IN ($ids_str)";
- }
- }
- if ($names_where && $values_where) {
- $wheres[] = "($names_where AND $values_where)";
- } elseif ($names_where) {
- $wheres[] = $names_where;
- } elseif ($values_where) {
- $wheres[] = $values_where;
- }
- $wheres[] = _elgg_get_access_where_sql(array(
- 'table_alias' => $table,
- 'guid_column' => 'entity_guid',
- ));
- if ($where = implode(' AND ', $wheres)) {
- $return['wheres'][] = "($where)";
- }
- return $return;
- }
- /**
- * Normalizes metadata / annotation option names to their corresponding metastrings name.
- *
- * @param array $options An options array
- * @return array
- * @access private
- */
- function _elgg_normalize_metastrings_options(array $options = array()) {
- // support either metastrings_type or metastring_type
- // because I've made this mistake many times and hunting it down is a pain...
- $type = elgg_extract('metastring_type', $options, null);
- $type = elgg_extract('metastrings_type', $options, $type);
- $options['metastring_type'] = $type;
- // support annotation_ and annotations_ because they're way too easy to confuse
- $prefixes = array('metadata_', 'annotation_', 'annotations_');
- // map the metadata_* options to metastring_* options
- $map = array(
- 'names' => 'metastring_names',
- 'values' => 'metastring_values',
- 'case_sensitive' => 'metastring_case_sensitive',
- 'owner_guids' => 'metastring_owner_guids',
- 'created_time_lower' => 'metastring_created_time_lower',
- 'created_time_upper' => 'metastring_created_time_upper',
- 'calculation' => 'metastring_calculation',
- 'ids' => 'metastring_ids',
- );
- foreach ($prefixes as $prefix) {
- $singulars = array("{$prefix}name", "{$prefix}value", "{$prefix}owner_guid", "{$prefix}id");
- $options = _elgg_normalize_plural_options_array($options, $singulars);
- foreach ($map as $specific => $normalized) {
- $key = $prefix . $specific;
- if (isset($options[$key])) {
- $options[$normalized] = $options[$key];
- }
- }
- }
- return $options;
- }
- /**
- * Enables or disables a metastrings-based object by its id.
- *
- * @warning To enable disabled metastrings you must first use
- * {@link access_show_hidden_entities()}.
- *
- * @param int $id The object's ID
- * @param string $enabled Value to set to: yes or no
- * @param string $type Metastring type: metadata or annotation
- *
- * @return bool
- * @throws InvalidParameterException
- * @access private
- */
- function _elgg_set_metastring_based_object_enabled_by_id($id, $enabled, $type) {
- $id = (int)$id;
- $db_prefix = elgg_get_config('dbprefix');
- $object = _elgg_get_metastring_based_object_from_id($id, $type);
- switch ($type) {
- case 'annotation':
- case 'annotations':
- $type = 'annotation';
- $table = "{$db_prefix}annotations";
- break;
- case 'metadata':
- $table = "{$db_prefix}metadata";
- break;
- }
- if ($enabled === 'yes' || $enabled === 1 || $enabled === true) {
- $enabled = 'yes';
- $event = 'enable';
- } elseif ($enabled === 'no' || $enabled === 0 || $enabled === false) {
- $enabled = 'no';
- $event = 'disable';
- } else {
- return false;
- }
- $return = false;
- if ($object) {
- // don't set it if it's already set.
- if ($object->enabled == $enabled) {
- $return = false;
- } elseif ($object->canEdit() && (elgg_trigger_event($event, $type, $object))) {
- $return = update_data("UPDATE $table SET enabled = '$enabled' where id = $id");
- }
- }
- return $return;
- }
- /**
- * Runs metastrings-based objects found using $options through $callback
- *
- * @warning Unlike _elgg_get_metastring_based_objects() this will not accept an
- * empty options array!
- *
- * @warning This returns null on no ops.
- *
- * @param array $options An options array. {@link _elgg_get_metastring_based_objects()}
- * @param string $callback The callback to pass each result through
- * @param bool $inc_offset Increment the offset? Pass false for callbacks that delete / disable
- *
- * @return bool|null true on success, false on failure, null if no objects are found.
- * @access private
- */
- function _elgg_batch_metastring_based_objects(array $options, $callback, $inc_offset = true) {
- if (!$options || !is_array($options)) {
- return false;
- }
- $batch = new \ElggBatch('_elgg_get_metastring_based_objects', $options, $callback, 50, $inc_offset);
- return $batch->callbackResult;
- }
- /**
- * Returns a singular metastring-based object by its ID.
- *
- * @param int $id The metastring-based object's ID
- * @param string $type The type: annotation or metadata
- * @return \ElggExtender
- * @access private
- */
- function _elgg_get_metastring_based_object_from_id($id, $type) {
- $id = (int)$id;
- if (!$id) {
- return false;
- }
- $options = array(
- 'metastring_type' => $type,
- 'metastring_id' => $id,
- );
- $obj = _elgg_get_metastring_based_objects($options);
- if ($obj && count($obj) == 1) {
- return $obj[0];
- }
- return false;
- }
- /**
- * Deletes a metastring-based object by its id
- *
- * @param int $id The object's ID
- * @param string $type The object's metastring type: annotation or metadata
- * @return bool
- * @access private
- */
- function _elgg_delete_metastring_based_object_by_id($id, $type) {
- $id = (int)$id;
- $db_prefix = elgg_get_config('dbprefix');
- switch ($type) {
- case 'annotations':
- case 'annotation':
- $table = $db_prefix . 'annotations';
- $type = 'annotation';
- break;
- case 'metadata':
- $table = $db_prefix . 'metadata';
- $type = 'metadata';
- break;
- default:
- return false;
- }
- $obj = _elgg_get_metastring_based_object_from_id($id, $type);
- if ($obj) {
- // Tidy up if memcache is enabled.
- // @todo only metadata is supported
- if ($type == 'metadata') {
- static $metabyname_memcache;
- if ((!$metabyname_memcache) && (is_memcache_available())) {
- $metabyname_memcache = new \ElggMemcache('metabyname_memcache');
- }
- if ($metabyname_memcache) {
- // @todo why name_id? is that even populated?
- $metabyname_memcache->delete("{$obj->entity_guid}:{$obj->name_id}");
- }
- }
- if ($obj->canEdit()) {
- // bc code for when we triggered 'delete', 'annotations' #4770
- $result = true;
- if ($type == "annotation") {
- $result = elgg_trigger_event('delete', 'annotations', $obj);
- if ($result === false) {
- elgg_deprecated_notice("Use the event 'delete', 'annotation'", 1.9);
- }
- }
- if (elgg_trigger_event('delete', $type, $obj) && $result) {
- return (bool)delete_data("DELETE FROM $table WHERE id = $id");
- }
- }
- }
- return false;
- }
- /**
- * Returns options to pass to elgg_get_entities() for metastrings operations.
- *
- * @param string $type Metastring type: annotation or metadata
- * @param array $options Options
- *
- * @return array
- * @access private
- */
- function _elgg_entities_get_metastrings_options($type, $options) {
- $valid_types = array('metadata', 'annotation');
- if (!in_array($type, $valid_types)) {
- return false;
- }
- // the options for annotations are singular (annotation_name) but the table
- // is plural (elgg_annotations) so rewrite for the table name.
- $n_table = ($type == 'annotation') ? 'annotations' : $type;
- $singulars = array("{$type}_name", "{$type}_value",
- "{$type}_name_value_pair", "{$type}_owner_guid");
- $options = _elgg_normalize_plural_options_array($options, $singulars);
- $clauses = _elgg_get_entity_metadata_where_sql('e', $n_table, $options["{$type}_names"],
- $options["{$type}_values"], $options["{$type}_name_value_pairs"],
- $options["{$type}_name_value_pairs_operator"], $options["{$type}_case_sensitive"],
- $options["order_by_{$type}"], $options["{$type}_owner_guids"]);
- if ($clauses) {
- // merge wheres to pass to elgg_get_entities()
- if (isset($options['wheres']) && !is_array($options['wheres'])) {
- $options['wheres'] = array($options['wheres']);
- } elseif (!isset($options['wheres'])) {
- $options['wheres'] = array();
- }
- $options['wheres'] = array_merge($options['wheres'], $clauses['wheres']);
- // merge joins to pass to elgg_get_entities()
- if (isset($options['joins']) && !is_array($options['joins'])) {
- $options['joins'] = array($options['joins']);
- } elseif (!isset($options['joins'])) {
- $options['joins'] = array();
- }
- $options['joins'] = array_merge($options['joins'], $clauses['joins']);
- if ($clauses['orders']) {
- $order_by_metadata = implode(", ", $clauses['orders']);
- if (isset($options['order_by']) && $options['order_by']) {
- $options['order_by'] = "$order_by_metadata, {$options['order_by']}";
- } else {
- $options['order_by'] = "$order_by_metadata, e.time_created DESC";
- }
- }
- }
- return $options;
- }
- /**
- * Metastring unit tests
- *
- * @param string $hook unit_test
- * @param string $type system
- * @param array $value Array of other tests
- *
- * @return array
- * @access private
- */
- function _elgg_metastrings_test($hook, $type, $value) {
- global $CONFIG;
- $value[] = $CONFIG->path . 'engine/tests/ElggCoreMetastringsTest.php';
- return $value;
- }
- return function(\Elgg\EventsService $events, \Elgg\HooksRegistrationService $hooks) {
- $hooks->registerHandler('unit_test', 'system', '_elgg_metastrings_test');
- };
|