Annotations.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. <?php
  2. namespace Elgg\Database;
  3. /**
  4. * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
  5. *
  6. * @access private
  7. *
  8. * @package Elgg.Core
  9. * @subpackage Database
  10. * @since 1.10.0
  11. */
  12. class Annotations {
  13. /**
  14. * Global Elgg configuration
  15. *
  16. * @var \stdClass
  17. */
  18. private $CONFIG;
  19. /**
  20. * Constructor
  21. */
  22. public function __construct() {
  23. global $CONFIG;
  24. $this->CONFIG = $CONFIG;
  25. }
  26. /**
  27. * Get a specific annotation by its id.
  28. * If you want multiple annotation objects, use
  29. * {@link elgg_get_annotations()}.
  30. *
  31. * @param int $id The id of the annotation object being retrieved.
  32. *
  33. * @return \ElggAnnotation|false
  34. */
  35. function get($id) {
  36. return _elgg_get_metastring_based_object_from_id($id, 'annotation');
  37. }
  38. /**
  39. * Deletes an annotation using its ID.
  40. *
  41. * @param int $id The annotation ID to delete.
  42. * @return bool
  43. */
  44. function delete($id) {
  45. $annotation = elgg_get_annotation_from_id($id);
  46. if (!$annotation) {
  47. return false;
  48. }
  49. return $annotation->delete();
  50. }
  51. /**
  52. * Create a new annotation.
  53. *
  54. * @param int $entity_guid GUID of entity to be annotated
  55. * @param string $name Name of annotation
  56. * @param string $value Value of annotation
  57. * @param string $value_type Type of value (default is auto detection)
  58. * @param int $owner_guid Owner of annotation (default is logged in user)
  59. * @param int $access_id Access level of annotation
  60. *
  61. * @return int|bool id on success or false on failure
  62. */
  63. function create($entity_guid, $name, $value, $value_type = '', $owner_guid = 0, $access_id = ACCESS_PRIVATE) {
  64. $result = false;
  65. $entity_guid = (int)$entity_guid;
  66. $value_type = detect_extender_valuetype($value, $value_type);
  67. $owner_guid = (int)$owner_guid;
  68. if ($owner_guid == 0) {
  69. $owner_guid = _elgg_services()->session->getLoggedInUserGuid();
  70. }
  71. $access_id = (int)$access_id;
  72. $time = time();
  73. $value_id = elgg_get_metastring_id($value);
  74. if (!$value_id) {
  75. return false;
  76. }
  77. $name_id = elgg_get_metastring_id($name);
  78. if (!$name_id) {
  79. return false;
  80. }
  81. // @todo we don't check that the entity is loaded which means the user may
  82. // not have access to the entity
  83. $entity = get_entity($entity_guid);
  84. if (_elgg_services()->events->trigger('annotate', $entity->type, $entity)) {
  85. $result = _elgg_services()->db->insertData("INSERT INTO {$this->CONFIG->dbprefix}annotations
  86. (entity_guid, name_id, value_id, value_type, owner_guid, time_created, access_id) VALUES
  87. ($entity_guid, $name_id, $value_id, '$value_type', $owner_guid, $time, $access_id)");
  88. if ($result !== false) {
  89. $obj = elgg_get_annotation_from_id($result);
  90. if (_elgg_services()->events->trigger('create', 'annotation', $obj)) {
  91. return $result;
  92. } else {
  93. // plugin returned false to reject annotation
  94. elgg_delete_annotation_by_id($result);
  95. return false;
  96. }
  97. }
  98. }
  99. return $result;
  100. }
  101. /**
  102. * Update an annotation.
  103. *
  104. * @param int $annotation_id Annotation ID
  105. * @param string $name Name of annotation
  106. * @param string $value Value of annotation
  107. * @param string $value_type Type of value
  108. * @param int $owner_guid Owner of annotation
  109. * @param int $access_id Access level of annotation
  110. *
  111. * @return bool
  112. */
  113. function update($annotation_id, $name, $value, $value_type, $owner_guid, $access_id) {
  114. $annotation_id = (int)$annotation_id;
  115. $annotation = elgg_get_annotation_from_id($annotation_id);
  116. if (!$annotation) {
  117. return false;
  118. }
  119. if (!$annotation->canEdit()) {
  120. return false;
  121. }
  122. $name = trim($name);
  123. $value_type = detect_extender_valuetype($value, $value_type);
  124. $owner_guid = (int)$owner_guid;
  125. if ($owner_guid == 0) {
  126. $owner_guid = _elgg_services()->session->getLoggedInUserGuid();
  127. }
  128. $access_id = (int)$access_id;
  129. $value_id = elgg_get_metastring_id($value);
  130. if (!$value_id) {
  131. return false;
  132. }
  133. $name_id = elgg_get_metastring_id($name);
  134. if (!$name_id) {
  135. return false;
  136. }
  137. $result = _elgg_services()->db->updateData("UPDATE {$this->CONFIG->dbprefix}annotations
  138. SET name_id = $name_id, value_id = $value_id, value_type = '$value_type',
  139. access_id = $access_id, owner_guid = $owner_guid
  140. WHERE id = $annotation_id");
  141. if ($result !== false) {
  142. // @todo add plugin hook that sends old and new annotation information before db access
  143. $obj = elgg_get_annotation_from_id($annotation_id);
  144. _elgg_services()->events->trigger('update', 'annotation', $obj);
  145. }
  146. return $result;
  147. }
  148. /**
  149. * Returns annotations. Accepts all elgg_get_entities() options for entity
  150. * restraints.
  151. *
  152. * @see elgg_get_entities
  153. *
  154. * @param array $options Array in format:
  155. *
  156. * annotation_names => null|ARR Annotation names
  157. * annotation_values => null|ARR Annotation values
  158. * annotation_ids => null|ARR annotation ids
  159. * annotation_case_sensitive => BOOL Overall Case sensitive
  160. * annotation_owner_guids => null|ARR guids for annotation owners
  161. * annotation_created_time_lower => INT Lower limit for created time.
  162. * annotation_created_time_upper => INT Upper limit for created time.
  163. * annotation_calculation => STR Perform the MySQL function on the annotation values returned.
  164. * Do not confuse this "annotation_calculation" option with the
  165. * "calculation" option to elgg_get_entities_from_annotation_calculation().
  166. * The "annotation_calculation" option causes this function to
  167. * return the result of performing a mathematical calculation on
  168. * all annotations that match the query instead of \ElggAnnotation
  169. * objects.
  170. * See the docs for elgg_get_entities_from_annotation_calculation()
  171. * for the proper use of the "calculation" option.
  172. *
  173. *
  174. * @return \ElggAnnotation[]|mixed
  175. */
  176. function find(array $options = array()) {
  177. // support shortcut of 'count' => true for 'annotation_calculation' => 'count'
  178. if (isset($options['count']) && $options['count']) {
  179. $options['annotation_calculation'] = 'count';
  180. unset($options['count']);
  181. }
  182. $options['metastring_type'] = 'annotations';
  183. return _elgg_get_metastring_based_objects($options);
  184. }
  185. /**
  186. * Deletes annotations based on $options.
  187. *
  188. * @warning Unlike elgg_get_annotations() this will not accept an empty options array!
  189. * This requires at least one constraint: annotation_owner_guid(s),
  190. * annotation_name(s), annotation_value(s), or guid(s) must be set.
  191. *
  192. * @param array $options An options array. {@link elgg_get_annotations()}
  193. * @return bool|null true on success, false on failure, null if no annotations to delete.
  194. */
  195. function deleteAll(array $options) {
  196. if (!_elgg_is_valid_options_for_batch_operation($options, 'annotation')) {
  197. return false;
  198. }
  199. $options['metastring_type'] = 'annotations';
  200. return _elgg_batch_metastring_based_objects($options, 'elgg_batch_delete_callback', false);
  201. }
  202. /**
  203. * Disables annotations based on $options.
  204. *
  205. * @warning Unlike elgg_get_annotations() this will not accept an empty options array!
  206. *
  207. * @param array $options An options array. {@link elgg_get_annotations()}
  208. * @return bool|null true on success, false on failure, null if no annotations disabled.
  209. */
  210. function disableAll(array $options) {
  211. if (!_elgg_is_valid_options_for_batch_operation($options, 'annotation')) {
  212. return false;
  213. }
  214. // if we can see hidden (disabled) we need to use the offset
  215. // otherwise we risk an infinite loop if there are more than 50
  216. $inc_offset = access_get_show_hidden_status();
  217. $options['metastring_type'] = 'annotations';
  218. return _elgg_batch_metastring_based_objects($options, 'elgg_batch_disable_callback', $inc_offset);
  219. }
  220. /**
  221. * Enables annotations based on $options.
  222. *
  223. * @warning Unlike elgg_get_annotations() this will not accept an empty options array!
  224. *
  225. * @warning In order to enable annotations, you must first use
  226. * {@link access_show_hidden_entities()}.
  227. *
  228. * @param array $options An options array. {@link elgg_get_annotations()}
  229. * @return bool|null true on success, false on failure, null if no metadata enabled.
  230. */
  231. function enableAll(array $options) {
  232. if (!$options || !is_array($options)) {
  233. return false;
  234. }
  235. $options['metastring_type'] = 'annotations';
  236. return _elgg_batch_metastring_based_objects($options, 'elgg_batch_enable_callback');
  237. }
  238. /**
  239. * Returns entities based upon annotations. Also accepts all options available
  240. * to elgg_get_entities() and elgg_get_entities_from_metadata().
  241. *
  242. * @see elgg_get_entities
  243. * @see elgg_get_entities_from_metadata
  244. *
  245. * @param array $options Array in format:
  246. *
  247. * annotation_names => null|ARR annotations names
  248. *
  249. * annotation_values => null|ARR annotations values
  250. *
  251. * annotation_name_value_pairs => null|ARR (name = 'name', value => 'value',
  252. * 'operator' => '=', 'case_sensitive' => true) entries.
  253. * Currently if multiple values are sent via an array (value => array('value1', 'value2')
  254. * the pair's operator will be forced to "IN".
  255. *
  256. * annotation_name_value_pairs_operator => null|STR The operator to use for combining
  257. * (name = value) OPERATOR (name = value); default AND
  258. *
  259. * annotation_case_sensitive => BOOL Overall Case sensitive
  260. *
  261. * order_by_annotation => null|ARR (array('name' => 'annotation_text1', 'direction' => ASC|DESC,
  262. * 'as' => text|integer),
  263. *
  264. * Also supports array('name' => 'annotation_text1')
  265. *
  266. * annotation_owner_guids => null|ARR guids for annotaiton owners
  267. *
  268. * @return mixed If count, int. If not count, array. false on errors.
  269. */
  270. function getEntities(array $options = array()) {
  271. $defaults = array(
  272. 'annotation_names' => ELGG_ENTITIES_ANY_VALUE,
  273. 'annotation_values' => ELGG_ENTITIES_ANY_VALUE,
  274. 'annotation_name_value_pairs' => ELGG_ENTITIES_ANY_VALUE,
  275. 'annotation_name_value_pairs_operator' => 'AND',
  276. 'annotation_case_sensitive' => true,
  277. 'order_by_annotation' => array(),
  278. 'annotation_created_time_lower' => ELGG_ENTITIES_ANY_VALUE,
  279. 'annotation_created_time_upper' => ELGG_ENTITIES_ANY_VALUE,
  280. 'annotation_owner_guids' => ELGG_ENTITIES_ANY_VALUE,
  281. );
  282. $options = array_merge($defaults, $options);
  283. $singulars = array('annotation_name', 'annotation_value',
  284. 'annotation_name_value_pair', 'annotation_owner_guid');
  285. $options = _elgg_normalize_plural_options_array($options, $singulars);
  286. $options = _elgg_entities_get_metastrings_options('annotation', $options);
  287. if (!$options) {
  288. return false;
  289. }
  290. // because of performance issues support for ordering by maxtime has been dropped
  291. // @see https://github.com/Elgg/Elgg/issues/6638
  292. if (isset($options['order_by']) && preg_match('~\bmaxtime\b~i', $options['order_by'])) {
  293. // check if the user provided maxtime
  294. $deprecated = true;
  295. if (isset($options['selects'])) {
  296. $selects = $options['selects'];
  297. if (!is_array($selects)) {
  298. $selects = array($selects);
  299. }
  300. foreach ($selects as $select) {
  301. if (preg_match('~\bmaxtime\b~i', $options['order_by'])) {
  302. $deprecated = false;
  303. break;
  304. }
  305. }
  306. }
  307. // the user didn't provide maxtime
  308. if ($deprecated) {
  309. // special sorting for annotations
  310. elgg_deprecated_notice(__FUNCTION__ . ": no longer orders by annotations by default. If you order"
  311. . " by maxtime, you must provide that column via \$options['selects']. See"
  312. . " https://github.com/Elgg/Elgg/issues/6638#issuecomment-41562034", "1.10");
  313. $options['selects'][] = "MAX(n_table.time_created) AS maxtime";
  314. $options['group_by'] = 'n_table.entity_guid';
  315. }
  316. }
  317. $time_wheres = _elgg_get_entity_time_where_sql('a', $options['annotation_created_time_upper'],
  318. $options['annotation_created_time_lower']);
  319. if ($time_wheres) {
  320. $options['wheres'] = array_merge($options['wheres'], $time_wheres);
  321. }
  322. return elgg_get_entities_from_metadata($options);
  323. }
  324. /**
  325. * Get entities ordered by a mathematical calculation on annotation values
  326. *
  327. * @tip Note that this function uses { @link elgg_get_annotations() } to return a list of entities ordered by a mathematical
  328. * calculation on annotation values, and { @link elgg_get_entities_from_annotations() } to return a count of entities
  329. * if $options['count'] is set to a truthy value
  330. *
  331. * @param array $options An options array:
  332. * 'calculation' => The calculation to use. Must be a valid MySQL function.
  333. * Defaults to sum. Result selected as 'annotation_calculation'.
  334. * Don't confuse this "calculation" option with the
  335. * "annotation_calculation" option to elgg_get_annotations().
  336. * This "calculation" option is applied to each entity's set of
  337. * annotations and is selected as annotation_calculation for that row.
  338. * See the docs for elgg_get_annotations() for proper use of the
  339. * "annotation_calculation" option.
  340. * 'order_by' => The order for the sorting. Defaults to 'annotation_calculation desc'.
  341. * 'annotation_names' => The names of annotations on the entity.
  342. * 'annotation_values' => The values of annotations on the entity.
  343. *
  344. * 'metadata_names' => The name of metadata on the entity.
  345. * 'metadata_values' => The value of metadata on the entitiy.
  346. * 'callback' => Callback function to pass each row through.
  347. * @tip This function is different from other ege* functions,
  348. * as it uses a metastring-based getter function { @link elgg_get_annotations() },
  349. * therefore the callback function should be a derivative of { @link entity_row_to_elggstar() }
  350. * and not of { @link row_to_annotation() }
  351. *
  352. * @return \ElggEntity[]|int An array or a count of entities
  353. * @see elgg_get_annotations()
  354. * @see elgg_get_entities_from_annotations()
  355. */
  356. function getEntitiesFromCalculation($options) {
  357. if (isset($options['count']) && $options['count']) {
  358. return elgg_get_entities_from_annotations($options);
  359. }
  360. $db_prefix = _elgg_services()->config->get('dbprefix');
  361. $defaults = array(
  362. 'calculation' => 'sum',
  363. 'order_by' => 'annotation_calculation desc'
  364. );
  365. $options = array_merge($defaults, $options);
  366. $function = sanitize_string(elgg_extract('calculation', $options, 'sum', false));
  367. // you must cast this as an int or it sorts wrong.
  368. $options['selects'][] = 'e.*';
  369. $options['selects'][] = "$function(CAST(a_msv.string AS signed)) AS annotation_calculation";
  370. // need our own join to get the values because the lower level functions don't
  371. // add all the joins if it's a different callback.
  372. $options['joins'][] = "JOIN {$db_prefix}metastrings a_msv ON n_table.value_id = a_msv.id";
  373. // don't need access control because it's taken care of by elgg_get_annotations.
  374. $options['group_by'] = 'n_table.entity_guid';
  375. // do not default to a callback function used in elgg_get_annotation()
  376. if (!isset($options['callback'])) {
  377. $options['callback'] = 'entity_row_to_elggstar';
  378. }
  379. return elgg_get_annotations($options);
  380. }
  381. /**
  382. * Check to see if a user has already created an annotation on an object
  383. *
  384. * @param int $entity_guid Entity guid
  385. * @param string $annotation_type Type of annotation
  386. * @param int $owner_guid Defaults to logged in user.
  387. *
  388. * @return bool
  389. */
  390. function exists($entity_guid, $annotation_type, $owner_guid = null) {
  391. if (!$owner_guid && !($owner_guid = _elgg_services()->session->getLoggedInUserGuid())) {
  392. return false;
  393. }
  394. $entity_guid = sanitize_int($entity_guid);
  395. $owner_guid = sanitize_int($owner_guid);
  396. $annotation_type = sanitize_string($annotation_type);
  397. $sql = "SELECT a.id FROM {$this->CONFIG->dbprefix}annotations a" .
  398. " JOIN {$this->CONFIG->dbprefix}metastrings m ON a.name_id = m.id" .
  399. " WHERE a.owner_guid = $owner_guid AND a.entity_guid = $entity_guid" .
  400. " AND m.string = '$annotation_type'";
  401. if (_elgg_services()->db->getDataRow($sql)) {
  402. return true;
  403. }
  404. return false;
  405. }
  406. }