MDL-40908 core_tag: created a 'tag_created' event
[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, $limitnum);
550         if (is_array($automatic_related_tags)) {
551             $related_tags = array_merge($related_tags, $automatic_related_tags);
552         }
553     }
555     return array_slice(object_array_unique($related_tags), 0 , $limitnum);
558 /**
559  * Get a comma-separated list of tags related to another tag.
560  *
561  * @package  core_tag
562  * @category tag
563  * @access   public
564  * @param    array    $related_tags the array returned by tag_get_related_tags
565  * @param    int      $html    either TAG_RETURN_HTML (default) or TAG_RETURN_TEXT : return html links, or just text.
566  * @return   string   comma-separated list
567  */
568 function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) {
569     global $CFG;
571     $tags_names = array();
572     foreach($related_tags as $tag) {
573         if ( $html == TAG_RETURN_TEXT) {
574             $tags_names[] = tag_display_name($tag, TAG_RETURN_TEXT);
575         } else {
576             // TAG_RETURN_HTML
577             $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
578         }
579     }
581     return implode(', ', $tags_names);
584 /**
585  * Change the "value" of a tag, and update the associated 'name'.
586  *
587  * @package  core_tag
588  * @category tag
589  * @access   public
590  * @param    int      $tagid  the id of the tag to modify
591  * @param    string   $newrawname the new rawname
592  * @return   bool     true on success, false otherwise
593  */
594 function tag_rename($tagid, $newrawname) {
595     global $COURSE, $DB;
597     $norm = tag_normalize($newrawname, TAG_CASE_ORIGINAL);
598     if (! $newrawname_clean = array_shift($norm) ) {
599         return false;
600     }
602     if (! $newname_clean = core_text::strtolower($newrawname_clean)) {
603         return false;
604     }
606     // Prevent the rename if a tag with that name already exists
607     if ($existing = tag_get('name', $newname_clean, 'id, name, rawname')) {
608         if ($existing->id != $tagid) {  // Another tag already exists with this name
609             return false;
610         }
611     }
613     if ($tag = tag_get('id', $tagid, 'id, userid, name, rawname')) {
614         // Store the name before we change it.
615         $oldname = $tag->name;
617         $tag->rawname = $newrawname_clean;
618         $tag->name = $newname_clean;
619         $tag->timemodified = time();
620         $DB->update_record('tag', $tag);
622         $event = \core\event\tag_updated::create(array(
623             'objectid' => $tag->id,
624             'relateduserid' => $tag->userid,
625             'context' => context_system::instance(),
626             'other' => array(
627                 'name' => $newname_clean,
628                 'rawname' => $newrawname_clean
629             )
630         ));
631         $event->set_legacy_logdata(array($COURSE->id, 'tag', 'update', 'index.php?id='. $tag->id, $oldname . '->'. $tag->name));
632         $event->trigger();
634         return true;
635     }
636     return false;
640 /**
641  * Delete one or more tag, and all their instances if there are any left.
642  *
643  * @package  core_tag
644  * @category tag
645  * @access   public
646  * @param    mixed    $tagids one tagid (int), or one array of tagids to delete
647  * @return   bool     true on success, false otherwise
648  */
649 function tag_delete($tagids) {
650     global $DB;
652     if (!is_array($tagids)) {
653         $tagids = array($tagids);
654     }
656     // Use the tagids to create a select statement to be used later.
657     list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids);
659     // Store the tags we are going to delete.
660     $tags = $DB->get_records_select('tag', 'id ' . $tagsql, $tagparams);
662     // Delete all the tag instances.
663     $select = 'WHERE tagid ' . $tagsql;
664     $sql = "DELETE FROM {tag_instance} $select";
665     $DB->execute($sql, $tagparams);
667     // Delete all the tag correlations.
668     $sql = "DELETE FROM {tag_correlation} $select";
669     $DB->execute($sql, $tagparams);
671     // Delete all the tags.
672     $select = 'WHERE id ' . $tagsql;
673     $sql = "DELETE FROM {tag} $select";
674     $DB->execute($sql, $tagparams);
676     // Fire an event that these tags were deleted.
677     if ($tags) {
678         $context = context_system::instance();
679         foreach ($tags as $tag) {
680             // Delete all files associated with this tag
681             $fs = get_file_storage();
682             $files = $fs->get_area_files($context->id, 'tag', 'description', $tag->id);
683             foreach ($files as $file) {
684                 $file->delete();
685             }
687             // Trigger an event for deleting this tag.
688             $event = \core\event\tag_deleted::create(array(
689                 'objectid' => $tag->id,
690                 'relateduserid' => $tag->userid,
691                 'context' => $context,
692                 'other' => array(
693                     'name' => $tag->name,
694                     'rawname' => $tag->rawname
695                 )
696             ));
697             $event->add_record_snapshot('tag', $tag);
698             $event->trigger();
699         }
700     }
702     return true;
705 /**
706  * Delete one instance of a tag.  If the last instance was deleted, it will also delete the tag, unless its type is 'official'.
707  *
708  * @package  core_tag
709  * @category tag
710  * @access   public
711  * @param    string $record_type the type of the record for which to remove the instance
712  * @param    int    $record_id   the id of the record for which to remove the instance
713  * @param    int    $tagid       the tagid that needs to be removed
714  * @return   bool   true on success, false otherwise
715  */
716 function tag_delete_instance($record_type, $record_id, $tagid) {
717     global $CFG, $DB;
719     if ($DB->delete_records('tag_instance', array('tagid'=>$tagid, 'itemtype'=>$record_type, 'itemid'=>$record_id))) {
720         if (!$DB->record_exists_sql("SELECT * ".
721                                       "FROM {tag} tg ".
722                                      "WHERE tg.id = ? AND ( tg.tagtype = 'official' OR ".
723                                         "EXISTS (SELECT 1
724                                                    FROM {tag_instance} ti
725                                                   WHERE ti.tagid = ?) )",
726                                      array($tagid, $tagid))) {
727             return tag_delete($tagid);
728         }
729     } else {
730         return false;
731     }
733     return true;
737 /**
738  * Function that returns the name that should be displayed for a specific tag
739  *
740  * @package  core_tag
741  * @category tag
742  * @access   public
743  * @param    object   $tagobject a line out of tag table, as returned by the adobd functions
744  * @param    int      $html TAG_RETURN_HTML (default) will return htmlspecialchars encoded string, TAG_RETURN_TEXT will not encode.
745  * @return   string
746  */
747 function tag_display_name($tagobject, $html=TAG_RETURN_HTML) {
748     global $CFG;
750     if (!isset($tagobject->name)) {
751         return '';
752     }
754     if (empty($CFG->keeptagnamecase)) {
755         //this is the normalized tag name
756         $tagname = core_text::strtotitle($tagobject->name);
757     } else {
758         //original casing of the tag name
759         $tagname = $tagobject->rawname;
760     }
762     // clean up a bit just in case the rules change again
763     $tagname = clean_param($tagname, PARAM_TAG);
765     if ($html == TAG_RETURN_TEXT) {
766         return $tagname;
767     } else { // TAG_RETURN_HTML
768         return htmlspecialchars($tagname);
769     }
772 /**
773  * Find all records tagged with a tag of a given type ('post', 'user', etc.)
774  *
775  * @package  core_tag
776  * @category tag
777  * @access   public
778  * @param    string   $tag       tag to look for
779  * @param    string   $type      type to restrict search to.  If null, every matching record will be returned
780  * @param    int      $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point.
781  * @param    int      $limitnum  (optional, required if $limitfrom is set) return a subset comprising this many records.
782  * @return   array of matching objects, indexed by record id, from the table containing the type requested
783  */
784 function tag_find_records($tag, $type, $limitfrom='', $limitnum='') {
785     global $CFG, $DB;
787     if (!$tag || !$type) {
788         return array();
789     }
791     $tagid = tag_get_id($tag);
793     $query = "SELECT it.*
794                 FROM {".$type."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid
795                WHERE tt.itemtype = ? AND tt.tagid = ?";
796     $params = array($type, $tagid);
798     return $DB->get_records_sql($query, $params, $limitfrom, $limitnum);
804 ///////////////////////////////////////////////////////
805 /////////////////// PRIVATE TAG API ///////////////////
807 /**
808  * Adds one or more tag in the database.  This function should not be called directly : you should
809  * use tag_set.
810  *
811  * @package core_tag
812  * @access  private
813  * @param   mixed    $tags     one tag, or an array of tags, to be created
814  * @param   string   $type     type of tag to be created ("default" is the default value and "official" is the only other supported
815  *                             value at this time). An official tag is kept even if there are no records tagged with it.
816  * @return array     $tags ids indexed by their lowercase normalized names. Any boolean false in the array indicates an error while
817  *                             adding the tag.
818  */
819 function tag_add($tags, $type="default") {
820     global $USER, $DB;
822     if (!is_array($tags)) {
823         $tags = array($tags);
824     }
826     $tag_object = new StdClass;
827     $tag_object->tagtype      = $type;
828     $tag_object->userid       = $USER->id;
829     $tag_object->timemodified = time();
831     $clean_tags = tag_normalize($tags, TAG_CASE_ORIGINAL);
833     $tags_ids = array();
834     foreach($clean_tags as $tag) {
835         $tag = trim($tag);
836         if (!$tag) {
837             $tags_ids[$tag] = false;
838         } else {
839             // note that the difference between rawname and name is only
840             // capitalization : the rawname is NOT the same at the rawtag.
841             $tag_object->rawname = $tag;
842             $tag_name_lc         = core_text::strtolower($tag);
843             $tag_object->name    = $tag_name_lc;
844             //var_dump($tag_object);
845             $tags_ids[$tag_name_lc] = $DB->insert_record('tag', $tag_object);
847             $event = \core\event\tag_created::create(array(
848                 'objectid' => $tags_ids[$tag_name_lc],
849                 'relateduserid' => $tag_object->userid,
850                 'context' => context_system::instance(),
851                 'other' => array(
852                     'name' => $tag_object->name,
853                     'rawname' => $tag_object->rawname
854                 )
855             ));
856             $event->trigger();
857         }
858     }
860     return $tags_ids;
863 /**
864  * Assigns a tag to a record; if the record already exists, the time and ordering will be updated.
865  *
866  * @package core_tag
867  * @access private
868  * @param string $record_type the type of the record that will be tagged
869  * @param int $record_id the id of the record that will be tagged
870  * @param string $tagid the tag id to set on the record.
871  * @param int $ordering the order of the instance for this record
872  * @param int $userid (optional) only required for course tagging
873  * @param string|null $component the component that was tagged
874  * @param int|null $contextid the context id of where this tag was assigned
875  * @return bool true on success, false otherwise
876  */
877 function tag_assign($record_type, $record_id, $tagid, $ordering, $userid = 0, $component = null, $contextid = null) {
878     global $DB;
880     if ($component === null || $contextid === null) {
881         debugging('You should specify the component and contextid of the item being tagged in your call to tag_assign.',
882             DEBUG_DEVELOPER);
883     }
885     // Get the tag.
886     $tag = $DB->get_record('tag', array('id' => $tagid), 'name, rawname', MUST_EXIST);
888     if ( $tag_instance_object = $DB->get_record('tag_instance', array('tagid'=>$tagid, 'itemtype'=>$record_type, 'itemid'=>$record_id, 'tiuserid'=>$userid), 'id')) {
889         $tag_instance_object->ordering     = $ordering;
890         $tag_instance_object->timemodified = time();
892         $DB->update_record('tag_instance', $tag_instance_object);
893     } else {
894         $tag_instance_object = new StdClass;
895         $tag_instance_object->tagid        = $tagid;
896         $tag_instance_object->component    = $component;
897         $tag_instance_object->itemid       = $record_id;
898         $tag_instance_object->itemtype     = $record_type;
899         $tag_instance_object->contextid    = $contextid;
900         $tag_instance_object->ordering     = $ordering;
901         $tag_instance_object->timecreated  = time();
902         $tag_instance_object->timemodified = $tag_instance_object->timecreated;
903         $tag_instance_object->tiuserid     = $userid;
905         $tag_instance_object->id = $DB->insert_record('tag_instance', $tag_instance_object);
906     }
908     // We can not fire an event with 'null' as the contextid.
909     if (is_null($contextid)) {
910         $contextid = context_system::instance()->id;
911     }
913     // Trigger item tagged event.
914     $event = \core\event\item_tagged::create(array(
915         'objectid' => $tag_instance_object->id,
916         'contextid' => $contextid,
917         'other' => array(
918             'tagid' => $tagid,
919             'tagname' => $tag->name,
920             'tagrawname' => $tag->rawname,
921             'itemid' => $record_id,
922             'itemtype' => $record_type
923         )
924     ));
925     $event->trigger();
927     return true;
930 /**
931  * Function that returns tags that start with some text, for use by the autocomplete feature
932  *
933  * @package core_tag
934  * @access  private
935  * @param   string   $text string that the tag names will be matched against
936  * @return  mixed    an array of objects, or false if no records were found or an error occured.
937  */
938 function tag_autocomplete($text) {
939     global $DB;
940     return $DB->get_records_sql("SELECT tg.id, tg.name, tg.rawname
941                                    FROM {tag} tg
942                                   WHERE tg.name LIKE ?", array(core_text::strtolower($text)."%"));
945 /**
946  * Clean up the tag tables, making sure all tagged object still exists.
947  *
948  * This should normally not be necessary, but in case related tags are not deleted when the tagged record is removed, this should be
949  * 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
950  * call: don't run at peak time.
951  *
952  * @package core_tag
953  * @access  private
954  * @todo    MDL-31212 Update tag cleanup sql so that it supports multiple types of tags
955  */
956 function tag_cleanup() {
957     global $DB;
959     $instances = $DB->get_recordset('tag_instance');
961     // cleanup tag instances
962     foreach ($instances as $instance) {
963         $delete = false;
965         if (!$DB->record_exists('tag', array('id'=>$instance->tagid))) {
966             // if the tag has been removed, instance should be deleted.
967             $delete = true;
968         } else {
969             switch ($instance->itemtype) {
970                 case 'user': // users are marked as deleted, but not actually deleted
971                     if ($DB->record_exists('user', array('id'=>$instance->itemid, 'deleted'=>1))) {
972                         $delete = true;
973                     }
974                     break;
975                 default: // anything else, if the instance is not there, delete.
976                     if (!$DB->record_exists($instance->itemtype, array('id'=>$instance->itemid))) {
977                         $delete = true;
978                     }
979                     break;
980             }
981         }
982         if ($delete) {
983             tag_delete_instance($instance->itemtype, $instance->itemid, $instance->tagid);
984             //debugging('deleting tag_instance #'. $instance->id .', linked to tag id #'. $instance->tagid, DEBUG_DEVELOPER);
985         }
986     }
987     $instances->close();
989     // TODO MDL-31212 this will only clean tags of type 'default'.  This is good as
990     // it won't delete 'official' tags, but the day we get more than two
991     // types, we need to fix this.
992     $unused_tags = $DB->get_recordset_sql("SELECT tg.id
993                                              FROM {tag} tg
994                                             WHERE tg.tagtype = 'default'
995                                                   AND NOT EXISTS (
996                                                       SELECT 'x'
997                                                         FROM {tag_instance} ti
998                                                        WHERE ti.tagid = tg.id
999                                                   )");
1001     // cleanup tags
1002     foreach ($unused_tags as $unused_tag) {
1003         tag_delete($unused_tag->id);
1004         //debugging('deleting unused tag #'. $unused_tag->id,  DEBUG_DEVELOPER);
1005     }
1006     $unused_tags->close();
1009 /**
1010  * Calculates and stores the correlated tags of all tags. The correlations are stored in the 'tag_correlation' table.
1011  *
1012  * Two tags are correlated if they appear together a lot. Ex.: Users tagged with "computers" will probably also be tagged with "algorithms".
1013  *
1014  * The rationale for the 'tag_correlation' table is performance. It works as a cache for a potentially heavy load query done at the
1015  * 'tag_instance' table. So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table.
1016  *
1017  * @package core_tag
1018  * @access  private
1019  * @param   int      $mincorrelation Only tags with more than $mincorrelation correlations will be identified.
1020  */
1021 function tag_compute_correlations($mincorrelation = 2) {
1022     global $DB;
1024     // This mighty one line query fetches a row from the database for every
1025     // individual tag correlation. We then need to process the rows collecting
1026     // the correlations for each tag id.
1027     // The fields used by this query are as follows:
1028     //   tagid         : This is the tag id, there should be at least $mincorrelation
1029     //                   rows for each tag id.
1030     //   correlation   : This is the tag id that correlates to the above tagid field.
1031     //   correlationid : This is the id of the row in the tag_correlation table that
1032     //                   relates to the tagid field and will be NULL if there are no
1033     //                   existing correlations
1034     $sql = 'SELECT pairs.tagid, pairs.correlation, pairs.ocurrences, co.id AS correlationid
1035               FROM (
1036                        SELECT ta.tagid, tb.tagid AS correlation, COUNT(*) AS ocurrences
1037                          FROM {tag_instance} ta
1038                          JOIN {tag_instance} tb ON (ta.itemtype = tb.itemtype AND ta.itemid = tb.itemid AND ta.tagid <> tb.tagid)
1039                      GROUP BY ta.tagid, tb.tagid
1040                        HAVING COUNT(*) > :mincorrelation
1041                    ) pairs
1042          LEFT JOIN {tag_correlation} co ON co.tagid = pairs.tagid
1043           ORDER BY pairs.tagid ASC, pairs.ocurrences DESC, pairs.correlation ASC';
1044     $rs = $DB->get_recordset_sql($sql, array('mincorrelation' => $mincorrelation));
1046     // Set up an empty tag correlation object
1047     $tagcorrelation = new stdClass;
1048     $tagcorrelation->id = null;
1049     $tagcorrelation->tagid = null;
1050     $tagcorrelation->correlatedtags = array();
1052     // We store each correlation id in this array so we can remove any correlations
1053     // that no longer exist.
1054     $correlations = array();
1056     // Iterate each row of the result set and build them into tag correlations.
1057     // We add all of a tag's correlations to $tagcorrelation->correlatedtags[]
1058     // then save the $tagcorrelation object
1059     foreach ($rs as $row) {
1060         if ($row->tagid != $tagcorrelation->tagid) {
1061             // The tag id has changed so we have all of the correlations for this tag
1062             $tagcorrelationid = tag_process_computed_correlation($tagcorrelation);
1063             if ($tagcorrelationid) {
1064                 $correlations[] = $tagcorrelationid;
1065             }
1066             // Now we reset the tag correlation object so we can reuse it and set it
1067             // up for the current record.
1068             $tagcorrelation = new stdClass;
1069             $tagcorrelation->id = $row->correlationid;
1070             $tagcorrelation->tagid = $row->tagid;
1071             $tagcorrelation->correlatedtags = array();
1072         }
1073         //Save the correlation on the tag correlation object
1074         $tagcorrelation->correlatedtags[] = $row->correlation;
1075     }
1076     // Update the current correlation after the last record.
1077     $tagcorrelationid = tag_process_computed_correlation($tagcorrelation);
1078     if ($tagcorrelationid) {
1079         $correlations[] = $tagcorrelationid;
1080     }
1083     // Close the recordset
1084     $rs->close();
1086     // Remove any correlations that weren't just identified
1087     if (empty($correlations)) {
1088         //there are no tag correlations
1089         $DB->delete_records('tag_correlation');
1090     } else {
1091         list($sql, $params) = $DB->get_in_or_equal($correlations, SQL_PARAMS_NAMED, 'param0000', false);
1092         $DB->delete_records_select('tag_correlation', 'id '.$sql, $params);
1093     }
1096 /**
1097  * This function processes a tag correlation and makes changes in the database as required.
1098  *
1099  * The tag correlation object needs have both a tagid property and a correlatedtags property that is an array.
1100  *
1101  * @package core_tag
1102  * @access  private
1103  * @param   stdClass $tagcorrelation
1104  * @return  int/bool The id of the tag correlation that was just processed or false.
1105  */
1106 function tag_process_computed_correlation(stdClass $tagcorrelation) {
1107     global $DB;
1109     // You must provide a tagid and correlatedtags must be set and be an array
1110     if (empty($tagcorrelation->tagid) || !isset($tagcorrelation->correlatedtags) || !is_array($tagcorrelation->correlatedtags)) {
1111         return false;
1112     }
1114     $tagcorrelation->correlatedtags = join(',', $tagcorrelation->correlatedtags);
1115     if (!empty($tagcorrelation->id)) {
1116         // The tag correlation already exists so update it
1117         $DB->update_record('tag_correlation', $tagcorrelation);
1118     } else {
1119         // This is a new correlation to insert
1120         $tagcorrelation->id = $DB->insert_record('tag_correlation', $tagcorrelation);
1121     }
1122     return $tagcorrelation->id;
1125 /**
1126  * Tasks that should be performed at cron time
1127  *
1128  * @package core_tag
1129  * @access private
1130  */
1131 function tag_cron() {
1132     tag_compute_correlations();
1133     tag_cleanup();
1136 /**
1137  * Search for tags with names that match some text
1138  *
1139  * @package core_tag
1140  * @access  private
1141  * @param   string        $text      escaped string that the tag names will be matched against
1142  * @param   bool          $ordered   If true, tags are ordered by their popularity. If false, no ordering.
1143  * @param   int/string    $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point.
1144  * @param   int/string    $limitnum  (optional, required if $limitfrom is set) return a subset comprising this many records.
1145  * @return  array/boolean an array of objects, or false if no records were found or an error occured.
1146  */
1147 function tag_find_tags($text, $ordered=true, $limitfrom='', $limitnum='') {
1148     global $DB;
1150     $norm = tag_normalize($text, TAG_CASE_LOWER);
1151     $text = array_shift($norm);
1153     if ($ordered) {
1154         $query = "SELECT tg.id, tg.name, tg.rawname, COUNT(ti.id) AS count
1155                     FROM {tag} tg LEFT JOIN {tag_instance} ti ON tg.id = ti.tagid
1156                    WHERE tg.name LIKE ?
1157                 GROUP BY tg.id, tg.name, tg.rawname
1158                 ORDER BY count DESC";
1159     } else {
1160         $query = "SELECT tg.id, tg.name, tg.rawname
1161                     FROM {tag} tg
1162                    WHERE tg.name LIKE ?";
1163     }
1164     $params = array("%{$text}%");
1165     return $DB->get_records_sql($query, $params, $limitfrom , $limitnum);
1168 /**
1169  * Get the name of a tag
1170  *
1171  * @package core_tag
1172  * @access  private
1173  * @param   mixed    $tagids the id of the tag, or an array of ids
1174  * @return  mixed    string name of one tag, or id-indexed array of strings
1175  */
1176 function tag_get_name($tagids) {
1177     global $DB;
1179     if (!is_array($tagids)) {
1180         if ($tag = $DB->get_record('tag', array('id'=>$tagids))) {
1181             return $tag->name;
1182         }
1183         return false;
1184     }
1186     $tag_names = array();
1187     foreach($DB->get_records_list('tag', 'id', $tagids) as $tag) {
1188         $tag_names[$tag->id] = $tag->name;
1189     }
1191     return $tag_names;
1194 /**
1195  * Returns the correlated tags of a tag, retrieved from the tag_correlation table. Make sure cron runs, otherwise the table will be
1196  * empty and this function won't return anything.
1197  *
1198  * @package core_tag
1199  * @access  private
1200  * @param   int      $tag_id   is a single tag id
1201  * @param   int      $limitnum this parameter does not appear to have any function???
1202  * @return  array    an array of tag objects or an empty if no correlated tags are found
1203  */
1204 function tag_get_correlated($tag_id, $limitnum=null) {
1205     global $DB;
1207     $tag_correlation = $DB->get_record('tag_correlation', array('tagid'=>$tag_id));
1209     if (!$tag_correlation || empty($tag_correlation->correlatedtags)) {
1210         return array();
1211     }
1213     // this is (and has to) return the same fields as the query in tag_get_tags
1214     $sql = "SELECT DISTINCT tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering
1215               FROM {tag} tg
1216         INNER JOIN {tag_instance} ti ON tg.id = ti.tagid
1217              WHERE tg.id IN ({$tag_correlation->correlatedtags})";
1218     $result = $DB->get_records_sql($sql);
1219     if (!$result) {
1220         return array();
1221     }
1223     return $result;
1226 /**
1227  * Function that normalizes a list of tag names.
1228  *
1229  * @package core_tag
1230  * @access  private
1231  * @param   array/string $rawtags array of tags, or a single tag.
1232  * @param   int          $case    case to use for returned value (default: lower case). Either TAG_CASE_LOWER (default) or TAG_CASE_ORIGINAL
1233  * @return  array        lowercased normalized tags, indexed by the normalized tag, in the same order as the original array.
1234  *                       (Eg: 'Banana' => 'banana').
1235  */
1236 function tag_normalize($rawtags, $case = TAG_CASE_LOWER) {
1238     // cache normalized tags, to prevent costly repeated calls to clean_param
1239     static $cleaned_tags_lc = array(); // lower case - use for comparison
1240     static $cleaned_tags_mc = array(); // mixed case - use for saving to database
1242     if ( !is_array($rawtags) ) {
1243         $rawtags = array($rawtags);
1244     }
1246     $result = array();
1247     foreach($rawtags as $rawtag) {
1248         $rawtag = trim($rawtag);
1249         if (!$rawtag) {
1250             continue;
1251         }
1252         if ( !array_key_exists($rawtag, $cleaned_tags_lc) ) {
1253             $cleaned_tags_lc[$rawtag] = core_text::strtolower( clean_param($rawtag, PARAM_TAG) );
1254             $cleaned_tags_mc[$rawtag] = clean_param($rawtag, PARAM_TAG);
1255         }
1256         if ( $case == TAG_CASE_LOWER ) {
1257             $result[$rawtag] = $cleaned_tags_lc[$rawtag];
1258         } else { // TAG_CASE_ORIGINAL
1259             $result[$rawtag] = $cleaned_tags_mc[$rawtag];
1260         }
1261     }
1263     return $result;
1266 /**
1267  * Count how many records are tagged with a specific tag.
1268  *
1269  * @package core_tag
1270  * @access  private
1271  * @param   string   $record_type record to look for ('post', 'user', etc.)
1272  * @param   int      $tagid       is a single tag id
1273  * @return  int      number of mathing tags.
1274  */
1275 function tag_record_count($record_type, $tagid) {
1276     global $DB;
1277     return $DB->count_records('tag_instance', array('itemtype'=>$record_type, 'tagid'=>$tagid));
1280 /**
1281  * Determine if a record is tagged with a specific tag
1282  *
1283  * @package core_tag
1284  * @access  private
1285  * @param   string   $record_type the record type to look for
1286  * @param   int      $record_id   the record id to look for
1287  * @param   string   $tag         a tag name
1288  * @return  bool/int true if it is tagged, 0 (false) otherwise
1289  */
1290 function tag_record_tagged_with($record_type, $record_id, $tag) {
1291     global $DB;
1292     if ($tagid = tag_get_id($tag)) {
1293         return $DB->count_records('tag_instance', array('itemtype'=>$record_type, 'itemid'=>$record_id, 'tagid'=>$tagid));
1294     } else {
1295         return 0; // tag doesn't exist
1296     }
1299 /**
1300  * Flag a tag as inappropriate.
1301  *
1302  * @param int|array $tagids a single tagid, or an array of tagids
1303  */
1304 function tag_set_flag($tagids) {
1305     global $DB;
1307     $tagids = (array) $tagids;
1309     // Use the tagids to create a select statement to be used later.
1310     list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids, SQL_PARAMS_NAMED);
1312     // Update all the tags to flagged.
1313     $sql = "UPDATE {tag}
1314                SET flag = flag + 1, timemodified = :time
1315              WHERE id $tagsql";
1317     // Update all the tags.
1318     $DB->execute($sql, array_merge(array('time' => time()), $tagparams));
1320     // Get all the tags.
1321     if ($tags = $DB->get_records_select('tag', 'id '. $tagsql, $tagparams, 'id ASC')) {
1322         // Loop through and fire an event for each tag that it was flagged.
1323         foreach ($tags as $tag) {
1324             $event = \core\event\tag_flagged::create(array(
1325                 'objectid' => $tag->id,
1326                 'relateduserid' => $tag->userid,
1327                 'context' => context_system::instance(),
1328                 'other' => array(
1329                     'name' => $tag->name,
1330                     'rawname' => $tag->rawname
1331                 )
1333             ));
1334             $event->add_record_snapshot('tag', $tag);
1335             $event->trigger();
1336         }
1337     }
1340 /**
1341  * Remove the inappropriate flag on a tag.
1342  *
1343  * @param int|array $tagids a single tagid, or an array of tagids
1344  */
1345 function tag_unset_flag($tagids) {
1346     global $DB;
1348     $tagids = (array) $tagids;
1350     // Use the tagids to create a select statement to be used later.
1351     list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids, SQL_PARAMS_NAMED);
1353     // Update all the tags to unflagged.
1354     $sql = "UPDATE {tag}
1355                SET flag = 0, timemodified = :time
1356              WHERE id $tagsql";
1358     // Update all the tags.
1359     $DB->execute($sql, array_merge(array('time' => time()), $tagparams));
1361     // Get all the tags.
1362     if ($tags = $DB->get_records_select('tag', 'id '. $tagsql, $tagparams, 'id ASC')) {
1363         // Loop through and fire an event for each tag that it was unflagged.
1364         foreach ($tags as $tag) {
1365             $event = \core\event\tag_unflagged::create(array(
1366                 'objectid' => $tag->id,
1367                 'relateduserid' => $tag->userid,
1368                 'context' => context_system::instance(),
1369                 'other' => array(
1370                     'name' => $tag->name,
1371                     'rawname' => $tag->rawname
1372                 )
1373             ));
1374             $event->add_record_snapshot('tag', $tag);
1375             $event->trigger();
1376         }
1377     }
1380 /**
1381  * Return a list of page types
1382  *
1383  * @package core_tag
1384  * @access  private
1385  * @param   string   $pagetype       current page type
1386  * @param   stdClass $parentcontext  Block's parent context
1387  * @param   stdClass $currentcontext Current context of block
1388  */
1389 function tag_page_type_list($pagetype, $parentcontext, $currentcontext) {
1390     return array(
1391         'tag-*'=>get_string('page-tag-x', 'tag'),
1392         'tag-index'=>get_string('page-tag-index', 'tag'),
1393         'tag-search'=>get_string('page-tag-search', 'tag'),
1394         'tag-manage'=>get_string('page-tag-manage', 'tag')
1395     );