Merged link to role assign screen from 1.9
[moodle.git] / tag / lib.php
CommitLineData
db32b054 1<?php // $Id$
4d4dac81 2
c933a060 3/**
db32b054 4 * lib.php - moodle tag library
d4be9d6d 5 *
db32b054 6 * @version: $Id$
7 * @licence http://www.gnu.org/copyleft/gpl.html GNU Public License
8 * @package moodlecore
9 *
10 * A "tag string" is always a rawurlencode'd string. This is the same behavior
11 * as http://del.icio.us
12 * @see http://www.php.net/manual/en/function.urlencode.php
13 *
14 * Tag strings : you can use any character in tags, except the comma (which is
15 * the separator) and the '\' (backslash). Note that many spaces (or other
16 * blank characters) will get "compressed" into one.
17 *
18 * A "record" is a php array (note that an object will work too) that contains
19 * the following variables :
20 * - type: the table containing the record that we are tagging (eg: for a
21 * blog, this is table 'post', and for a user it is 'user')
22 * - id: the id of the record
d4be9d6d 23 *
db32b054 24 * TODO: turn this into a full-fledged categorization system. This could start
25 * by modifying (removing, probably) the 'tag type' to use another table
26 * describing the relationship between tags (parents, sibling, etc.), which
27 * could then be merged with the 'course categorization' system...
c933a060 28 *
db32b054 29 * BASIC INSTRUCTIONS :
30 * - to "tag a blog post" (for example):
31 * tag_set('post', $blog_post->id, $array_of_tags);
d4be9d6d 32 *
db32b054 33 * - to "remove all the tags on a blog post":
34 * tag_set('post', $blog_post->id, array());
d4be9d6d 35 *
db32b054 36 * Tag set will create tags that need to be created.
c933a060 37 */
38
db32b054 39define('TAG_RETURN_ARRAY', 0);
40define('TAG_RETURN_OBJECT', 1);
41define('TAG_RETURN_TEXT', 2);
42define('TAG_RETURN_HTML', 3);
c933a060 43
db32b054 44define('TAG_CASE_LOWER', 0);
45define('TAG_CASE_ORIGINAL', 1);
d4be9d6d 46
537aa65c 47define('TAG_RELATED_ALL', 0);
48define('TAG_RELATED_MANUAL', 1);
49define('TAG_RELATED_CORRELATED', 2);
50
51
bb062447 52require_once($CFG->dirroot .'/tag/locallib.php');
53
db32b054 54///////////////////////////////////////////////////////
55/////////////////// PUBLIC TAG API ////////////////////
c933a060 56
57/**
db32b054 58 * Delete one or more tag, and all their instances if there are any left.
59 *
60 * @param mixed $tagids one tagid (int), or one array of tagids to delete
61 * @return bool true on success, false otherwise
c933a060 62 */
db32b054 63function tag_delete($tagids) {
3d436d8a 64
db32b054 65 if (!is_array($tagids)) {
66 $tagids = array($tagids);
c933a060 67 }
3d436d8a 68
db32b054 69 $success = true;
70 foreach( $tagids as $tagid ) {
71 if (is_null($tagid)) { // can happen if tag doesn't exists
72 continue;
73 }
74 // only delete the main entry if there were no problems deleting all the
75 // instances - that (and the fact we won't often delete lots of tags)
76 // is the reason for not using delete_records_select()
77 if ( delete_records('tag_instance', 'tagid', $tagid) ) {
78 $success &= (bool) delete_records('tag', 'id', $tagid);
79 }
80 }
3d436d8a 81
db32b054 82 return $success;
c933a060 83}
84
85/**
db32b054 86 * Delete one instance of a tag. If the last instance was deleted, it will
87 * also delete the tag, unless it's type is 'official'.
d4be9d6d 88 *
db32b054 89 * @param array $record the record for which to remove the instance
90 * @param int $tagid the tagid that needs to be removed
91 * @return bool true on success, false otherwise
c933a060 92 */
db32b054 93function tag_delete_instance($record, $tagid) {
94 global $CFG;
70e9b59d 95
db32b054 96 if ( delete_records('tag_instance', 'tagid', $tagid, 'itemtype', $record['type'], 'itemid', $record['id']) ) {
2f9ba531 97 if ( !record_exists_sql("SELECT * FROM {$CFG->prefix}tag tg, {$CFG->prefix}tag_instance ti ".
98 "WHERE (tg.id = ti.tagid AND ti.tagid = {$tagid} ) OR ".
99 "(tg.id = {$tagid} AND tg.tagtype = 'official')") ) {
db32b054 100 return tag_delete($tagid);
101 }
70e9b59d 102 } else {
db32b054 103 return false;
c933a060 104 }
105}
106
b50d2245 107/**
108 * Set the description of a tag
109 *
110 * @param int $tagid the id of the tag
111 * @param string $description the description
112 * @param int $descriptionformat the moodle text format of the description
113 * @return true on success, false otherwise
114 */
115function tag_description_set($tagid, $description, $descriptionformat) {
116 if ($tag = get_record('tag', 'id', $tagid, '', '', '', '', 'id')) {
117 $tag->description = addslashes($description);
118 $tag->descriptionformat = addslashes($descriptionformat);
119 $tag->timemodified = time();
120 return update_record('tag', $tag);
121 }
122 return false;
123}
124
c933a060 125/**
db32b054 126 * Function that returns the name that should be displayed for a specific tag
c933a060 127 *
db32b054 128 * @param object $tag_object a line out of tag table, as returned by the adobd functions
129 * @return string
c933a060 130 */
db32b054 131function tag_display_name($tag_object) {
3d436d8a 132
6361e02a 133 global $CFG;
134
db32b054 135 if(!isset($tag_object->name)) {
136 return '';
4f81c51d 137 }
db32b054 138
139 if( empty($CFG->keeptagnamecase) ) {
140 //this is the normalized tag name
141 $textlib = textlib_get_instance();
142 return htmlspecialchars($textlib->strtotitle($tag_object->name));
143 }
144 else {
145 //original casing of the tag name
146 return htmlspecialchars($tag_object->rawname);
3d436d8a 147 }
c933a060 148}
149
150/**
db32b054 151 * Find all records tagged with a tag of a given type ('post', 'user', etc.)
d4be9d6d 152 *
db32b054 153 * @param string $tag tag to look for
154 * @param string $type type to restrict search to. If null, every matching
155 * record will be returned
2dc038ff 156 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
157 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
db32b054 158 * @return array of matching objects, indexed by record id, from the table containing the type requested
c933a060 159 */
2dc038ff 160function tag_find_records($tag, $type, $limitfrom='', $limitnum='') {
db32b054 161
162 global $CFG;
70e9b59d 163
db32b054 164 if (!$tag || !$type) {
165 return array();
c933a060 166 }
167
db32b054 168 $tagid = tag_get_id($tag);
169
170 $query = "SELECT it.* ".
171 "FROM {$CFG->prefix}{$type} it INNER JOIN {$CFG->prefix}tag_instance tt ON it.id = tt.itemid ".
172 "WHERE tt.itemtype = '{$type}' AND tt.tagid = '{$tagid}'";
173
2dc038ff 174 return get_records_sql($query, $limitfrom, $limitnum);
c933a060 175}
176
2f4b82f4 177
178/**
179 * Simple function to just return a single tag object
180 *
181 * @param string $field which field do we use to identify the tag: id, name or rawname
182 * @param string $value the required value of the aforementioned field
183 * @param string $returnfields which fields do we want returned?
184 * @return tag object
185 *
186 **/
187function tag_get($field, $value, $returnfields='id, name, rawname') {
188 return get_record('tag', $field, $value, '', '', '', '', $returnfields);
189}
190
191
c933a060 192/**
db32b054 193 * Get the array of db record of tags associated to a record (instances). Use
194 * tag_get_tags_csv to get the same information in a comma-separated string.
c933a060 195 *
537aa65c 196 * @param string $record_type the record type for which we want to get the tags
197 * @param int $record_id the record id for which we want to get the tags
db32b054 198 * @param string $type the tag type (either 'default' or 'official'). By default,
199 * all tags are returned.
200 * @return array the array of tags
c933a060 201 */
537aa65c 202function tag_get_tags($record_type, $record_id, $type=null) {
db32b054 203
204 global $CFG;
3d436d8a 205
db32b054 206 if ($type) {
207 $type = "AND tg.tagtype = '$type'";
208 }
537aa65c 209
73c32e86 210 // if the fields in this query are changed, you need to do the same changes in tag_get_correlated_tags
2f9ba531 211 $tags = get_records_sql("SELECT tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ".
212 "FROM {$CFG->prefix}tag_instance ti INNER JOIN {$CFG->prefix}tag tg ON tg.id = ti.tagid ".
537aa65c 213 "WHERE ti.itemtype = '{$record_type}' AND ti.itemid = '{$record_id}' {$type} ".
2f9ba531 214 "ORDER BY ti.ordering ASC");
db32b054 215 // This version of the query, reversing the ON clause, "correctly" returns
216 // a row with NULL values for instances that are still in the DB even though
217 // the tag has been deleted. This shouldn't happen, but if it did, using
218 // this query could help "clean it up". This causes bugs at this time.
2f9ba531 219 //$tags = get_records_sql("SELECT ti.tagid, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ".
220 // "FROM {$CFG->prefix}tag_instance ti LEFT JOIN {$CFG->prefix}tag tg ON ti.tagid = tg.id ".
537aa65c 221 // "WHERE ti.itemtype = '{$record_type}' AND ti.itemid = '{$record_id}' {$type} ".
2f9ba531 222 // "ORDER BY ti.ordering ASC");
db32b054 223
224 if (!$tags) {
225 return array();
226 } else {
227 return $tags;
4f81c51d 228 }
db32b054 229}
230
231/**
232 * Get the array of tags display names, indexed by id.
233 *
537aa65c 234 * @param string $record_type the record type for which we want to get the tags
235 * @param int $record_id the record id for which we want to get the tags
db32b054 236 * @param string $type the tag type (either 'default' or 'official'). By default,
237 * all tags are returned.
238 * @return array the array of tags (with the value returned by tag_display_name), indexed by id
239 */
537aa65c 240function tag_get_tags_array($record_type, $record_id, $type=null) {
db32b054 241 $tags = array();
537aa65c 242 foreach(tag_get_tags($record_type, $record_id, $type) as $tag) {
db32b054 243 $tags[$tag->id] = tag_display_name($tag);
4f81c51d 244 }
db32b054 245 return $tags;
c933a060 246}
247
248/**
db32b054 249 * Get a comma-separated string of tags associated to a record. Use tag_get_tags
250 * to get the same information in an array.
c933a060 251 *
537aa65c 252 * @param string $record_type the record type for which we want to get the tags
253 * @param int $record_id the record id for which we want to get the tags
db32b054 254 * @param int $html either TAG_RETURN_HTML or TAG_RETURN_TEXT, depending
255 * on the type of output desired
256 * @param string $type either 'official' or 'default', if null, all tags are
257 * returned
258 * @return string the comma-separated list of tags.
c933a060 259 */
537aa65c 260function tag_get_tags_csv($record_type, $record_id, $html=TAG_RETURN_HTML, $type=null) {
db32b054 261 global $CFG;
c933a060 262
263 $tags_names = array();
537aa65c 264 foreach( tag_get_tags($record_type, $record_id, $type) as $tag ) {
db32b054 265 if ($html == TAG_RETURN_TEXT) {
266 $tags_names[] = tag_display_name($tag);
267 } else { // TAG_RETURN_HTML
268 $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
269 }
c933a060 270 }
db32b054 271 return implode(', ', $tags_names);
c933a060 272}
273
4f81c51d 274/**
db32b054 275 * Get an array of tag ids associated to a record.
d4be9d6d 276 *
537aa65c 277 * @param string $record the record type for which we want to get the tags
278 * @param int $record_id the record id for which we want to get the tags
db32b054 279 * @return array of tag ids, indexed and sorted by 'ordering'
4f81c51d 280 */
537aa65c 281function tag_get_tags_ids($record_type, $record_id) {
db32b054 282
283 $tag_ids = array();
537aa65c 284 foreach( tag_get_tags($record_type, $record_id) as $tag ) {
db32b054 285 $tag_ids[$tag->ordering] = $tag->id;
286 }
287 ksort($tag_ids);
288 return $tag_ids;
289}
3d436d8a 290
db32b054 291/**
292 * Returns the database ID of a set of tags.
293 *
294 * @param mixed $tags one tag, or array of tags, to look for.
dee71a16 295 * @param bool $return_value specify the type of the returned value. Either
296 * TAG_RETURN_OBJECT, or TAG_RETURN_ARRAY (default). If TAG_RETURN_ARRAY
297 * is specified, an array will be returned even if only one tag was
298 * passed in $tags.
299 * @return mixed tag-indexed array of ids (or objects, if second parameter is
300 * TAG_RETURN_OBJECT), or only an int, if only one tag is given *and* the
301 * second parameter is null. No value for a key means the tag wasn't found.
db32b054 302 */
303function tag_get_id($tags, $return_value=null) {
4f81c51d 304 global $CFG;
db32b054 305 static $tag_id_cache = array();
3d436d8a 306
db32b054 307 $return_an_int = false;
308 if (!is_array($tags)) {
309 if(is_null($return_value) || $return_value == TAG_RETURN_OBJECT) {
310 $return_an_int = true;
311 }
312 $tags = array($tags);
313 }
314
315 $result = array();
316
317 //TODO: test this and see if it helps performance without breaking anything
318 //foreach($tags as $key => $tag) {
319 // $clean_tag = moodle_strtolower($tag);
320 // if ( array_key_exists($clean_tag), $tag_id_cache) ) {
321 // $result[$clean_tag] = $tag_id_cache[$clean_tag];
322 // $tags[$key] = ''; // prevent further processing for this one.
323 // }
324 //}
325
326 $tags = array_values(tag_normalize($tags));
327 foreach($tags as $key => $tag) {
328 $tags[$key] = addslashes(moodle_strtolower($tag));
329 $result[moodle_strtolower($tag)] = null; // key must exists : no value for a key means the tag wasn't found.
330 }
331 $tag_string = "'". implode("', '", $tags) ."'";
332
2f9ba531 333 if ($rs = get_recordset_sql("SELECT * FROM {$CFG->prefix}tag WHERE name in ({$tag_string}) order by name")) {
db32b054 334 while ($record = rs_fetch_next_record($rs)) {
335 if ($return_value == TAG_RETURN_OBJECT) {
336 $result[$record->name] = $record;
337 } else { // TAG_RETURN_ARRAY
338 $result[$record->name] = $record->id;
339 }
340 }
4f81c51d 341 }
3d436d8a 342
db32b054 343 if ($return_an_int) {
344 return array_pop($result);
345 }
3d436d8a 346
db32b054 347 return $result;
c933a060 348}
349
350/**
db32b054 351 * Get a tag as an object (line) returned by get_recordset_sql
c933a060 352 *
db32b054 353 * @param int $tagid a tag id
354 * @return object a line returned from get_recordset_sql, or false
c933a060 355 */
db32b054 356function tag_get_tag_by_id($tagid) {
2f4b82f4 357 return get_record('tag', 'id', $tagid);
c933a060 358}
359
360/**
db32b054 361 * Returns tags related to a tag
d4be9d6d 362 *
db32b054 363 * Related tags of a tag come from two sources:
364 * - manually added related tags, which are tag_instance entries for that tag
365 * - correlated tags, which are a calculated
d4be9d6d 366 *
537aa65c 367 * @param string $tag_name_or_id is a single **normalized** tag name or the id
368 * of a tag
369 * @param int $type the function will return either manually
370 * (TAG_RELATED_MANUAL) related tags or correlated (TAG_RELATED_CORRELATED)
371 * tags. Default is TAG_RELATED_ALL, which returns everything.
372 * @param int $limitnum return a subset comprising this many records (optional,
373 * default is 10)
db32b054 374 * @return array an array of tag objects
c933a060 375 */
537aa65c 376function tag_get_related_tags($tagid, $type=TAG_RELATED_ALL, $limitnum=10) {
c933a060 377
537aa65c 378 $related_tags = array();
379
380 if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_MANUAL) {
381 //gets the manually added related tags
382 $related_tags = tag_get_tags('tag', $tagid);
c933a060 383 }
384
537aa65c 385 if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_CORRELATED ) {
386 //gets the correlated tags
387 $automatic_related_tags = tag_get_correlated($tagid, $limitnum);
388 if (is_array($automatic_related_tags)) {
389 $related_tags = array_merge($related_tags, $automatic_related_tags);
390 }
db32b054 391 }
3d436d8a 392
db32b054 393 return array_slice(object_array_unique($related_tags), 0 , $limitnum);
c933a060 394}
395
db32b054 396/**
397 * Get a comma-separated list of tags related to another tag.
d4be9d6d 398 *
db32b054 399 * @param array $related_tags the array returned by tag_get_related_tags
400 * @param int $html either TAG_RETURN_HTML (default) or TAG_RETURN_TEXT : return html links, or just text.
401 * @return string comma-separated list
c933a060 402 */
db32b054 403function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) {
404 global $CFG;
c933a060 405
db32b054 406 $tags_names = array();
407 foreach($related_tags as $tag) {
408 if ( $html == TAG_RETURN_TEXT) {
0f039283 409 $tags_names[] = tag_display_name($tag);
db32b054 410 } else {
411 // TAG_RETURN_HTML
9fccb889 412 $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
c933a060 413 }
c933a060 414 }
b50d2245 415
db32b054 416 return implode(', ', $tags_names);
c933a060 417}
418
3a597ace 419/**
db32b054 420 * Change the "value" of a tag, and update the associated 'name'.
3a597ace 421 *
db32b054 422 * @param int $tagid the id of the tag to modify
73c32e86 423 * @param string $newtag the new rawname
db32b054 424 * @return bool true on success, false otherwise
3a597ace 425 */
73c32e86 426function tag_rename($tagid, $newrawname) {
3a597ace 427
73c32e86 428 if (! $newrawname_clean = array_shift(tag_normalize($newrawname, TAG_CASE_ORIGINAL)) ) {
db32b054 429 return false;
430 }
3a597ace 431
2f4b82f4 432 if (! $newname_clean = moodle_strtolower($newrawname_clean)) {
433 return false;
434 }
435
436 // Prevent the rename if a tag with that name already exists
437 if ($existing = tag_get('name', $newname_clean, 'id, name, rawname')) {
438 if ($existing->id != $tagid) { // Another tag already exists with this name
439 return false;
440 }
db32b054 441 }
3a597ace 442
2f4b82f4 443 if ($tag = tag_get('id', $tagid, 'id, name, rawname')) {
73c32e86 444 $tag->rawname = addslashes($newrawname_clean);
2f4b82f4 445 $tag->name = addslashes($newname_clean);
db32b054 446 $tag->timemodified = time();
447 return update_record('tag', $tag);
448 }
449 return false;
3a597ace 450}
451
c933a060 452/**
db32b054 453 * Set the tags assigned to a record. This overwrites the current tags.
454 *
455 * This function is meant to be fed the string coming up from the user
456 * interface, which contains all tags assigned to a record.
d4be9d6d 457 *
db32b054 458 * @param string $record_type the type of record to tag ('post' for blogs,
459 * 'user' for users, 'tag' for tags, etc.
460 * @param int $record_id the id of the record to tag
461 * @param array $tags the array of tags to set on the record. If
462 * given an empty array, all tags will be removed.
463 * @return void
c933a060 464 */
db32b054 465function tag_set($record_type, $record_id, $tags) {
466 global $db;
c933a060 467
db32b054 468 $record = array('type' => $record_type, 'id' => $record_id);
3a597ace 469
db32b054 470 $tags_ids = tag_get_id($tags, TAG_RETURN_ARRAY); // force an array, even if we only have one tag.
471 $cleaned_tags = tag_normalize($tags);
472 //echo 'tags-in-tag_set'; var_dump($tags); var_dump($tags_ids); var_dump($cleaned_tags);
3a597ace 473
537aa65c 474 $current_ids = tag_get_tags_ids($record['type'], $record['id']);
db32b054 475 //var_dump($current_ids);
476 $tags_to_assign = array();
3d436d8a 477
db32b054 478 // for data coherence reasons, it's better to remove deleted tags
479 // before adding new data: ordering could be duplicated.
480 foreach($current_ids as $current_id) {
481 if (!in_array($current_id, $tags_ids)) {
482 tag_delete_instance($record, $current_id);
483 }
484 }
3d436d8a 485
db32b054 486 foreach($tags as $ordering => $tag) {
487 $tag = trim($tag);
488 if (!$tag) {
489 continue;
26d8649f 490 }
db32b054 491
492 $clean_tag = $cleaned_tags[$tag];
493 $tag_current_id = $tags_ids[$clean_tag];
494
495 if ( is_null($tag_current_id) ) {
496 // create new tags
497 //echo "call to add tag $tag\n";
498 $new_tag = tag_add($tag);
499 tag_assign($record, $new_tag[$clean_tag], $ordering);
500 }
501 elseif ( empty($current_ids) || !in_array($tag_current_id, $current_ids) ) {
502 // assign existing tags
503 tag_assign($record, $tag_current_id, $ordering);
504 }
505 elseif ( isset($current_ids[$ordering]) && $current_ids[$ordering] != $tag_current_id ) {
506 // this actually checks if the ordering number points to the same tag
507 //recompute ordering, if necessary
508 //echo 'ordering changed for ', $tag, ':', $ordering, "\n";
509 tag_assign($record, $tag_current_id, $ordering);
1acd661d 510 }
c933a060 511 }
db32b054 512}
c933a060 513
db32b054 514/**
515 * Adds a tag to a record, without overwriting the current tags.
516 *
517 * @param string $record_type the type of record to tag ('post' for blogs,
518 * 'user' for users, etc.
519 * @param int $record_id the id of the record to tag
520 * @param string $tag the tag to add
521 * @return void
522 */
523function tag_set_add($record_type, $record_id, $tag) {
3d436d8a 524
db32b054 525 $new_tags = array();
537aa65c 526 foreach( tag_get_tags($record_type, $record_id) as $current_tag ) {
db32b054 527 $new_tags[] = $current_tag->rawname;
528 }
529 $new_tags[] = $tag;
530
531 return tag_set($record_type, $record_id, $new_tags);
532}
c933a060 533
3627b0e3 534/**
535 * Removes a tag from a record, without overwriting other current tags.
536 *
537 * @param string $record_type the type of record to tag ('post' for blogs,
538 * 'user' for users, etc.
539 * @param int $record_id the id of the record to tag
540 * @param string $tag the tag to delete
541 * @return void
542 */
543function tag_set_delete($record_type, $record_id, $tag) {
544
3627b0e3 545 $new_tags = array();
537aa65c 546 foreach( tag_get_tags($record_type, $record_id) as $current_tag ) {
3627b0e3 547 if ($current_tag->name != $tag) { // Keep all tags but the one specified
548 $new_tags[] = $current_tag->name;
549 }
550 }
551
552 return tag_set($record_type, $record_id, $new_tags);
553}
554
db32b054 555/**
556 * Set the type of a tag. At this time (version 1.9) the possible values
557 * are 'default' or 'official'. Official tags will be displayed separately "at
558 * tagging time" (while selecting the tags to apply to a record).
559 *
560 * @param string $tagid tagid to modify
561 * @param string $type either 'default' or 'official'
562 * @return true on success, false otherwise
563 */
564function tag_type_set($tagid, $type) {
73c32e86 565 if ($tag = get_record('tag', 'id', $tagid, '', '', '', '', 'id')) {
566 $tag->tagtype = addslashes($type);
db32b054 567 $tag->timemodified = time();
568 return update_record('tag', $tag);
569 }
570 return false;
c933a060 571}
572
db32b054 573///////////////////////////////////////////////////////
574/////////////////// PRIVATE TAG API ///////////////////
c933a060 575
576/**
db32b054 577 * A * @param array $record the record that will be tagged
578 * @param string $tags the comma-separated tags to set on the record. If
579 * given an empty array, all tags will be removed.
580dds one or more tag in the database. This function should not be called
581 * directly : you should use tag_set.
d4be9d6d 582 *
db32b054 583 * @param mixed $tags one tag, or an array of tags, to be created
584 * @param string $tag_type type of tag to be created ("default" is the default
585 * value and "official" is the only other supported value at this time). An
586 * official tag is kept even if there are no records tagged with it.
587 * @return an array of tags ids, indexed by their lowercase normalized names.
588 * Any boolean false in the array indicates an error while adding the tag.
c933a060 589 */
db32b054 590function tag_add($tags, $type="default") {
591 global $USER;
c933a060 592
db32b054 593 require_capability('moodle/tag:create', get_context_instance(CONTEXT_SYSTEM));
3d436d8a 594
db32b054 595 if (!is_array($tags)) {
596 $tags = array($tags);
c933a060 597 }
3d436d8a 598
db32b054 599 $tag_object = new StdClass;
600 $tag_object->tagtype = $type;
601 $tag_object->userid = $USER->id;
602 $tag_object->timemodified = time();
c933a060 603
db32b054 604 $clean_tags = tag_normalize($tags, TAG_CASE_ORIGINAL);
c933a060 605
db32b054 606 $tags_ids = array();
607 foreach($clean_tags as $tag) {
608 $tag = trim($tag);
609 if (!$tag) {
610 $tags_ids[$tag] = false;
611 } else {
612 // note that the difference between rawname and name is only
613 // capitalization : the rawname is NOT the same at the rawtag.
614 $tag_object->rawname = addslashes($tag);
615 $tag_name_lc = moodle_strtolower($tag);
b50d2245 616 $tag_object->name = addslashes($tag_name_lc);
db32b054 617 //var_dump($tag_object);
618 $tags_ids[$tag_name_lc] = insert_record('tag', $tag_object);
619 }
620 }
c933a060 621
db32b054 622 return $tags_ids;
c933a060 623}
624
625/**
db32b054 626 * Assigns a tag to a record: if the record already exists, the time and
627 * ordering will be updated.
628 *
629 * @param array $record the record that will be tagged
630 * @param string $tagid the tag id to set on the record.
631 * @param int $ordering the order of the instance for this record
632 * @return bool true on success, false otherwise
c933a060 633 */
db32b054 634function tag_assign($record, $tagid, $ordering) {
c933a060 635
db32b054 636 require_capability('moodle/tag:create', get_context_instance(CONTEXT_SYSTEM));
c933a060 637
73c32e86 638 if ( $tag_instance_object = get_record('tag_instance', 'tagid', $tagid, 'itemtype', $record['type'], 'itemid', $record['id'], 'tagid') ) {
db32b054 639 $tag_instance_object->ordering = $ordering;
640 $tag_instance_object->timemodified = time();
641 return update_record('tag_instance', $tag_instance_object);
642 } else {
643 $tag_instance_object = new StdClass;
644 $tag_instance_object->tagid = $tagid;
645 $tag_instance_object->itemid = $record['id'];
646 $tag_instance_object->itemtype = $record['type'];
647 $tag_instance_object->ordering = $ordering;
648 $tag_instance_object->timemodified = time();
649 return insert_record('tag_instance', $tag_instance_object);
c933a060 650 }
c933a060 651}
652
653/**
db32b054 654 * Function that returns tags that start with some text, for use by the autocomplete feature
d4be9d6d 655 *
db32b054 656 * @param string $text string that the tag names will be matched against
c933a060 657 * @return mixed an array of objects, or false if no records were found or an error occured.
658 */
db32b054 659function tag_autocomplete($text) {
c933a060 660 global $CFG;
2f9ba531 661 return get_records_sql("SELECT tg.id, tg.name, tg.rawname FROM {$CFG->prefix}tag tg WHERE tg.name LIKE '". moodle_strtolower($text) ."%'");
c933a060 662}
663
c933a060 664/**
db32b054 665 * Calculates and stores the correlated tags of all tags.
666 * The correlations are stored in the 'tag_correlation' table.
667 *
668 * Two tags are correlated if they appear together a lot.
669 * Ex.: Users tagged with "computers" will probably also be tagged with "algorithms".
d4be9d6d 670 *
db32b054 671 * The rationale for the 'tag_correlation' table is performance.
672 * It works as a cache for a potentially heavy load query done at the 'tag_instance' table.
673 * So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table.
d4be9d6d 674 *
db32b054 675 * @param number $min_correlation cutoff percentage (optional, default is 2)
c933a060 676 */
db32b054 677function tag_compute_correlations($min_correlation=2) {
3d436d8a 678
c933a060 679 global $CFG;
3d436d8a 680
db32b054 681 $all_tags = get_records_list('tag');
db32b054 682
683 $tag_correlation_obj = new object();
684 foreach($all_tags as $tag) {
685
686 // query that counts how many times any tag appears together in items
687 // with the tag passed as argument ($tag_id)
688 $query = "SELECT tb.tagid , COUNT(*) AS nr ".
689 "FROM {$CFG->prefix}tag_instance ta INNER JOIN {$CFG->prefix}tag_instance tb ON ta.itemid = tb.itemid ".
690 "WHERE ta.tagid = {$tag->id} AND tb.tagid != {$tag->id} ".
691 "GROUP BY tb.tagid ".
692 "HAVING nr > $min_correlation ".
844f9c15 693 "ORDER BY nr DESC";
db32b054 694
695 $correlated = array();
696
697 // Correlated tags happen when they appear together in more occasions
698 // than $min_correlation.
699 if ($tag_correlations = get_records_sql($query)) {
700 foreach($tag_correlations as $correlation) {
844f9c15 701 // commented out - now done in query. kept here in case it breaks on some db
702 // if($correlation->nr >= $min_correlation){
db32b054 703 $correlated[] = $correlation->tagid;
844f9c15 704 // }
db32b054 705 }
706 }
c933a060 707
844f9c15 708 if (empty($correlated)) {
709 continue;
710 }
711
db32b054 712 $correlated = implode(',', $correlated);
713 //var_dump($correlated);
714
715 //saves correlation info in the caching table
73c32e86 716 if ($tag_correlation_obj = get_record('tag_correlation', 'tagid', $tag->id, '', '', '', '', 'tagid')) {
db32b054 717 $tag_correlation_obj->correlatedtags = $correlated;
718 update_record('tag_correlation', $tag_correlation_obj);
719 } else {
720 $tag_correlation_obj->tagid = $tag->id;
721 $tag_correlation_obj->correlatedtags = $correlated;
722 insert_record('tag_correlation', $tag_correlation_obj);
723 }
3d436d8a 724 }
db32b054 725}
726
727/**
728 * Tasks that should be performed at cron time
729 */
730function tag_cron() {
731 tag_compute_correlations();
732}
733
0f039283 734/**
735 * Search for tags with names that match some text
736 *
737 * @param string $text escaped string that the tag names will be matched against
738 * @param boolean $ordered If true, tags are ordered by their popularity. If false, no ordering.
739 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
740 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
741 * @return mixed an array of objects, or false if no records were found or an error occured.
742 */
743function tag_find_tags($text, $ordered=true, $limitfrom='', $limitnum='') {
744
745 global $CFG;
746
747 $text = array_shift(tag_normalize($text, TAG_CASE_LOWER));
748
749 if ($ordered) {
750 $query = "SELECT tg.id, tg.name, tg.rawname, COUNT(ti.id) AS count ".
751 "FROM {$CFG->prefix}tag tg LEFT JOIN {$CFG->prefix}tag_instance ti ON tg.id = ti.tagid ".
752 "WHERE tg.name LIKE '%{$text}%' ".
753 "GROUP BY tg.id, tg.name, tg.rawname ".
754 "ORDER BY count DESC";
755 } else {
756 $query = "SELECT tg.id, tg.name, tg.rawname ".
757 "FROM {$CFG->prefix}tag tg ".
758 "WHERE tg.name LIKE '%{$text}%'";
759 }
760 return get_records_sql($query, $limitfrom , $limitnum);
761}
762
db32b054 763/**
764 * Get the name of a tag
765 *
766 * @param mixed $tagids the id of the tag, or an array of ids
767 * @return mixed string name of one tag, or id-indexed array of strings
768 */
769function tag_get_name($tagids) {
3d436d8a 770
db32b054 771 $return_a_string = false;
772 if ( !is_array($tagids) ) {
773 $return_a_string = true;
774 $tagids = array($tagids);
775 }
c933a060 776
db32b054 777 $tag_names = array();
778 foreach(get_records_list('tag', 'id', implode(',', $tagids)) as $tag) {
779 $tag_names[$tag->id] = $tag->name;
780 }
c933a060 781
db32b054 782 if ($return_a_string) {
783 return array_pop($tag_names);
784 }
c933a060 785
db32b054 786 return $tag_names;
c933a060 787}
788
789/**
db32b054 790 * Returns the correlated tags of a tag, retrieved from the tag_correlation
791 * table. Make sure cron runs, otherwise the table will be empty and this
792 * function won't return anything.
c933a060 793 *
db32b054 794 * @param int $tag_id is a single tag id
795 * @return array an array of tag objects, empty if no correlated tags are found
c933a060 796 */
db32b054 797function tag_get_correlated($tag_id, $limitnum=null) {
537aa65c 798 global $CFG;
3d436d8a 799
db32b054 800 $tag_correlation = get_record('tag_correlation', 'tagid', $tag_id);
c933a060 801
db32b054 802 if (!$tag_correlation || empty($tag_correlation->correlatedtags)) {
803 return array();
c933a060 804 }
b50d2245 805
73c32e86 806 // this is (and has to) return the same fields as the query in tag_get_tags
b50d2245 807 if ( !$result = get_records_sql("SELECT tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ".
808 "FROM {$CFG->prefix}tag tg INNER JOIN {$CFG->prefix}tag_instance ti ON tg.id = ti.tagid ".
809 "WHERE tg.id IN ({$tag_correlation->correlatedtags})") ) {
db32b054 810 return array();
c933a060 811 }
537aa65c 812
db32b054 813 return $result;
c933a060 814}
815
c933a060 816/**
db32b054 817 * Function that normalizes a list of tag names.
c933a060 818 *
db32b054 819 * @param mixed $tags array of tags, or a single tag.
0f039283 820 * @param int $case case to use for returned value (default: lower case).
821 * Either TAG_CASE_LOWER (default) or TAG_CASE_ORIGINAL
822 * @return array of lowercased normalized tags, indexed by the normalized tag,
823 * in the same order as the original array. (Eg: 'Banana' => 'banana').
c933a060 824 */
db32b054 825function tag_normalize($rawtags, $case = TAG_CASE_LOWER) {
826
0f039283 827 // cache normalized tags, to prevent costly repeated calls to clean_param
db32b054 828 static $cleaned_tags_lc = array(); // lower case - use for comparison
829 static $cleaned_tags_mc = array(); // mixed case - use for saving to database
c933a060 830
db32b054 831 if ( !is_array($rawtags) ) {
832 $rawtags = array($rawtags);
833 }
c933a060 834
db32b054 835 $result = array();
836 foreach($rawtags as $rawtag) {
837 $rawtag = trim($rawtag);
838 if (!$rawtag) {
839 continue;
840 }
841 if ( !array_key_exists($rawtag, $cleaned_tags_lc) ) {
842 $cleaned_tags_lc[$rawtag] = moodle_strtolower( clean_param($rawtag, PARAM_TAG) );
843 $cleaned_tags_mc[$rawtag] = clean_param($rawtag, PARAM_TAG);
844 }
845 if ( $case == TAG_CASE_LOWER ) {
846 $result[$rawtag] = $cleaned_tags_lc[$rawtag];
847 } else { // TAG_CASE_ORIGINAL
848 $result[$rawtag] = $cleaned_tags_mc[$rawtag];
849 }
850 }
851
852 return $result;
c933a060 853}
854
855/**
0f039283 856 * Count how many records are tagged with a specific tag,
c933a060 857 *
0f039283 858 * @param string $record record to look for ('post', 'user', etc.)
859 * @param int $tag is a single tag id
860 * @return int number of mathing tags.
c933a060 861 */
0f039283 862function tag_record_count($record_type, $tagid) {
863 return count_records('tag_instance', 'itemtype', $record_type, 'tagid', $tagid);
864}
c933a060 865
0f039283 866/**
867 * Determine if a record is tagged with a specific tag
868 *
869 * @param string $record_type the record type to look for
870 * @param int $record_id the record id to look for
871 * @param string $tag a tag name
872 * @return bool true if it is tagged, false otherwise
873 */
874function tag_record_tagged_with($record_type, $record_id, $tag) {
875 if ($tagid = tag_get_id($tag)) {
876 return count_records('tag_instance', 'itemtype', $record_type, 'itemid', $record_id, 'tagid', $tagid);
c933a060 877 } else {
0f039283 878 return 0; // tag doesn't exist
c933a060 879 }
c933a060 880}
881
882/**
db32b054 883 * Flag a tag as inapropriate
884 *
885 * @param mixed $tagids one (int) tagid, or an array of tagids
886 * @return void
c933a060 887 */
db32b054 888function tag_set_flag($tagids) {
889 if ( !is_array($tagids) ) {
890 $tagids = array($tagids);
1e550d2f 891 }
db32b054 892 foreach ($tagids as $tagid) {
73c32e86 893 $tag = get_record('tag', 'id', $tagid, '', '', '', '', 'id, flag');
c933a060 894 $tag->flag++;
895 $tag->timemodified = time();
c933a060 896 update_record('tag', $tag);
897 }
898}
899
db32b054 900/**
901 * Remove the inapropriate flag on a tag
902 *
903 * @param mixed $tagids one (int) tagid, or an array of tagids
904 * @return bool true if function succeeds, false otherwise
c933a060 905 */
db32b054 906function tag_unset_flag($tagids) {
c933a060 907 global $CFG;
d4be9d6d 908
db32b054 909 require_capability('moodle/tag:manage', get_context_instance(CONTEXT_SYSTEM));
c933a060 910
db32b054 911 if ( is_array($tagids) ) {
912 $tagids = implode(',', $tagids);
ab86d9c7 913 }
db32b054 914 $timemodified = time();
2f9ba531 915 return execute_sql("UPDATE {$CFG->prefix}tag tg SET tg.flag = 0, tg.timemodified = $timemodified WHERE tg.id IN ($tagids)", false);
c933a060 916}
917
c933a060 918?>