MDL-8566, add lang string to MOODLE_19_STABLE, see tracker
[moodle-linuxchix.git] / tag / lib.php
blob9d91cee794dc74d0bf402819b74cbad24a2356b0
1 <?php // $Id$
3 /**
4 * lib.php - moodle tag library
6 * @version: $Id$
7 * @licence http://www.gnu.org/copyleft/gpl.html GNU Public License
8 * @package moodlecore
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
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.
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
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...
29 * BASIC INSTRUCTIONS :
30 * - to "tag a blog post" (for example):
31 * tag_set('post', $blog_post->id, $array_of_tags);
33 * - to "remove all the tags on a blog post":
34 * tag_set('post', $blog_post->id, array());
36 * Tag set will create tags that need to be created.
39 define('TAG_RETURN_ARRAY', 0);
40 define('TAG_RETURN_OBJECT', 1);
41 define('TAG_RETURN_TEXT', 2);
42 define('TAG_RETURN_HTML', 3);
44 define('TAG_CASE_LOWER', 0);
45 define('TAG_CASE_ORIGINAL', 1);
47 define('TAG_RELATED_ALL', 0);
48 define('TAG_RELATED_MANUAL', 1);
49 define('TAG_RELATED_CORRELATED', 2);
52 require_once($CFG->dirroot .'/tag/locallib.php');
54 ///////////////////////////////////////////////////////
55 /////////////////// PUBLIC TAG API ////////////////////
57 /// Functions for settings tags //////////////////////
59 /**
60 * Set the tags assigned to a record. This overwrites the current tags.
62 * This function is meant to be fed the string coming up from the user
63 * interface, which contains all tags assigned to a record.
65 * @param string $record_type the type of record to tag ('post' for blogs,
66 * 'user' for users, 'tag' for tags, etc.
67 * @param int $record_id the id of the record to tag
68 * @param array $tags the array of tags to set on the record. If
69 * given an empty array, all tags will be removed.
70 * @return void
72 function tag_set($record_type, $record_id, $tags) {
74 static $in_recursion_semaphore = false; // this is to prevent loops when tagging a tag
75 if ( $record_type == 'tag' && !$in_recursion_semaphore) {
76 $current_tagged_tag_name = tag_get_name($record_id);
79 $tags_ids = tag_get_id($tags, TAG_RETURN_ARRAY); // force an array, even if we only have one tag.
80 $cleaned_tags = tag_normalize($tags);
81 //echo 'tags-in-tag_set'; var_dump($tags); var_dump($tags_ids); var_dump($cleaned_tags);
83 $current_ids = tag_get_tags_ids($record_type, $record_id);
84 //var_dump($current_ids);
86 // for data coherence reasons, it's better to remove deleted tags
87 // before adding new data: ordering could be duplicated.
88 foreach($current_ids as $current_id) {
89 if (!in_array($current_id, $tags_ids)) {
90 tag_delete_instance($record_type, $record_id, $current_id);
91 if ( $record_type == 'tag' && !$in_recursion_semaphore) {
92 // if we are removing a tag-on-a-tag (manually related tag),
93 // we need to remove the opposite relationship as well.
94 tag_delete_instance('tag', $current_id, $record_id);
99 foreach($tags as $ordering => $tag) {
100 $tag = trim($tag);
101 if (!$tag) {
102 continue;
105 $clean_tag = $cleaned_tags[$tag];
106 $tag_current_id = $tags_ids[$clean_tag];
108 if ( is_null($tag_current_id) ) {
109 // create new tags
110 //echo "call to add tag $tag\n";
111 $new_tag = tag_add($tag);
112 $tag_current_id = $new_tag[$clean_tag];
115 tag_assign($record_type, $record_id, $tag_current_id, $ordering);
117 // if we are tagging a tag (adding a manually-assigned related tag), we
118 // need to create the opposite relationship as well.
119 if ( $record_type == 'tag' && !$in_recursion_semaphore) {
120 $in_recursion_semaphore = true;
121 tag_set_add('tag', $tag_current_id, $current_tagged_tag_name);
122 $in_recursion_semaphore = false;
128 * Adds a tag to a record, without overwriting the current tags.
130 * @param string $record_type the type of record to tag ('post' for blogs,
131 * 'user' for users, etc.
132 * @param int $record_id the id of the record to tag
133 * @param string $tag the tag to add
134 * @return void
136 function tag_set_add($record_type, $record_id, $tag) {
138 $new_tags = array();
139 foreach( tag_get_tags($record_type, $record_id) as $current_tag ) {
140 $new_tags[] = $current_tag->rawname;
142 $new_tags[] = $tag;
144 return tag_set($record_type, $record_id, $new_tags);
148 * Removes a tag from a record, without overwriting other current tags.
150 * @param string $record_type the type of record to tag ('post' for blogs,
151 * 'user' for users, etc.
152 * @param int $record_id the id of the record to tag
153 * @param string $tag the tag to delete
154 * @return void
156 function tag_set_delete($record_type, $record_id, $tag) {
158 $new_tags = array();
159 foreach( tag_get_tags($record_type, $record_id) as $current_tag ) {
160 if ($current_tag->name != $tag) { // Keep all tags but the one specified
161 $new_tags[] = $current_tag->name;
165 return tag_set($record_type, $record_id, $new_tags);
169 * Set the type of a tag. At this time (version 1.9) the possible values
170 * are 'default' or 'official'. Official tags will be displayed separately "at
171 * tagging time" (while selecting the tags to apply to a record).
173 * @param string $tagid tagid to modify
174 * @param string $type either 'default' or 'official'
175 * @return true on success, false otherwise
177 function tag_type_set($tagid, $type) {
178 if ($tag = get_record('tag', 'id', $tagid, '', '', '', '', 'id')) {
179 $tag->tagtype = addslashes($type);
180 $tag->timemodified = time();
181 return update_record('tag', $tag);
183 return false;
187 /**
188 * Set the description of a tag
190 * @param int $tagid the id of the tag
191 * @param string $description the description
192 * @param int $descriptionformat the moodle text format of the description
193 * @return true on success, false otherwise
195 function tag_description_set($tagid, $description, $descriptionformat) {
196 if ($tag = get_record('tag', 'id', $tagid, '', '', '', '', 'id')) {
197 $tag->description = addslashes($description);
198 $tag->descriptionformat = addslashes($descriptionformat);
199 $tag->timemodified = time();
200 return update_record('tag', $tag);
202 return false;
210 /// Functions for getting information about tags //////
213 * Simple function to just return a single tag object when you know the name or something
215 * @param string $field which field do we use to identify the tag: id, name or rawname
216 * @param string $value the required value of the aforementioned field
217 * @param string $returnfields which fields do we want returned?
218 * @return tag object
221 function tag_get($field, $value, $returnfields='id, name, rawname') {
222 if ($field == 'name') {
223 $value = addslashes(moodle_strtolower($value)); // To cope with input that might just be wrong case
225 return get_record('tag', $field, $value, '', '', '', '', $returnfields);
230 * Get the array of db record of tags associated to a record (instances). Use
231 * tag_get_tags_csv to get the same information in a comma-separated string.
233 * @param string $record_type the record type for which we want to get the tags
234 * @param int $record_id the record id for which we want to get the tags
235 * @param string $type the tag type (either 'default' or 'official'). By default,
236 * all tags are returned.
237 * @return array the array of tags
239 function tag_get_tags($record_type, $record_id, $type=null) {
241 global $CFG;
243 if ($type) {
244 $type = "AND tg.tagtype = '$type'";
247 // if the fields in this query are changed, you need to do the same changes in tag_get_correlated_tags
248 $tags = get_records_sql("SELECT tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ".
249 "FROM {$CFG->prefix}tag_instance ti INNER JOIN {$CFG->prefix}tag tg ON tg.id = ti.tagid ".
250 "WHERE ti.itemtype = '{$record_type}' AND ti.itemid = '{$record_id}' {$type} ".
251 "ORDER BY ti.ordering ASC");
252 // This version of the query, reversing the ON clause, "correctly" returns
253 // a row with NULL values for instances that are still in the DB even though
254 // the tag has been deleted. This shouldn't happen, but if it did, using
255 // this query could help "clean it up". This causes bugs at this time.
256 //$tags = get_records_sql("SELECT ti.tagid, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ".
257 // "FROM {$CFG->prefix}tag_instance ti LEFT JOIN {$CFG->prefix}tag tg ON ti.tagid = tg.id ".
258 // "WHERE ti.itemtype = '{$record_type}' AND ti.itemid = '{$record_id}' {$type} ".
259 // "ORDER BY ti.ordering ASC");
261 if (!$tags) {
262 return array();
263 } else {
264 return $tags;
269 * Get the array of tags display names, indexed by id.
271 * @param string $record_type the record type for which we want to get the tags
272 * @param int $record_id the record id for which we want to get the tags
273 * @param string $type the tag type (either 'default' or 'official'). By default,
274 * all tags are returned.
275 * @return array the array of tags (with the value returned by tag_display_name), indexed by id
277 function tag_get_tags_array($record_type, $record_id, $type=null) {
278 $tags = array();
279 foreach(tag_get_tags($record_type, $record_id, $type) as $tag) {
280 $tags[$tag->id] = tag_display_name($tag);
282 return $tags;
286 * Get a comma-separated string of tags associated to a record. Use tag_get_tags
287 * to get the same information in an array.
289 * @param string $record_type the record type for which we want to get the tags
290 * @param int $record_id the record id for which we want to get the tags
291 * @param int $html either TAG_RETURN_HTML or TAG_RETURN_TEXT, depending
292 * on the type of output desired
293 * @param string $type either 'official' or 'default', if null, all tags are
294 * returned
295 * @return string the comma-separated list of tags.
297 function tag_get_tags_csv($record_type, $record_id, $html=TAG_RETURN_HTML, $type=null) {
298 global $CFG;
300 $tags_names = array();
301 foreach(tag_get_tags($record_type, $record_id, $type) as $tag) {
302 if ($html == TAG_RETURN_TEXT) {
303 $tags_names[] = tag_display_name($tag);
304 } else { // TAG_RETURN_HTML
305 $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
308 return implode(', ', $tags_names);
312 * Get an array of tag ids associated to a record.
314 * @param string $record_type the record type for which we want to get the tags
315 * @param int $record_id the record id for which we want to get the tags
316 * @return array of tag ids, indexed and sorted by 'ordering'
318 function tag_get_tags_ids($record_type, $record_id) {
320 $tag_ids = array();
321 foreach (tag_get_tags($record_type, $record_id) as $tag) {
322 if ( array_key_exists($tag->ordering, $tag_ids) ) {
323 // until we can add a unique constraint, in table tag_instance,
324 // on (itemtype, itemid, ordering), this is needed to prevent a bug
325 // TODO : modify database in 2.0
326 $tag->ordering++;
328 $tag_ids[$tag->ordering] = $tag->id;
330 ksort($tag_ids);
331 return $tag_ids;
334 /**
335 * Returns the database ID of a set of tags.
337 * @param mixed $tags one tag, or array of tags, to look for.
338 * @param bool $return_value specify the type of the returned value. Either
339 * TAG_RETURN_OBJECT, or TAG_RETURN_ARRAY (default). If TAG_RETURN_ARRAY
340 * is specified, an array will be returned even if only one tag was
341 * passed in $tags.
342 * @return mixed tag-indexed array of ids (or objects, if second parameter is
343 * TAG_RETURN_OBJECT), or only an int, if only one tag is given *and* the
344 * second parameter is null. No value for a key means the tag wasn't found.
346 function tag_get_id($tags, $return_value=null) {
347 global $CFG;
348 static $tag_id_cache = array();
350 $return_an_int = false;
351 if (!is_array($tags)) {
352 if(is_null($return_value) || $return_value == TAG_RETURN_OBJECT) {
353 $return_an_int = true;
355 $tags = array($tags);
358 $result = array();
360 //TODO: test this and see if it helps performance without breaking anything
361 //foreach($tags as $key => $tag) {
362 // $clean_tag = moodle_strtolower($tag);
363 // if ( array_key_exists($clean_tag), $tag_id_cache) ) {
364 // $result[$clean_tag] = $tag_id_cache[$clean_tag];
365 // $tags[$key] = ''; // prevent further processing for this one.
366 // }
369 $tags = array_values(tag_normalize($tags));
370 foreach($tags as $key => $tag) {
371 $tags[$key] = addslashes(moodle_strtolower($tag));
372 $result[moodle_strtolower($tag)] = null; // key must exists : no value for a key means the tag wasn't found.
374 $tag_string = "'". implode("', '", $tags) ."'";
376 if ($rs = get_recordset_sql("SELECT * FROM {$CFG->prefix}tag WHERE name in ({$tag_string}) order by name")) {
377 while ($record = rs_fetch_next_record($rs)) {
378 if ($return_value == TAG_RETURN_OBJECT) {
379 $result[$record->name] = $record;
380 } else { // TAG_RETURN_ARRAY
381 $result[$record->name] = $record->id;
386 if ($return_an_int) {
387 return array_pop($result);
390 return $result;
395 * Returns tags related to a tag
397 * Related tags of a tag come from two sources:
398 * - manually added related tags, which are tag_instance entries for that tag
399 * - correlated tags, which are a calculated
401 * @param string $tag_name_or_id is a single **normalized** tag name or the id
402 * of a tag
403 * @param int $type the function will return either manually
404 * (TAG_RELATED_MANUAL) related tags or correlated (TAG_RELATED_CORRELATED)
405 * tags. Default is TAG_RELATED_ALL, which returns everything.
406 * @param int $limitnum return a subset comprising this many records (optional,
407 * default is 10)
408 * @return array an array of tag objects
410 function tag_get_related_tags($tagid, $type=TAG_RELATED_ALL, $limitnum=10) {
412 $related_tags = array();
414 if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_MANUAL) {
415 //gets the manually added related tags
416 $related_tags = tag_get_tags('tag', $tagid);
419 if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_CORRELATED ) {
420 //gets the correlated tags
421 $automatic_related_tags = tag_get_correlated($tagid, $limitnum);
422 if (is_array($automatic_related_tags)) {
423 $related_tags = array_merge($related_tags, $automatic_related_tags);
427 return array_slice(object_array_unique($related_tags), 0 , $limitnum);
430 /**
431 * Get a comma-separated list of tags related to another tag.
433 * @param array $related_tags the array returned by tag_get_related_tags
434 * @param int $html either TAG_RETURN_HTML (default) or TAG_RETURN_TEXT : return html links, or just text.
435 * @return string comma-separated list
437 function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) {
438 global $CFG;
440 $tags_names = array();
441 foreach($related_tags as $tag) {
442 if ( $html == TAG_RETURN_TEXT) {
443 $tags_names[] = tag_display_name($tag);
444 } else {
445 // TAG_RETURN_HTML
446 $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
450 return implode(', ', $tags_names);
454 * Change the "value" of a tag, and update the associated 'name'.
456 * @param int $tagid the id of the tag to modify
457 * @param string $newtag the new rawname
458 * @return bool true on success, false otherwise
460 function tag_rename($tagid, $newrawname) {
462 if (! $newrawname_clean = array_shift(tag_normalize($newrawname, TAG_CASE_ORIGINAL)) ) {
463 return false;
466 if (! $newname_clean = moodle_strtolower($newrawname_clean)) {
467 return false;
470 // Prevent the rename if a tag with that name already exists
471 if ($existing = tag_get('name', $newname_clean, 'id, name, rawname')) {
472 if ($existing->id != $tagid) { // Another tag already exists with this name
473 return false;
477 if ($tag = tag_get('id', $tagid, 'id, name, rawname')) {
478 $tag->rawname = addslashes($newrawname_clean);
479 $tag->name = addslashes($newname_clean);
480 $tag->timemodified = time();
481 return update_record('tag', $tag);
483 return false;
488 * Delete one or more tag, and all their instances if there are any left.
490 * @param mixed $tagids one tagid (int), or one array of tagids to delete
491 * @return bool true on success, false otherwise
493 function tag_delete($tagids) {
495 if (!is_array($tagids)) {
496 $tagids = array($tagids);
499 $success = true;
500 foreach( $tagids as $tagid ) {
501 if (is_null($tagid)) { // can happen if tag doesn't exists
502 continue;
504 // only delete the main entry if there were no problems deleting all the
505 // instances - that (and the fact we won't often delete lots of tags)
506 // is the reason for not using delete_records_select()
507 if ( delete_records('tag_instance', 'tagid', $tagid) ) {
508 $success &= (bool) delete_records('tag', 'id', $tagid);
512 return $success;
516 * Delete one instance of a tag. If the last instance was deleted, it will
517 * also delete the tag, unless it's type is 'official'.
519 * @param string $record_type the type of the record for which to remove the instance
520 * @param int $record_id the id of the record for which to remove the instance
521 * @param int $tagid the tagid that needs to be removed
522 * @return bool true on success, false otherwise
524 function tag_delete_instance($record_type, $record_id, $tagid) {
525 global $CFG;
527 if ( delete_records('tag_instance', 'tagid', $tagid, 'itemtype', $record_type, 'itemid', $record_id) ) {
528 if ( !record_exists_sql("SELECT * FROM {$CFG->prefix}tag tg, {$CFG->prefix}tag_instance ti ".
529 "WHERE (tg.id = ti.tagid AND ti.tagid = {$tagid} ) OR ".
530 "(tg.id = {$tagid} AND tg.tagtype = 'official')") ) {
531 return tag_delete($tagid);
533 } else {
534 return false;
540 * Function that returns the name that should be displayed for a specific tag
542 * @param object $tag_object a line out of tag table, as returned by the adobd functions
543 * @return string
545 function tag_display_name($tag_object) {
547 global $CFG;
549 if(!isset($tag_object->name)) {
550 return '';
553 if (empty($CFG->keeptagnamecase)) {
554 //this is the normalized tag name
555 $textlib = textlib_get_instance();
556 return htmlspecialchars($textlib->strtotitle($tag_object->name));
557 } else {
558 //original casing of the tag name
559 return htmlspecialchars($tag_object->rawname);
564 * Find all records tagged with a tag of a given type ('post', 'user', etc.)
566 * @param string $tag tag to look for
567 * @param string $type type to restrict search to. If null, every matching
568 * record will be returned
569 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
570 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
571 * @return array of matching objects, indexed by record id, from the table containing the type requested
573 function tag_find_records($tag, $type, $limitfrom='', $limitnum='') {
575 global $CFG;
577 if (!$tag || !$type) {
578 return array();
581 $tagid = tag_get_id($tag);
583 $query = "SELECT it.* ".
584 "FROM {$CFG->prefix}{$type} it INNER JOIN {$CFG->prefix}tag_instance tt ON it.id = tt.itemid ".
585 "WHERE tt.itemtype = '{$type}' AND tt.tagid = '{$tagid}'";
587 return get_records_sql($query, $limitfrom, $limitnum);
593 ///////////////////////////////////////////////////////
594 /////////////////// PRIVATE TAG API ///////////////////
597 * A * @param array $record the record that will be tagged
598 * @param string $tags the comma-separated tags to set on the record. If
599 * given an empty array, all tags will be removed.
600 dds one or more tag in the database. This function should not be called
601 * directly : you should use tag_set.
603 * @param mixed $tags one tag, or an array of tags, to be created
604 * @param string $tag_type type of tag to be created ("default" is the default
605 * value and "official" is the only other supported value at this time). An
606 * official tag is kept even if there are no records tagged with it.
607 * @return an array of tags ids, indexed by their lowercase normalized names.
608 * Any boolean false in the array indicates an error while adding the tag.
610 function tag_add($tags, $type="default") {
611 global $USER;
613 require_capability('moodle/tag:create', get_context_instance(CONTEXT_SYSTEM));
615 if (!is_array($tags)) {
616 $tags = array($tags);
619 $tag_object = new StdClass;
620 $tag_object->tagtype = $type;
621 $tag_object->userid = $USER->id;
622 $tag_object->timemodified = time();
624 $clean_tags = tag_normalize($tags, TAG_CASE_ORIGINAL);
626 $tags_ids = array();
627 foreach($clean_tags as $tag) {
628 $tag = trim($tag);
629 if (!$tag) {
630 $tags_ids[$tag] = false;
631 } else {
632 // note that the difference between rawname and name is only
633 // capitalization : the rawname is NOT the same at the rawtag.
634 $tag_object->rawname = addslashes($tag);
635 $tag_name_lc = moodle_strtolower($tag);
636 $tag_object->name = addslashes($tag_name_lc);
637 //var_dump($tag_object);
638 $tags_ids[$tag_name_lc] = insert_record('tag', $tag_object);
642 return $tags_ids;
646 * Assigns a tag to a record: if the record already exists, the time and
647 * ordering will be updated.
649 * @param string $record_type the type of the record that will be tagged
650 * @param int $record_id the id of the record that will be tagged
651 * @param string $tagid the tag id to set on the record.
652 * @param int $ordering the order of the instance for this record
653 * @return bool true on success, false otherwise
655 function tag_assign($record_type, $record_id, $tagid, $ordering) {
657 require_capability('moodle/tag:create', get_context_instance(CONTEXT_SYSTEM));
659 if ( $tag_instance_object = get_record('tag_instance', 'tagid', $tagid, 'itemtype', $record_type, 'itemid', $record_id, 'id') ) {
660 $tag_instance_object->ordering = $ordering;
661 $tag_instance_object->timemodified = time();
662 return update_record('tag_instance', $tag_instance_object);
663 } else {
664 $tag_instance_object = new StdClass;
665 $tag_instance_object->tagid = $tagid;
666 $tag_instance_object->itemid = $record_id;
667 $tag_instance_object->itemtype = $record_type;
668 $tag_instance_object->ordering = $ordering;
669 $tag_instance_object->timemodified = time();
670 return insert_record('tag_instance', $tag_instance_object);
675 * Function that returns tags that start with some text, for use by the autocomplete feature
677 * @param string $text string that the tag names will be matched against
678 * @return mixed an array of objects, or false if no records were found or an error occured.
680 function tag_autocomplete($text) {
681 global $CFG;
682 return get_records_sql("SELECT tg.id, tg.name, tg.rawname FROM {$CFG->prefix}tag tg WHERE tg.name LIKE '". moodle_strtolower($text) ."%'");
686 * Calculates and stores the correlated tags of all tags.
687 * The correlations are stored in the 'tag_correlation' table.
689 * Two tags are correlated if they appear together a lot.
690 * Ex.: Users tagged with "computers" will probably also be tagged with "algorithms".
692 * The rationale for the 'tag_correlation' table is performance.
693 * It works as a cache for a potentially heavy load query done at the 'tag_instance' table.
694 * So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table.
696 * @param number $min_correlation cutoff percentage (optional, default is 2)
698 function tag_compute_correlations($min_correlation=2) {
700 global $CFG;
702 if (!$all_tags = get_records_list('tag')) {
703 return;
706 $tag_correlation_obj = new object();
707 foreach($all_tags as $tag) {
709 // query that counts how many times any tag appears together in items
710 // with the tag passed as argument ($tag_id)
711 $query = "SELECT tb.tagid , COUNT(*) AS nr ".
712 "FROM {$CFG->prefix}tag_instance ta INNER JOIN {$CFG->prefix}tag_instance tb ON ta.itemid = tb.itemid ".
713 "WHERE ta.tagid = {$tag->id} AND tb.tagid != {$tag->id} ".
714 "GROUP BY tb.tagid ".
715 "HAVING nr > $min_correlation ".
716 "ORDER BY nr DESC";
718 $correlated = array();
720 // Correlated tags happen when they appear together in more occasions
721 // than $min_correlation.
722 if ($tag_correlations = get_records_sql($query)) {
723 foreach($tag_correlations as $correlation) {
724 // commented out - now done in query. kept here in case it breaks on some db
725 // if($correlation->nr >= $min_correlation){
726 $correlated[] = $correlation->tagid;
727 // }
731 if (empty($correlated)) {
732 continue;
735 $correlated = implode(',', $correlated);
736 //var_dump($correlated);
738 //saves correlation info in the caching table
739 if ($tag_correlation_obj = get_record('tag_correlation', 'tagid', $tag->id, '', '', '', '', 'tagid')) {
740 $tag_correlation_obj->correlatedtags = $correlated;
741 update_record('tag_correlation', $tag_correlation_obj);
742 } else {
743 $tag_correlation_obj->tagid = $tag->id;
744 $tag_correlation_obj->correlatedtags = $correlated;
745 insert_record('tag_correlation', $tag_correlation_obj);
751 * Tasks that should be performed at cron time
753 function tag_cron() {
754 tag_compute_correlations();
758 * Search for tags with names that match some text
760 * @param string $text escaped string that the tag names will be matched against
761 * @param boolean $ordered If true, tags are ordered by their popularity. If false, no ordering.
762 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
763 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
764 * @return mixed an array of objects, or false if no records were found or an error occured.
766 function tag_find_tags($text, $ordered=true, $limitfrom='', $limitnum='') {
768 global $CFG;
770 $text = array_shift(tag_normalize($text, TAG_CASE_LOWER));
772 if ($ordered) {
773 $query = "SELECT tg.id, tg.name, tg.rawname, COUNT(ti.id) AS count ".
774 "FROM {$CFG->prefix}tag tg LEFT JOIN {$CFG->prefix}tag_instance ti ON tg.id = ti.tagid ".
775 "WHERE tg.name LIKE '%{$text}%' ".
776 "GROUP BY tg.id, tg.name, tg.rawname ".
777 "ORDER BY count DESC";
778 } else {
779 $query = "SELECT tg.id, tg.name, tg.rawname ".
780 "FROM {$CFG->prefix}tag tg ".
781 "WHERE tg.name LIKE '%{$text}%'";
783 return get_records_sql($query, $limitfrom , $limitnum);
786 /**
787 * Get the name of a tag
789 * @param mixed $tagids the id of the tag, or an array of ids
790 * @return mixed string name of one tag, or id-indexed array of strings
792 function tag_get_name($tagids) {
794 $return_a_string = false;
795 if ( !is_array($tagids) ) {
796 $return_a_string = true;
797 $tagids = array($tagids);
800 $tag_names = array();
801 foreach(get_records_list('tag', 'id', implode(',', $tagids)) as $tag) {
802 $tag_names[$tag->id] = $tag->name;
805 if ($return_a_string) {
806 return array_pop($tag_names);
809 return $tag_names;
813 * Returns the correlated tags of a tag, retrieved from the tag_correlation
814 * table. Make sure cron runs, otherwise the table will be empty and this
815 * function won't return anything.
817 * @param int $tag_id is a single tag id
818 * @return array an array of tag objects, empty if no correlated tags are found
820 function tag_get_correlated($tag_id, $limitnum=null) {
821 global $CFG;
823 $tag_correlation = get_record('tag_correlation', 'tagid', $tag_id);
825 if (!$tag_correlation || empty($tag_correlation->correlatedtags)) {
826 return array();
829 // this is (and has to) return the same fields as the query in tag_get_tags
830 if ( !$result = get_records_sql("SELECT tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ".
831 "FROM {$CFG->prefix}tag tg INNER JOIN {$CFG->prefix}tag_instance ti ON tg.id = ti.tagid ".
832 "WHERE tg.id IN ({$tag_correlation->correlatedtags})") ) {
833 return array();
836 return $result;
840 * Function that normalizes a list of tag names.
842 * @param mixed $tags array of tags, or a single tag.
843 * @param int $case case to use for returned value (default: lower case).
844 * Either TAG_CASE_LOWER (default) or TAG_CASE_ORIGINAL
845 * @return array of lowercased normalized tags, indexed by the normalized tag,
846 * in the same order as the original array. (Eg: 'Banana' => 'banana').
848 function tag_normalize($rawtags, $case = TAG_CASE_LOWER) {
850 // cache normalized tags, to prevent costly repeated calls to clean_param
851 static $cleaned_tags_lc = array(); // lower case - use for comparison
852 static $cleaned_tags_mc = array(); // mixed case - use for saving to database
854 if ( !is_array($rawtags) ) {
855 $rawtags = array($rawtags);
858 $result = array();
859 foreach($rawtags as $rawtag) {
860 $rawtag = trim($rawtag);
861 if (!$rawtag) {
862 continue;
864 if ( !array_key_exists($rawtag, $cleaned_tags_lc) ) {
865 $cleaned_tags_lc[$rawtag] = moodle_strtolower( clean_param($rawtag, PARAM_TAG) );
866 $cleaned_tags_mc[$rawtag] = clean_param($rawtag, PARAM_TAG);
868 if ( $case == TAG_CASE_LOWER ) {
869 $result[$rawtag] = $cleaned_tags_lc[$rawtag];
870 } else { // TAG_CASE_ORIGINAL
871 $result[$rawtag] = $cleaned_tags_mc[$rawtag];
875 return $result;
879 * Count how many records are tagged with a specific tag,
881 * @param string $record record to look for ('post', 'user', etc.)
882 * @param int $tag is a single tag id
883 * @return int number of mathing tags.
885 function tag_record_count($record_type, $tagid) {
886 return count_records('tag_instance', 'itemtype', $record_type, 'tagid', $tagid);
890 * Determine if a record is tagged with a specific tag
892 * @param string $record_type the record type to look for
893 * @param int $record_id the record id to look for
894 * @param string $tag a tag name
895 * @return bool true if it is tagged, false otherwise
897 function tag_record_tagged_with($record_type, $record_id, $tag) {
898 if ($tagid = tag_get_id($tag)) {
899 return count_records('tag_instance', 'itemtype', $record_type, 'itemid', $record_id, 'tagid', $tagid);
900 } else {
901 return 0; // tag doesn't exist
906 * Flag a tag as inapropriate
908 * @param mixed $tagids one (int) tagid, or an array of tagids
909 * @return void
911 function tag_set_flag($tagids) {
912 if ( !is_array($tagids) ) {
913 $tagids = array($tagids);
915 foreach ($tagids as $tagid) {
916 $tag = get_record('tag', 'id', $tagid, '', '', '', '', 'id, flag');
917 $tag->flag++;
918 $tag->timemodified = time();
919 update_record('tag', $tag);
923 /**
924 * Remove the inapropriate flag on a tag
926 * @param mixed $tagids one (int) tagid, or an array of tagids
927 * @return bool true if function succeeds, false otherwise
929 function tag_unset_flag($tagids) {
930 global $CFG;
932 require_capability('moodle/tag:manage', get_context_instance(CONTEXT_SYSTEM));
934 if ( is_array($tagids) ) {
935 $tagids = implode(',', $tagids);
937 $timemodified = time();
938 return execute_sql("UPDATE {$CFG->prefix}tag tg SET tg.flag = 0, tg.timemodified = $timemodified WHERE tg.id IN ($tagids)", false);