MDL-13638 Merged from stable
[moodle.git] / tag / lib.php
1 <?php // $Id$
3 /**
4  * lib.php - moodle tag library
5  *
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 
23  *
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...
28  *
29  * BASIC INSTRUCTIONS : 
30  *  - to "tag a blog post" (for example): 
31  *      tag_set('post', $blog_post->id, $array_of_tags);
32  *
33  *  - to "remove all the tags on a blog post":
34  *      tag_set('post', $blog_post->id, array());
35  *
36  * Tag set will create tags that need to be created.  
37  */
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 require_once($CFG->dirroot .'/tag/locallib.php');
49 ///////////////////////////////////////////////////////
50 /////////////////// PUBLIC TAG API ////////////////////
52 /**
53  * Delete one or more tag, and all their instances if there are any left.
54  * 
55  * @param mixed $tagids one tagid (int), or one array of tagids to delete
56  * @return bool true on success, false otherwise 
57  */
58 function tag_delete($tagids) {
60     if (!is_array($tagids)) {
61         $tagids = array($tagids);
62     }
64     $success = true;
65     foreach( $tagids as $tagid ) {
66         if (is_null($tagid)) { // can happen if tag doesn't exists
67             continue;
68         }
69         // only delete the main entry if there were no problems deleting all the 
70         // instances - that (and the fact we won't often delete lots of tags) 
71         // is the reason for not using delete_records_select()
72         if ( delete_records('tag_instance', 'tagid', $tagid) ) {
73             $success &= (bool) delete_records('tag', 'id', $tagid);
74         }
75     }
77     return $success;
78 }
80 /**
81  * Delete one instance of a tag.  If the last instance was deleted, it will
82  * also delete the tag, unless it's type is 'official'.
83  *
84  * @param array $record the record for which to remove the instance
85  * @param int $tagid the tagid that needs to be removed
86  * @return bool true on success, false otherwise
87  */
88 function tag_delete_instance($record, $tagid) {
89     global $CFG;
91     if ( delete_records('tag_instance', 'tagid', $tagid, 'itemtype', $record['type'], 'itemid', $record['id']) ) {
92         if ( !record_exists_sql('SELECT * FROM '. $CFG->prefix .'tag tg, '. $CFG->prefix .'tag_instance ti '.
93                 'WHERE (tg.id = ti.tagid AND ti.tagid = '. $tagid .') OR '.
94                 '(tg.id = '. $tagid .' AND tg.tagtype = "official")') ) {
95             return tag_delete($tagid);
96         }
97     } else {
98         return false;
99     }
102 /**
103  * Function that returns the name that should be displayed for a specific tag
104  *
105  * @param object $tag_object a line out of tag table, as returned by the adobd functions
106  * @return string
107  */
108 function tag_display_name($tag_object) {
110     global $CFG;
112     if(!isset($tag_object->name)) {
113         return '';
114     }
116     if( empty($CFG->keeptagnamecase) ) {
117         //this is the normalized tag name
118         $textlib = textlib_get_instance();
119         return htmlspecialchars($textlib->strtotitle($tag_object->name));
120     }
121     else {
122         //original casing of the tag name
123         return htmlspecialchars($tag_object->rawname);
124     }
127 /**
128  * Find all records tagged with a tag of a given type ('post', 'user', etc.)
129  *
130  * @param string $tag tag to look for
131  * @param string $type type to restrict search to.  If null, every matching
132  *     record will be returned
133  * @return array of matching objects, indexed by record id, from the table containing the type requested
134  */
135 function tag_find_records($tag, $type) {
136     
137     global $CFG;
139     if (!$tag || !$type) {
140         return array();
141     }
143     $tagid = tag_get_id($tag);
145     $query = "SELECT it.* ".
146         "FROM {$CFG->prefix}{$type} it INNER JOIN {$CFG->prefix}tag_instance tt ON it.id = tt.itemid ".
147         "WHERE tt.itemtype = '{$type}' AND tt.tagid = '{$tagid}'";
148     
149     return get_records_sql($query); 
152 /**
153  * Get the array of db record of tags associated to a record (instances).  Use 
154  * tag_get_tags_csv to get the same information in a comma-separated string.
155  *
156  * @param array $record the record for which we want to get the tags 
157  * @param string $type the tag type (either 'default' or 'official'). By default,
158  *     all tags are returned.
159  * @return array the array of tags
160  */
161 function tag_get_tags($record, $type=null) {
162     
163     global $CFG;
165     if ($type) {
166         $type = "AND tg.tagtype = '$type'";
167     }
168     
169     $tags = get_records_sql('SELECT tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering '.
170         'FROM '. $CFG->prefix .'tag_instance ti INNER JOIN '. $CFG->prefix .'tag tg ON tg.id = ti.tagid '.
171         'WHERE ti.itemtype = "'. $record['type'] .'" AND ti.itemid = "'. $record['id'] .'" '. $type .' '.
172         'ORDER BY ti.ordering ASC');
173     // This version of the query, reversing the ON clause, "correctly" returns 
174     // a row with NULL values for instances that are still in the DB even though 
175     // the tag has been deleted.  This shouldn't happen, but if it did, using 
176     // this query could help "clean it up".  This causes bugs at this time.
177     //$tags = get_records_sql('SELECT ti.tagid, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering '.
178     //    'FROM '. $CFG->prefix .'tag_instance ti LEFT JOIN '. $CFG->prefix .'tag tg ON ti.tagid = tg.id '.
179     //    'WHERE ti.itemtype = "'. $record['type'] .'" AND ti.itemid = "'. $record['id'] .'" '. $type .' '.
180     //    'ORDER BY ti.ordering ASC');
182     if (!$tags) { 
183         return array();
184     } else {
185         return $tags;
186     }
189 /**
190  * Get the array of tags display names, indexed by id.
191  * 
192  * @param array $record the record for which we want to get the tags
193  * @param string $type the tag type (either 'default' or 'official'). By default,
194  *     all tags are returned.
195  * @return array the array of tags (with the value returned by tag_display_name), indexed by id
196  */
197 function tag_get_tags_array($record, $type=null) {
198     $tags = array();
199     foreach(tag_get_tags($record, $type) as $tag) {
200         $tags[$tag->id] = tag_display_name($tag);
201     }
202     return $tags;
205 /**
206  * Get a comma-separated string of tags associated to a record.  Use tag_get_tags
207  * to get the same information in an array.
208  *
209  * @param array $record the record for which we want to get the tags
210  * @param int $html either TAG_RETURN_HTML or TAG_RETURN_TEXT, depending
211  *     on the type of output desired
212  * @param string $type either 'official' or 'default', if null, all tags are
213  *     returned
214  * @return string the comma-separated list of tags.
215  */
216 function tag_get_tags_csv($record, $html=TAG_RETURN_HTML, $type=null) {
217     global $CFG;
219     $tags_names = array();
220     foreach( tag_get_tags($record, $type) as $tag ) {
221         if ($html == TAG_RETURN_TEXT) {
222             $tags_names[] = tag_display_name($tag);
223         } else { // TAG_RETURN_HTML
224             $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
225         }
226     }
227     return implode(', ', $tags_names);
230 /**
231  * Get an array of tag ids associated to a record.
232  *
233  * @param array $record the record for which we want to get the tags
234  * @return array of tag ids, indexed and sorted by 'ordering'
235  */
236 function tag_get_tags_ids($record) {
237     
238     $tag_ids = array();
239     foreach( tag_get_tags($record) as $tag ) {
240         $tag_ids[$tag->ordering] = $tag->id;
241     }
242     ksort($tag_ids);
243     return $tag_ids;
246 /** 
247  * Returns the database ID of a set of tags.
248  * 
249  * @param mixed $tags one tag, or array of tags, to look for.
250  * @param bool $return_value specify the type of the returned value. Either 
251  *     TAG_RETURN_OBJECT, or TAG_RETURN_ARRAY (default). If TAG_RETURN_ARRAY 
252  *     is specified, an array will be returned even if only one tag was 
253  *     passed in $tags.
254  * @return mixed tag-indexed array of ids (or objects, if second parameter is 
255  *     TAG_RETURN_OBJECT), or only an int, if only one tag is given *and* the 
256  *     second parameter is null. No value for a key means the tag wasn't found.
257  */
258 function tag_get_id($tags, $return_value=null) {
259     global $CFG;
260     static $tag_id_cache = array();
262     $return_an_int = false;
263     if (!is_array($tags)) {
264         if(is_null($return_value) || $return_value == TAG_RETURN_OBJECT) {
265             $return_an_int = true; 
266         }
267         $tags = array($tags);
268     }
269    
270     $result = array();
271     
272     //TODO: test this and see if it helps performance without breaking anything
273     //foreach($tags as $key => $tag) {
274     //    $clean_tag = moodle_strtolower($tag);
275     //    if ( array_key_exists($clean_tag), $tag_id_cache) ) {
276     //        $result[$clean_tag] = $tag_id_cache[$clean_tag];
277     //        $tags[$key] = ''; // prevent further processing for this one.
278     //    }
279     //}
281     $tags = array_values(tag_normalize($tags));
282     foreach($tags as $key => $tag) {
283         $tags[$key] = addslashes(moodle_strtolower($tag)); 
284         $result[moodle_strtolower($tag)] = null; // key must exists : no value for a key means the tag wasn't found.
285     }
286     $tag_string = "'". implode("', '", $tags) ."'";
288     if ($rs = get_recordset_sql('SELECT * FROM '. $CFG->prefix .'tag WHERE name in ('. $tag_string .') order by name')) {
289         while ($record = rs_fetch_next_record($rs)) {
290             if ($return_value == TAG_RETURN_OBJECT) {
291                 $result[$record->name] = $record;
292             } else { // TAG_RETURN_ARRAY
293                 $result[$record->name] = $record->id;
294             }
295         }
296     }
298     if ($return_an_int) {
299         return array_pop($result);
300     }
302     return $result;
305 /**
306  * Get a tag as an object (line) returned by get_recordset_sql 
307  *
308  * @param int $tagid a tag id
309  * @return object a line returned from get_recordset_sql, or false
310  */
311 function tag_get_tag_by_id($tagid) {
312     global $CFG;
313     $rs = get_recordset_sql('SELECT * FROM '. $CFG->prefix .'tag WHERE id = '. $tagid);
314     return rs_fetch_next_record($rs);
317 /**
318  * Returns tags related to a tag
319  *
320  * Related tags of a tag come from two sources:
321  *   - manually added related tags, which are tag_instance entries for that tag
322  *   - correlated tags, which are a calculated
323  *
324  * @param string $tag_name_or_id is a single **normalized** tag name or the id of a tag
325  * @param int $limitnum return a subset comprising this many records (optional, default is 10)
326  * @return array an array of tag objects
327  */
328 function tag_get_related_tags($tagid, $limitnum=10) {
330     //gets the manually added related tags
331     if (!$related_tags = tag_get_tags(array('type'=>'tag', 'id'=>$tagid))) {
332         $related_tags = array();
333     }
335     //gets the correlated tags
336     $automatic_related_tags = tag_get_correlated($tagid, $limitnum);
337     if (is_array($automatic_related_tags)) {
338         $related_tags = array_merge($related_tags, $automatic_related_tags);
339     }
341     return array_slice(object_array_unique($related_tags), 0 , $limitnum);
344 /** 
345  * Get a comma-separated list of tags related to another tag.
346  *
347  * @param array $related_tags the array returned by tag_get_related_tags
348  * @param int $html either TAG_RETURN_HTML (default) or TAG_RETURN_TEXT : return html links, or just text.
349  * @return string comma-separated list
350  */
351 function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) {
352     global $CFG;
354     $tags_names = array();
355     foreach($related_tags as $tag) {
356         if ( $html == TAG_RETURN_TEXT) {
357             $tags_names[] = rawurlencode(tag_display_name($tag));
358         } else {
359             // TAG_RETURN_HTML
360             $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
361         }
362     }
363     return implode(', ', $tags_names);
366 /**
367  * Change the "value" of a tag, and update the associated 'name'.
368  *
369  * @param int $tagid the id of the tag to modify
370  * @param string $newtag the new name
371  * @return bool true on success, false otherwise
372  */
373 function tag_rename($tagid, $newtag) {
375     if (! $newtag_clean = array_shift(tag_normalize($newtag, TAG_CASE_ORIGINAL)) ) {
376         return false;
377     }
379     if ( tag_get_id($newtag_clean) ) {
380         // 'newtag' already exists and merging tags is not yet supported.
381         return false; 
382     }
384     if ($tag = get_record('tag', 'id', $tagid)) {
385         $tag->rawname = addslashes($newtag_clean); 
386         $tag->name = addslashes(moodle_strtolower($newtag_clean)); 
387         $tag->timemodified = time();
388         return update_record('tag', $tag);
389     }
390     return false;
393 /**
394  * Set the tags assigned to a record.  This overwrites the current tags.
395  * 
396  * This function is meant to be fed the string coming up from the user 
397  * interface, which contains all tags assigned to a record.
398  *
399  * @param string $record_type the type of record to tag ('post' for blogs, 
400  *     'user' for users, 'tag' for tags, etc.
401  * @param int $record_id the id of the record to tag
402  * @param array $tags the array of tags to set on the record. If 
403  *     given an empty array, all tags will be removed.
404  * @return void 
405  */
406 function tag_set($record_type, $record_id, $tags) {
407     global $db;
409     $record = array('type' => $record_type, 'id' => $record_id);
411     $tags_ids = tag_get_id($tags, TAG_RETURN_ARRAY); // force an array, even if we only have one tag.
412     $cleaned_tags = tag_normalize($tags);
413     //echo 'tags-in-tag_set'; var_dump($tags); var_dump($tags_ids); var_dump($cleaned_tags);
415     $current_ids = tag_get_tags_ids($record);
416     //var_dump($current_ids);
417     $tags_to_assign = array();
419     // for data coherence reasons, it's better to remove deleted tags
420     // before adding new data: ordering could be duplicated.
421     foreach($current_ids as $current_id) {
422         if (!in_array($current_id, $tags_ids)) {
423             tag_delete_instance($record, $current_id);
424         }
425     }
427     foreach($tags as $ordering => $tag) {
428         $tag = trim($tag);
429         if (!$tag) {
430             continue;
431         }
433         $clean_tag = $cleaned_tags[$tag];
434         $tag_current_id = $tags_ids[$clean_tag];
435         
436         if ( is_null($tag_current_id) ) {
437             // create new tags
438             //echo "call to add tag $tag\n";
439             $new_tag = tag_add($tag);
440             tag_assign($record, $new_tag[$clean_tag], $ordering);
441         } 
442         elseif ( empty($current_ids) || !in_array($tag_current_id, $current_ids) ) {
443             // assign existing tags
444             tag_assign($record, $tag_current_id, $ordering);
445         } 
446         elseif ( isset($current_ids[$ordering]) && $current_ids[$ordering] != $tag_current_id ) { 
447             // this actually checks if the ordering number points to the same tag
448             //recompute ordering, if necessary
449             //echo 'ordering changed for ', $tag, ':', $ordering, "\n";
450             tag_assign($record, $tag_current_id, $ordering);
451         }
452     }
455 /**
456  * Adds a tag to a record, without overwriting the current tags.
457  * 
458  * @param string $record_type the type of record to tag ('post' for blogs, 
459  *     'user' for users, etc.
460  * @param int $record_id the id of the record to tag
461  * @param string $tag the tag to add
462  * @return void
463  */
464 function tag_set_add($record_type, $record_id, $tag) {
466     $record = array('type' => $record_type, 'id' => $record_id);
467     
468     $new_tags = array();
469     foreach( tag_get_tags($record) as $current_tag ) {
470         $new_tags[] = $current_tag->rawname;
471     }
472     $new_tags[] = $tag;
473     
474     return tag_set($record_type, $record_id, $new_tags);
477 /**
478  * Removes a tag from a record, without overwriting other current tags.
479  * 
480  * @param string $record_type the type of record to tag ('post' for blogs, 
481  *     'user' for users, etc.
482  * @param int $record_id the id of the record to tag
483  * @param string $tag the tag to delete
484  * @return void
485  */
486 function tag_set_delete($record_type, $record_id, $tag) {
488     $record = array('type' => $record_type, 'id' => $record_id);
489     
490     $new_tags = array();
491     foreach( tag_get_tags($record) as $current_tag ) {
492         if ($current_tag->name != $tag) {  // Keep all tags but the one specified
493             $new_tags[] = $current_tag->name;
494         }
495     }
496     
497     return tag_set($record_type, $record_id, $new_tags);
500 /**
501  * Set the type of a tag.  At this time (version 1.9) the possible values
502  * are 'default' or 'official'.  Official tags will be displayed separately "at
503  * tagging time" (while selecting the tags to apply to a record).
504  *
505  * @param string $tagid tagid to modify
506  * @param string $type either 'default' or 'official'
507  * @return true on success, false otherwise
508  */
509 function tag_type_set($tagid, $type) {
510     if ($tag = get_record('tag', 'id', $tagid)) {
511         $tag->tagtype = $type;
512         $tag->timemodified = time();
513         return update_record('tag', $tag);
514     }
515     return false;
518 ///////////////////////////////////////////////////////
519 /////////////////// PRIVATE TAG API ///////////////////
521 /**
522  * A * @param array $record the record that will be tagged
523  * @param string $tags the comma-separated tags to set on the record. If 
524  *     given an empty array, all tags will be removed.
525 dds one or more tag in the database.  This function should not be called 
526  * directly : you should use tag_set.
527  *
528  * @param mixed $tags one tag, or an array of tags, to be created
529  * @param string $tag_type type of tag to be created ("default" is the default 
530  *     value and "official" is the only other supported value at this time). An
531  *     official tag is kept even if there are no records tagged with it.
532  * @return an array of tags ids, indexed by their lowercase normalized names. 
533  *     Any boolean false in the array indicates an error while adding the tag.
534  */
535 function tag_add($tags, $type="default") {
536     global $USER;
538     require_capability('moodle/tag:create', get_context_instance(CONTEXT_SYSTEM)); 
540     if (!is_array($tags)) {
541         $tags = array($tags);
542     }
544     $tag_object = new StdClass;
545     $tag_object->tagtype = $type;
546     $tag_object->userid = $USER->id;
547     $tag_object->timemodified   = time();
549     $clean_tags = tag_normalize($tags, TAG_CASE_ORIGINAL);
551     $tags_ids = array();
552     foreach($clean_tags as $tag) {
553         $tag = trim($tag);
554         if (!$tag) {
555             $tags_ids[$tag] = false;
556         } else {
557             // note that the difference between rawname and name is only 
558             // capitalization : the rawname is NOT the same at the rawtag. 
559             $tag_object->rawname = addslashes($tag); 
560             $tag_name_lc = moodle_strtolower($tag);
561             $tag_object->name = addslashes($tag_name_lc); 
562             //var_dump($tag_object);
563             $tags_ids[$tag_name_lc] = insert_record('tag', $tag_object);
564         }
565     }
567     return $tags_ids;
570 /**
571  * Assigns a tag to a record: if the record already exists, the time and
572  * ordering will be updated.
573  * 
574  * @param array $record the record that will be tagged
575  * @param string $tagid the tag id to set on the record. 
576  * @param int $ordering the order of the instance for this record
577  * @return bool true on success, false otherwise
578  */
579 function tag_assign($record, $tagid, $ordering) {
581     require_capability('moodle/tag:create', get_context_instance(CONTEXT_SYSTEM));
583     if ( $tag_instance_object = get_record('tag_instance', 'tagid', $tagid, 'itemtype', $record['type'], 'itemid', $record['id']) ) {
584         $tag_instance_object->ordering = $ordering;
585         $tag_instance_object->timemodified = time();
586         return update_record('tag_instance', $tag_instance_object);
587     } else { 
588         $tag_instance_object = new StdClass;
589         $tag_instance_object->tagid = $tagid;
590         $tag_instance_object->itemid = $record['id'];
591         $tag_instance_object->itemtype = $record['type'];
592         $tag_instance_object->ordering = $ordering;
593         $tag_instance_object->timemodified = time();
594         return insert_record('tag_instance', $tag_instance_object);
595     }
598 /**
599  * Function that returns tags that start with some text, for use by the autocomplete feature
600  *
601  * @param string $text string that the tag names will be matched against
602  * @return mixed an array of objects, or false if no records were found or an error occured.
603  */
604 function tag_autocomplete($text) {
605     global $CFG;
606     return get_records_sql('SELECT tg.id, tg.name, tg.rawname FROM '. $CFG->prefix .'tag tg WHERE tg.name LIKE "'. moodle_strtolower($text) .'%"');
609 /**
610  * Calculates and stores the correlated tags of all tags.
611  * The correlations are stored in the 'tag_correlation' table.
612  *
613  * Two tags are correlated if they appear together a lot.
614  * Ex.: Users tagged with "computers" will probably also be tagged with "algorithms".
615  *
616  * The rationale for the 'tag_correlation' table is performance.
617  * It works as a cache for a potentially heavy load query done at the 'tag_instance' table.
618  * So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table.
619  *
620  * @param number $min_correlation cutoff percentage (optional, default is 2)
621  */
622 function tag_compute_correlations($min_correlation=2) {
624     global $CFG;
626     $all_tags = get_records_list('tag');
627     
629     $tag_correlation_obj = new object();
630     foreach($all_tags as $tag) {
632         // query that counts how many times any tag appears together in items
633         // with the tag passed as argument ($tag_id)
634         $query = "SELECT tb.tagid , COUNT(*) AS nr ".
635             "FROM {$CFG->prefix}tag_instance ta INNER JOIN {$CFG->prefix}tag_instance tb ON ta.itemid = tb.itemid ".
636             "WHERE ta.tagid = {$tag->id} AND tb.tagid != {$tag->id} ".
637             "GROUP BY tb.tagid ".
638             "HAVING nr > $min_correlation ".
639             "ORDER BY nr DESC";  // todo: find out if it's necessary to order.
641         $correlated = array();
643         // Correlated tags happen when they appear together in more occasions 
644         // than $min_correlation.
645         if ($tag_correlations = get_records_sql($query)) {
646             foreach($tag_correlations as $correlation) {
647         //        if($correlation->nr >= $min_correlation){
648                     $correlated[] = $correlation->tagid;
649         //        }
650             }
651         }
653         $correlated = implode(',', $correlated);
654         //var_dump($correlated);
656         //saves correlation info in the caching table
657         if ($tag_correlation_obj = get_record('tag_correlation', 'tagid', $tag->id)) {
658             $tag_correlation_obj->correlatedtags = $correlated;
659             update_record('tag_correlation', $tag_correlation_obj);
660         } else {
661             $tag_correlation_obj->tagid          = $tag->id;
662             $tag_correlation_obj->correlatedtags = $correlated;
663             insert_record('tag_correlation', $tag_correlation_obj);
664         }
665     }
668 /**
669  * Tasks that should be performed at cron time
670  */
671 function tag_cron() {
672     tag_compute_correlations();
675 /** 
676  * Get the name of a tag
677  * 
678  * @param mixed $tagids the id of the tag, or an array of ids
679  * @return mixed string name of one tag, or id-indexed array of strings
680  */
681 function tag_get_name($tagids) {
683     $return_a_string = false;
684     if ( !is_array($tagids) ) {
685         $return_a_string = true;
686         $tagids = array($tagids);
687     }
689     $tag_names = array();
690     foreach(get_records_list('tag', 'id', implode(',', $tagids)) as $tag) { 
691         $tag_names[$tag->id] = $tag->name;
692     }
694     if ($return_a_string) {
695         return array_pop($tag_names);
696     }
698     return $tag_names;
701 /**
702  * Returns the correlated tags of a tag, retrieved from the tag_correlation
703  * table.  Make sure cron runs, otherwise the table will be empty and this 
704  * function won't return anything.
705  *
706  * @param int $tag_id is a single tag id
707  * @return array an array of tag objects, empty if no correlated tags are found
708  */
709 function tag_get_correlated($tag_id, $limitnum=null) {
711     $tag_correlation = get_record('tag_correlation', 'tagid', $tag_id);
713     if (!$tag_correlation || empty($tag_correlation->correlatedtags)) {
714         return array();
715     }
716     
717     if (!$result = get_records_select('tag', "id IN ({$tag_correlation->correlatedtags})", '', '*', 0, $limitnum)) {
718         return array();
719     }
721     return $result;
724 /**
725  * Function that normalizes a list of tag names.
726  *
727  * @param mixed $tags array of tags, or a single tag.
728  * @param int $case case to use for returned value (default: lower case). Either CASE_LOWER or CASE_UPPER
729  * @return array of lowercased normalized tags, indexed by the normalized tag. (Eg: 'Banana' => 'banana')
730  */
731 function tag_normalize($rawtags, $case = TAG_CASE_LOWER) {
733     // cache normalized tags, to prevent (in some cases) costly (repeated) calls to clean_param
734     static $cleaned_tags_lc = array(); // lower case - use for comparison
735     static $cleaned_tags_mc = array(); // mixed case - use for saving to database
737     if ( !is_array($rawtags) ) {
738         $rawtags = array($rawtags);
739     }
741     $result = array();
742     foreach($rawtags as $rawtag) {
743         $rawtag = trim($rawtag);
744         if (!$rawtag) {
745             continue;
746         }
747         if ( !array_key_exists($rawtag, $cleaned_tags_lc) ) {
748             $cleaned_tags_lc[$rawtag] = moodle_strtolower( clean_param($rawtag, PARAM_TAG) );
749             $cleaned_tags_mc[$rawtag] = clean_param($rawtag, PARAM_TAG);
750         }
751         if ( $case == TAG_CASE_LOWER ) { 
752             $result[$rawtag] = $cleaned_tags_lc[$rawtag];
753         } else { // TAG_CASE_ORIGINAL
754             $result[$rawtag] = $cleaned_tags_mc[$rawtag];
755         }
756     }
757     
758     return $result;
762 /**
763  * Search for tags with names that match some text
764  *
765  * @param string $text escaped string that the tag names will be matched against
766  * @param boolean $ordered If true, tags are ordered by their popularity. If false, no ordering.
767  * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
768  * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
769  * @return mixed an array of objects, or false if no records were found or an error occured.
770  */
771 function tag_find_tags($text, $ordered=true, $limitfrom='', $limitnum='') {
773     global $CFG;
775     $text = array_shift(tag_normalize($text, TAG_CASE_LOWER));
777     if ($ordered) {
778         $query = "SELECT tg.id, tg.name, tg.rawname, COUNT(ti.id) AS count ".
779             "FROM {$CFG->prefix}tag tg LEFT JOIN {$CFG->prefix}tag_instance ti ON tg.id = ti.tagid ".
780             "WHERE tg.name LIKE '%{$text}%' ".
781             "GROUP BY tg.id, tg.name, tg.rawname ".
782             "ORDER BY count DESC";
783     } else {
784         $query = "SELECT tg.id, tg.name, tg.rawname ".
785             "FROM {$CFG->prefix}tag tg ".
786             "WHERE tg.name LIKE '%{$text}%'";
787     }
788     return get_records_sql($query, $limitfrom , $limitnum);
791 ///////////////////////////////////////////////////////////
792 ////// functions copied over from the first version //////
794 /**
795  * Flag a tag as inapropriate
796  * 
797  * @param mixed $tagids one (int) tagid, or an array of tagids
798  * @return void
799  */
800 function tag_set_flag($tagids) {
801     if ( !is_array($tagids) ) {
802         $tagids = array($tagids);
803     }
804     foreach ($tagids as $tagid) {
805         $tag = get_record('tag', 'id', $tagid);
806         $tag->flag++;
807         $tag->timemodified = time();
808         update_record('tag', $tag);
809     }
812 /** 
813  * Remove the inapropriate flag on a tag
814  * 
815  * @param mixed $tagids one (int) tagid, or an array of tagids
816  * @return bool true if function succeeds, false otherwise
817  */
818 function tag_unset_flag($tagids) {
819     global $CFG;
821     require_capability('moodle/tag:manage', get_context_instance(CONTEXT_SYSTEM));
823     if ( is_array($tagids) ) {
824         $tagids = implode(',', $tagids);
825     }
826     $timemodified = time();
827     return execute_sql('UPDATE '. $CFG->prefix .'tag tg SET tg.flag = 0, tg.timemodified = '. $timemodified .' WHERE tg.id IN ('. $tagids .')', false);
830 /**
831  * Count how many records are tagged with a specific tag,
832  *
833  * @param string $record record to look for ('post', 'user', etc.)
834  * @param int $tag is a single tag id
835  * @return int number of mathing tags.
836  */
837 function tag_record_count($record_type, $tagid) {
838     return count_records('tag_instance', 'itemtype', $record_type, 'tagid', $tagid);
841 /**
842  * Determine if a record is tagged with a specific tag  
843  *
844  * @param array $record the record to look for
845  * @param string $tag a tag name
846  * @return bool true if it is tagged, false otherwise
847  */
848 function tag_record_tagged_with($record, $tag) {
849     if ($tagid = tag_get_id($tag)) {
850         return count_records('tag_instance', 'itemtype', $record['type'], 'itemid', $record['id'], 'tagid', $tagid);
851     } else {
852         return 0; // tag doesn't exist
853     }
856 ?>