MDL-50550 wiki: Added unit tests for mod_wiki_get_wikis_by_courses
[moodle.git] / tag / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Moodle tag library
20  *
21  * Tag strings : you can use any character in tags, except the comma (which is the separator) and
22  * the '\' (backslash).  Note that many spaces (or other blank characters) will get "compressed"
23  * into one. A tag string is always a rawurlencode'd string. This is the same behavior as
24  * http://del.icio.us.
25  *
26  * A "record" is a php array (note that an object will work too) that contains the following
27  * variables :
28  *  - type: The database table containing the record that we are tagging (eg: for a blog, this is
29  *          the table named 'post', and for a user it is the table name 'user')
30  *  - id:   The id of the record
31  *
32  * BASIC INSTRUCTIONS :
33  *  - to "tag a blog post" (for example):
34  *        tag_set('post', $blog_post->id, $array_of_tags, 'core', $thecontext);
35  *
36  *  - to "remove all the tags on a blog post":
37  *        tag_set('post', $blog_post->id, array(), 'core', $thecontext);
38  *
39  * Tag set will create tags that need to be created.
40  *
41  * @package    core_tag
42  * @category   tag
43  * @todo       MDL-31090 turn this into a full-fledged categorization system. This could start by
44  *             modifying (removing, probably) the 'tag type' to use another table describing the
45  *             relationship between tags (parents, sibling, etc.), which could then be merged with
46  *             the 'course categorization' system.
47  * @see        http://www.php.net/manual/en/function.urlencode.php
48  * @copyright  2007 Luiz Cruz <luiz.laydner@gmail.com>
49  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
50  */
52 /**
53  * Used to require that the return value from a function is an array.
54  * @see tag_set()
55  */
56 define('TAG_RETURN_ARRAY', 0);
57 /**
58  * Used to require that the return value from a function is an object.
59  * @see tag_set()
60  */
61 define('TAG_RETURN_OBJECT', 1);
62 /**
63  * Use to specify that HTML free text is expected to be returned from a function.
64  * @see tag_display_name()
65  */
66 define('TAG_RETURN_TEXT', 2);
67 /**
68  * Use to specify that encoded HTML is expected to be returned from a function.
69  * @see tag_display_name()
70  */
71 define('TAG_RETURN_HTML', 3);
73 /**
74  * Used to specify that we wish a lowercased string to be returned
75  * @see tag_normal()
76  */
77 define('TAG_CASE_LOWER', 0);
78 /**
79  * Used to specify that we do not wish the case of the returned string to change
80  * @see tag_normal()
81  */
82 define('TAG_CASE_ORIGINAL', 1);
84 /**
85  * Used to specify that we want all related tags returned, no matter how they are related.
86  * @see tag_get_related_tags()
87  */
88 define('TAG_RELATED_ALL', 0);
89 /**
90  * Used to specify that we only want back tags that were manually related.
91  * @see tag_get_related_tags()
92  */
93 define('TAG_RELATED_MANUAL', 1);
94 /**
95  * Used to specify that we only want back tags where the relationship was automatically correlated.
96  * @see tag_get_related_tags()
97  */
98 define('TAG_RELATED_CORRELATED', 2);
100 ///////////////////////////////////////////////////////
101 /////////////////// PUBLIC TAG API ////////////////////
103 /// Functions for settings tags  //////////////////////
105 /**
106  * Set the tags assigned to a record.  This overwrites the current tags.
107  *
108  * This function is meant to be fed the string coming up from the user interface, which contains all tags assigned to a record.
109  *
110  * @package core_tag
111  * @category tag
112  * @access public
113  * @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, 'tag' for tags, etc.)
114  * @param int $record_id the id of the record to tag
115  * @param array $tags the array of tags to set on the record. If given an empty array, all tags will be removed.
116  * @param string|null $component the component that was tagged
117  * @param int|null $contextid the context id of where this tag was assigned
118  * @return bool|null
119  */
120 function tag_set($record_type, $record_id, $tags, $component = null, $contextid = null) {
122     static $in_recursion_semaphore = false; // this is to prevent loops when tagging a tag
124     if ( $record_type == 'tag' && !$in_recursion_semaphore) {
125         $current_tagged_tag_name = tag_get_name($record_id);
126     }
128     $tags_ids = tag_get_id($tags, TAG_RETURN_ARRAY); // force an array, even if we only have one tag.
129     $cleaned_tags = tag_normalize($tags);
130     //echo 'tags-in-tag_set'; var_dump($tags); var_dump($tags_ids); var_dump($cleaned_tags);
132     $current_ids = tag_get_tags_ids($record_type, $record_id);
133     //var_dump($current_ids);
135     // for data coherence reasons, it's better to remove deleted tags
136     // before adding new data: ordering could be duplicated.
137     foreach($current_ids as $current_id) {
138         if (!in_array($current_id, $tags_ids)) {
139             tag_delete_instance($record_type, $record_id, $current_id);
140             if ( $record_type == 'tag' && !$in_recursion_semaphore) {
141                 // if we are removing a tag-on-a-tag (manually related tag),
142                 // we need to remove the opposite relationship as well.
143                 tag_delete_instance('tag', $current_id, $record_id);
144             }
145         }
146     }
148     if (empty($tags)) {
149         return true;
150     }
152     foreach($tags as $ordering => $tag) {
153         $tag = trim($tag);
154         if (!$tag) {
155             continue;
156         }
158         $clean_tag = $cleaned_tags[$tag];
159         $tag_current_id = $tags_ids[$clean_tag];
161         if ( is_null($tag_current_id) ) {
162             // create new tags
163             //echo "call to add tag $tag\n";
164             $new_tag = tag_add($tag);
165             $tag_current_id = $new_tag[$clean_tag];
166         }
168         tag_assign($record_type, $record_id, $tag_current_id, $ordering, 0, $component, $contextid);
170         // if we are tagging a tag (adding a manually-assigned related tag), we
171         // need to create the opposite relationship as well.
172         if ( $record_type == 'tag' && !$in_recursion_semaphore) {
173             $in_recursion_semaphore = true;
174             tag_set_add('tag', $tag_current_id, $current_tagged_tag_name, $component, $contextid);
175             $in_recursion_semaphore = false;
176         }
177     }
180 /**
181  * Adds a tag to a record, without overwriting the current tags.
182  *
183  * @package core_tag
184  * @category tag
185  * @access public
186  * @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, etc.)
187  * @param int $record_id the id of the record to tag
188  * @param string $tag the tag to add
189  * @param string|null $component the component that was tagged
190  * @param int|null $contextid the context id of where this tag was assigned
191  * @return bool|null
192  */
193 function tag_set_add($record_type, $record_id, $tag, $component = null, $contextid = null) {
195     $new_tags = array();
196     foreach( tag_get_tags($record_type, $record_id) as $current_tag ) {
197         $new_tags[] = $current_tag->rawname;
198     }
199     $new_tags[] = $tag;
201     return tag_set($record_type, $record_id, $new_tags, $component, $contextid);
204 /**
205  * Removes a tag from a record, without overwriting other current tags.
206  *
207  * @package core_tag
208  * @category tag
209  * @access public
210  * @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, etc.)
211  * @param int $record_id the id of the record to tag
212  * @param string $tag the tag to delete
213  * @param string|null $component the component that was tagged
214  * @param int|null $contextid the context id of where this tag was assigned
215  * @return bool|null
216  */
217 function tag_set_delete($record_type, $record_id, $tag, $component = null, $contextid = null) {
219     $new_tags = array();
220     foreach( tag_get_tags($record_type, $record_id) as $current_tag ) {
221         if ($current_tag->name != $tag) {  // Keep all tags but the one specified
222             $new_tags[] = $current_tag->name;
223         }
224     }
226     return tag_set($record_type, $record_id, $new_tags, $component, $contextid);
229 /**
230  * Set the type of a tag.  At this time (version 2.2) the possible values are 'default' or 'official'.  Official tags will be
231  * displayed separately "at tagging time" (while selecting the tags to apply to a record).
232  *
233  * @package  core_tag
234  * @category tag
235  * @access   public
236  * @param    string   $tagid tagid to modify
237  * @param    string   $type either 'default' or 'official'
238  * @return   bool     true on success, false otherwise
239  */
240 function tag_type_set($tagid, $type) {
241     global $DB;
243     if ($tag = $DB->get_record('tag', array('id' => $tagid), 'id, userid, name, rawname')) {
244         $tag->tagtype = $type;
245         $tag->timemodified = time();
246         $DB->update_record('tag', $tag);
248         $event = \core\event\tag_updated::create(array(
249             'objectid' => $tag->id,
250             'relateduserid' => $tag->userid,
251             'context' => context_system::instance(),
252             'other' => array(
253                 'name' => $tag->name,
254                 'rawname' => $tag->rawname
255             )
256         ));
257         $event->trigger();
259         return true;
260     }
261     return false;
264 /**
265  * Set the description of a tag
266  *
267  * @package  core_tag
268  * @category tag
269  * @access   public
270  * @param    int      $tagid the id of the tag
271  * @param    string   $description the tag's description string to be set
272  * @param    int      $descriptionformat the moodle text format of the description
273  *                    {@link http://docs.moodle.org/dev/Text_formats_2.0#Database_structure}
274  * @return   bool     true on success, false otherwise
275  */
276 function tag_description_set($tagid, $description, $descriptionformat) {
277     global $DB;
279     if ($tag = $DB->get_record('tag', array('id' => $tagid), 'id, userid, name, rawname')) {
280         $tag->description = $description;
281         $tag->descriptionformat = $descriptionformat;
282         $tag->timemodified = time();
283         $DB->update_record('tag', $tag);
285         $event = \core\event\tag_updated::create(array(
286             'objectid' => $tag->id,
287             'relateduserid' => $tag->userid,
288             'context' => context_system::instance(),
289             'other' => array(
290                 'name' => $tag->name,
291                 'rawname' => $tag->rawname
292             )
293         ));
294         $event->trigger();
296         return true;
297     }
299     return false;
307 /// Functions for getting information about tags //////
309 /**
310  * Simple function to just return a single tag object when you know the name or something
311  *
312  * @package  core_tag
313  * @category tag
314  * @access   public
315  * @param    string $field        which field do we use to identify the tag: id, name or rawname
316  * @param    string $value        the required value of the aforementioned field
317  * @param    string $returnfields which fields do we want returned. This is a comma seperated string containing any combination of
318  *                                'id', 'name', 'rawname' or '*' to include all fields.
319  * @return   mixed  tag object
320  */
321 function tag_get($field, $value, $returnfields='id, name, rawname') {
322     global $DB;
324     if ($field == 'name') {
325         $value = core_text::strtolower($value);   // To cope with input that might just be wrong case
326     }
327     return $DB->get_record('tag', array($field=>$value), $returnfields);
331 /**
332  * Get the array of db record of tags associated to a record (instances).  Use {@see tag_get_tags_csv()} if you wish to get the same
333  * data in a comma-separated string, for instances such as needing to simply display a list of tags to the end user. This should
334  * really be called tag_get_tag_instances().
335  *
336  * @package core_tag
337  * @category tag
338  * @access public
339  * @param string $record_type the record type for which we want to get the tags
340  * @param int $record_id the record id for which we want to get the tags
341  * @param string $type the tag type (either 'default' or 'official'). By default, all tags are returned.
342  * @param int $userid (optional) only required for course tagging
343  * @return array the array of tags
344  */
345 function tag_get_tags($record_type, $record_id, $type=null, $userid=0) {
346     global $CFG, $DB;
348     $params = array();
350     if ($type) {
351         $sql_type = "AND tg.tagtype = :type";
352         $params['type'] = $type;
353     } else {
354         $sql_type = '';
355     }
357    $u = null;
358     if ($userid) {
359         $u =  "AND ti.tiuserid = :userid ";
360         $params['userid'] = $userid;
361     }
363     $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering
364               FROM {tag_instance} ti
365               JOIN {tag} tg ON tg.id = ti.tagid
366               WHERE ti.itemtype = :recordtype AND ti.itemid = :recordid $u $sql_type
367            ORDER BY ti.ordering ASC";
368     $params['recordtype'] = $record_type;
369     $params['recordid']   = $record_id;
371     // if the fields in this query are changed, you need to do the same changes in tag_get_correlated_tags
372     return $DB->get_records_sql($sql, $params);
373     // This version of the query, reversing the ON clause, "correctly" returns
374     // a row with NULL values for instances that are still in the DB even though
375     // the tag has been deleted.  This shouldn't happen, but if it did, using
376     // this query could help "clean it up".  This causes bugs at this time.
377     //$tags = $DB->get_records_sql("SELECT ti.tagid, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ".
378     //    "FROM {tag_instance} ti LEFT JOIN {tag} tg ON ti.tagid = tg.id ".
379     //    "WHERE ti.itemtype = '{$record_type}' AND ti.itemid = '{$record_id}' {$type} ".
380     //    "ORDER BY ti.ordering ASC");
383 /**
384  * Get the array of tags display names, indexed by id.
385  *
386  * @package  core_tag
387  * @category tag
388  * @access   public
389  * @param    string $record_type the record type for which we want to get the tags
390  * @param    int    $record_id   the record id for which we want to get the tags
391  * @param    string $type        the tag type (either 'default' or 'official'). By default, all tags are returned.
392  * @return   array  the array of tags (with the value returned by tag_display_name), indexed by id
393  */
394 function tag_get_tags_array($record_type, $record_id, $type=null) {
395     $tags = array();
396     foreach(tag_get_tags($record_type, $record_id, $type) as $tag) {
397         $tags[$tag->id] = tag_display_name($tag);
398     }
399     return $tags;
402 /**
403  * Get a comma-separated string of tags associated to a record.  Use {@see tag_get_tags()} to get the same information in an array.
404  *
405  * @package  core_tag
406  * @category tag
407  * @access   public
408  * @param    string   $record_type the record type for which we want to get the tags
409  * @param    int      $record_id   the record id for which we want to get the tags
410  * @param    int      $html        either TAG_RETURN_HTML or TAG_RETURN_TEXT, depending on the type of output desired
411  * @param    string   $type        either 'official' or 'default', if null, all tags are returned
412  * @return   string   the comma-separated list of tags.
413  */
414 function tag_get_tags_csv($record_type, $record_id, $html=TAG_RETURN_HTML, $type=null) {
415     global $CFG;
417     $tags_names = array();
418     foreach(tag_get_tags($record_type, $record_id, $type) as $tag) {
419         if ($html == TAG_RETURN_TEXT) {
420             $tags_names[] = tag_display_name($tag, TAG_RETURN_TEXT);
421         } else { // TAG_RETURN_HTML
422             $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
423         }
424     }
425     return implode(', ', $tags_names);
428 /**
429  * Get an array of tag ids associated to a record.
430  *
431  * @package  core_tag
432  * @category tag
433  * @access   public
434  * @todo     MDL-31150 Update ordering property
435  * @param    string    $record_type the record type for which we want to get the tags
436  * @param    int       $record_id the record id for which we want to get the tags
437  * @return   array     tag ids, indexed and sorted by 'ordering'
438  */
439 function tag_get_tags_ids($record_type, $record_id) {
440     $tag_ids = array();
441     foreach (tag_get_tags($record_type, $record_id) as $tag) {
442         if ( array_key_exists($tag->ordering, $tag_ids) ) {
443             // until we can add a unique constraint, in table tag_instance,
444             // on (itemtype, itemid, ordering), this is needed to prevent a bug
445             // TODO MDL-31150 modify database in 2.0
446             $tag->ordering++;
447         }
448         $tag_ids[$tag->ordering] = $tag->id;
449     }
450     ksort($tag_ids);
451     return $tag_ids;
454 /**
455  * Returns the database ID of a set of tags.
456  *
457  * @package  core_tag
458  * @category tag
459  * @access   public
460  * @todo     MDL-31152 Test the commented MDL-31152 todo in this function to see if it helps performance
461  *                     without breaking anything.
462  * @param    mixed $tags one tag, or array of tags, to look for.
463  * @param    bool  $return_value specify the type of the returned value. Either TAG_RETURN_OBJECT, or TAG_RETURN_ARRAY (default).
464  *                               If TAG_RETURN_ARRAY is specified, an array will be returned even if only one tag was passed in $tags.
465  * @return   mixed tag-indexed array of ids (or objects, if second parameter is TAG_RETURN_OBJECT), or only an int, if only one tag
466  *                 is given *and* the second parameter is null. No value for a key means the tag wasn't found.
467  */
468 function tag_get_id($tags, $return_value=null) {
469     global $CFG, $DB;
471     static $tag_id_cache = array();
473     $return_an_int = false;
474     if (!is_array($tags)) {
475         if(is_null($return_value) || $return_value == TAG_RETURN_OBJECT) {
476             $return_an_int = true;
477         }
478         $tags = array($tags);
479     }
481     $result = array();
483     //TODO MDL-31152 test this and see if it helps performance without breaking anything
484     //foreach($tags as $key => $tag) {
485     //    $clean_tag = core_text::strtolower($tag);
486     //    if ( array_key_exists($clean_tag), $tag_id_cache) ) {
487     //        $result[$clean_tag] = $tag_id_cache[$clean_tag];
488     //        $tags[$key] = ''; // prevent further processing for this one.
489     //    }
490     //}
492     $tags = array_values(tag_normalize($tags));
493     foreach($tags as $key => $tag) {
494         $tags[$key] = core_text::strtolower($tag);
495         $result[core_text::strtolower($tag)] = null; // key must exists : no value for a key means the tag wasn't found.
496     }
498     if (empty($tags)) {
499         return array();
500     }
502     list($tag_string, $params) = $DB->get_in_or_equal($tags);
504     $rs = $DB->get_recordset_sql("SELECT * FROM {tag} WHERE name $tag_string ORDER BY name", $params);
505     foreach ($rs as $record) {
506         if ($return_value == TAG_RETURN_OBJECT) {
507             $result[$record->name] = $record;
508         } else { // TAG_RETURN_ARRAY
509             $result[$record->name] = $record->id;
510         }
511     }
512     $rs->close();
514     if ($return_an_int) {
515         return array_pop($result);
516     }
518     return $result;
522 /**
523  * Returns tags related to a tag
524  *
525  * Related tags of a tag come from two sources:
526  *   - manually added related tags, which are tag_instance entries for that tag
527  *   - correlated tags, which are calculated
528  *
529  * @package  core_tag
530  * @category tag
531  * @access   public
532  * @param    string   $tagid          is a single **normalized** tag name or the id of a tag
533  * @param    int      $type           the function will return either manually (TAG_RELATED_MANUAL) related tags or correlated
534  *                                    (TAG_RELATED_CORRELATED) tags. Default is TAG_RELATED_ALL, which returns everything.
535  * @param    int      $limitnum       (optional) return a subset comprising this many records, the default is 10
536  * @return   array    an array of tag objects
537  */
538 function tag_get_related_tags($tagid, $type=TAG_RELATED_ALL, $limitnum=10) {
540     $related_tags = array();
542     if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_MANUAL) {
543         //gets the manually added related tags
544         $related_tags = tag_get_tags('tag', $tagid);
545     }
547     if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_CORRELATED ) {
548         //gets the correlated tags
549         $automatic_related_tags = tag_get_correlated($tagid);
550         $related_tags = array_merge($related_tags, $automatic_related_tags);
551     }
553     // Remove duplicated tags (multiple instances of the same tag).
554     $seen = array();
555     foreach ($related_tags as $instance => $tag) {
556         if (isset($seen[$tag->id])) {
557             unset($related_tags[$instance]);
558         } else {
559             $seen[$tag->id] = 1;
560         }
561     }
563     return array_slice($related_tags, 0 , $limitnum);
566 /**
567  * Get a comma-separated list of tags related to another tag.
568  *
569  * @package  core_tag
570  * @category tag
571  * @access   public
572  * @param    array    $related_tags the array returned by tag_get_related_tags
573  * @param    int      $html    either TAG_RETURN_HTML (default) or TAG_RETURN_TEXT : return html links, or just text.
574  * @return   string   comma-separated list
575  */
576 function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) {
577     global $CFG;
579     $tags_names = array();
580     foreach($related_tags as $tag) {
581         if ( $html == TAG_RETURN_TEXT) {
582             $tags_names[] = tag_display_name($tag, TAG_RETURN_TEXT);
583         } else {
584             // TAG_RETURN_HTML
585             $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
586         }
587     }
589     return implode(', ', $tags_names);
592 /**
593  * Change the "value" of a tag, and update the associated 'name'.
594  *
595  * @package  core_tag
596  * @category tag
597  * @access   public
598  * @param    int      $tagid  the id of the tag to modify
599  * @param    string   $newrawname the new rawname
600  * @return   bool     true on success, false otherwise
601  */
602 function tag_rename($tagid, $newrawname) {
603     global $COURSE, $DB;
605     $norm = tag_normalize($newrawname, TAG_CASE_ORIGINAL);
606     if (! $newrawname_clean = array_shift($norm) ) {
607         return false;
608     }
610     if (! $newname_clean = core_text::strtolower($newrawname_clean)) {
611         return false;
612     }
614     // Prevent the rename if a tag with that name already exists
615     if ($existing = tag_get('name', $newname_clean, 'id, name, rawname')) {
616         if ($existing->id != $tagid) {  // Another tag already exists with this name
617             return false;
618         }
619     }
621     if ($tag = tag_get('id', $tagid, 'id, userid, name, rawname')) {
622         // Store the name before we change it.
623         $oldname = $tag->name;
625         $tag->rawname = $newrawname_clean;
626         $tag->name = $newname_clean;
627         $tag->timemodified = time();
628         $DB->update_record('tag', $tag);
630         $event = \core\event\tag_updated::create(array(
631             'objectid' => $tag->id,
632             'relateduserid' => $tag->userid,
633             'context' => context_system::instance(),
634             'other' => array(
635                 'name' => $newname_clean,
636                 'rawname' => $newrawname_clean
637             )
638         ));
639         $event->set_legacy_logdata(array($COURSE->id, 'tag', 'update', 'index.php?id='. $tag->id, $oldname . '->'. $tag->name));
640         $event->trigger();
642         return true;
643     }
644     return false;
648 /**
649  * Delete one or more tag, and all their instances if there are any left.
650  *
651  * @package  core_tag
652  * @category tag
653  * @access   public
654  * @param    mixed    $tagids one tagid (int), or one array of tagids to delete
655  * @return   bool     true on success, false otherwise
656  */
657 function tag_delete($tagids) {
658     global $DB;
660     if (!is_array($tagids)) {
661         $tagids = array($tagids);
662     }
664     // Use the tagids to create a select statement to be used later.
665     list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids);
667     // Store the tags and tag instances we are going to delete.
668     $tags = $DB->get_records_select('tag', 'id ' . $tagsql, $tagparams);
669     $taginstances = $DB->get_records_select('tag_instance', 'tagid ' . $tagsql, $tagparams);
671     // Delete all the tag instances.
672     $select = 'WHERE tagid ' . $tagsql;
673     $sql = "DELETE FROM {tag_instance} $select";
674     $DB->execute($sql, $tagparams);
676     // Delete all the tag correlations.
677     $sql = "DELETE FROM {tag_correlation} $select";
678     $DB->execute($sql, $tagparams);
680     // Delete all the tags.
681     $select = 'WHERE id ' . $tagsql;
682     $sql = "DELETE FROM {tag} $select";
683     $DB->execute($sql, $tagparams);
685     // Fire an event that these items were untagged.
686     if ($taginstances) {
687         // Save the system context in case the 'contextid' column in the 'tag_instance' table is null.
688         $syscontextid = context_system::instance()->id;
689         // Loop through the tag instances and fire a 'tag_removed'' event.
690         foreach ($taginstances as $taginstance) {
691             // We can not fire an event with 'null' as the contextid.
692             if (is_null($taginstance->contextid)) {
693                 $taginstance->contextid = $syscontextid;
694             }
696             // Trigger tag removed event.
697             $event = \core\event\tag_removed::create(array(
698                 'objectid' => $taginstance->id,
699                 'contextid' => $taginstance->contextid,
700                 'other' => array(
701                     'tagid' => $taginstance->tagid,
702                     'tagname' => $tags[$taginstance->tagid]->name,
703                     'tagrawname' => $tags[$taginstance->tagid]->rawname,
704                     'itemid' => $taginstance->itemid,
705                     'itemtype' => $taginstance->itemtype
706                 )
707             ));
708             $event->add_record_snapshot('tag_instance', $taginstance);
709             $event->trigger();
710         }
711     }
713     // Fire an event that these tags were deleted.
714     if ($tags) {
715         $context = context_system::instance();
716         foreach ($tags as $tag) {
717             // Delete all files associated with this tag
718             $fs = get_file_storage();
719             $files = $fs->get_area_files($context->id, 'tag', 'description', $tag->id);
720             foreach ($files as $file) {
721                 $file->delete();
722             }
724             // Trigger an event for deleting this tag.
725             $event = \core\event\tag_deleted::create(array(
726                 'objectid' => $tag->id,
727                 'relateduserid' => $tag->userid,
728                 'context' => $context,
729                 'other' => array(
730                     'name' => $tag->name,
731                     'rawname' => $tag->rawname
732                 )
733             ));
734             $event->add_record_snapshot('tag', $tag);
735             $event->trigger();
736         }
737     }
739     return true;
742 /**
743  * Deletes all the tag instances given a component and an optional contextid.
744  *
745  * @param string $component
746  * @param int $contextid if null, then we delete all tag instances for the $component
747  */
748 function tag_delete_instances($component, $contextid = null) {
749     global $DB;
751     $sql = "SELECT ti.*, t.name, t.rawname
752               FROM {tag_instance} ti
753               JOIN {tag} t
754                 ON ti.tagid = t.id ";
755     if (is_null($contextid)) {
756         $params = array('component' => $component);
757         $sql .= "WHERE ti.component = :component";
758     } else {
759         $params = array('component' => $component, 'contextid' => $contextid);
760         $sql .= "WHERE ti.component = :component
761                    AND ti.contextid = :contextid";
762     }
763     if ($taginstances = $DB->get_records_sql($sql, $params)) {
764         // Now remove all the tag instances.
765         $DB->delete_records('tag_instance',$params);
766         // Save the system context in case the 'contextid' column in the 'tag_instance' table is null.
767         $syscontextid = context_system::instance()->id;
768         // Loop through the tag instances and fire an 'tag_removed' event.
769         foreach ($taginstances as $taginstance) {
770             // We can not fire an event with 'null' as the contextid.
771             if (is_null($taginstance->contextid)) {
772                 $taginstance->contextid = $syscontextid;
773             }
775             // Trigger tag removed event.
776             $event = \core\event\tag_removed::create(array(
777                 'objectid' => $taginstance->id,
778                 'contextid' => $taginstance->contextid,
779                 'other' => array(
780                     'tagid' => $taginstance->tagid,
781                     'tagname' => $taginstance->name,
782                     'tagrawname' => $taginstance->rawname,
783                     'itemid' => $taginstance->itemid,
784                     'itemtype' => $taginstance->itemtype
785                 )
786             ));
787             $event->add_record_snapshot('tag_instance', $taginstance);
788             $event->trigger();
789         }
790     }
793 /**
794  * Delete one instance of a tag.  If the last instance was deleted, it will also delete the tag, unless its type is 'official'.
795  *
796  * @package  core_tag
797  * @category tag
798  * @access   public
799  * @param    string $record_type the type of the record for which to remove the instance
800  * @param    int    $record_id   the id of the record for which to remove the instance
801  * @param    int    $tagid       the tagid that needs to be removed
802  * @param    int    $userid      (optional) the userid
803  * @return   bool   true on success, false otherwise
804  */
805 function tag_delete_instance($record_type, $record_id, $tagid, $userid = null) {
806     global $DB;
808     if (is_null($userid)) {
809         $taginstance = $DB->get_record('tag_instance', array('tagid' => $tagid, 'itemtype' => $record_type, 'itemid' => $record_id));
810     } else {
811         $taginstance = $DB->get_record('tag_instance', array('tagid' => $tagid, 'itemtype' => $record_type, 'itemid' => $record_id,
812             'tiuserid' => $userid));
813     }
814     if ($taginstance) {
815         // Get the tag.
816         $tag = $DB->get_record('tag', array('id' => $tagid));
818         $DB->delete_records('tag_instance', array('id' => $taginstance->id));
820         // We can not fire an event with 'null' as the contextid.
821         if (is_null($taginstance->contextid)) {
822             $taginstance->contextid = context_system::instance()->id;
823         }
825         // Trigger tag removed event.
826         $event = \core\event\tag_removed::create(array(
827             'objectid' => $taginstance->id,
828             'contextid' => $taginstance->contextid,
829             'other' => array(
830                 'tagid' => $tag->id,
831                 'tagname' => $tag->name,
832                 'tagrawname' => $tag->rawname,
833                 'itemid' => $taginstance->itemid,
834                 'itemtype' => $taginstance->itemtype
835             )
836         ));
837         $event->add_record_snapshot('tag_instance', $taginstance);
838         $event->trigger();
840         // If there are no other instances of the tag then consider deleting the tag as well.
841         if (!$DB->record_exists('tag_instance', array('tagid' => $tagid))) {
842             // If the tag is a personal tag then delete it - don't delete official tags.
843             if ($tag->tagtype == 'default') {
844                 tag_delete($tagid);
845             }
846         }
847     } else {
848         return false;
849     }
851     return true;
855 /**
856  * Function that returns the name that should be displayed for a specific tag
857  *
858  * @package  core_tag
859  * @category tag
860  * @access   public
861  * @param    object   $tagobject a line out of tag table, as returned by the adobd functions
862  * @param    int      $html TAG_RETURN_HTML (default) will return htmlspecialchars encoded string, TAG_RETURN_TEXT will not encode.
863  * @return   string
864  */
865 function tag_display_name($tagobject, $html=TAG_RETURN_HTML) {
866     global $CFG;
868     if (!isset($tagobject->name)) {
869         return '';
870     }
872     if (empty($CFG->keeptagnamecase)) {
873         //this is the normalized tag name
874         $tagname = core_text::strtotitle($tagobject->name);
875     } else {
876         //original casing of the tag name
877         $tagname = $tagobject->rawname;
878     }
880     // clean up a bit just in case the rules change again
881     $tagname = clean_param($tagname, PARAM_TAG);
883     if ($html == TAG_RETURN_TEXT) {
884         return $tagname;
885     } else { // TAG_RETURN_HTML
886         return htmlspecialchars($tagname);
887     }
890 /**
891  * Find all records tagged with a tag of a given type ('post', 'user', etc.)
892  *
893  * @package  core_tag
894  * @category tag
895  * @access   public
896  * @param    string   $tag       tag to look for
897  * @param    string   $type      type to restrict search to.  If null, every matching record will be returned
898  * @param    int      $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point.
899  * @param    int      $limitnum  (optional, required if $limitfrom is set) return a subset comprising this many records.
900  * @return   array of matching objects, indexed by record id, from the table containing the type requested
901  */
902 function tag_find_records($tag, $type, $limitfrom='', $limitnum='') {
903     global $CFG, $DB;
905     if (!$tag || !$type) {
906         return array();
907     }
909     $tagid = tag_get_id($tag);
911     $query = "SELECT it.*
912                 FROM {".$type."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid
913                WHERE tt.itemtype = ? AND tt.tagid = ?";
914     $params = array($type, $tagid);
916     return $DB->get_records_sql($query, $params, $limitfrom, $limitnum);
922 ///////////////////////////////////////////////////////
923 /////////////////// PRIVATE TAG API ///////////////////
925 /**
926  * Adds one or more tag in the database.  This function should not be called directly : you should
927  * use tag_set.
928  *
929  * @package core_tag
930  * @access  private
931  * @param   mixed    $tags     one tag, or an array of tags, to be created
932  * @param   string   $type     type of tag to be created ("default" is the default value and "official" is the only other supported
933  *                             value at this time). An official tag is kept even if there are no records tagged with it.
934  * @return array     $tags ids indexed by their lowercase normalized names. Any boolean false in the array indicates an error while
935  *                             adding the tag.
936  */
937 function tag_add($tags, $type="default") {
938     global $USER, $DB;
940     if (!is_array($tags)) {
941         $tags = array($tags);
942     }
944     $tag_object = new StdClass;
945     $tag_object->tagtype      = $type;
946     $tag_object->userid       = $USER->id;
947     $tag_object->timemodified = time();
949     $clean_tags = tag_normalize($tags, TAG_CASE_ORIGINAL);
951     $tags_ids = array();
952     foreach($clean_tags as $tag) {
953         $tag = trim($tag);
954         if (!$tag) {
955             $tags_ids[$tag] = false;
956         } else {
957             // note that the difference between rawname and name is only
958             // capitalization : the rawname is NOT the same at the rawtag.
959             $tag_object->rawname = $tag;
960             $tag_name_lc         = core_text::strtolower($tag);
961             $tag_object->name    = $tag_name_lc;
962             //var_dump($tag_object);
963             $tags_ids[$tag_name_lc] = $DB->insert_record('tag', $tag_object);
965             $event = \core\event\tag_created::create(array(
966                 'objectid' => $tags_ids[$tag_name_lc],
967                 'relateduserid' => $tag_object->userid,
968                 'context' => context_system::instance(),
969                 'other' => array(
970                     'name' => $tag_object->name,
971                     'rawname' => $tag_object->rawname
972                 )
973             ));
974             $event->trigger();
975         }
976     }
978     return $tags_ids;
981 /**
982  * Assigns a tag to a record; if the record already exists, the time and ordering will be updated.
983  *
984  * @package core_tag
985  * @access private
986  * @param string $record_type the type of the record that will be tagged
987  * @param int $record_id the id of the record that will be tagged
988  * @param string $tagid the tag id to set on the record.
989  * @param int $ordering the order of the instance for this record
990  * @param int $userid (optional) only required for course tagging
991  * @param string|null $component the component that was tagged
992  * @param int|null $contextid the context id of where this tag was assigned
993  * @return bool true on success, false otherwise
994  */
995 function tag_assign($record_type, $record_id, $tagid, $ordering, $userid = 0, $component = null, $contextid = null) {
996     global $DB;
998     if ($component === null || $contextid === null) {
999         debugging('You should specify the component and contextid of the item being tagged in your call to tag_assign.',
1000             DEBUG_DEVELOPER);
1001     }
1003     // Get the tag.
1004     $tag = $DB->get_record('tag', array('id' => $tagid), 'name, rawname', MUST_EXIST);
1006     if ( $tag_instance_object = $DB->get_record('tag_instance', array('tagid'=>$tagid, 'itemtype'=>$record_type, 'itemid'=>$record_id, 'tiuserid'=>$userid), 'id')) {
1007         $tag_instance_object->ordering     = $ordering;
1008         $tag_instance_object->timemodified = time();
1010         $DB->update_record('tag_instance', $tag_instance_object);
1011     } else {
1012         $tag_instance_object = new StdClass;
1013         $tag_instance_object->tagid        = $tagid;
1014         $tag_instance_object->component    = $component;
1015         $tag_instance_object->itemid       = $record_id;
1016         $tag_instance_object->itemtype     = $record_type;
1017         $tag_instance_object->contextid    = $contextid;
1018         $tag_instance_object->ordering     = $ordering;
1019         $tag_instance_object->timecreated  = time();
1020         $tag_instance_object->timemodified = $tag_instance_object->timecreated;
1021         $tag_instance_object->tiuserid     = $userid;
1023         $tag_instance_object->id = $DB->insert_record('tag_instance', $tag_instance_object);
1024     }
1026     // We can not fire an event with 'null' as the contextid.
1027     if (is_null($contextid)) {
1028         $contextid = context_system::instance()->id;
1029     }
1031     // Trigger tag added event.
1032     $event = \core\event\tag_added::create(array(
1033         'objectid' => $tag_instance_object->id,
1034         'contextid' => $contextid,
1035         'other' => array(
1036             'tagid' => $tagid,
1037             'tagname' => $tag->name,
1038             'tagrawname' => $tag->rawname,
1039             'itemid' => $record_id,
1040             'itemtype' => $record_type
1041         )
1042     ));
1043     $event->trigger();
1045     return true;
1048 /**
1049  * Clean up the tag tables, making sure all tagged object still exists.
1050  *
1051  * This should normally not be necessary, but in case related tags are not deleted when the tagged record is removed, this should be
1052  * done once in a while, perhaps on an occasional cron run.  On a site with lots of tags, this could become an expensive function to
1053  * call: don't run at peak time.
1054  *
1055  * @package core_tag
1056  * @access  private
1057  * @todo    MDL-31212 Update tag cleanup sql so that it supports multiple types of tags
1058  */
1059 function tag_cleanup() {
1060     global $DB;
1062     // Get ids to delete from instances where the tag has been deleted. This should never happen apparently.
1063     $sql = "SELECT ti.id
1064               FROM {tag_instance} ti
1065          LEFT JOIN {tag} t ON t.id = ti.tagid
1066              WHERE t.id IS null";
1067     $tagids = $DB->get_records_sql($sql);
1068     $tagarray = array();
1069     foreach ($tagids as $tagid) {
1070         $tagarray[] = $tagid->id;
1071     }
1073     // Next get ids from instances that have an owner that has been deleted.
1074     $sql = "SELECT ti.id
1075               FROM {tag_instance} ti, {user} u
1076              WHERE ti.itemid = u.id
1077                AND ti.itemtype = 'user'
1078                AND u.deleted = 1";
1079     $tagids = $DB->get_records_sql($sql);
1080     foreach ($tagids as $tagid) {
1081         $tagarray[] = $tagid->id;
1082     }
1084     // Get the other itemtypes.
1085     $sql = "SELECT itemtype
1086               FROM {tag_instance}
1087              WHERE itemtype <> 'user'
1088           GROUP BY itemtype";
1089     $tagitemtypes = $DB->get_records_sql($sql);
1090     foreach ($tagitemtypes as $key => $notused) {
1091         $sql = 'SELECT ti.id
1092                   FROM {tag_instance} ti
1093              LEFT JOIN {' . $key . '} it ON it.id = ti.itemid
1094                  WHERE it.id IS null
1095                  AND ti.itemtype = \'' . $key . '\'';
1096         $tagids = $DB->get_records_sql($sql);
1097         foreach ($tagids as $tagid) {
1098             $tagarray[] = $tagid->id;
1099         }
1100     }
1102     // Get instances for each of the ids to be deleted.
1103     if (count($tagarray) > 0) {
1104         list($sqlin, $params) = $DB->get_in_or_equal($tagarray);
1105         $sql = "SELECT ti.*, COALESCE(t.name, 'deleted') AS name, COALESCE(t.rawname, 'deleted') AS rawname
1106                   FROM {tag_instance} ti
1107              LEFT JOIN {tag} t ON t.id = ti.tagid
1108                  WHERE ti.id $sqlin";
1109         $instances = $DB->get_records_sql($sql, $params);
1110         tag_bulk_delete_instances($instances);
1111     }
1113     // TODO MDL-31212 this will only clean tags of type 'default'.  This is good as
1114     // it won't delete 'official' tags, but the day we get more than two
1115     // types, we need to fix this.
1116     $unused_tags = $DB->get_recordset_sql("SELECT tg.id
1117                                              FROM {tag} tg
1118                                             WHERE tg.tagtype = 'default'
1119                                                   AND NOT EXISTS (
1120                                                       SELECT 'x'
1121                                                         FROM {tag_instance} ti
1122                                                        WHERE ti.tagid = tg.id
1123                                                   )");
1125     // cleanup tags
1126     foreach ($unused_tags as $unused_tag) {
1127         tag_delete($unused_tag->id);
1128         //debugging('deleting unused tag #'. $unused_tag->id,  DEBUG_DEVELOPER);
1129     }
1130     $unused_tags->close();
1133 /**
1134  * This function will delete numerous tag instances efficiently.
1135  * This removes tag instances only. It doesn't check to see if it is the last use of a tag.
1136  *
1137  * @param array $instances An array of tag instance objects with the addition of the tagname and tagrawname
1138  *        (used for recording a delete event).
1139  */
1140 function tag_bulk_delete_instances($instances) {
1141     global $DB;
1143     $instanceids = array();
1144     foreach ($instances as $instance) {
1145         $instanceids[] = $instance->id;
1146     }
1148     // This is a multi db compatible method of creating the correct sql when using the 'IN' value.
1149     // $insql is the sql statement, $params are the id numbers.
1150     list($insql, $params) = $DB->get_in_or_equal($instanceids);
1151     $sql = 'id ' . $insql;
1152     $DB->delete_records_select('tag_instance', $sql, $params);
1154     // Now go through and record each tag individually with the event system.
1155     foreach ($instances as $instance) {
1156         // Trigger tag removed event (i.e. The tag instance has been removed).
1157         $event = \core\event\tag_removed::create(array(
1158             'objectid' => $instance->id,
1159             'contextid' => $instance->contextid,
1160             'other' => array(
1161                 'tagid' => $instance->tagid,
1162                 'tagname' => $instance->name,
1163                 'tagrawname' => $instance->rawname,
1164                 'itemid' => $instance->itemid,
1165                 'itemtype' => $instance->itemtype
1166             )
1167         ));
1168         unset($instance->name);
1169         unset($instance->rawname);
1170         $event->add_record_snapshot('tag_instance', $instance);
1171         $event->trigger();
1172     }
1175 /**
1176  * Calculates and stores the correlated tags of all tags. The correlations are stored in the 'tag_correlation' table.
1177  *
1178  * Two tags are correlated if they appear together a lot. Ex.: Users tagged with "computers" will probably also be tagged with "algorithms".
1179  *
1180  * The rationale for the 'tag_correlation' table is performance. It works as a cache for a potentially heavy load query done at the
1181  * 'tag_instance' table. So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table.
1182  *
1183  * @package core_tag
1184  * @access  private
1185  * @param   int      $mincorrelation Only tags with more than $mincorrelation correlations will be identified.
1186  */
1187 function tag_compute_correlations($mincorrelation = 2) {
1188     global $DB;
1190     // This mighty one line query fetches a row from the database for every
1191     // individual tag correlation. We then need to process the rows collecting
1192     // the correlations for each tag id.
1193     // The fields used by this query are as follows:
1194     //   tagid         : This is the tag id, there should be at least $mincorrelation
1195     //                   rows for each tag id.
1196     //   correlation   : This is the tag id that correlates to the above tagid field.
1197     //   correlationid : This is the id of the row in the tag_correlation table that
1198     //                   relates to the tagid field and will be NULL if there are no
1199     //                   existing correlations
1200     $sql = 'SELECT pairs.tagid, pairs.correlation, pairs.ocurrences, co.id AS correlationid
1201               FROM (
1202                        SELECT ta.tagid, tb.tagid AS correlation, COUNT(*) AS ocurrences
1203                          FROM {tag_instance} ta
1204                          JOIN {tag_instance} tb ON (ta.itemtype = tb.itemtype AND ta.itemid = tb.itemid AND ta.tagid <> tb.tagid)
1205                      GROUP BY ta.tagid, tb.tagid
1206                        HAVING COUNT(*) > :mincorrelation
1207                    ) pairs
1208          LEFT JOIN {tag_correlation} co ON co.tagid = pairs.tagid
1209           ORDER BY pairs.tagid ASC, pairs.ocurrences DESC, pairs.correlation ASC';
1210     $rs = $DB->get_recordset_sql($sql, array('mincorrelation' => $mincorrelation));
1212     // Set up an empty tag correlation object
1213     $tagcorrelation = new stdClass;
1214     $tagcorrelation->id = null;
1215     $tagcorrelation->tagid = null;
1216     $tagcorrelation->correlatedtags = array();
1218     // We store each correlation id in this array so we can remove any correlations
1219     // that no longer exist.
1220     $correlations = array();
1222     // Iterate each row of the result set and build them into tag correlations.
1223     // We add all of a tag's correlations to $tagcorrelation->correlatedtags[]
1224     // then save the $tagcorrelation object
1225     foreach ($rs as $row) {
1226         if ($row->tagid != $tagcorrelation->tagid) {
1227             // The tag id has changed so we have all of the correlations for this tag
1228             $tagcorrelationid = tag_process_computed_correlation($tagcorrelation);
1229             if ($tagcorrelationid) {
1230                 $correlations[] = $tagcorrelationid;
1231             }
1232             // Now we reset the tag correlation object so we can reuse it and set it
1233             // up for the current record.
1234             $tagcorrelation = new stdClass;
1235             $tagcorrelation->id = $row->correlationid;
1236             $tagcorrelation->tagid = $row->tagid;
1237             $tagcorrelation->correlatedtags = array();
1238         }
1239         //Save the correlation on the tag correlation object
1240         $tagcorrelation->correlatedtags[] = $row->correlation;
1241     }
1242     // Update the current correlation after the last record.
1243     $tagcorrelationid = tag_process_computed_correlation($tagcorrelation);
1244     if ($tagcorrelationid) {
1245         $correlations[] = $tagcorrelationid;
1246     }
1249     // Close the recordset
1250     $rs->close();
1252     // Remove any correlations that weren't just identified
1253     if (empty($correlations)) {
1254         //there are no tag correlations
1255         $DB->delete_records('tag_correlation');
1256     } else {
1257         list($sql, $params) = $DB->get_in_or_equal($correlations, SQL_PARAMS_NAMED, 'param0000', false);
1258         $DB->delete_records_select('tag_correlation', 'id '.$sql, $params);
1259     }
1262 /**
1263  * This function processes a tag correlation and makes changes in the database as required.
1264  *
1265  * The tag correlation object needs have both a tagid property and a correlatedtags property that is an array.
1266  *
1267  * @package core_tag
1268  * @access  private
1269  * @param   stdClass $tagcorrelation
1270  * @return  int/bool The id of the tag correlation that was just processed or false.
1271  */
1272 function tag_process_computed_correlation(stdClass $tagcorrelation) {
1273     global $DB;
1275     // You must provide a tagid and correlatedtags must be set and be an array
1276     if (empty($tagcorrelation->tagid) || !isset($tagcorrelation->correlatedtags) || !is_array($tagcorrelation->correlatedtags)) {
1277         return false;
1278     }
1280     $tagcorrelation->correlatedtags = join(',', $tagcorrelation->correlatedtags);
1281     if (!empty($tagcorrelation->id)) {
1282         // The tag correlation already exists so update it
1283         $DB->update_record('tag_correlation', $tagcorrelation);
1284     } else {
1285         // This is a new correlation to insert
1286         $tagcorrelation->id = $DB->insert_record('tag_correlation', $tagcorrelation);
1287     }
1288     return $tagcorrelation->id;
1291 /**
1292  * Tasks that should be performed at cron time
1293  *
1294  * @package core_tag
1295  * @access private
1296  */
1297 function tag_cron() {
1298     tag_compute_correlations();
1299     tag_cleanup();
1302 /**
1303  * Search for tags with names that match some text
1304  *
1305  * @package core_tag
1306  * @access  private
1307  * @param   string        $text      escaped string that the tag names will be matched against
1308  * @param   bool          $ordered   If true, tags are ordered by their popularity. If false, no ordering.
1309  * @param   int/string    $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point.
1310  * @param   int/string    $limitnum  (optional, required if $limitfrom is set) return a subset comprising this many records.
1311  * @return  array/boolean an array of objects, or false if no records were found or an error occured.
1312  */
1313 function tag_find_tags($text, $ordered=true, $limitfrom='', $limitnum='') {
1314     global $DB;
1316     $norm = tag_normalize($text, TAG_CASE_LOWER);
1317     $text = array_shift($norm);
1319     if ($ordered) {
1320         $query = "SELECT tg.id, tg.name, tg.rawname, COUNT(ti.id) AS count
1321                     FROM {tag} tg LEFT JOIN {tag_instance} ti ON tg.id = ti.tagid
1322                    WHERE tg.name LIKE ?
1323                 GROUP BY tg.id, tg.name, tg.rawname
1324                 ORDER BY count DESC";
1325     } else {
1326         $query = "SELECT tg.id, tg.name, tg.rawname
1327                     FROM {tag} tg
1328                    WHERE tg.name LIKE ?";
1329     }
1330     $params = array("%{$text}%");
1331     return $DB->get_records_sql($query, $params, $limitfrom , $limitnum);
1334 /**
1335  * Get the name of a tag
1336  *
1337  * @package core_tag
1338  * @access  private
1339  * @param   mixed    $tagids the id of the tag, or an array of ids
1340  * @return  mixed    string name of one tag, or id-indexed array of strings
1341  */
1342 function tag_get_name($tagids) {
1343     global $DB;
1345     if (!is_array($tagids)) {
1346         if ($tag = $DB->get_record('tag', array('id'=>$tagids))) {
1347             return $tag->name;
1348         }
1349         return false;
1350     }
1352     $tag_names = array();
1353     foreach($DB->get_records_list('tag', 'id', $tagids) as $tag) {
1354         $tag_names[$tag->id] = $tag->name;
1355     }
1357     return $tag_names;
1360 /**
1361  * Returns the correlated tags of a tag, retrieved from the tag_correlation table. Make sure cron runs, otherwise the table will be
1362  * empty and this function won't return anything.
1363  *
1364  * Correlated tags are calculated in cron based on existing tag instances.
1365  *
1366  * This function will return as many entries as there are existing tag instances,
1367  * which means that there will be duplicates for each tag.
1368  *
1369  * If you need only one record for each correlated tag please call:
1370  *      tag_get_related_tags($tag_id, TAG_RELATED_CORRELATED);
1371  *
1372  * @package core_tag
1373  * @access  private
1374  * @param   int      $tag_id   is a single tag id
1375  * @param   int      $notused  this argument is no longer used
1376  * @return  array    an array of tag objects or an empty if no correlated tags are found
1377  */
1378 function tag_get_correlated($tag_id, $notused = null) {
1379     global $DB;
1381     $tag_correlation = $DB->get_record('tag_correlation', array('tagid'=>$tag_id));
1383     if (!$tag_correlation || empty($tag_correlation->correlatedtags)) {
1384         return array();
1385     }
1387     // this is (and has to) return the same fields as the query in tag_get_tags
1388     $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering
1389               FROM {tag} tg
1390         INNER JOIN {tag_instance} ti ON tg.id = ti.tagid
1391              WHERE tg.id IN ({$tag_correlation->correlatedtags})
1392           ORDER BY ti.ordering ASC";
1393     return $DB->get_records_sql($sql);
1396 /**
1397  * Function that normalizes a list of tag names.
1398  *
1399  * @package core_tag
1400  * @access  private
1401  * @param   array/string $rawtags array of tags, or a single tag.
1402  * @param   int          $case    case to use for returned value (default: lower case). Either TAG_CASE_LOWER (default) or TAG_CASE_ORIGINAL
1403  * @return  array        lowercased normalized tags, indexed by the normalized tag, in the same order as the original array.
1404  *                       (Eg: 'Banana' => 'banana').
1405  */
1406 function tag_normalize($rawtags, $case = TAG_CASE_LOWER) {
1408     // cache normalized tags, to prevent costly repeated calls to clean_param
1409     static $cleaned_tags_lc = array(); // lower case - use for comparison
1410     static $cleaned_tags_mc = array(); // mixed case - use for saving to database
1412     if ( !is_array($rawtags) ) {
1413         $rawtags = array($rawtags);
1414     }
1416     $result = array();
1417     foreach($rawtags as $rawtag) {
1418         $rawtag = trim($rawtag);
1419         if (!$rawtag) {
1420             continue;
1421         }
1422         if ( !array_key_exists($rawtag, $cleaned_tags_lc) ) {
1423             $cleaned_tags_lc[$rawtag] = core_text::strtolower( clean_param($rawtag, PARAM_TAG) );
1424             $cleaned_tags_mc[$rawtag] = clean_param($rawtag, PARAM_TAG);
1425         }
1426         if ( $case == TAG_CASE_LOWER ) {
1427             $result[$rawtag] = $cleaned_tags_lc[$rawtag];
1428         } else { // TAG_CASE_ORIGINAL
1429             $result[$rawtag] = $cleaned_tags_mc[$rawtag];
1430         }
1431     }
1433     return $result;
1436 /**
1437  * Count how many records are tagged with a specific tag.
1438  *
1439  * @package core_tag
1440  * @access  private
1441  * @param   string   $record_type record to look for ('post', 'user', etc.)
1442  * @param   int      $tagid       is a single tag id
1443  * @return  int      number of mathing tags.
1444  */
1445 function tag_record_count($record_type, $tagid) {
1446     global $DB;
1447     return $DB->count_records('tag_instance', array('itemtype'=>$record_type, 'tagid'=>$tagid));
1450 /**
1451  * Determine if a record is tagged with a specific tag
1452  *
1453  * @package core_tag
1454  * @access  private
1455  * @param   string   $record_type the record type to look for
1456  * @param   int      $record_id   the record id to look for
1457  * @param   string   $tag         a tag name
1458  * @return  bool/int true if it is tagged, 0 (false) otherwise
1459  */
1460 function tag_record_tagged_with($record_type, $record_id, $tag) {
1461     global $DB;
1462     if ($tagid = tag_get_id($tag)) {
1463         return $DB->count_records('tag_instance', array('itemtype'=>$record_type, 'itemid'=>$record_id, 'tagid'=>$tagid));
1464     } else {
1465         return 0; // tag doesn't exist
1466     }
1469 /**
1470  * Flag a tag as inappropriate.
1471  *
1472  * @param int|array $tagids a single tagid, or an array of tagids
1473  */
1474 function tag_set_flag($tagids) {
1475     global $DB;
1477     $tagids = (array) $tagids;
1479     // Use the tagids to create a select statement to be used later.
1480     list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids, SQL_PARAMS_NAMED);
1482     // Update all the tags to flagged.
1483     $sql = "UPDATE {tag}
1484                SET flag = flag + 1, timemodified = :time
1485              WHERE id $tagsql";
1487     // Update all the tags.
1488     $DB->execute($sql, array_merge(array('time' => time()), $tagparams));
1490     // Get all the tags.
1491     if ($tags = $DB->get_records_select('tag', 'id '. $tagsql, $tagparams, 'id ASC')) {
1492         // Loop through and fire an event for each tag that it was flagged.
1493         foreach ($tags as $tag) {
1494             $event = \core\event\tag_flagged::create(array(
1495                 'objectid' => $tag->id,
1496                 'relateduserid' => $tag->userid,
1497                 'context' => context_system::instance(),
1498                 'other' => array(
1499                     'name' => $tag->name,
1500                     'rawname' => $tag->rawname
1501                 )
1503             ));
1504             $event->add_record_snapshot('tag', $tag);
1505             $event->trigger();
1506         }
1507     }
1510 /**
1511  * Remove the inappropriate flag on a tag.
1512  *
1513  * @param int|array $tagids a single tagid, or an array of tagids
1514  */
1515 function tag_unset_flag($tagids) {
1516     global $DB;
1518     $tagids = (array) $tagids;
1520     // Use the tagids to create a select statement to be used later.
1521     list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids, SQL_PARAMS_NAMED);
1523     // Update all the tags to unflagged.
1524     $sql = "UPDATE {tag}
1525                SET flag = 0, timemodified = :time
1526              WHERE id $tagsql";
1528     // Update all the tags.
1529     $DB->execute($sql, array_merge(array('time' => time()), $tagparams));
1531     // Get all the tags.
1532     if ($tags = $DB->get_records_select('tag', 'id '. $tagsql, $tagparams, 'id ASC')) {
1533         // Loop through and fire an event for each tag that it was unflagged.
1534         foreach ($tags as $tag) {
1535             $event = \core\event\tag_unflagged::create(array(
1536                 'objectid' => $tag->id,
1537                 'relateduserid' => $tag->userid,
1538                 'context' => context_system::instance(),
1539                 'other' => array(
1540                     'name' => $tag->name,
1541                     'rawname' => $tag->rawname
1542                 )
1543             ));
1544             $event->add_record_snapshot('tag', $tag);
1545             $event->trigger();
1546         }
1547     }
1550 /**
1551  * Return a list of page types
1552  *
1553  * @package core_tag
1554  * @access  private
1555  * @param   string   $pagetype       current page type
1556  * @param   stdClass $parentcontext  Block's parent context
1557  * @param   stdClass $currentcontext Current context of block
1558  */
1559 function tag_page_type_list($pagetype, $parentcontext, $currentcontext) {
1560     return array(
1561         'tag-*'=>get_string('page-tag-x', 'tag'),
1562         'tag-index'=>get_string('page-tag-index', 'tag'),
1563         'tag-search'=>get_string('page-tag-search', 'tag'),
1564         'tag-manage'=>get_string('page-tag-manage', 'tag')
1565     );