EntityTable.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242
  1. <?php
  2. namespace Elgg\Database;
  3. use IncompleteEntityException;
  4. /**
  5. * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
  6. *
  7. * @access private
  8. *
  9. * @package Elgg.Core
  10. * @subpackage Database
  11. * @since 1.10.0
  12. */
  13. class EntityTable {
  14. /**
  15. * Global Elgg configuration
  16. *
  17. * @var \stdClass
  18. */
  19. private $CONFIG;
  20. /**
  21. * Constructor
  22. */
  23. public function __construct() {
  24. global $CONFIG;
  25. $this->CONFIG = $CONFIG;
  26. }
  27. /**
  28. * Returns a database row from the entities table.
  29. *
  30. * @tip Use get_entity() to return the fully loaded entity.
  31. *
  32. * @warning This will only return results if a) it exists, b) you have access to it.
  33. * see {@link _elgg_get_access_where_sql()}.
  34. *
  35. * @param int $guid The GUID of the object to extract
  36. *
  37. * @return \stdClass|false
  38. * @see entity_row_to_elggstar()
  39. * @access private
  40. */
  41. function getRow($guid) {
  42. if (!$guid) {
  43. return false;
  44. }
  45. $guid = (int) $guid;
  46. $access = _elgg_get_access_where_sql(array('table_alias' => ''));
  47. return _elgg_services()->db->getDataRow("SELECT * from {$this->CONFIG->dbprefix}entities where guid=$guid and $access");
  48. }
  49. /**
  50. * Create an Elgg* object from a given entity row.
  51. *
  52. * Handles loading all tables into the correct class.
  53. *
  54. * @param \stdClass $row The row of the entry in the entities table.
  55. *
  56. * @return \ElggEntity|false
  57. * @see get_entity_as_row()
  58. * @see add_subtype()
  59. * @see get_entity()
  60. * @access private
  61. *
  62. * @throws \ClassException|\InstallationException
  63. */
  64. function rowToElggStar($row) {
  65. if (!($row instanceof \stdClass)) {
  66. return $row;
  67. }
  68. if ((!isset($row->guid)) || (!isset($row->subtype))) {
  69. return $row;
  70. }
  71. $new_entity = false;
  72. // Create a memcache cache if we can
  73. static $newentity_cache;
  74. if ((!$newentity_cache) && (is_memcache_available())) {
  75. $newentity_cache = new \ElggMemcache('new_entity_cache');
  76. }
  77. if ($newentity_cache) {
  78. $new_entity = $newentity_cache->load($row->guid);
  79. }
  80. if ($new_entity) {
  81. return $new_entity;
  82. }
  83. // load class for entity if one is registered
  84. $classname = get_subtype_class_from_id($row->subtype);
  85. if ($classname != "") {
  86. if (class_exists($classname)) {
  87. $new_entity = new $classname($row);
  88. if (!($new_entity instanceof \ElggEntity)) {
  89. $msg = $classname . " is not a " . '\ElggEntity' . ".";
  90. throw new \ClassException($msg);
  91. }
  92. } else {
  93. error_log("Class '" . $classname . "' was not found, missing plugin?");
  94. }
  95. }
  96. if (!$new_entity) {
  97. //@todo Make this into a function
  98. switch ($row->type) {
  99. case 'object' :
  100. $new_entity = new \ElggObject($row);
  101. break;
  102. case 'user' :
  103. $new_entity = new \ElggUser($row);
  104. break;
  105. case 'group' :
  106. $new_entity = new \ElggGroup($row);
  107. break;
  108. case 'site' :
  109. $new_entity = new \ElggSite($row);
  110. break;
  111. default:
  112. $msg = "Entity type " . $row->type . " is not supported.";
  113. throw new \InstallationException($msg);
  114. }
  115. }
  116. // Cache entity if we have a cache available
  117. if (($newentity_cache) && ($new_entity)) {
  118. $newentity_cache->save($new_entity->guid, $new_entity);
  119. }
  120. return $new_entity;
  121. }
  122. /**
  123. * Loads and returns an entity object from a guid.
  124. *
  125. * @param int $guid The GUID of the entity
  126. * @param string $type The type of the entity. If given, even an existing entity with the given GUID
  127. * will not be returned unless its type matches.
  128. *
  129. * @return \ElggEntity The correct Elgg or custom object based upon entity type and subtype
  130. */
  131. function get($guid, $type = '') {
  132. // We could also use: if (!(int) $guid) { return false },
  133. // but that evaluates to a false positive for $guid = true.
  134. // This is a bit slower, but more thorough.
  135. if (!is_numeric($guid) || $guid === 0 || $guid === '0') {
  136. return false;
  137. }
  138. // Check local cache first
  139. $new_entity = _elgg_retrieve_cached_entity($guid);
  140. if ($new_entity) {
  141. if ($type) {
  142. return elgg_instanceof($new_entity, $type) ? $new_entity : false;
  143. }
  144. return $new_entity;
  145. }
  146. $options = [
  147. 'guid' => $guid,
  148. 'limit' => 1,
  149. 'site_guids' => ELGG_ENTITIES_ANY_VALUE, // for BC with get_entity, allow matching any site
  150. ];
  151. if ($type) {
  152. $options['type'] = $type;
  153. }
  154. $entities = $this->getEntities($options);
  155. return $entities ? $entities[0] : false;
  156. }
  157. /**
  158. * Does an entity exist?
  159. *
  160. * This function checks for the existence of an entity independent of access
  161. * permissions. It is useful for situations when a user cannot access an entity
  162. * and it must be determined whether entity has been deleted or the access level
  163. * has changed.
  164. *
  165. * @param int $guid The GUID of the entity
  166. *
  167. * @return bool
  168. */
  169. function exists($guid) {
  170. $guid = sanitize_int($guid);
  171. $query = "SELECT count(*) as total FROM {$this->CONFIG->dbprefix}entities WHERE guid = $guid";
  172. $result = _elgg_services()->db->getDataRow($query);
  173. if ($result->total == 0) {
  174. return false;
  175. } else {
  176. return true;
  177. }
  178. }
  179. /**
  180. * Enable an entity.
  181. *
  182. * @param int $guid GUID of entity to enable
  183. * @param bool $recursive Recursively enable all entities disabled with the entity?
  184. *
  185. * @return bool
  186. */
  187. function enable($guid, $recursive = true) {
  188. // Override access only visible entities
  189. $old_access_status = access_get_show_hidden_status();
  190. access_show_hidden_entities(true);
  191. $result = false;
  192. $entity = get_entity($guid);
  193. if ($entity) {
  194. $result = $entity->enable($recursive);
  195. }
  196. access_show_hidden_entities($old_access_status);
  197. return $result;
  198. }
  199. /**
  200. * Returns an array of entities with optional filtering.
  201. *
  202. * Entities are the basic unit of storage in Elgg. This function
  203. * provides the simplest way to get an array of entities. There
  204. * are many options available that can be passed to filter
  205. * what sorts of entities are returned.
  206. *
  207. * @tip To output formatted strings of entities, use {@link elgg_list_entities()} and
  208. * its cousins.
  209. *
  210. * @tip Plural arguments can be written as singular if only specifying a
  211. * single element. ('type' => 'object' vs 'types' => array('object')).
  212. *
  213. * @param array $options Array in format:
  214. *
  215. * types => null|STR entity type (type IN ('type1', 'type2')
  216. * Joined with subtypes by AND. See below)
  217. *
  218. * subtypes => null|STR entity subtype (SQL: subtype IN ('subtype1', 'subtype2))
  219. * Use ELGG_ENTITIES_NO_VALUE to match the default subtype.
  220. * Use ELGG_ENTITIES_ANY_VALUE to match any subtype.
  221. *
  222. * type_subtype_pairs => null|ARR (array('type' => 'subtype'))
  223. * array(
  224. * 'object' => array('blog', 'file'), // All objects with subtype of 'blog' or 'file'
  225. * 'user' => ELGG_ENTITY_ANY_VALUE, // All users irrespective of subtype
  226. * );
  227. *
  228. * guids => null|ARR Array of entity guids
  229. *
  230. * owner_guids => null|ARR Array of owner guids
  231. *
  232. * container_guids => null|ARR Array of container_guids
  233. *
  234. * site_guids => null (current_site)|ARR Array of site_guid
  235. *
  236. * order_by => null (time_created desc)|STR SQL order by clause
  237. *
  238. * reverse_order_by => BOOL Reverse the default order by clause
  239. *
  240. * limit => null (10)|INT SQL limit clause (0 means no limit)
  241. *
  242. * offset => null (0)|INT SQL offset clause
  243. *
  244. * created_time_lower => null|INT Created time lower boundary in epoch time
  245. *
  246. * created_time_upper => null|INT Created time upper boundary in epoch time
  247. *
  248. * modified_time_lower => null|INT Modified time lower boundary in epoch time
  249. *
  250. * modified_time_upper => null|INT Modified time upper boundary in epoch time
  251. *
  252. * count => true|false return a count instead of entities
  253. *
  254. * wheres => array() Additional where clauses to AND together
  255. *
  256. * joins => array() Additional joins
  257. *
  258. * preload_owners => bool (false) If set to true, this function will preload
  259. * all the owners of the returned entities resulting in better
  260. * performance if those owners need to be displayed
  261. *
  262. * preload_containers => bool (false) If set to true, this function will preload
  263. * all the containers of the returned entities resulting in better
  264. * performance if those containers need to be displayed
  265. *
  266. *
  267. * callback => string A callback function to pass each row through
  268. *
  269. * distinct => bool (true) If set to false, Elgg will drop the DISTINCT clause from
  270. * the MySQL query, which will improve performance in some situations.
  271. * Avoid setting this option without a full understanding of the underlying
  272. * SQL query Elgg creates.
  273. *
  274. * @return mixed If count, int. If not count, array. false on errors.
  275. * @see elgg_get_entities_from_metadata()
  276. * @see elgg_get_entities_from_relationship()
  277. * @see elgg_get_entities_from_access_id()
  278. * @see elgg_get_entities_from_annotations()
  279. * @see elgg_list_entities()
  280. */
  281. function getEntities(array $options = array()) {
  282. $defaults = array(
  283. 'types' => ELGG_ENTITIES_ANY_VALUE,
  284. 'subtypes' => ELGG_ENTITIES_ANY_VALUE,
  285. 'type_subtype_pairs' => ELGG_ENTITIES_ANY_VALUE,
  286. 'guids' => ELGG_ENTITIES_ANY_VALUE,
  287. 'owner_guids' => ELGG_ENTITIES_ANY_VALUE,
  288. 'container_guids' => ELGG_ENTITIES_ANY_VALUE,
  289. 'site_guids' => $this->CONFIG->site_guid,
  290. 'modified_time_lower' => ELGG_ENTITIES_ANY_VALUE,
  291. 'modified_time_upper' => ELGG_ENTITIES_ANY_VALUE,
  292. 'created_time_lower' => ELGG_ENTITIES_ANY_VALUE,
  293. 'created_time_upper' => ELGG_ENTITIES_ANY_VALUE,
  294. 'reverse_order_by' => false,
  295. 'order_by' => 'e.time_created desc',
  296. 'group_by' => ELGG_ENTITIES_ANY_VALUE,
  297. 'limit' => _elgg_services()->config->get('default_limit'),
  298. 'offset' => 0,
  299. 'count' => false,
  300. 'selects' => array(),
  301. 'wheres' => array(),
  302. 'joins' => array(),
  303. 'preload_owners' => false,
  304. 'preload_containers' => false,
  305. 'callback' => 'entity_row_to_elggstar',
  306. 'distinct' => true,
  307. // private API
  308. '__ElggBatch' => null,
  309. );
  310. $options = array_merge($defaults, $options);
  311. // can't use helper function with type_subtype_pair because
  312. // it's already an array...just need to merge it
  313. if (isset($options['type_subtype_pair'])) {
  314. if (isset($options['type_subtype_pairs'])) {
  315. $options['type_subtype_pairs'] = array_merge($options['type_subtype_pairs'],
  316. $options['type_subtype_pair']);
  317. } else {
  318. $options['type_subtype_pairs'] = $options['type_subtype_pair'];
  319. }
  320. }
  321. $singulars = array('type', 'subtype', 'guid', 'owner_guid', 'container_guid', 'site_guid');
  322. $options = _elgg_normalize_plural_options_array($options, $singulars);
  323. $options = $this->autoJoinTables($options);
  324. // evaluate where clauses
  325. if (!is_array($options['wheres'])) {
  326. $options['wheres'] = array($options['wheres']);
  327. }
  328. $wheres = $options['wheres'];
  329. $wheres[] = _elgg_get_entity_type_subtype_where_sql('e', $options['types'],
  330. $options['subtypes'], $options['type_subtype_pairs']);
  331. $wheres[] = _elgg_get_guid_based_where_sql('e.guid', $options['guids']);
  332. $wheres[] = _elgg_get_guid_based_where_sql('e.owner_guid', $options['owner_guids']);
  333. $wheres[] = _elgg_get_guid_based_where_sql('e.container_guid', $options['container_guids']);
  334. $wheres[] = _elgg_get_guid_based_where_sql('e.site_guid', $options['site_guids']);
  335. $wheres[] = _elgg_get_entity_time_where_sql('e', $options['created_time_upper'],
  336. $options['created_time_lower'], $options['modified_time_upper'], $options['modified_time_lower']);
  337. // see if any functions failed
  338. // remove empty strings on successful functions
  339. foreach ($wheres as $i => $where) {
  340. if ($where === false) {
  341. return false;
  342. } elseif (empty($where)) {
  343. unset($wheres[$i]);
  344. }
  345. }
  346. // remove identical where clauses
  347. $wheres = array_unique($wheres);
  348. // evaluate join clauses
  349. if (!is_array($options['joins'])) {
  350. $options['joins'] = array($options['joins']);
  351. }
  352. // remove identical join clauses
  353. $joins = array_unique($options['joins']);
  354. foreach ($joins as $i => $join) {
  355. if ($join === false) {
  356. return false;
  357. } elseif (empty($join)) {
  358. unset($joins[$i]);
  359. }
  360. }
  361. // evalutate selects
  362. if ($options['selects']) {
  363. $selects = '';
  364. foreach ($options['selects'] as $select) {
  365. $selects .= ", $select";
  366. }
  367. } else {
  368. $selects = '';
  369. }
  370. if (!$options['count']) {
  371. $distinct = $options['distinct'] ? "DISTINCT" : "";
  372. $query = "SELECT $distinct e.*{$selects} FROM {$this->CONFIG->dbprefix}entities e ";
  373. } else {
  374. // note: when DISTINCT unneeded, it's slightly faster to compute COUNT(*) than GUIDs
  375. $count_expr = $options['distinct'] ? "DISTINCT e.guid" : "*";
  376. $query = "SELECT COUNT($count_expr) as total FROM {$this->CONFIG->dbprefix}entities e ";
  377. }
  378. // add joins
  379. foreach ($joins as $j) {
  380. $query .= " $j ";
  381. }
  382. // add wheres
  383. $query .= ' WHERE ';
  384. foreach ($wheres as $w) {
  385. $query .= " $w AND ";
  386. }
  387. // Add access controls
  388. $query .= _elgg_get_access_where_sql();
  389. // reverse order by
  390. if ($options['reverse_order_by']) {
  391. $options['order_by'] = _elgg_sql_reverse_order_by_clause($options['order_by']);
  392. }
  393. if ($options['count']) {
  394. $total = _elgg_services()->db->getDataRow($query);
  395. return (int)$total->total;
  396. }
  397. if ($options['group_by']) {
  398. $query .= " GROUP BY {$options['group_by']}";
  399. }
  400. if ($options['order_by']) {
  401. $query .= " ORDER BY {$options['order_by']}";
  402. }
  403. if ($options['limit']) {
  404. $limit = sanitise_int($options['limit'], false);
  405. $offset = sanitise_int($options['offset'], false);
  406. $query .= " LIMIT $offset, $limit";
  407. }
  408. if ($options['callback'] === 'entity_row_to_elggstar') {
  409. $results = _elgg_fetch_entities_from_sql($query, $options['__ElggBatch']);
  410. } else {
  411. $results = _elgg_services()->db->getData($query, $options['callback']);
  412. }
  413. if (!$results) {
  414. // no results, no preloading
  415. return $results;
  416. }
  417. // populate entity and metadata caches, and prepare $entities for preloader
  418. $guids = array();
  419. foreach ($results as $item) {
  420. // A custom callback could result in items that aren't \ElggEntity's, so check for them
  421. if ($item instanceof \ElggEntity) {
  422. _elgg_cache_entity($item);
  423. // plugins usually have only settings
  424. if (!$item instanceof \ElggPlugin) {
  425. $guids[] = $item->guid;
  426. }
  427. }
  428. }
  429. // @todo Without this, recursive delete fails. See #4568
  430. reset($results);
  431. if ($guids) {
  432. // there were entities in the result set, preload metadata for them
  433. _elgg_services()->metadataCache->populateFromEntities($guids);
  434. }
  435. if (count($results) > 1) {
  436. $props_to_preload = [];
  437. if ($options['preload_owners']) {
  438. $props_to_preload[] = 'owner_guid';
  439. }
  440. if ($options['preload_containers']) {
  441. $props_to_preload[] = 'container_guid';
  442. }
  443. if ($props_to_preload) {
  444. // note, ElggEntityPreloaderIntegrationTest assumes it can swap out
  445. // the preloader after boot. If you inject this component at construction
  446. // time that unit test will break. :/
  447. _elgg_services()->entityPreloader->preload($results, $props_to_preload);
  448. }
  449. }
  450. return $results;
  451. }
  452. /**
  453. * Decorate getEntities() options in order to auto-join secondary tables where it's
  454. * safe to do so.
  455. *
  456. * @param array $options Options array in getEntities() after normalization
  457. * @return array
  458. */
  459. protected function autoJoinTables(array $options) {
  460. // we must be careful that the query doesn't specify any options that may join
  461. // tables or change the selected columns
  462. if (!is_array($options['types'])
  463. || count($options['types']) !== 1
  464. || !empty($options['selects'])
  465. || !empty($options['wheres'])
  466. || !empty($options['joins'])
  467. || $options['callback'] !== 'entity_row_to_elggstar'
  468. || $options['count']) {
  469. // Too dangerous to auto-join
  470. return $options;
  471. }
  472. $join_types = [
  473. // Each class must have a static getExternalAttributes() : array
  474. 'object' => 'ElggObject',
  475. 'user' => 'ElggUser',
  476. 'group' => 'ElggGroup',
  477. 'site' => 'ElggSite',
  478. ];
  479. // We use reset() because $options['types'] may not have a numeric key
  480. $type = reset($options['types']);
  481. if (empty($join_types[$type])) {
  482. return $options;
  483. }
  484. // Get the columns we'll need to select. We can't use st.* because the order_by
  485. // clause may reference "guid", which MySQL will complain about being ambiguous
  486. if (!is_callable([$join_types[$type], 'getExternalAttributes'])) {
  487. // for some reason can't get external attributes.
  488. return $options;
  489. }
  490. $attributes = $join_types[$type]::getExternalAttributes();
  491. foreach (array_keys($attributes) as $col) {
  492. $options['selects'][] = "st.$col";
  493. }
  494. // join the secondary table
  495. $options['joins'][] = "JOIN {$this->CONFIG->dbprefix}{$type}s_entity st ON (e.guid = st.guid)";
  496. return $options;
  497. }
  498. /**
  499. * Return entities from an SQL query generated by elgg_get_entities.
  500. *
  501. * @param string $sql
  502. * @param \ElggBatch $batch
  503. * @return \ElggEntity[]
  504. *
  505. * @access private
  506. * @throws \LogicException
  507. */
  508. function fetchFromSql($sql, \ElggBatch $batch = null) {
  509. static $plugin_subtype;
  510. if (null === $plugin_subtype) {
  511. $plugin_subtype = get_subtype_id('object', 'plugin');
  512. }
  513. // Keys are types, values are columns that, if present, suggest that the secondary
  514. // table is already JOINed. Note it's OK if guess incorrectly because entity load()
  515. // will fetch any missing attributes.
  516. $types_to_optimize = array(
  517. 'object' => 'title',
  518. 'user' => 'password',
  519. 'group' => 'name',
  520. 'site' => 'url',
  521. );
  522. $rows = _elgg_services()->db->getData($sql);
  523. // guids to look up in each type
  524. $lookup_types = array();
  525. // maps GUIDs to the $rows key
  526. $guid_to_key = array();
  527. if (isset($rows[0]->type, $rows[0]->subtype)
  528. && $rows[0]->type === 'object'
  529. && $rows[0]->subtype == $plugin_subtype) {
  530. // Likely the entire resultset is plugins, which have already been optimized
  531. // to JOIN the secondary table. In this case we allow retrieving from cache,
  532. // but abandon the extra queries.
  533. $types_to_optimize = array();
  534. }
  535. // First pass: use cache where possible, gather GUIDs that we're optimizing
  536. foreach ($rows as $i => $row) {
  537. if (empty($row->guid) || empty($row->type)) {
  538. throw new \LogicException('Entity row missing guid or type');
  539. }
  540. $entity = _elgg_retrieve_cached_entity($row->guid);
  541. if ($entity) {
  542. $entity->refresh($row);
  543. $rows[$i] = $entity;
  544. continue;
  545. }
  546. if (isset($types_to_optimize[$row->type])) {
  547. // check if row already looks JOINed.
  548. if (isset($row->{$types_to_optimize[$row->type]})) {
  549. // Row probably already contains JOINed secondary table. Don't make another query just
  550. // to pull data that's already there
  551. continue;
  552. }
  553. $lookup_types[$row->type][] = $row->guid;
  554. $guid_to_key[$row->guid] = $i;
  555. }
  556. }
  557. // Do secondary queries and merge rows
  558. if ($lookup_types) {
  559. $dbprefix = _elgg_services()->config->get('dbprefix');
  560. foreach ($lookup_types as $type => $guids) {
  561. $set = "(" . implode(',', $guids) . ")";
  562. $sql = "SELECT * FROM {$dbprefix}{$type}s_entity WHERE guid IN $set";
  563. $secondary_rows = _elgg_services()->db->getData($sql);
  564. if ($secondary_rows) {
  565. foreach ($secondary_rows as $secondary_row) {
  566. $key = $guid_to_key[$secondary_row->guid];
  567. // cast to arrays to merge then cast back
  568. $rows[$key] = (object)array_merge((array)$rows[$key], (array)$secondary_row);
  569. }
  570. }
  571. }
  572. }
  573. // Second pass to finish conversion
  574. foreach ($rows as $i => $row) {
  575. if ($row instanceof \ElggEntity) {
  576. continue;
  577. } else {
  578. try {
  579. $rows[$i] = entity_row_to_elggstar($row);
  580. } catch (IncompleteEntityException $e) {
  581. // don't let incomplete entities throw fatal errors
  582. unset($rows[$i]);
  583. // report incompletes to the batch process that spawned this query
  584. if ($batch) {
  585. $batch->reportIncompleteEntity($row);
  586. }
  587. }
  588. }
  589. }
  590. return $rows;
  591. }
  592. /**
  593. * Returns SQL where clause for type and subtype on main entity table
  594. *
  595. * @param string $table Entity table prefix as defined in SELECT...FROM entities $table
  596. * @param null|array $types Array of types or null if none.
  597. * @param null|array $subtypes Array of subtypes or null if none
  598. * @param null|array $pairs Array of pairs of types and subtypes
  599. *
  600. * @return false|string
  601. * @access private
  602. */
  603. function getEntityTypeSubtypeWhereSql($table, $types, $subtypes, $pairs) {
  604. // subtype depends upon type.
  605. if ($subtypes && !$types) {
  606. _elgg_services()->logger->warn("Cannot set subtypes without type.");
  607. return false;
  608. }
  609. // short circuit if nothing is requested
  610. if (!$types && !$subtypes && !$pairs) {
  611. return '';
  612. }
  613. // these are the only valid types for entities in elgg
  614. $valid_types = _elgg_services()->config->get('entity_types');
  615. // pairs override
  616. $wheres = array();
  617. if (!is_array($pairs)) {
  618. if (!is_array($types)) {
  619. $types = array($types);
  620. }
  621. if ($subtypes && !is_array($subtypes)) {
  622. $subtypes = array($subtypes);
  623. }
  624. // decrementer for valid types. Return false if no valid types
  625. $valid_types_count = count($types);
  626. $valid_subtypes_count = 0;
  627. // remove invalid types to get an accurate count of
  628. // valid types for the invalid subtype detection to use
  629. // below.
  630. // also grab the count of ALL subtypes on valid types to decrement later on
  631. // and check against.
  632. //
  633. // yes this is duplicating a foreach on $types.
  634. foreach ($types as $type) {
  635. if (!in_array($type, $valid_types)) {
  636. $valid_types_count--;
  637. unset($types[array_search($type, $types)]);
  638. } else {
  639. // do the checking (and decrementing) in the subtype section.
  640. $valid_subtypes_count += count($subtypes);
  641. }
  642. }
  643. // return false if nothing is valid.
  644. if (!$valid_types_count) {
  645. return false;
  646. }
  647. // subtypes are based upon types, so we need to look at each
  648. // type individually to get the right subtype id.
  649. foreach ($types as $type) {
  650. $subtype_ids = array();
  651. if ($subtypes) {
  652. foreach ($subtypes as $subtype) {
  653. // check that the subtype is valid
  654. if (!$subtype && ELGG_ENTITIES_NO_VALUE === $subtype) {
  655. // subtype value is 0
  656. $subtype_ids[] = ELGG_ENTITIES_NO_VALUE;
  657. } elseif (!$subtype) {
  658. // subtype is ignored.
  659. // this handles ELGG_ENTITIES_ANY_VALUE, '', and anything falsy that isn't 0
  660. continue;
  661. } else {
  662. $subtype_id = get_subtype_id($type, $subtype);
  663. if ($subtype_id) {
  664. $subtype_ids[] = $subtype_id;
  665. } else {
  666. $valid_subtypes_count--;
  667. _elgg_services()->logger->notice("Type-subtype '$type:$subtype' does not exist!");
  668. continue;
  669. }
  670. }
  671. }
  672. // return false if we're all invalid subtypes in the only valid type
  673. if ($valid_subtypes_count <= 0) {
  674. return false;
  675. }
  676. }
  677. if (is_array($subtype_ids) && count($subtype_ids)) {
  678. $subtype_ids_str = implode(',', $subtype_ids);
  679. $wheres[] = "({$table}.type = '$type' AND {$table}.subtype IN ($subtype_ids_str))";
  680. } else {
  681. $wheres[] = "({$table}.type = '$type')";
  682. }
  683. }
  684. } else {
  685. // using type/subtype pairs
  686. $valid_pairs_count = count($pairs);
  687. $valid_pairs_subtypes_count = 0;
  688. // same deal as above--we need to know how many valid types
  689. // and subtypes we have before hitting the subtype section.
  690. // also normalize the subtypes into arrays here.
  691. foreach ($pairs as $paired_type => $paired_subtypes) {
  692. if (!in_array($paired_type, $valid_types)) {
  693. $valid_pairs_count--;
  694. unset($pairs[array_search($paired_type, $pairs)]);
  695. } else {
  696. if ($paired_subtypes && !is_array($paired_subtypes)) {
  697. $pairs[$paired_type] = array($paired_subtypes);
  698. }
  699. $valid_pairs_subtypes_count += count($paired_subtypes);
  700. }
  701. }
  702. if ($valid_pairs_count <= 0) {
  703. return false;
  704. }
  705. foreach ($pairs as $paired_type => $paired_subtypes) {
  706. // this will always be an array because of line 2027, right?
  707. // no...some overly clever person can say pair => array('object' => null)
  708. if (is_array($paired_subtypes)) {
  709. $paired_subtype_ids = array();
  710. foreach ($paired_subtypes as $paired_subtype) {
  711. if (ELGG_ENTITIES_NO_VALUE === $paired_subtype
  712. || ($paired_subtype_id = get_subtype_id($paired_type, $paired_subtype))) {
  713. $paired_subtype_ids[] = (ELGG_ENTITIES_NO_VALUE === $paired_subtype) ?
  714. ELGG_ENTITIES_NO_VALUE : $paired_subtype_id;
  715. } else {
  716. $valid_pairs_subtypes_count--;
  717. _elgg_services()->logger->notice("Type-subtype '$paired_type:$paired_subtype' does not exist!");
  718. // return false if we're all invalid subtypes in the only valid type
  719. continue;
  720. }
  721. }
  722. // return false if there are no valid subtypes.
  723. if ($valid_pairs_subtypes_count <= 0) {
  724. return false;
  725. }
  726. if ($paired_subtype_ids_str = implode(',', $paired_subtype_ids)) {
  727. $wheres[] = "({$table}.type = '$paired_type'"
  728. . " AND {$table}.subtype IN ($paired_subtype_ids_str))";
  729. }
  730. } else {
  731. $wheres[] = "({$table}.type = '$paired_type')";
  732. }
  733. }
  734. }
  735. // pairs override the above. return false if they don't exist.
  736. if (is_array($wheres) && count($wheres)) {
  737. $where = implode(' OR ', $wheres);
  738. return "($where)";
  739. }
  740. return '';
  741. }
  742. /**
  743. * Returns SQL where clause for owner and containers.
  744. *
  745. * @param string $column Column name the guids should be checked against. Usually
  746. * best to provide in table.column format.
  747. * @param null|array $guids Array of GUIDs.
  748. *
  749. * @return false|string
  750. * @access private
  751. */
  752. function getGuidBasedWhereSql($column, $guids) {
  753. // short circuit if nothing requested
  754. // 0 is a valid guid
  755. if (!$guids && $guids !== 0) {
  756. return '';
  757. }
  758. // normalize and sanitise owners
  759. if (!is_array($guids)) {
  760. $guids = array($guids);
  761. }
  762. $guids_sanitized = array();
  763. foreach ($guids as $guid) {
  764. if ($guid !== ELGG_ENTITIES_NO_VALUE) {
  765. $guid = sanitise_int($guid);
  766. if (!$guid) {
  767. return false;
  768. }
  769. }
  770. $guids_sanitized[] = $guid;
  771. }
  772. $where = '';
  773. $guid_str = implode(',', $guids_sanitized);
  774. // implode(',', 0) returns 0.
  775. if ($guid_str !== false && $guid_str !== '') {
  776. $where = "($column IN ($guid_str))";
  777. }
  778. return $where;
  779. }
  780. /**
  781. * Returns SQL where clause for entity time limits.
  782. *
  783. * @param string $table Entity table prefix as defined in
  784. * SELECT...FROM entities $table
  785. * @param null|int $time_created_upper Time created upper limit
  786. * @param null|int $time_created_lower Time created lower limit
  787. * @param null|int $time_updated_upper Time updated upper limit
  788. * @param null|int $time_updated_lower Time updated lower limit
  789. *
  790. * @return false|string false on fail, string on success.
  791. * @access private
  792. */
  793. function getEntityTimeWhereSql($table, $time_created_upper = null,
  794. $time_created_lower = null, $time_updated_upper = null, $time_updated_lower = null) {
  795. $wheres = array();
  796. // exploit PHP's loose typing (quack) to check that they are INTs and not str cast to 0
  797. if ($time_created_upper && $time_created_upper == sanitise_int($time_created_upper)) {
  798. $wheres[] = "{$table}.time_created <= $time_created_upper";
  799. }
  800. if ($time_created_lower && $time_created_lower == sanitise_int($time_created_lower)) {
  801. $wheres[] = "{$table}.time_created >= $time_created_lower";
  802. }
  803. if ($time_updated_upper && $time_updated_upper == sanitise_int($time_updated_upper)) {
  804. $wheres[] = "{$table}.time_updated <= $time_updated_upper";
  805. }
  806. if ($time_updated_lower && $time_updated_lower == sanitise_int($time_updated_lower)) {
  807. $wheres[] = "{$table}.time_updated >= $time_updated_lower";
  808. }
  809. if (is_array($wheres) && count($wheres) > 0) {
  810. $where_str = implode(' AND ', $wheres);
  811. return "($where_str)";
  812. }
  813. return '';
  814. }
  815. /**
  816. * Gets entities based upon attributes in secondary tables.
  817. * Also accepts all options available to elgg_get_entities(),
  818. * elgg_get_entities_from_metadata(), and elgg_get_entities_from_relationship().
  819. *
  820. * @warning requires that the entity type be specified and there can only be one
  821. * type.
  822. *
  823. * @see elgg_get_entities
  824. * @see elgg_get_entities_from_metadata
  825. * @see elgg_get_entities_from_relationship
  826. *
  827. * @param array $options Array in format:
  828. *
  829. * attribute_name_value_pairs => ARR (
  830. * 'name' => 'name',
  831. * 'value' => 'value',
  832. * 'operand' => '=', (optional)
  833. * 'case_sensitive' => false (optional)
  834. * )
  835. * If multiple values are sent via
  836. * an array ('value' => array('value1', 'value2')
  837. * the pair's operand will be forced to "IN".
  838. *
  839. * attribute_name_value_pairs_operator => null|STR The operator to use for combining
  840. * (name = value) OPERATOR (name = value); default is AND
  841. *
  842. * @return \ElggEntity[]|mixed If count, int. If not count, array. false on errors.
  843. * @throws InvalidArgumentException
  844. * @todo Does not support ordering by attributes or using an attribute pair shortcut like this ('title' => 'foo')
  845. */
  846. function getEntitiesFromAttributes(array $options = array()) {
  847. $defaults = array(
  848. 'attribute_name_value_pairs' => ELGG_ENTITIES_ANY_VALUE,
  849. 'attribute_name_value_pairs_operator' => 'AND',
  850. );
  851. $options = array_merge($defaults, $options);
  852. $singulars = array('type', 'attribute_name_value_pair');
  853. $options = _elgg_normalize_plural_options_array($options, $singulars);
  854. $clauses = _elgg_get_entity_attribute_where_sql($options);
  855. if ($clauses) {
  856. // merge wheres to pass to elgg_get_entities()
  857. if (isset($options['wheres']) && !is_array($options['wheres'])) {
  858. $options['wheres'] = array($options['wheres']);
  859. } elseif (!isset($options['wheres'])) {
  860. $options['wheres'] = array();
  861. }
  862. $options['wheres'] = array_merge($options['wheres'], $clauses['wheres']);
  863. // merge joins to pass to elgg_get_entities()
  864. if (isset($options['joins']) && !is_array($options['joins'])) {
  865. $options['joins'] = array($options['joins']);
  866. } elseif (!isset($options['joins'])) {
  867. $options['joins'] = array();
  868. }
  869. $options['joins'] = array_merge($options['joins'], $clauses['joins']);
  870. }
  871. return elgg_get_entities_from_relationship($options);
  872. }
  873. /**
  874. * Get the join and where clauses for working with entity attributes
  875. *
  876. * @return false|array False on fail, array('joins', 'wheres')
  877. * @access private
  878. * @throws InvalidArgumentException
  879. */
  880. function getEntityAttributeWhereSql(array $options = array()) {
  881. if (!isset($options['types'])) {
  882. throw new \InvalidArgumentException("The entity type must be defined for elgg_get_entities_from_attributes()");
  883. }
  884. if (is_array($options['types']) && count($options['types']) !== 1) {
  885. throw new \InvalidArgumentException("Only one type can be passed to elgg_get_entities_from_attributes()");
  886. }
  887. // type can be passed as string or array
  888. $type = $options['types'];
  889. if (is_array($type)) {
  890. $type = $type[0];
  891. }
  892. // @todo the types should be defined somewhere (as constant on \ElggEntity?)
  893. if (!in_array($type, array('group', 'object', 'site', 'user'))) {
  894. throw new \InvalidArgumentException("Invalid type '$type' passed to elgg_get_entities_from_attributes()");
  895. }
  896. $type_table = "{$this->CONFIG->dbprefix}{$type}s_entity";
  897. $return = array(
  898. 'joins' => array(),
  899. 'wheres' => array(),
  900. );
  901. // short circuit if nothing requested
  902. if ($options['attribute_name_value_pairs'] == ELGG_ENTITIES_ANY_VALUE) {
  903. return $return;
  904. }
  905. if (!is_array($options['attribute_name_value_pairs'])) {
  906. throw new \InvalidArgumentException("attribute_name_value_pairs must be an array for elgg_get_entities_from_attributes()");
  907. }
  908. $wheres = array();
  909. // check if this is an array of pairs or just a single pair.
  910. $pairs = $options['attribute_name_value_pairs'];
  911. if (isset($pairs['name']) || isset($pairs['value'])) {
  912. $pairs = array($pairs);
  913. }
  914. $pair_wheres = array();
  915. foreach ($pairs as $index => $pair) {
  916. // must have at least a name and value
  917. if (!isset($pair['name']) || !isset($pair['value'])) {
  918. continue;
  919. }
  920. if (isset($pair['operand'])) {
  921. $operand = sanitize_string($pair['operand']);
  922. } else {
  923. $operand = '=';
  924. }
  925. if (is_numeric($pair['value'])) {
  926. $value = sanitize_string($pair['value']);
  927. } else if (is_array($pair['value'])) {
  928. $values_array = array();
  929. foreach ($pair['value'] as $pair_value) {
  930. if (is_numeric($pair_value)) {
  931. $values_array[] = sanitize_string($pair_value);
  932. } else {
  933. $values_array[] = "'" . sanitize_string($pair_value) . "'";
  934. }
  935. }
  936. $operand = 'IN';
  937. if ($values_array) {
  938. $value = '(' . implode(', ', $values_array) . ')';
  939. }
  940. } else {
  941. $value = "'" . sanitize_string($pair['value']) . "'";
  942. }
  943. $name = sanitize_string($pair['name']);
  944. // case sensitivity can be specified per pair
  945. $pair_binary = '';
  946. if (isset($pair['case_sensitive'])) {
  947. $pair_binary = ($pair['case_sensitive']) ? 'BINARY ' : '';
  948. }
  949. $pair_wheres[] = "({$pair_binary}type_table.$name $operand $value)";
  950. }
  951. if ($where = implode(" {$options['attribute_name_value_pairs_operator']} ", $pair_wheres)) {
  952. $return['wheres'][] = "($where)";
  953. $return['joins'][] = "JOIN $type_table type_table ON e.guid = type_table.guid";
  954. }
  955. return $return;
  956. }
  957. /**
  958. * Returns a list of months in which entities were updated or created.
  959. *
  960. * @tip Use this to generate a list of archives by month for when entities were added or updated.
  961. *
  962. * @todo document how to pass in array for $subtype
  963. *
  964. * @warning Months are returned in the form YYYYMM.
  965. *
  966. * @param string $type The type of entity
  967. * @param string $subtype The subtype of entity
  968. * @param int $container_guid The container GUID that the entities belong to
  969. * @param int $site_guid The site GUID
  970. * @param string $order_by Order_by SQL order by clause
  971. *
  972. * @return array|false Either an array months as YYYYMM, or false on failure
  973. */
  974. function getDates($type = '', $subtype = '', $container_guid = 0, $site_guid = 0,
  975. $order_by = 'time_created') {
  976. $site_guid = (int) $site_guid;
  977. if ($site_guid == 0) {
  978. $site_guid = $this->CONFIG->site_guid;
  979. }
  980. $where = array();
  981. if ($type != "") {
  982. $type = sanitise_string($type);
  983. $where[] = "type='$type'";
  984. }
  985. if (is_array($subtype)) {
  986. $tempwhere = "";
  987. if (sizeof($subtype)) {
  988. foreach ($subtype as $typekey => $subtypearray) {
  989. foreach ($subtypearray as $subtypeval) {
  990. $typekey = sanitise_string($typekey);
  991. if (!empty($subtypeval)) {
  992. if (!$subtypeval = (int) get_subtype_id($typekey, $subtypeval)) {
  993. return false;
  994. }
  995. } else {
  996. $subtypeval = 0;
  997. }
  998. if (!empty($tempwhere)) {
  999. $tempwhere .= " or ";
  1000. }
  1001. $tempwhere .= "(type = '{$typekey}' and subtype = {$subtypeval})";
  1002. }
  1003. }
  1004. }
  1005. if (!empty($tempwhere)) {
  1006. $where[] = "({$tempwhere})";
  1007. }
  1008. } else {
  1009. if ($subtype) {
  1010. if (!$subtype_id = get_subtype_id($type, $subtype)) {
  1011. return false;
  1012. } else {
  1013. $where[] = "subtype=$subtype_id";
  1014. }
  1015. }
  1016. }
  1017. if ($container_guid !== 0) {
  1018. if (is_array($container_guid)) {
  1019. foreach ($container_guid as $key => $val) {
  1020. $container_guid[$key] = (int) $val;
  1021. }
  1022. $where[] = "container_guid in (" . implode(",", $container_guid) . ")";
  1023. } else {
  1024. $container_guid = (int) $container_guid;
  1025. $where[] = "container_guid = {$container_guid}";
  1026. }
  1027. }
  1028. if ($site_guid > 0) {
  1029. $where[] = "site_guid = {$site_guid}";
  1030. }
  1031. $where[] = _elgg_get_access_where_sql(array('table_alias' => ''));
  1032. $sql = "SELECT DISTINCT EXTRACT(YEAR_MONTH FROM FROM_UNIXTIME(time_created)) AS yearmonth
  1033. FROM {$this->CONFIG->dbprefix}entities where ";
  1034. foreach ($where as $w) {
  1035. $sql .= " $w and ";
  1036. }
  1037. $sql .= "1=1 ORDER BY $order_by";
  1038. if ($result = _elgg_services()->db->getData($sql)) {
  1039. $endresult = array();
  1040. foreach ($result as $res) {
  1041. $endresult[] = $res->yearmonth;
  1042. }
  1043. return $endresult;
  1044. }
  1045. return false;
  1046. }
  1047. /**
  1048. * Update the last_action column in the entities table for $guid.
  1049. *
  1050. * @warning This is different to time_updated. Time_updated is automatically set,
  1051. * while last_action is only set when explicitly called.
  1052. *
  1053. * @param int $guid Entity annotation|relationship action carried out on
  1054. * @param int $posted Timestamp of last action
  1055. *
  1056. * @return bool
  1057. * @access private
  1058. */
  1059. function updateLastAction($guid, $posted = null) {
  1060. $guid = (int)$guid;
  1061. $posted = (int)$posted;
  1062. if (!$posted) {
  1063. $posted = time();
  1064. }
  1065. if ($guid) {
  1066. //now add to the river updated table
  1067. $query = "UPDATE {$this->CONFIG->dbprefix}entities SET last_action = {$posted} WHERE guid = {$guid}";
  1068. $result = _elgg_services()->db->updateData($query);
  1069. if ($result) {
  1070. return true;
  1071. } else {
  1072. return false;
  1073. }
  1074. } else {
  1075. return false;
  1076. }
  1077. }
  1078. }