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