MetadataTable.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  1. <?php
  2. namespace Elgg\Database;
  3. use Elgg\Database;
  4. use Elgg\Database\EntityTable;
  5. use Elgg\Database\MetastringsTable;
  6. use Elgg\EventsService as Events;
  7. use ElggSession as Session;
  8. use Elgg\Cache\MetadataCache as Cache;
  9. /**
  10. * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
  11. *
  12. * @access private
  13. *
  14. * @package Elgg.Core
  15. * @subpackage Database
  16. * @since 1.10.0
  17. */
  18. class MetadataTable {
  19. /** @var array */
  20. private $independents = array();
  21. /** @var Cache */
  22. private $cache;
  23. /** @var Database */
  24. private $db;
  25. /** @var EntityTable */
  26. private $entityTable;
  27. /** @var MetastringsTable */
  28. private $metastringsTable;
  29. /** @var Events */
  30. private $events;
  31. /** @var Session */
  32. private $session;
  33. /** @var string */
  34. private $table;
  35. /**
  36. * Constructor
  37. *
  38. * @param Cache $cache A cache for this table
  39. * @param Database $db The Elgg database
  40. * @param EntityTable $entityTable The entities table
  41. * @param Events $events The events registry
  42. * @param MetastringsTable $metastringsTable The metastrings table
  43. * @param Session $session The session
  44. */
  45. public function __construct(
  46. Cache $cache,
  47. Database $db,
  48. EntityTable $entityTable,
  49. Events $events,
  50. MetastringsTable $metastringsTable,
  51. Session $session) {
  52. $this->cache = $cache;
  53. $this->db = $db;
  54. $this->entityTable = $entityTable;
  55. $this->events = $events;
  56. $this->metastringsTable = $metastringsTable;
  57. $this->session = $session;
  58. $this->table = $this->db->getTablePrefix() . "metadata";
  59. }
  60. /**
  61. * Get a specific metadata object by its id.
  62. * If you want multiple metadata objects, use
  63. * {@link elgg_get_metadata()}.
  64. *
  65. * @param int $id The id of the metadata object being retrieved.
  66. *
  67. * @return \ElggMetadata|false false if not found
  68. */
  69. function get($id) {
  70. return _elgg_get_metastring_based_object_from_id($id, 'metadata');
  71. }
  72. /**
  73. * Deletes metadata using its ID.
  74. *
  75. * @param int $id The metadata ID to delete.
  76. * @return bool
  77. */
  78. function delete($id) {
  79. $metadata = $this->get($id);
  80. return $metadata ? $metadata->delete() : false;
  81. }
  82. /**
  83. * Create a new metadata object, or update an existing one.
  84. *
  85. * Metadata can be an array by setting allow_multiple to true, but it is an
  86. * indexed array with no control over the indexing.
  87. *
  88. * @param int $entity_guid The entity to attach the metadata to
  89. * @param string $name Name of the metadata
  90. * @param string $value Value of the metadata
  91. * @param string $value_type 'text', 'integer', or '' for automatic detection
  92. * @param int $owner_guid GUID of entity that owns the metadata. Default is logged in user.
  93. * @param int $access_id Default is ACCESS_PRIVATE
  94. * @param bool $allow_multiple Allow multiple values for one key. Default is false
  95. *
  96. * @return int|false id of metadata or false if failure
  97. */
  98. function create($entity_guid, $name, $value, $value_type = '', $owner_guid = 0,
  99. $access_id = ACCESS_PRIVATE, $allow_multiple = false) {
  100. $entity_guid = (int)$entity_guid;
  101. // name and value are encoded in add_metastring()
  102. $value_type = detect_extender_valuetype($value, $this->db->sanitizeString(trim($value_type)));
  103. $time = time();
  104. $owner_guid = (int)$owner_guid;
  105. $allow_multiple = (boolean)$allow_multiple;
  106. if (!isset($value)) {
  107. return false;
  108. }
  109. if ($owner_guid == 0) {
  110. $owner_guid = $this->session->getLoggedInUserGuid();
  111. }
  112. $access_id = (int)$access_id;
  113. $query = "SELECT * from {$this->table}"
  114. . " WHERE entity_guid = $entity_guid and name_id=" . $this->metastringsTable->getId($name) . " limit 1";
  115. $existing = $this->db->getDataRow($query);
  116. if ($existing && !$allow_multiple) {
  117. $id = (int)$existing->id;
  118. $result = $this->update($id, $name, $value, $value_type, $owner_guid, $access_id);
  119. if (!$result) {
  120. return false;
  121. }
  122. } else {
  123. // Support boolean types
  124. if (is_bool($value)) {
  125. $value = (int)$value;
  126. }
  127. // Add the metastrings
  128. $value_id = $this->metastringsTable->getId($value);
  129. if (!$value_id) {
  130. return false;
  131. }
  132. $name_id = $this->metastringsTable->getId($name);
  133. if (!$name_id) {
  134. return false;
  135. }
  136. // If ok then add it
  137. $query = "INSERT into {$this->table}"
  138. . " (entity_guid, name_id, value_id, value_type, owner_guid, time_created, access_id)"
  139. . " VALUES ($entity_guid, '$name_id','$value_id','$value_type', $owner_guid, $time, $access_id)";
  140. $id = $this->db->insertData($query);
  141. if ($id !== false) {
  142. $obj = $this->get($id);
  143. if ($this->events->trigger('create', 'metadata', $obj)) {
  144. $this->cache->clear($entity_guid);
  145. return $id;
  146. } else {
  147. $this->delete($id);
  148. }
  149. }
  150. }
  151. return $id;
  152. }
  153. /**
  154. * Update a specific piece of metadata.
  155. *
  156. * @param int $id ID of the metadata to update
  157. * @param string $name Metadata name
  158. * @param string $value Metadata value
  159. * @param string $value_type Value type
  160. * @param int $owner_guid Owner guid
  161. * @param int $access_id Access ID
  162. *
  163. * @return bool
  164. */
  165. function update($id, $name, $value, $value_type, $owner_guid, $access_id) {
  166. $id = (int)$id;
  167. if (!$md = $this->get($id)) {
  168. return false;
  169. }
  170. if (!$md->canEdit()) {
  171. return false;
  172. }
  173. // If memcached then we invalidate the cache for this entry
  174. static $metabyname_memcache;
  175. if ((!$metabyname_memcache) && (is_memcache_available())) {
  176. $metabyname_memcache = new \ElggMemcache('metabyname_memcache');
  177. }
  178. if ($metabyname_memcache) {
  179. // @todo fix memcache (name_id is not a property of \ElggMetadata)
  180. $metabyname_memcache->delete("{$md->entity_guid}:{$md->name_id}");
  181. }
  182. $value_type = detect_extender_valuetype($value, $this->db->sanitizeString(trim($value_type)));
  183. $owner_guid = (int)$owner_guid;
  184. if ($owner_guid == 0) {
  185. $owner_guid = $this->session->getLoggedInUserGuid();
  186. }
  187. $access_id = (int)$access_id;
  188. // Support boolean types (as integers)
  189. if (is_bool($value)) {
  190. $value = (int)$value;
  191. }
  192. $value_id = $this->metastringsTable->getId($value);
  193. if (!$value_id) {
  194. return false;
  195. }
  196. $name_id = $this->metastringsTable->getId($name);
  197. if (!$name_id) {
  198. return false;
  199. }
  200. // If ok then add it
  201. $query = "UPDATE {$this->table}"
  202. . " set name_id='$name_id', value_id='$value_id', value_type='$value_type', access_id=$access_id,"
  203. . " owner_guid=$owner_guid where id=$id";
  204. $result = $this->db->updateData($query);
  205. if ($result !== false) {
  206. $this->cache->clear($md->entity_guid);
  207. // @todo this event tells you the metadata has been updated, but does not
  208. // let you do anything about it. What is needed is a plugin hook before
  209. // the update that passes old and new values.
  210. $obj = $this->get($id);
  211. $this->events->trigger('update', 'metadata', $obj);
  212. }
  213. return $result;
  214. }
  215. /**
  216. * This function creates metadata from an associative array of "key => value" pairs.
  217. *
  218. * To achieve an array for a single key, pass in the same key multiple times with
  219. * allow_multiple set to true. This creates an indexed array. It does not support
  220. * associative arrays and there is no guarantee on the ordering in the array.
  221. *
  222. * @param int $entity_guid The entity to attach the metadata to
  223. * @param array $name_and_values Associative array - a value can be a string, number, bool
  224. * @param string $value_type 'text', 'integer', or '' for automatic detection
  225. * @param int $owner_guid GUID of entity that owns the metadata
  226. * @param int $access_id Default is ACCESS_PRIVATE
  227. * @param bool $allow_multiple Allow multiple values for one key. Default is false
  228. *
  229. * @return bool
  230. */
  231. function createFromArray($entity_guid, array $name_and_values, $value_type, $owner_guid,
  232. $access_id = ACCESS_PRIVATE, $allow_multiple = false) {
  233. foreach ($name_and_values as $k => $v) {
  234. $result = $this->create($entity_guid, $k, $v, $value_type, $owner_guid,
  235. $access_id, $allow_multiple);
  236. if (!$result) {
  237. return false;
  238. }
  239. }
  240. return true;
  241. }
  242. /**
  243. * Returns metadata. Accepts all elgg_get_entities() options for entity
  244. * restraints.
  245. *
  246. * @see elgg_get_entities
  247. *
  248. * @warning 1.7's find_metadata() didn't support limits and returned all metadata.
  249. * This function defaults to a limit of 25. There is probably not a reason
  250. * for you to return all metadata unless you're exporting an entity,
  251. * have other restraints in place, or are doing something horribly
  252. * wrong in your code.
  253. *
  254. * @param array $options Array in format:
  255. *
  256. * metadata_names => null|ARR metadata names
  257. * metadata_values => null|ARR metadata values
  258. * metadata_ids => null|ARR metadata ids
  259. * metadata_case_sensitive => BOOL Overall Case sensitive
  260. * metadata_owner_guids => null|ARR guids for metadata owners
  261. * metadata_created_time_lower => INT Lower limit for created time.
  262. * metadata_created_time_upper => INT Upper limit for created time.
  263. * metadata_calculation => STR Perform the MySQL function on the metadata values returned.
  264. * The "metadata_calculation" option causes this function to
  265. * return the result of performing a mathematical calculation on
  266. * all metadata that match the query instead of returning
  267. * \ElggMetadata objects.
  268. *
  269. * @return \ElggMetadata[]|mixed
  270. */
  271. function getAll(array $options = array()) {
  272. // @todo remove support for count shortcut - see #4393
  273. // support shortcut of 'count' => true for 'metadata_calculation' => 'count'
  274. if (isset($options['count']) && $options['count']) {
  275. $options['metadata_calculation'] = 'count';
  276. unset($options['count']);
  277. }
  278. $options['metastring_type'] = 'metadata';
  279. return _elgg_get_metastring_based_objects($options);
  280. }
  281. /**
  282. * Deletes metadata based on $options.
  283. *
  284. * @warning Unlike elgg_get_metadata() this will not accept an empty options array!
  285. * This requires at least one constraint: metadata_owner_guid(s),
  286. * metadata_name(s), metadata_value(s), or guid(s) must be set.
  287. *
  288. * @param array $options An options array. {@link elgg_get_metadata()}
  289. * @return bool|null true on success, false on failure, null if no metadata to delete.
  290. */
  291. function deleteAll(array $options) {
  292. if (!_elgg_is_valid_options_for_batch_operation($options, 'metadata')) {
  293. return false;
  294. }
  295. $options['metastring_type'] = 'metadata';
  296. $result = _elgg_batch_metastring_based_objects($options, 'elgg_batch_delete_callback', false);
  297. // This moved last in case an object's constructor sets metadata. Currently the batch
  298. // delete process has to create the entity to delete its metadata. See #5214
  299. $this->cache->invalidateByOptions($options);
  300. return $result;
  301. }
  302. /**
  303. * Disables metadata based on $options.
  304. *
  305. * @warning Unlike elgg_get_metadata() this will not accept an empty options array!
  306. *
  307. * @param array $options An options array. {@link elgg_get_metadata()}
  308. * @return bool|null true on success, false on failure, null if no metadata disabled.
  309. */
  310. function disableAll(array $options) {
  311. if (!_elgg_is_valid_options_for_batch_operation($options, 'metadata')) {
  312. return false;
  313. }
  314. $this->cache->invalidateByOptions($options);
  315. // if we can see hidden (disabled) we need to use the offset
  316. // otherwise we risk an infinite loop if there are more than 50
  317. $inc_offset = access_get_show_hidden_status();
  318. $options['metastring_type'] = 'metadata';
  319. return _elgg_batch_metastring_based_objects($options, 'elgg_batch_disable_callback', $inc_offset);
  320. }
  321. /**
  322. * Enables metadata based on $options.
  323. *
  324. * @warning Unlike elgg_get_metadata() this will not accept an empty options array!
  325. *
  326. * @warning In order to enable metadata, you must first use
  327. * {@link access_show_hidden_entities()}.
  328. *
  329. * @param array $options An options array. {@link elgg_get_metadata()}
  330. * @return bool|null true on success, false on failure, null if no metadata enabled.
  331. */
  332. function enableAll(array $options) {
  333. if (!$options || !is_array($options)) {
  334. return false;
  335. }
  336. $this->cache->invalidateByOptions($options);
  337. $options['metastring_type'] = 'metadata';
  338. return _elgg_batch_metastring_based_objects($options, 'elgg_batch_enable_callback');
  339. }
  340. /**
  341. * Returns entities based upon metadata. Also accepts all
  342. * options available to elgg_get_entities(). Supports
  343. * the singular option shortcut.
  344. *
  345. * @note Using metadata_names and metadata_values results in a
  346. * "names IN (...) AND values IN (...)" clause. This is subtly
  347. * differently than default multiple metadata_name_value_pairs, which use
  348. * "(name = value) AND (name = value)" clauses.
  349. *
  350. * When in doubt, use name_value_pairs.
  351. *
  352. * To ask for entities that do not have a metadata value, use a custom
  353. * where clause like this:
  354. *
  355. * $options['wheres'][] = "NOT EXISTS (
  356. * SELECT 1 FROM {$dbprefix}metadata md
  357. * WHERE md.entity_guid = e.guid
  358. * AND md.name_id = $name_metastring_id
  359. * AND md.value_id = $value_metastring_id)";
  360. *
  361. * Note the metadata name and value has been denormalized in the above example.
  362. *
  363. * @see elgg_get_entities
  364. *
  365. * @param array $options Array in format:
  366. *
  367. * metadata_names => null|ARR metadata names
  368. *
  369. * metadata_values => null|ARR metadata values
  370. *
  371. * metadata_name_value_pairs => null|ARR (
  372. * name => 'name',
  373. * value => 'value',
  374. * 'operand' => '=',
  375. * 'case_sensitive' => true
  376. * )
  377. * Currently if multiple values are sent via
  378. * an array (value => array('value1', 'value2')
  379. * the pair's operand will be forced to "IN".
  380. * If passing "IN" as the operand and a string as the value,
  381. * the value must be a properly quoted and escaped string.
  382. *
  383. * metadata_name_value_pairs_operator => null|STR The operator to use for combining
  384. * (name = value) OPERATOR (name = value); default AND
  385. *
  386. * metadata_case_sensitive => BOOL Overall Case sensitive
  387. *
  388. * order_by_metadata => null|ARR array(
  389. * 'name' => 'metadata_text1',
  390. * 'direction' => ASC|DESC,
  391. * 'as' => text|integer
  392. * )
  393. * Also supports array('name' => 'metadata_text1')
  394. *
  395. * metadata_owner_guids => null|ARR guids for metadata owners
  396. *
  397. * @return \ElggEntity[]|mixed If count, int. If not count, array. false on errors.
  398. */
  399. function getEntities(array $options = array()) {
  400. $defaults = array(
  401. 'metadata_names' => ELGG_ENTITIES_ANY_VALUE,
  402. 'metadata_values' => ELGG_ENTITIES_ANY_VALUE,
  403. 'metadata_name_value_pairs' => ELGG_ENTITIES_ANY_VALUE,
  404. 'metadata_name_value_pairs_operator' => 'AND',
  405. 'metadata_case_sensitive' => true,
  406. 'order_by_metadata' => array(),
  407. 'metadata_owner_guids' => ELGG_ENTITIES_ANY_VALUE,
  408. );
  409. $options = array_merge($defaults, $options);
  410. $singulars = array('metadata_name', 'metadata_value',
  411. 'metadata_name_value_pair', 'metadata_owner_guid');
  412. $options = _elgg_normalize_plural_options_array($options, $singulars);
  413. if (!$options = _elgg_entities_get_metastrings_options('metadata', $options)) {
  414. return false;
  415. }
  416. return $this->entityTable->getEntities($options);
  417. }
  418. /**
  419. * Returns metadata name and value SQL where for entities.
  420. * NB: $names and $values are not paired. Use $pairs for this.
  421. * Pairs default to '=' operand.
  422. *
  423. * This function is reused for annotations because the tables are
  424. * exactly the same.
  425. *
  426. * @param string $e_table Entities table name
  427. * @param string $n_table Normalized metastrings table name (Where entities,
  428. * values, and names are joined. annotations / metadata)
  429. * @param array|null $names Array of names
  430. * @param array|null $values Array of values
  431. * @param array|null $pairs Array of names / values / operands
  432. * @param string $pair_operator ("AND" or "OR") Operator to use to join the where clauses for pairs
  433. * @param bool $case_sensitive Case sensitive metadata names?
  434. * @param array|null $order_by_metadata Array of names / direction
  435. * @param array|null $owner_guids Array of owner GUIDs
  436. *
  437. * @return false|array False on fail, array('joins', 'wheres')
  438. * @access private
  439. */
  440. function getEntityMetadataWhereSql($e_table, $n_table, $names = null, $values = null,
  441. $pairs = null, $pair_operator = 'AND', $case_sensitive = true, $order_by_metadata = null,
  442. $owner_guids = null) {
  443. // short circuit if nothing requested
  444. // 0 is a valid (if not ill-conceived) metadata name.
  445. // 0 is also a valid metadata value for false, null, or 0
  446. // 0 is also a valid(ish) owner_guid
  447. if ((!$names && $names !== 0)
  448. && (!$values && $values !== 0)
  449. && (!$pairs && $pairs !== 0)
  450. && (!$owner_guids && $owner_guids !== 0)
  451. && !$order_by_metadata) {
  452. return '';
  453. }
  454. // join counter for incremental joins.
  455. $i = 1;
  456. // binary forces byte-to-byte comparision of strings, making
  457. // it case- and diacritical-mark- sensitive.
  458. // only supported on values.
  459. $binary = ($case_sensitive) ? ' BINARY ' : '';
  460. $access = _elgg_get_access_where_sql(array(
  461. 'table_alias' => 'n_table',
  462. 'guid_column' => 'entity_guid',
  463. ));
  464. $return = array (
  465. 'joins' => array (),
  466. 'wheres' => array(),
  467. 'orders' => array()
  468. );
  469. // will always want to join these tables if pulling metastrings.
  470. $return['joins'][] = "JOIN {$this->db->getTablePrefix()}{$n_table} n_table on
  471. {$e_table}.guid = n_table.entity_guid";
  472. $wheres = array();
  473. // get names wheres and joins
  474. $names_where = '';
  475. if ($names !== null) {
  476. if (!is_array($names)) {
  477. $names = array($names);
  478. }
  479. $sanitised_names = array();
  480. foreach ($names as $name) {
  481. // normalise to 0.
  482. if (!$name) {
  483. $name = '0';
  484. }
  485. $sanitised_names[] = '\'' . $this->db->sanitizeString($name) . '\'';
  486. }
  487. if ($names_str = implode(',', $sanitised_names)) {
  488. $return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msn on n_table.name_id = msn.id";
  489. $names_where = "(msn.string IN ($names_str))";
  490. }
  491. }
  492. // get values wheres and joins
  493. $values_where = '';
  494. if ($values !== null) {
  495. if (!is_array($values)) {
  496. $values = array($values);
  497. }
  498. $sanitised_values = array();
  499. foreach ($values as $value) {
  500. // normalize to 0
  501. if (!$value) {
  502. $value = 0;
  503. }
  504. $sanitised_values[] = '\'' . $this->db->sanitizeString($value) . '\'';
  505. }
  506. if ($values_str = implode(',', $sanitised_values)) {
  507. $return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msv on n_table.value_id = msv.id";
  508. $values_where = "({$binary}msv.string IN ($values_str))";
  509. }
  510. }
  511. if ($names_where && $values_where) {
  512. $wheres[] = "($names_where AND $values_where AND $access)";
  513. } elseif ($names_where) {
  514. $wheres[] = "($names_where AND $access)";
  515. } elseif ($values_where) {
  516. $wheres[] = "($values_where AND $access)";
  517. }
  518. // add pairs
  519. // pairs must be in arrays.
  520. if (is_array($pairs)) {
  521. // check if this is an array of pairs or just a single pair.
  522. if (isset($pairs['name']) || isset($pairs['value'])) {
  523. $pairs = array($pairs);
  524. }
  525. $pair_wheres = array();
  526. // @todo when the pairs are > 3 should probably split the query up to
  527. // denormalize the strings table.
  528. foreach ($pairs as $index => $pair) {
  529. // @todo move this elsewhere?
  530. // support shortcut 'n' => 'v' method.
  531. if (!is_array($pair)) {
  532. $pair = array(
  533. 'name' => $index,
  534. 'value' => $pair
  535. );
  536. }
  537. // must have at least a name and value
  538. if (!isset($pair['name']) || !isset($pair['value'])) {
  539. // @todo should probably return false.
  540. continue;
  541. }
  542. // case sensitivity can be specified per pair.
  543. // default to higher level setting.
  544. if (isset($pair['case_sensitive'])) {
  545. $pair_binary = ($pair['case_sensitive']) ? ' BINARY ' : '';
  546. } else {
  547. $pair_binary = $binary;
  548. }
  549. if (isset($pair['operand'])) {
  550. $operand = $this->db->sanitizeString($pair['operand']);
  551. } else {
  552. $operand = ' = ';
  553. }
  554. // for comparing
  555. $trimmed_operand = trim(strtolower($operand));
  556. $access = _elgg_get_access_where_sql(array(
  557. 'table_alias' => "n_table{$i}",
  558. 'guid_column' => 'entity_guid',
  559. ));
  560. // certain operands can't work well with strings that can be interpreted as numbers
  561. // for direct comparisons like IN, =, != we treat them as strings
  562. // gt/lt comparisons need to stay unencapsulated because strings '5' > '15'
  563. // see https://github.com/Elgg/Elgg/issues/7009
  564. $num_safe_operands = array('>', '<', '>=', '<=');
  565. $num_test_operand = trim(strtoupper($operand));
  566. if (is_numeric($pair['value']) && in_array($num_test_operand, $num_safe_operands)) {
  567. $value = $this->db->sanitizeString($pair['value']);
  568. } else if (is_bool($pair['value'])) {
  569. $value = (int)$pair['value'];
  570. } else if (is_array($pair['value'])) {
  571. $values_array = array();
  572. foreach ($pair['value'] as $pair_value) {
  573. if (is_numeric($pair_value) && !in_array($num_test_operand, $num_safe_operands)) {
  574. $values_array[] = $this->db->sanitizeString($pair_value);
  575. } else {
  576. $values_array[] = "'" . $this->db->sanitizeString($pair_value) . "'";
  577. }
  578. }
  579. if ($values_array) {
  580. $value = '(' . implode(', ', $values_array) . ')';
  581. }
  582. // @todo allow support for non IN operands with array of values.
  583. // will have to do more silly joins.
  584. $operand = 'IN';
  585. } else if ($trimmed_operand == 'in') {
  586. $value = "({$pair['value']})";
  587. } else {
  588. $value = "'" . $this->db->sanitizeString($pair['value']) . "'";
  589. }
  590. $name = $this->db->sanitizeString($pair['name']);
  591. // @todo The multiple joins are only needed when the operator is AND
  592. $return['joins'][] = "JOIN {$this->db->getTablePrefix()}{$n_table} n_table{$i}
  593. on {$e_table}.guid = n_table{$i}.entity_guid";
  594. $return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msn{$i}
  595. on n_table{$i}.name_id = msn{$i}.id";
  596. $return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msv{$i}
  597. on n_table{$i}.value_id = msv{$i}.id";
  598. $pair_wheres[] = "(msn{$i}.string = '$name' AND {$pair_binary}msv{$i}.string
  599. $operand $value AND $access)";
  600. $i++;
  601. }
  602. if ($where = implode(" $pair_operator ", $pair_wheres)) {
  603. $wheres[] = "($where)";
  604. }
  605. }
  606. // add owner_guids
  607. if ($owner_guids) {
  608. if (is_array($owner_guids)) {
  609. $sanitised = array_map('sanitise_int', $owner_guids);
  610. $owner_str = implode(',', $sanitised);
  611. } else {
  612. $owner_str = (int)$owner_guids;
  613. }
  614. $wheres[] = "(n_table.owner_guid IN ($owner_str))";
  615. }
  616. if ($where = implode(' AND ', $wheres)) {
  617. $return['wheres'][] = "($where)";
  618. }
  619. if (is_array($order_by_metadata)) {
  620. if ((count($order_by_metadata) > 0) && !isset($order_by_metadata[0])) {
  621. // singleton, so fix
  622. $order_by_metadata = array($order_by_metadata);
  623. }
  624. foreach ($order_by_metadata as $order_by) {
  625. if (is_array($order_by) && isset($order_by['name'])) {
  626. $name = $this->db->sanitizeString($order_by['name']);
  627. if (isset($order_by['direction'])) {
  628. $direction = $this->db->sanitizeString($order_by['direction']);
  629. } else {
  630. $direction = 'ASC';
  631. }
  632. $return['joins'][] = "JOIN {$this->db->getTablePrefix()}{$n_table} n_table{$i}
  633. on {$e_table}.guid = n_table{$i}.entity_guid";
  634. $return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msn{$i}
  635. on n_table{$i}.name_id = msn{$i}.id";
  636. $return['joins'][] = "JOIN {$this->metastringsTable->getTableName()} msv{$i}
  637. on n_table{$i}.value_id = msv{$i}.id";
  638. $access = _elgg_get_access_where_sql(array(
  639. 'table_alias' => "n_table{$i}",
  640. 'guid_column' => 'entity_guid',
  641. ));
  642. $return['wheres'][] = "(msn{$i}.string = '$name' AND $access)";
  643. if (isset($order_by['as']) && $order_by['as'] == 'integer') {
  644. $return['orders'][] = "CAST(msv{$i}.string AS SIGNED) $direction";
  645. } else {
  646. $return['orders'][] = "msv{$i}.string $direction";
  647. }
  648. $i++;
  649. }
  650. }
  651. }
  652. return $return;
  653. }
  654. /**
  655. * Get the URL for this metadata
  656. *
  657. * By default this links to the export handler in the current view.
  658. *
  659. * @param int $id Metadata ID
  660. *
  661. * @return mixed
  662. */
  663. function getUrl($id) {
  664. $extender = $this->get($id);
  665. return $extender ? $extender->getURL() : false;
  666. }
  667. /**
  668. * Mark entities with a particular type and subtype as having access permissions
  669. * that can be changed independently from their parent entity
  670. *
  671. * @param string $type The type - object, user, etc
  672. * @param string $subtype The subtype; all subtypes by default
  673. *
  674. * @return void
  675. */
  676. function registerMetadataAsIndependent($type, $subtype = '*') {
  677. if (!isset($this->independents[$type])) {
  678. $this->independents[$type] = array();
  679. }
  680. $this->independents[$type][$subtype] = true;
  681. }
  682. /**
  683. * Determines whether entities of a given type and subtype should not change
  684. * their metadata in line with their parent entity
  685. *
  686. * @param string $type The type - object, user, etc
  687. * @param string $subtype The entity subtype
  688. *
  689. * @return bool
  690. */
  691. function isMetadataIndependent($type, $subtype) {
  692. if (empty($this->independents[$type])) {
  693. return false;
  694. }
  695. return !empty($this->independents[$type][$subtype])
  696. || !empty($this->independents[$type]['*']);
  697. }
  698. /**
  699. * When an entity is updated, resets the access ID on all of its child metadata
  700. *
  701. * @param string $event The name of the event
  702. * @param string $object_type The type of object
  703. * @param \ElggEntity $object The entity itself
  704. *
  705. * @return true
  706. * @access private Set as private in 1.9.0
  707. */
  708. function handleUpdate($event, $object_type, $object) {
  709. if ($object instanceof \ElggEntity) {
  710. if (!$this->isMetadataIndependent($object->getType(), $object->getSubtype())) {
  711. $access_id = (int)$object->access_id;
  712. $guid = (int)$object->getGUID();
  713. $query = "update {$this->table} set access_id = {$access_id} where entity_guid = {$guid}";
  714. $this->db->updateData($query);
  715. }
  716. }
  717. return true;
  718. }
  719. }