| 1 | efrain | 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/>.
 | 
        
           |  |  | 16 |   | 
        
           |  |  | 17 | /**
 | 
        
           |  |  | 18 |  * Contains class core_tag_tag
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * @package   core_tag
 | 
        
           |  |  | 21 |  * @copyright  2015 Marina Glancy
 | 
        
           |  |  | 22 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 23 |  */
 | 
        
           |  |  | 24 |   | 
        
           |  |  | 25 | defined('MOODLE_INTERNAL') || die();
 | 
        
           |  |  | 26 |   | 
        
           |  |  | 27 | /**
 | 
        
           |  |  | 28 |  * Represents one tag and also contains lots of useful tag-related methods as static functions.
 | 
        
           |  |  | 29 |  *
 | 
        
           |  |  | 30 |  * Tags can be added to any database records.
 | 
        
           |  |  | 31 |  * $itemtype refers to the DB table name
 | 
        
           |  |  | 32 |  * $itemid refers to id field in this DB table
 | 
        
           |  |  | 33 |  * $component is the component that is responsible for the tag instance
 | 
        
           |  |  | 34 |  * $context is the affected context
 | 
        
           |  |  | 35 |  *
 | 
        
           |  |  | 36 |  * BASIC INSTRUCTIONS :
 | 
        
           |  |  | 37 |  *  - to "tag a blog post" (for example):
 | 
        
           |  |  | 38 |  *        core_tag_tag::set_item_tags('post', 'core', $blogpost->id, $context, $arrayoftags);
 | 
        
           |  |  | 39 |  *
 | 
        
           |  |  | 40 |  *  - to "remove all the tags on a blog post":
 | 
        
           |  |  | 41 |  *        core_tag_tag::remove_all_item_tags('post', 'core', $blogpost->id);
 | 
        
           |  |  | 42 |  *
 | 
        
           |  |  | 43 |  * set_item_tags() will create tags that do not exist yet.
 | 
        
           |  |  | 44 |  *
 | 
        
           |  |  | 45 |  * @property-read int $id
 | 
        
           |  |  | 46 |  * @property-read string $name
 | 
        
           |  |  | 47 |  * @property-read string $rawname
 | 
        
           |  |  | 48 |  * @property-read int $tagcollid
 | 
        
           |  |  | 49 |  * @property-read int $userid
 | 
        
           |  |  | 50 |  * @property-read int $isstandard
 | 
        
           |  |  | 51 |  * @property-read string $description
 | 
        
           |  |  | 52 |  * @property-read int $descriptionformat
 | 
        
           |  |  | 53 |  * @property-read int $flag 0 if not flagged or positive integer if flagged
 | 
        
           |  |  | 54 |  * @property-read int $timemodified
 | 
        
           |  |  | 55 |  *
 | 
        
           |  |  | 56 |  * @package   core_tag
 | 
        
           |  |  | 57 |  * @copyright  2015 Marina Glancy
 | 
        
           |  |  | 58 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 59 |  */
 | 
        
           |  |  | 60 | class core_tag_tag {
 | 
        
           |  |  | 61 |   | 
        
           |  |  | 62 |     /** @var stdClass data about the tag */
 | 
        
           |  |  | 63 |     protected $record = null;
 | 
        
           |  |  | 64 |   | 
        
           |  |  | 65 |     /** @var int indicates that both standard and not standard tags can be used (or should be returned) */
 | 
        
           |  |  | 66 |     const BOTH_STANDARD_AND_NOT = 0;
 | 
        
           |  |  | 67 |   | 
        
           |  |  | 68 |     /** @var int indicates that only standard tags can be used (or must be returned) */
 | 
        
           |  |  | 69 |     const STANDARD_ONLY = 1;
 | 
        
           |  |  | 70 |   | 
        
           |  |  | 71 |     /** @var int indicates that only non-standard tags should be returned - this does not really have use cases, left for BC  */
 | 
        
           |  |  | 72 |     const NOT_STANDARD_ONLY = -1;
 | 
        
           |  |  | 73 |   | 
        
           |  |  | 74 |     /** @var int option to hide standard tags when editing item tags */
 | 
        
           |  |  | 75 |     const HIDE_STANDARD = 2;
 | 
        
           |  |  | 76 |   | 
        
           |  |  | 77 |     /** @var int|null tag context ID. */
 | 
        
           |  |  | 78 |     public $taginstancecontextid;
 | 
        
           |  |  | 79 |   | 
        
           |  |  | 80 |     /** @var int|null time modification. */
 | 
        
           |  |  | 81 |     public $timemodified;
 | 
        
           |  |  | 82 |   | 
        
           |  |  | 83 |     /** @var int|null 0 if not flagged or positive integer if flagged. */
 | 
        
           |  |  | 84 |     public $flag;
 | 
        
           |  |  | 85 |   | 
        
           |  |  | 86 |     /**
 | 
        
           |  |  | 87 |      * Constructor. Use functions get(), get_by_name(), etc.
 | 
        
           |  |  | 88 |      *
 | 
        
           |  |  | 89 |      * @param stdClass $record
 | 
        
           |  |  | 90 |      */
 | 
        
           |  |  | 91 |     protected function __construct($record) {
 | 
        
           |  |  | 92 |         if (empty($record->id)) {
 | 
        
           |  |  | 93 |             throw new coding_exception("Record must contain at least field 'id'");
 | 
        
           |  |  | 94 |         }
 | 
        
           |  |  | 95 |         // The following three variables must be added because the database ($record) does not contain them.
 | 
        
           |  |  | 96 |         $this->taginstancecontextid = $record->taginstancecontextid ?? null;
 | 
        
           |  |  | 97 |         $this->flag = $record->flag ?? null;
 | 
        
           |  |  | 98 |         $this->record = $record;
 | 
        
           |  |  | 99 |     }
 | 
        
           |  |  | 100 |   | 
        
           |  |  | 101 |     /**
 | 
        
           |  |  | 102 |      * Magic getter
 | 
        
           |  |  | 103 |      *
 | 
        
           |  |  | 104 |      * @param string $name
 | 
        
           |  |  | 105 |      * @return mixed
 | 
        
           |  |  | 106 |      */
 | 
        
           |  |  | 107 |     public function __get($name) {
 | 
        
           |  |  | 108 |         return $this->record->$name;
 | 
        
           |  |  | 109 |     }
 | 
        
           |  |  | 110 |   | 
        
           |  |  | 111 |     /**
 | 
        
           |  |  | 112 |      * Magic isset method
 | 
        
           |  |  | 113 |      *
 | 
        
           |  |  | 114 |      * @param string $name
 | 
        
           |  |  | 115 |      * @return bool
 | 
        
           |  |  | 116 |      */
 | 
        
           |  |  | 117 |     public function __isset($name) {
 | 
        
           |  |  | 118 |         return isset($this->record->$name);
 | 
        
           |  |  | 119 |     }
 | 
        
           |  |  | 120 |   | 
        
           |  |  | 121 |     /**
 | 
        
           |  |  | 122 |      * Converts to object
 | 
        
           |  |  | 123 |      *
 | 
        
           |  |  | 124 |      * @return stdClass
 | 
        
           |  |  | 125 |      */
 | 
        
           |  |  | 126 |     public function to_object() {
 | 
        
           |  |  | 127 |         return fullclone($this->record);
 | 
        
           |  |  | 128 |     }
 | 
        
           |  |  | 129 |   | 
        
           |  |  | 130 |     /**
 | 
        
           |  |  | 131 |      * Returns tag name ready to be displayed
 | 
        
           |  |  | 132 |      *
 | 
        
           |  |  | 133 |      * @param bool $ashtml (default true) if true will return htmlspecialchars encoded string
 | 
        
           |  |  | 134 |      * @return string
 | 
        
           |  |  | 135 |      */
 | 
        
           |  |  | 136 |     public function get_display_name($ashtml = true) {
 | 
        
           |  |  | 137 |         return static::make_display_name($this->record, $ashtml);
 | 
        
           |  |  | 138 |     }
 | 
        
           |  |  | 139 |   | 
        
           |  |  | 140 |     /**
 | 
        
           |  |  | 141 |      * Prepares tag name ready to be displayed
 | 
        
           |  |  | 142 |      *
 | 
        
           |  |  | 143 |      * @param stdClass|core_tag_tag $tag record from db table tag, must contain properties name and rawname
 | 
        
           |  |  | 144 |      * @param bool $ashtml (default true) if true will return htmlspecialchars encoded string
 | 
        
           |  |  | 145 |      * @return string
 | 
        
           |  |  | 146 |      */
 | 
        
           |  |  | 147 |     public static function make_display_name($tag, $ashtml = true) {
 | 
        
           |  |  | 148 |         global $CFG;
 | 
        
           |  |  | 149 |   | 
        
           |  |  | 150 |         if (empty($CFG->keeptagnamecase)) {
 | 
        
           |  |  | 151 |             // This is the normalized tag name.
 | 
        
           |  |  | 152 |             $tagname = core_text::strtotitle($tag->name);
 | 
        
           |  |  | 153 |         } else {
 | 
        
           |  |  | 154 |             // Original casing of the tag name.
 | 
        
           |  |  | 155 |             $tagname = $tag->rawname;
 | 
        
           |  |  | 156 |         }
 | 
        
           |  |  | 157 |   | 
        
           |  |  | 158 |         // Clean up a bit just in case the rules change again.
 | 
        
           |  |  | 159 |         $tagname = clean_param($tagname, PARAM_TAG);
 | 
        
           |  |  | 160 |   | 
        
           |  |  | 161 |         return $ashtml ? htmlspecialchars($tagname, ENT_COMPAT) : $tagname;
 | 
        
           |  |  | 162 |     }
 | 
        
           |  |  | 163 |   | 
        
           |  |  | 164 |     /**
 | 
        
           |  |  | 165 |      * Adds one or more tag in the database.  This function should not be called directly : you should
 | 
        
           |  |  | 166 |      * use tag_set.
 | 
        
           |  |  | 167 |      *
 | 
        
           |  |  | 168 |      * @param   int      $tagcollid
 | 
        
           |  |  | 169 |      * @param   string|array $tags     one tag, or an array of tags, to be created
 | 
        
           |  |  | 170 |      * @param   bool     $isstandard type of tag to be created. A standard tag is kept even if there are no records tagged with it.
 | 
        
           |  |  | 171 |      * @return  array    tag objects indexed by their lowercase normalized names. Any boolean false in the array
 | 
        
           |  |  | 172 |      *                             indicates an error while adding the tag.
 | 
        
           |  |  | 173 |      */
 | 
        
           |  |  | 174 |     protected static function add($tagcollid, $tags, $isstandard = false) {
 | 
        
           |  |  | 175 |         global $USER, $DB;
 | 
        
           |  |  | 176 |   | 
        
           |  |  | 177 |         $tagobject = new stdClass();
 | 
        
           |  |  | 178 |         $tagobject->isstandard   = $isstandard ? 1 : 0;
 | 
        
           |  |  | 179 |         $tagobject->userid       = $USER->id;
 | 
        
           |  |  | 180 |         $tagobject->timemodified = time();
 | 
        
           |  |  | 181 |         $tagobject->tagcollid    = $tagcollid;
 | 
        
           |  |  | 182 |   | 
        
           |  |  | 183 |         $rv = array();
 | 
        
           |  |  | 184 |         foreach ($tags as $veryrawname) {
 | 
        
           |  |  | 185 |             $rawname = clean_param($veryrawname, PARAM_TAG);
 | 
        
           |  |  | 186 |             if (!$rawname) {
 | 
        
           |  |  | 187 |                 $rv[$rawname] = false;
 | 
        
           |  |  | 188 |             } else {
 | 
        
           |  |  | 189 |                 $obj = (object)(array)$tagobject;
 | 
        
           |  |  | 190 |                 $obj->rawname = $rawname;
 | 
        
           |  |  | 191 |                 $obj->name    = core_text::strtolower($rawname);
 | 
        
           |  |  | 192 |                 $obj->id      = $DB->insert_record('tag', $obj);
 | 
        
           |  |  | 193 |                 $rv[$obj->name] = new static($obj);
 | 
        
           |  |  | 194 |   | 
        
           |  |  | 195 |                 \core\event\tag_created::create_from_tag($rv[$obj->name])->trigger();
 | 
        
           |  |  | 196 |             }
 | 
        
           |  |  | 197 |         }
 | 
        
           |  |  | 198 |   | 
        
           |  |  | 199 |         return $rv;
 | 
        
           |  |  | 200 |     }
 | 
        
           |  |  | 201 |   | 
        
           |  |  | 202 |     /**
 | 
        
           |  |  | 203 |      * Simple function to just return a single tag object by its id
 | 
        
           |  |  | 204 |      *
 | 
        
           |  |  | 205 |      * @param    int    $id
 | 
        
           |  |  | 206 |      * @param    string $returnfields which fields do we want returned from table {tag}.
 | 
        
           |  |  | 207 |      *                        Default value is 'id,name,rawname,tagcollid',
 | 
        
           |  |  | 208 |      *                        specify '*' to include all fields.
 | 
        
           |  |  | 209 |      * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
 | 
        
           |  |  | 210 |      *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
 | 
        
           |  |  | 211 |      *                        MUST_EXIST means throw exception if no record or multiple records found
 | 
        
           |  |  | 212 |      * @return   core_tag_tag|false  tag object
 | 
        
           |  |  | 213 |      */
 | 
        
           |  |  | 214 |     public static function get($id, $returnfields = 'id, name, rawname, tagcollid', $strictness = IGNORE_MISSING) {
 | 
        
           |  |  | 215 |         global $DB;
 | 
        
           |  |  | 216 |         $record = $DB->get_record('tag', array('id' => $id), $returnfields, $strictness);
 | 
        
           |  |  | 217 |         if ($record) {
 | 
        
           |  |  | 218 |             return new static($record);
 | 
        
           |  |  | 219 |         }
 | 
        
           |  |  | 220 |         return false;
 | 
        
           |  |  | 221 |     }
 | 
        
           |  |  | 222 |   | 
        
           |  |  | 223 |     /**
 | 
        
           |  |  | 224 |      * Simple function to just return an array of tag objects by their ids
 | 
        
           |  |  | 225 |      *
 | 
        
           |  |  | 226 |      * @param    int[]  $ids
 | 
        
           |  |  | 227 |      * @param    string $returnfields which fields do we want returned from table {tag}.
 | 
        
           |  |  | 228 |      *                        Default value is 'id,name,rawname,tagcollid',
 | 
        
           |  |  | 229 |      *                        specify '*' to include all fields.
 | 
        
           |  |  | 230 |      * @return   core_tag_tag[] array of retrieved tags
 | 
        
           |  |  | 231 |      */
 | 
        
           |  |  | 232 |     public static function get_bulk($ids, $returnfields = 'id, name, rawname, tagcollid') {
 | 
        
           |  |  | 233 |         global $DB;
 | 
        
           |  |  | 234 |         $result = array();
 | 
        
           |  |  | 235 |         if (empty($ids)) {
 | 
        
           |  |  | 236 |             return $result;
 | 
        
           |  |  | 237 |         }
 | 
        
           |  |  | 238 |         list($sql, $params) = $DB->get_in_or_equal($ids);
 | 
        
           |  |  | 239 |         $records = $DB->get_records_select('tag', 'id '.$sql, $params, '', $returnfields);
 | 
        
           |  |  | 240 |         foreach ($records as $record) {
 | 
        
           |  |  | 241 |             $result[$record->id] = new static($record);
 | 
        
           |  |  | 242 |         }
 | 
        
           |  |  | 243 |         return $result;
 | 
        
           |  |  | 244 |     }
 | 
        
           |  |  | 245 |   | 
        
           |  |  | 246 |     /**
 | 
        
           |  |  | 247 |      * Simple function to just return a single tag object by tagcollid and name
 | 
        
           |  |  | 248 |      *
 | 
        
           |  |  | 249 |      * @param int $tagcollid tag collection to use,
 | 
        
           |  |  | 250 |      *        if 0 is given we will try to guess the tag collection and return the first match
 | 
        
           |  |  | 251 |      * @param string $name tag name
 | 
        
           |  |  | 252 |      * @param string $returnfields which fields do we want returned. This is a comma separated string
 | 
        
           |  |  | 253 |      *         containing any combination of 'id', 'name', 'rawname', 'tagcollid' or '*' to include all fields.
 | 
        
           |  |  | 254 |      * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
 | 
        
           |  |  | 255 |      *                        IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);
 | 
        
           |  |  | 256 |      *                        MUST_EXIST means throw exception if no record or multiple records found
 | 
        
           |  |  | 257 |      * @return core_tag_tag|false tag object
 | 
        
           |  |  | 258 |      */
 | 
        
           |  |  | 259 |     public static function get_by_name($tagcollid, $name, $returnfields='id, name, rawname, tagcollid',
 | 
        
           |  |  | 260 |                         $strictness = IGNORE_MISSING) {
 | 
        
           |  |  | 261 |         global $DB;
 | 
        
           |  |  | 262 |         if ($tagcollid == 0) {
 | 
        
           |  |  | 263 |             $tags = static::guess_by_name($name, $returnfields);
 | 
        
           |  |  | 264 |             if ($tags) {
 | 
        
           |  |  | 265 |                 $tag = reset($tags);
 | 
        
           |  |  | 266 |                 return $tag;
 | 
        
           |  |  | 267 |             } else if ($strictness == MUST_EXIST) {
 | 
        
           |  |  | 268 |                 throw new dml_missing_record_exception('tag', 'name=?', array($name));
 | 
        
           |  |  | 269 |             }
 | 
        
           |  |  | 270 |             return false;
 | 
        
           |  |  | 271 |         }
 | 
        
           |  |  | 272 |         $name = core_text::strtolower($name);   // To cope with input that might just be wrong case.
 | 
        
           |  |  | 273 |         $params = array('name' => $name, 'tagcollid' => $tagcollid);
 | 
        
           |  |  | 274 |         $record = $DB->get_record('tag', $params, $returnfields, $strictness);
 | 
        
           |  |  | 275 |         if ($record) {
 | 
        
           |  |  | 276 |             return new static($record);
 | 
        
           |  |  | 277 |         }
 | 
        
           |  |  | 278 |         return false;
 | 
        
           |  |  | 279 |     }
 | 
        
           |  |  | 280 |   | 
        
           |  |  | 281 |     /**
 | 
        
           |  |  | 282 |      * Looking in all tag collections for the tag with the given name
 | 
        
           |  |  | 283 |      *
 | 
        
           |  |  | 284 |      * @param string $name tag name
 | 
        
           |  |  | 285 |      * @param string $returnfields
 | 
        
           |  |  | 286 |      * @return array array of core_tag_tag instances
 | 
        
           |  |  | 287 |      */
 | 
        
           |  |  | 288 |     public static function guess_by_name($name, $returnfields='id, name, rawname, tagcollid') {
 | 
        
           |  |  | 289 |         global $DB;
 | 
        
           |  |  | 290 |         if (empty($name)) {
 | 
        
           |  |  | 291 |             return array();
 | 
        
           |  |  | 292 |         }
 | 
        
           |  |  | 293 |         $tagcolls = core_tag_collection::get_collections();
 | 
        
           |  |  | 294 |         list($sql, $params) = $DB->get_in_or_equal(array_keys($tagcolls), SQL_PARAMS_NAMED);
 | 
        
           |  |  | 295 |         $params['name'] = core_text::strtolower($name);
 | 
        
           |  |  | 296 |         $tags = $DB->get_records_select('tag', 'name = :name AND tagcollid ' . $sql, $params, '', $returnfields);
 | 
        
           |  |  | 297 |         if (count($tags) > 1) {
 | 
        
           |  |  | 298 |             // Sort in the same order as tag collections.
 | 
        
           |  |  | 299 |             $tagcolls = core_tag_collection::get_collections();
 | 
        
           |  |  | 300 |             uasort($tags, function($a, $b) use ($tagcolls) {
 | 
        
           |  |  | 301 |                 return $tagcolls[$a->tagcollid]->sortorder < $tagcolls[$b->tagcollid]->sortorder ? -1 : 1;
 | 
        
           |  |  | 302 |             });
 | 
        
           |  |  | 303 |         }
 | 
        
           |  |  | 304 |         $rv = array();
 | 
        
           |  |  | 305 |         foreach ($tags as $id => $tag) {
 | 
        
           |  |  | 306 |             $rv[$id] = new static($tag);
 | 
        
           |  |  | 307 |         }
 | 
        
           |  |  | 308 |         return $rv;
 | 
        
           |  |  | 309 |     }
 | 
        
           |  |  | 310 |   | 
        
           |  |  | 311 |     /**
 | 
        
           |  |  | 312 |      * Returns the list of tag objects by tag collection id and the list of tag names
 | 
        
           |  |  | 313 |      *
 | 
        
           |  |  | 314 |      * @param    int   $tagcollid
 | 
        
           |  |  | 315 |      * @param    array $tags array of tags to look for
 | 
        
           |  |  | 316 |      * @param    string $returnfields list of DB fields to return, must contain 'id', 'name' and 'rawname'
 | 
        
           |  |  | 317 |      * @return   array tag-indexed array of objects. No value for a key means the tag wasn't found.
 | 
        
           |  |  | 318 |      */
 | 
        
           |  |  | 319 |     public static function get_by_name_bulk($tagcollid, $tags, $returnfields = 'id, name, rawname, tagcollid') {
 | 
        
           |  |  | 320 |         global $DB;
 | 
        
           |  |  | 321 |   | 
        
           |  |  | 322 |         if (empty($tags)) {
 | 
        
           |  |  | 323 |             return array();
 | 
        
           |  |  | 324 |         }
 | 
        
           |  |  | 325 |   | 
        
           |  |  | 326 |         $cleantags = self::normalize(self::normalize($tags, false)); // Format: rawname => normalised name.
 | 
        
           |  |  | 327 |   | 
        
           |  |  | 328 |         list($namesql, $params) = $DB->get_in_or_equal(array_values($cleantags));
 | 
        
           |  |  | 329 |         array_unshift($params, $tagcollid);
 | 
        
           |  |  | 330 |   | 
        
           |  |  | 331 |         $recordset = $DB->get_recordset_sql("SELECT $returnfields FROM {tag} WHERE tagcollid = ? AND name $namesql", $params);
 | 
        
           |  |  | 332 |   | 
        
           |  |  | 333 |         $result = array_fill_keys($cleantags, null);
 | 
        
           |  |  | 334 |         foreach ($recordset as $record) {
 | 
        
           |  |  | 335 |             $result[$record->name] = new static($record);
 | 
        
           |  |  | 336 |         }
 | 
        
           |  |  | 337 |         $recordset->close();
 | 
        
           |  |  | 338 |         return $result;
 | 
        
           |  |  | 339 |     }
 | 
        
           |  |  | 340 |   | 
        
           |  |  | 341 |   | 
        
           |  |  | 342 |     /**
 | 
        
           |  |  | 343 |      * Function that normalizes a list of tag names.
 | 
        
           |  |  | 344 |      *
 | 
        
           |  |  | 345 |      * @param   array        $rawtags array of tags
 | 
        
           |  |  | 346 |      * @param   bool         $tolowercase convert to lower case?
 | 
        
           |  |  | 347 |      * @return  array        lowercased normalized tags, indexed by the normalized tag, in the same order as the original array.
 | 
        
           |  |  | 348 |      *                       (Eg: 'Banana' => 'banana').
 | 
        
           |  |  | 349 |      */
 | 
        
           |  |  | 350 |     public static function normalize($rawtags, $tolowercase = true) {
 | 
        
           |  |  | 351 |         $result = array();
 | 
        
           |  |  | 352 |         foreach ($rawtags as $rawtag) {
 | 
        
           |  |  | 353 |             $rawtag = trim($rawtag);
 | 
        
           |  |  | 354 |             if (strval($rawtag) !== '') {
 | 
        
           |  |  | 355 |                 $clean = clean_param($rawtag, PARAM_TAG);
 | 
        
           |  |  | 356 |                 if ($tolowercase) {
 | 
        
           |  |  | 357 |                     $result[$rawtag] = core_text::strtolower($clean);
 | 
        
           |  |  | 358 |                 } else {
 | 
        
           |  |  | 359 |                     $result[$rawtag] = $clean;
 | 
        
           |  |  | 360 |                 }
 | 
        
           |  |  | 361 |             }
 | 
        
           |  |  | 362 |         }
 | 
        
           |  |  | 363 |         return $result;
 | 
        
           |  |  | 364 |     }
 | 
        
           |  |  | 365 |   | 
        
           |  |  | 366 |     /**
 | 
        
           |  |  | 367 |      * Retrieves tags and/or creates them if do not exist yet
 | 
        
           |  |  | 368 |      *
 | 
        
           |  |  | 369 |      * @param int $tagcollid
 | 
        
           |  |  | 370 |      * @param array $tags array of raw tag names, do not have to be normalised
 | 
        
           |  |  | 371 |      * @param bool $isstandard create as standard tag (default false)
 | 
        
           |  |  | 372 |      * @return core_tag_tag[] array of tag objects indexed with lowercase normalised tag name
 | 
        
           |  |  | 373 |      */
 | 
        
           |  |  | 374 |     public static function create_if_missing($tagcollid, $tags, $isstandard = false) {
 | 
        
           |  |  | 375 |         $cleantags = self::normalize(array_filter(self::normalize($tags, false))); // Array rawname => normalised name .
 | 
        
           |  |  | 376 |   | 
        
           |  |  | 377 |         $result = static::get_by_name_bulk($tagcollid, $tags, '*');
 | 
        
           |  |  | 378 |         $existing = array_filter($result);
 | 
        
           |  |  | 379 |         $missing = array_diff_key(array_flip($cleantags), $existing); // Array normalised name => rawname.
 | 
        
           |  |  | 380 |         if ($missing) {
 | 
        
           |  |  | 381 |             $newtags = static::add($tagcollid, array_values($missing), $isstandard);
 | 
        
           |  |  | 382 |             foreach ($newtags as $tag) {
 | 
        
           |  |  | 383 |                 $result[$tag->name] = $tag;
 | 
        
           |  |  | 384 |             }
 | 
        
           |  |  | 385 |         }
 | 
        
           |  |  | 386 |         return $result;
 | 
        
           |  |  | 387 |     }
 | 
        
           |  |  | 388 |   | 
        
           |  |  | 389 |     /**
 | 
        
           |  |  | 390 |      * Creates a URL to view a tag
 | 
        
           |  |  | 391 |      *
 | 
        
           |  |  | 392 |      * @param int $tagcollid
 | 
        
           |  |  | 393 |      * @param string $name
 | 
        
           |  |  | 394 |      * @param int $exclusivemode
 | 
        
           |  |  | 395 |      * @param int $fromctx context id where this tag cloud is displayed
 | 
        
           |  |  | 396 |      * @param int $ctx context id for tag view link
 | 
        
           |  |  | 397 |      * @param int $rec recursive argument for tag view link
 | 
        
           |  |  | 398 |      * @return \moodle_url
 | 
        
           |  |  | 399 |      */
 | 
        
           |  |  | 400 |     public static function make_url($tagcollid, $name, $exclusivemode = 0, $fromctx = 0, $ctx = 0, $rec = 1) {
 | 
        
           |  |  | 401 |         $coll = core_tag_collection::get_by_id($tagcollid);
 | 
        
           |  |  | 402 |         if (!empty($coll->customurl)) {
 | 
        
           |  |  | 403 |             $url = '/' . ltrim(trim($coll->customurl), '/');
 | 
        
           |  |  | 404 |         } else {
 | 
        
           |  |  | 405 |             $url = '/tag/index.php';
 | 
        
           |  |  | 406 |         }
 | 
        
           |  |  | 407 |         $params = array('tc' => $tagcollid, 'tag' => $name);
 | 
        
           |  |  | 408 |         if ($exclusivemode) {
 | 
        
           |  |  | 409 |             $params['excl'] = 1;
 | 
        
           |  |  | 410 |         }
 | 
        
           |  |  | 411 |         if ($fromctx) {
 | 
        
           |  |  | 412 |             $params['from'] = $fromctx;
 | 
        
           |  |  | 413 |         }
 | 
        
           |  |  | 414 |         if ($ctx) {
 | 
        
           |  |  | 415 |             $params['ctx'] = $ctx;
 | 
        
           |  |  | 416 |         }
 | 
        
           |  |  | 417 |         if (!$rec) {
 | 
        
           |  |  | 418 |             $params['rec'] = 0;
 | 
        
           |  |  | 419 |         }
 | 
        
           |  |  | 420 |         return new moodle_url($url, $params);
 | 
        
           |  |  | 421 |     }
 | 
        
           |  |  | 422 |   | 
        
           |  |  | 423 |     /**
 | 
        
           |  |  | 424 |      * Returns URL to view the tag
 | 
        
           |  |  | 425 |      *
 | 
        
           |  |  | 426 |      * @param int $exclusivemode
 | 
        
           |  |  | 427 |      * @param int $fromctx context id where this tag cloud is displayed
 | 
        
           |  |  | 428 |      * @param int $ctx context id for tag view link
 | 
        
           |  |  | 429 |      * @param int $rec recursive argument for tag view link
 | 
        
           |  |  | 430 |      * @return \moodle_url
 | 
        
           |  |  | 431 |      */
 | 
        
           |  |  | 432 |     public function get_view_url($exclusivemode = 0, $fromctx = 0, $ctx = 0, $rec = 1) {
 | 
        
           |  |  | 433 |         return static::make_url($this->record->tagcollid, $this->record->rawname,
 | 
        
           |  |  | 434 |             $exclusivemode, $fromctx, $ctx, $rec);
 | 
        
           |  |  | 435 |     }
 | 
        
           |  |  | 436 |   | 
        
           |  |  | 437 |     /**
 | 
        
           |  |  | 438 |      * Validates that the required fields were retrieved and retrieves them if missing
 | 
        
           |  |  | 439 |      *
 | 
        
           |  |  | 440 |      * @param array $list array of the fields that need to be validated
 | 
        
           |  |  | 441 |      * @param string $caller name of the function that requested it, for the debugging message
 | 
        
           |  |  | 442 |      */
 | 
        
           |  |  | 443 |     protected function ensure_fields_exist($list, $caller) {
 | 
        
           |  |  | 444 |         global $DB;
 | 
        
           |  |  | 445 |         $missing = array_diff($list, array_keys((array)$this->record));
 | 
        
           |  |  | 446 |         if ($missing) {
 | 
        
           |  |  | 447 |             debugging('core_tag_tag::' . $caller . '() must be called on fully retrieved tag object. Missing fields: '.
 | 
        
           |  |  | 448 |                     join(', ', $missing), DEBUG_DEVELOPER);
 | 
        
           |  |  | 449 |             $this->record = $DB->get_record('tag', array('id' => $this->record->id), '*', MUST_EXIST);
 | 
        
           |  |  | 450 |         }
 | 
        
           |  |  | 451 |     }
 | 
        
           |  |  | 452 |   | 
        
           |  |  | 453 |     /**
 | 
        
           |  |  | 454 |      * Deletes the tag instance given the record from tag_instance DB table
 | 
        
           |  |  | 455 |      *
 | 
        
           |  |  | 456 |      * @param stdClass $taginstance
 | 
        
           |  |  | 457 |      * @param bool $fullobject whether $taginstance contains all fields from DB table tag_instance
 | 
        
           |  |  | 458 |      *          (in this case it is safe to add a record snapshot to the event)
 | 
        
           |  |  | 459 |      * @return bool
 | 
        
           |  |  | 460 |      */
 | 
        
           |  |  | 461 |     protected function delete_instance_as_record($taginstance, $fullobject = false) {
 | 
        
           |  |  | 462 |         global $DB;
 | 
        
           |  |  | 463 |   | 
        
           |  |  | 464 |         $this->ensure_fields_exist(array('name', 'rawname', 'isstandard'), 'delete_instance_as_record');
 | 
        
           |  |  | 465 |   | 
        
           |  |  | 466 |         $DB->delete_records('tag_instance', array('id' => $taginstance->id));
 | 
        
           |  |  | 467 |   | 
        
           |  |  | 468 |         // We can not fire an event with 'null' as the contextid.
 | 
        
           |  |  | 469 |         if (is_null($taginstance->contextid)) {
 | 
        
           |  |  | 470 |             $taginstance->contextid = context_system::instance()->id;
 | 
        
           |  |  | 471 |         }
 | 
        
           |  |  | 472 |   | 
        
           |  |  | 473 |         // Trigger tag removed event.
 | 
        
           |  |  | 474 |         $taginstance->tagid = $this->id;
 | 
        
           |  |  | 475 |         \core\event\tag_removed::create_from_tag_instance($taginstance, $this->name, $this->rawname, $fullobject)->trigger();
 | 
        
           |  |  | 476 |   | 
        
           |  |  | 477 |         // If there are no other instances of the tag then consider deleting the tag as well.
 | 
        
           |  |  | 478 |         if (!$this->isstandard) {
 | 
        
           |  |  | 479 |             if (!$DB->record_exists('tag_instance', array('tagid' => $this->id))) {
 | 
        
           |  |  | 480 |                 self::delete_tags($this->id);
 | 
        
           |  |  | 481 |             }
 | 
        
           |  |  | 482 |         }
 | 
        
           |  |  | 483 |   | 
        
           |  |  | 484 |         return true;
 | 
        
           |  |  | 485 |     }
 | 
        
           |  |  | 486 |   | 
        
           |  |  | 487 |     /**
 | 
        
           |  |  | 488 |      * Delete one instance of a tag.  If the last instance was deleted, it will also delete the tag, unless it is standard.
 | 
        
           |  |  | 489 |      *
 | 
        
           |  |  | 490 |      * @param    string $component component responsible for tagging. For BC it can be empty but in this case the
 | 
        
           |  |  | 491 |      *                  query will be slow because DB index will not be used.
 | 
        
           |  |  | 492 |      * @param    string $itemtype the type of the record for which to remove the instance
 | 
        
           |  |  | 493 |      * @param    int    $itemid   the id of the record for which to remove the instance
 | 
        
           |  |  | 494 |      * @param    int    $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
 | 
        
           |  |  | 495 |      */
 | 
        
           |  |  | 496 |     protected function delete_instance($component, $itemtype, $itemid, $tiuserid = 0) {
 | 
        
           |  |  | 497 |         global $DB;
 | 
        
           |  |  | 498 |         $params = array('tagid' => $this->id,
 | 
        
           |  |  | 499 |                 'itemtype' => $itemtype, 'itemid' => $itemid);
 | 
        
           |  |  | 500 |         if ($tiuserid) {
 | 
        
           |  |  | 501 |             $params['tiuserid'] = $tiuserid;
 | 
        
           |  |  | 502 |         }
 | 
        
           |  |  | 503 |         if ($component) {
 | 
        
           |  |  | 504 |             $params['component'] = $component;
 | 
        
           |  |  | 505 |         }
 | 
        
           |  |  | 506 |   | 
        
           |  |  | 507 |         $taginstance = $DB->get_record('tag_instance', $params);
 | 
        
           |  |  | 508 |         if (!$taginstance) {
 | 
        
           |  |  | 509 |             return;
 | 
        
           |  |  | 510 |         }
 | 
        
           |  |  | 511 |         $this->delete_instance_as_record($taginstance, true);
 | 
        
           |  |  | 512 |     }
 | 
        
           |  |  | 513 |   | 
        
           |  |  | 514 |     /**
 | 
        
           |  |  | 515 |      * Bulk delete all tag instances.
 | 
        
           |  |  | 516 |      *
 | 
        
           |  |  | 517 |      * @param stdClass[] $taginstances A list of tag_instance records to delete. Each
 | 
        
           |  |  | 518 |      *                                 record must also contain the name and rawname
 | 
        
           |  |  | 519 |      *                                 columns from the related tag record.
 | 
        
           |  |  | 520 |      */
 | 
        
           |  |  | 521 |     public static function delete_instances_as_record(array $taginstances) {
 | 
        
           |  |  | 522 |         global $DB;
 | 
        
           |  |  | 523 |   | 
        
           |  |  | 524 |         if (empty($taginstances)) {
 | 
        
           |  |  | 525 |             return;
 | 
        
           |  |  | 526 |         }
 | 
        
           |  |  | 527 |   | 
        
           |  |  | 528 |         $taginstanceids = array_map(function($taginstance) {
 | 
        
           |  |  | 529 |             return $taginstance->id;
 | 
        
           |  |  | 530 |         }, $taginstances);
 | 
        
           |  |  | 531 |         // Now remove all the tag instances.
 | 
        
           |  |  | 532 |         $DB->delete_records_list('tag_instance', 'id', $taginstanceids);
 | 
        
           |  |  | 533 |         // Save the system context in case the 'contextid' column in the 'tag_instance' table is null.
 | 
        
           |  |  | 534 |         $syscontextid = context_system::instance()->id;
 | 
        
           |  |  | 535 |         // Loop through the tag instances and fire an 'tag_removed' event.
 | 
        
           |  |  | 536 |         foreach ($taginstances as $taginstance) {
 | 
        
           |  |  | 537 |             // We can not fire an event with 'null' as the contextid.
 | 
        
           |  |  | 538 |             if (is_null($taginstance->contextid)) {
 | 
        
           |  |  | 539 |                 $taginstance->contextid = $syscontextid;
 | 
        
           |  |  | 540 |             }
 | 
        
           |  |  | 541 |   | 
        
           |  |  | 542 |             // Trigger tag removed event.
 | 
        
           |  |  | 543 |             \core\event\tag_removed::create_from_tag_instance($taginstance, $taginstance->name,
 | 
        
           |  |  | 544 |                     $taginstance->rawname, true)->trigger();
 | 
        
           |  |  | 545 |         }
 | 
        
           |  |  | 546 |     }
 | 
        
           |  |  | 547 |   | 
        
           |  |  | 548 |     /**
 | 
        
           |  |  | 549 |      * Bulk delete all tag instances by tag id.
 | 
        
           |  |  | 550 |      *
 | 
        
           |  |  | 551 |      * @param int[] $taginstanceids List of tag instance ids to be deleted.
 | 
        
           |  |  | 552 |      */
 | 
        
           |  |  | 553 |     public static function delete_instances_by_id(array $taginstanceids) {
 | 
        
           |  |  | 554 |         global $DB;
 | 
        
           |  |  | 555 |   | 
        
           |  |  | 556 |         if (empty($taginstanceids)) {
 | 
        
           |  |  | 557 |             return;
 | 
        
           |  |  | 558 |         }
 | 
        
           |  |  | 559 |   | 
        
           |  |  | 560 |         list($idsql, $params) = $DB->get_in_or_equal($taginstanceids);
 | 
        
           |  |  | 561 |         $sql = "SELECT ti.*, t.name, t.rawname, t.isstandard
 | 
        
           |  |  | 562 |                   FROM {tag_instance} ti
 | 
        
           |  |  | 563 |                   JOIN {tag} t
 | 
        
           |  |  | 564 |                     ON ti.tagid = t.id
 | 
        
           |  |  | 565 |                  WHERE ti.id {$idsql}";
 | 
        
           |  |  | 566 |   | 
        
           |  |  | 567 |         if ($taginstances = $DB->get_records_sql($sql, $params)) {
 | 
        
           |  |  | 568 |             static::delete_instances_as_record($taginstances);
 | 
        
           |  |  | 569 |         }
 | 
        
           |  |  | 570 |     }
 | 
        
           |  |  | 571 |   | 
        
           |  |  | 572 |     /**
 | 
        
           |  |  | 573 |      * Bulk delete all tag instances for a component or tag area
 | 
        
           |  |  | 574 |      *
 | 
        
           |  |  | 575 |      * @param string $component
 | 
        
           |  |  | 576 |      * @param string $itemtype (optional)
 | 
        
           |  |  | 577 |      * @param int $contextid (optional)
 | 
        
           |  |  | 578 |      */
 | 
        
           |  |  | 579 |     public static function delete_instances($component, $itemtype = null, $contextid = null) {
 | 
        
           |  |  | 580 |         global $DB;
 | 
        
           |  |  | 581 |   | 
        
           |  |  | 582 |         $sql = "SELECT ti.*, t.name, t.rawname, t.isstandard
 | 
        
           |  |  | 583 |                   FROM {tag_instance} ti
 | 
        
           |  |  | 584 |                   JOIN {tag} t
 | 
        
           |  |  | 585 |                     ON ti.tagid = t.id
 | 
        
           |  |  | 586 |                  WHERE ti.component = :component";
 | 
        
           |  |  | 587 |         $params = array('component' => $component);
 | 
        
           |  |  | 588 |         if (!is_null($contextid)) {
 | 
        
           |  |  | 589 |             $sql .= " AND ti.contextid = :contextid";
 | 
        
           |  |  | 590 |             $params['contextid'] = $contextid;
 | 
        
           |  |  | 591 |         }
 | 
        
           |  |  | 592 |         if (!is_null($itemtype)) {
 | 
        
           |  |  | 593 |             $sql .= " AND ti.itemtype = :itemtype";
 | 
        
           |  |  | 594 |             $params['itemtype'] = $itemtype;
 | 
        
           |  |  | 595 |         }
 | 
        
           |  |  | 596 |   | 
        
           |  |  | 597 |         if ($taginstances = $DB->get_records_sql($sql, $params)) {
 | 
        
           |  |  | 598 |             static::delete_instances_as_record($taginstances);
 | 
        
           |  |  | 599 |         }
 | 
        
           |  |  | 600 |     }
 | 
        
           |  |  | 601 |   | 
        
           |  |  | 602 |     /**
 | 
        
           |  |  | 603 |      * Adds a tag instance
 | 
        
           |  |  | 604 |      *
 | 
        
           |  |  | 605 |      * @param string $component
 | 
        
           |  |  | 606 |      * @param string $itemtype
 | 
        
           |  |  | 607 |      * @param string $itemid
 | 
        
           |  |  | 608 |      * @param context $context
 | 
        
           |  |  | 609 |      * @param int $ordering
 | 
        
           |  |  | 610 |      * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
 | 
        
           |  |  | 611 |      * @return int id of tag_instance
 | 
        
           |  |  | 612 |      */
 | 
        
           |  |  | 613 |     protected function add_instance($component, $itemtype, $itemid, context $context, $ordering, $tiuserid = 0) {
 | 
        
           |  |  | 614 |         global $DB;
 | 
        
           |  |  | 615 |         $this->ensure_fields_exist(array('name', 'rawname'), 'add_instance');
 | 
        
           |  |  | 616 |   | 
        
           |  |  | 617 |         $taginstance = new stdClass;
 | 
        
           |  |  | 618 |         $taginstance->tagid        = $this->id;
 | 
        
           |  |  | 619 |         $taginstance->component    = $component ? $component : '';
 | 
        
           |  |  | 620 |         $taginstance->itemid       = $itemid;
 | 
        
           |  |  | 621 |         $taginstance->itemtype     = $itemtype;
 | 
        
           |  |  | 622 |         $taginstance->contextid    = $context->id;
 | 
        
           |  |  | 623 |         $taginstance->ordering     = $ordering;
 | 
        
           |  |  | 624 |         $taginstance->timecreated  = time();
 | 
        
           |  |  | 625 |         $taginstance->timemodified = $taginstance->timecreated;
 | 
        
           |  |  | 626 |         $taginstance->tiuserid     = $tiuserid;
 | 
        
           |  |  | 627 |   | 
        
           |  |  | 628 |         $taginstance->id = $DB->insert_record('tag_instance', $taginstance);
 | 
        
           |  |  | 629 |   | 
        
           |  |  | 630 |         // Trigger tag added event.
 | 
        
           |  |  | 631 |         \core\event\tag_added::create_from_tag_instance($taginstance, $this->name, $this->rawname, true)->trigger();
 | 
        
           |  |  | 632 |   | 
        
           |  |  | 633 |         return $taginstance->id;
 | 
        
           |  |  | 634 |     }
 | 
        
           |  |  | 635 |   | 
        
           |  |  | 636 |     /**
 | 
        
           |  |  | 637 |      * Updates the ordering on tag instance
 | 
        
           |  |  | 638 |      *
 | 
        
           |  |  | 639 |      * @param int $instanceid
 | 
        
           |  |  | 640 |      * @param int $ordering
 | 
        
           |  |  | 641 |      */
 | 
        
           |  |  | 642 |     protected function update_instance_ordering($instanceid, $ordering) {
 | 
        
           |  |  | 643 |         global $DB;
 | 
        
           |  |  | 644 |         $data = new stdClass();
 | 
        
           |  |  | 645 |         $data->id = $instanceid;
 | 
        
           |  |  | 646 |         $data->ordering = $ordering;
 | 
        
           |  |  | 647 |         $data->timemodified = time();
 | 
        
           |  |  | 648 |   | 
        
           |  |  | 649 |         $DB->update_record('tag_instance', $data);
 | 
        
           |  |  | 650 |     }
 | 
        
           |  |  | 651 |   | 
        
           |  |  | 652 |     /**
 | 
        
           |  |  | 653 |      * Get the array of core_tag_tag objects associated with a list of items.
 | 
        
           |  |  | 654 |      *
 | 
        
           |  |  | 655 |      * Use {@link core_tag_tag::get_item_tags_array()} if you wish to get the same data as simple array.
 | 
        
           |  |  | 656 |      *
 | 
        
           |  |  | 657 |      * @param string $component component responsible for tagging. For BC it can be empty but in this case the
 | 
        
           |  |  | 658 |      *               query will be slow because DB index will not be used.
 | 
        
           |  |  | 659 |      * @param string $itemtype type of the tagged item
 | 
        
           |  |  | 660 |      * @param int[] $itemids
 | 
        
           |  |  | 661 |      * @param int $standardonly wether to return only standard tags or any
 | 
        
           |  |  | 662 |      * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging
 | 
        
           |  |  | 663 |      * @return core_tag_tag[][] first array key is itemid. For each itemid,
 | 
        
           |  |  | 664 |      *      an array tagid => tag object with additional fields taginstanceid, taginstancecontextid and ordering
 | 
        
           |  |  | 665 |      */
 | 
        
           |  |  | 666 |     public static function get_items_tags($component, $itemtype, $itemids, $standardonly = self::BOTH_STANDARD_AND_NOT,
 | 
        
           |  |  | 667 |             $tiuserid = 0) {
 | 
        
           |  |  | 668 |         global $DB;
 | 
        
           |  |  | 669 |   | 
        
           |  |  | 670 |         if (static::is_enabled($component, $itemtype) === false) {
 | 
        
           |  |  | 671 |             // Tagging area is properly defined but not enabled - return empty array.
 | 
        
           |  |  | 672 |             return array();
 | 
        
           |  |  | 673 |         }
 | 
        
           |  |  | 674 |   | 
        
           |  |  | 675 |         if (empty($itemids)) {
 | 
        
           |  |  | 676 |             return array();
 | 
        
           |  |  | 677 |         }
 | 
        
           |  |  | 678 |   | 
        
           |  |  | 679 |         $standardonly = (int)$standardonly; // In case somebody passed bool.
 | 
        
           |  |  | 680 |   | 
        
           |  |  | 681 |         list($idsql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
 | 
        
           |  |  | 682 |         // Note: if the fields in this query are changed, you need to do the same changes in core_tag_tag::get_correlated_tags().
 | 
        
           |  |  | 683 |         $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.isstandard, tg.name, tg.rawname, tg.flag,
 | 
        
           |  |  | 684 |                     tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid, ti.itemid
 | 
        
           |  |  | 685 |                   FROM {tag_instance} ti
 | 
        
           |  |  | 686 |                   JOIN {tag} tg ON tg.id = ti.tagid
 | 
        
           |  |  | 687 |                   WHERE ti.itemtype = :itemtype AND ti.itemid $idsql ".
 | 
        
           |  |  | 688 |                 ($component ? "AND ti.component = :component " : "").
 | 
        
           |  |  | 689 |                 ($tiuserid ? "AND ti.tiuserid = :tiuserid " : "").
 | 
        
           |  |  | 690 |                 (($standardonly == self::STANDARD_ONLY) ? "AND tg.isstandard = 1 " : "").
 | 
        
           |  |  | 691 |                 (($standardonly == self::NOT_STANDARD_ONLY) ? "AND tg.isstandard = 0 " : "").
 | 
        
           |  |  | 692 |                "ORDER BY ti.ordering ASC, ti.id";
 | 
        
           |  |  | 693 |   | 
        
           |  |  | 694 |         $params['itemtype'] = $itemtype;
 | 
        
           |  |  | 695 |         $params['component'] = $component;
 | 
        
           |  |  | 696 |         $params['tiuserid'] = $tiuserid;
 | 
        
           |  |  | 697 |   | 
        
           |  |  | 698 |         $records = $DB->get_records_sql($sql, $params);
 | 
        
           |  |  | 699 |         $result = array();
 | 
        
           |  |  | 700 |         foreach ($itemids as $itemid) {
 | 
        
           |  |  | 701 |             $result[$itemid] = [];
 | 
        
           |  |  | 702 |         }
 | 
        
           |  |  | 703 |         foreach ($records as $id => $record) {
 | 
        
           |  |  | 704 |             $result[$record->itemid][$id] = new static($record);
 | 
        
           |  |  | 705 |         }
 | 
        
           |  |  | 706 |         return $result;
 | 
        
           |  |  | 707 |     }
 | 
        
           |  |  | 708 |   | 
        
           |  |  | 709 |     /**
 | 
        
           |  |  | 710 |      * Get the array of core_tag_tag objects associated with an item (instances).
 | 
        
           |  |  | 711 |      *
 | 
        
           |  |  | 712 |      * Use {@link core_tag_tag::get_item_tags_array()} if you wish to get the same data as simple array.
 | 
        
           |  |  | 713 |      *
 | 
        
           |  |  | 714 |      * @param string $component component responsible for tagging. For BC it can be empty but in this case the
 | 
        
           |  |  | 715 |      *               query will be slow because DB index will not be used.
 | 
        
           |  |  | 716 |      * @param string $itemtype type of the tagged item
 | 
        
           |  |  | 717 |      * @param int $itemid
 | 
        
           |  |  | 718 |      * @param int $standardonly wether to return only standard tags or any
 | 
        
           |  |  | 719 |      * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging
 | 
        
           |  |  | 720 |      * @return core_tag_tag[] each object contains additional fields taginstanceid, taginstancecontextid and ordering
 | 
        
           |  |  | 721 |      */
 | 
        
           |  |  | 722 |     public static function get_item_tags($component, $itemtype, $itemid, $standardonly = self::BOTH_STANDARD_AND_NOT,
 | 
        
           |  |  | 723 |             $tiuserid = 0) {
 | 
        
           |  |  | 724 |         $tagobjects = static::get_items_tags($component, $itemtype, [$itemid], $standardonly, $tiuserid);
 | 
        
           |  |  | 725 |         return empty($tagobjects) ? [] : $tagobjects[$itemid];
 | 
        
           |  |  | 726 |     }
 | 
        
           |  |  | 727 |   | 
        
           |  |  | 728 |     /**
 | 
        
           |  |  | 729 |      * Returns the list of display names of the tags that are associated with an item
 | 
        
           |  |  | 730 |      *
 | 
        
           |  |  | 731 |      * This method is usually used to prefill the form data for the 'tags' form element
 | 
        
           |  |  | 732 |      *
 | 
        
           |  |  | 733 |      * @param string $component component responsible for tagging. For BC it can be empty but in this case the
 | 
        
           |  |  | 734 |      *               query will be slow because DB index will not be used.
 | 
        
           |  |  | 735 |      * @param string $itemtype type of the tagged item
 | 
        
           |  |  | 736 |      * @param int $itemid
 | 
        
           |  |  | 737 |      * @param int $standardonly wether to return only standard tags or any
 | 
        
           |  |  | 738 |      * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging
 | 
        
           |  |  | 739 |      * @param bool $ashtml (default true) if true will return htmlspecialchars encoded tag names
 | 
        
           |  |  | 740 |      * @return string[] array of tags display names
 | 
        
           |  |  | 741 |      */
 | 
        
           |  |  | 742 |     public static function get_item_tags_array($component, $itemtype, $itemid, $standardonly = self::BOTH_STANDARD_AND_NOT,
 | 
        
           |  |  | 743 |             $tiuserid = 0, $ashtml = true) {
 | 
        
           |  |  | 744 |         $tags = array();
 | 
        
           |  |  | 745 |         foreach (static::get_item_tags($component, $itemtype, $itemid, $standardonly, $tiuserid) as $tag) {
 | 
        
           |  |  | 746 |             $tags[$tag->id] = $tag->get_display_name($ashtml);
 | 
        
           |  |  | 747 |         }
 | 
        
           |  |  | 748 |         return $tags;
 | 
        
           |  |  | 749 |     }
 | 
        
           |  |  | 750 |   | 
        
           |  |  | 751 |     /**
 | 
        
           |  |  | 752 |      * Sets the list of tag instances for one item (table record).
 | 
        
           |  |  | 753 |      *
 | 
        
           |  |  | 754 |      * Extra exsisting instances are removed, new ones are added. New tags are created if needed.
 | 
        
           |  |  | 755 |      *
 | 
        
           |  |  | 756 |      * This method can not be used for setting tags relations, please use set_related_tags()
 | 
        
           |  |  | 757 |      *
 | 
        
           |  |  | 758 |      * @param string $component component responsible for tagging
 | 
        
           |  |  | 759 |      * @param string $itemtype type of the tagged item
 | 
        
           |  |  | 760 |      * @param int $itemid
 | 
        
           |  |  | 761 |      * @param context $context
 | 
        
           |  |  | 762 |      * @param array $tagnames
 | 
        
           |  |  | 763 |      * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
 | 
        
           |  |  | 764 |      */
 | 
        
           |  |  | 765 |     public static function set_item_tags($component, $itemtype, $itemid, context $context, $tagnames, $tiuserid = 0) {
 | 
        
           |  |  | 766 |         if ($itemtype === 'tag') {
 | 
        
           |  |  | 767 |             if ($tiuserid) {
 | 
        
           |  |  | 768 |                 throw new coding_exception('Related tags can not have tag instance userid');
 | 
        
           |  |  | 769 |             }
 | 
        
           |  |  | 770 |             debugging('You can not use set_item_tags() for tagging a tag, please use set_related_tags()', DEBUG_DEVELOPER);
 | 
        
           |  |  | 771 |             static::get($itemid, '*', MUST_EXIST)->set_related_tags($tagnames);
 | 
        
           |  |  | 772 |             return;
 | 
        
           |  |  | 773 |         }
 | 
        
           |  |  | 774 |   | 
        
           |  |  | 775 |         if ($tagnames !== null && static::is_enabled($component, $itemtype) === false) {
 | 
        
           |  |  | 776 |             // Tagging area is properly defined but not enabled - do nothing.
 | 
        
           |  |  | 777 |             // Unless we are deleting the item tags ($tagnames === null), in which case proceed with deleting.
 | 
        
           |  |  | 778 |             return;
 | 
        
           |  |  | 779 |         }
 | 
        
           |  |  | 780 |   | 
        
           |  |  | 781 |         // Apply clean_param() to all tags.
 | 
        
           |  |  | 782 |         if ($tagnames) {
 | 
        
           |  |  | 783 |             $tagcollid = core_tag_area::get_collection($component, $itemtype);
 | 
        
           |  |  | 784 |             $tagobjects = static::create_if_missing($tagcollid, $tagnames);
 | 
        
           |  |  | 785 |         } else {
 | 
        
           |  |  | 786 |             $tagobjects = array();
 | 
        
           |  |  | 787 |         }
 | 
        
           |  |  | 788 |   | 
        
           |  |  | 789 |         $allowmultiplecontexts = core_tag_area::allows_tagging_in_multiple_contexts($component, $itemtype);
 | 
        
           |  |  | 790 |         $currenttags = static::get_item_tags($component, $itemtype, $itemid, self::BOTH_STANDARD_AND_NOT, $tiuserid);
 | 
        
           |  |  | 791 |         $taginstanceidstomovecontext = [];
 | 
        
           |  |  | 792 |   | 
        
           |  |  | 793 |         // For data coherence reasons, it's better to remove deleted tags
 | 
        
           |  |  | 794 |         // before adding new data: ordering could be duplicated.
 | 
        
           |  |  | 795 |         foreach ($currenttags as $currenttag) {
 | 
        
           |  |  | 796 |             $hasbeenrequested = array_key_exists($currenttag->name, $tagobjects);
 | 
        
           |  |  | 797 |             $issamecontext = $currenttag->taginstancecontextid == $context->id;
 | 
        
           |  |  | 798 |   | 
        
           |  |  | 799 |             if ($allowmultiplecontexts) {
 | 
        
           |  |  | 800 |                 // If the tag area allows multiple contexts then we should only be
 | 
        
           |  |  | 801 |                 // managing tags in the given $context. All other tags can be ignored.
 | 
        
           |  |  | 802 |                 $shoulddelete = $issamecontext && !$hasbeenrequested;
 | 
        
           |  |  | 803 |             } else {
 | 
        
           |  |  | 804 |                 // If the tag area only allows tag instances in a single context then
 | 
        
           |  |  | 805 |                 // all tags that aren't in the requested tags should be deleted, regardless
 | 
        
           |  |  | 806 |                 // of their context, if they are not part of the new set of tags.
 | 
        
           |  |  | 807 |                 $shoulddelete = !$hasbeenrequested;
 | 
        
           |  |  | 808 |                 // If the tag instance isn't in the correct context (legacy data)
 | 
        
           |  |  | 809 |                 // then we should take this opportunity to update it with the correct
 | 
        
           |  |  | 810 |                 // context id.
 | 
        
           |  |  | 811 |                 if (!$shoulddelete && !$issamecontext) {
 | 
        
           |  |  | 812 |                     $currenttag->taginstancecontextid = $context->id;
 | 
        
           |  |  | 813 |                     $taginstanceidstomovecontext[] = $currenttag->taginstanceid;
 | 
        
           |  |  | 814 |                 }
 | 
        
           |  |  | 815 |             }
 | 
        
           |  |  | 816 |   | 
        
           |  |  | 817 |             if ($shoulddelete) {
 | 
        
           |  |  | 818 |                 $taginstance = (object)array('id' => $currenttag->taginstanceid,
 | 
        
           |  |  | 819 |                     'itemtype' => $itemtype, 'itemid' => $itemid,
 | 
        
           |  |  | 820 |                     'contextid' => $currenttag->taginstancecontextid, 'tiuserid' => $tiuserid);
 | 
        
           |  |  | 821 |                 $currenttag->delete_instance_as_record($taginstance, false);
 | 
        
           |  |  | 822 |             }
 | 
        
           |  |  | 823 |         }
 | 
        
           |  |  | 824 |   | 
        
           |  |  | 825 |         if (!empty($taginstanceidstomovecontext)) {
 | 
        
           |  |  | 826 |             static::change_instances_context($taginstanceidstomovecontext, $context);
 | 
        
           |  |  | 827 |         }
 | 
        
           |  |  | 828 |   | 
        
           |  |  | 829 |         $ordering = -1;
 | 
        
           |  |  | 830 |         foreach ($tagobjects as $name => $tag) {
 | 
        
           |  |  | 831 |             $ordering++;
 | 
        
           |  |  | 832 |             foreach ($currenttags as $currenttag) {
 | 
        
           |  |  | 833 |                 $namesmatch = strval($currenttag->name) === strval($name);
 | 
        
           |  |  | 834 |   | 
        
           |  |  | 835 |                 if ($allowmultiplecontexts) {
 | 
        
           |  |  | 836 |                     // If the tag area allows multiple contexts then we should only
 | 
        
           |  |  | 837 |                     // skip adding a new instance if the existing one is in the correct
 | 
        
           |  |  | 838 |                     // context.
 | 
        
           |  |  | 839 |                     $contextsmatch = $currenttag->taginstancecontextid == $context->id;
 | 
        
           |  |  | 840 |                     $shouldskipinstance = $namesmatch && $contextsmatch;
 | 
        
           |  |  | 841 |                 } else {
 | 
        
           |  |  | 842 |                     // The existing behaviour for single context tag areas is to
 | 
        
           |  |  | 843 |                     // skip adding a new instance regardless of whether the existing
 | 
        
           |  |  | 844 |                     // instance is in the same context as the provided $context.
 | 
        
           |  |  | 845 |                     $shouldskipinstance = $namesmatch;
 | 
        
           |  |  | 846 |                 }
 | 
        
           |  |  | 847 |   | 
        
           |  |  | 848 |                 if ($shouldskipinstance) {
 | 
        
           |  |  | 849 |                     if ($currenttag->ordering != $ordering) {
 | 
        
           |  |  | 850 |                         $currenttag->update_instance_ordering($currenttag->taginstanceid, $ordering);
 | 
        
           |  |  | 851 |                     }
 | 
        
           |  |  | 852 |                     continue 2;
 | 
        
           |  |  | 853 |                 }
 | 
        
           |  |  | 854 |             }
 | 
        
           |  |  | 855 |             $tag->add_instance($component, $itemtype, $itemid, $context, $ordering, $tiuserid);
 | 
        
           |  |  | 856 |         }
 | 
        
           |  |  | 857 |     }
 | 
        
           |  |  | 858 |   | 
        
           |  |  | 859 |     /**
 | 
        
           |  |  | 860 |      * Removes all tags from an item.
 | 
        
           |  |  | 861 |      *
 | 
        
           |  |  | 862 |      * All tags will be removed even if tagging is disabled in this area. This is
 | 
        
           |  |  | 863 |      * usually called when the item itself has been deleted.
 | 
        
           |  |  | 864 |      *
 | 
        
           |  |  | 865 |      * @param string $component component responsible for tagging
 | 
        
           |  |  | 866 |      * @param string $itemtype type of the tagged item
 | 
        
           |  |  | 867 |      * @param int $itemid
 | 
        
           |  |  | 868 |      * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
 | 
        
           |  |  | 869 |      */
 | 
        
           |  |  | 870 |     public static function remove_all_item_tags($component, $itemtype, $itemid, $tiuserid = 0) {
 | 
        
           |  |  | 871 |         $context = context_system::instance(); // Context will not be used.
 | 
        
           |  |  | 872 |         static::set_item_tags($component, $itemtype, $itemid, $context, null, $tiuserid);
 | 
        
           |  |  | 873 |     }
 | 
        
           |  |  | 874 |   | 
        
           |  |  | 875 |     /**
 | 
        
           |  |  | 876 |      * Adds a tag to an item, without overwriting the current tags.
 | 
        
           |  |  | 877 |      *
 | 
        
           |  |  | 878 |      * If the tag has already been added to the record, no changes are made.
 | 
        
           |  |  | 879 |      *
 | 
        
           |  |  | 880 |      * @param string $component the component that was tagged
 | 
        
           |  |  | 881 |      * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)
 | 
        
           |  |  | 882 |      * @param int $itemid the id of the record to tag
 | 
        
           |  |  | 883 |      * @param context $context the context of where this tag was assigned
 | 
        
           |  |  | 884 |      * @param string $tagname the tag to add
 | 
        
           |  |  | 885 |      * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
 | 
        
           |  |  | 886 |      * @return int id of tag_instance that was either created or already existed or null if tagging is not enabled
 | 
        
           |  |  | 887 |      */
 | 
        
           |  |  | 888 |     public static function add_item_tag($component, $itemtype, $itemid, context $context, $tagname, $tiuserid = 0) {
 | 
        
           |  |  | 889 |         global $DB;
 | 
        
           |  |  | 890 |   | 
        
           |  |  | 891 |         if (static::is_enabled($component, $itemtype) === false) {
 | 
        
           |  |  | 892 |             // Tagging area is properly defined but not enabled - do nothing.
 | 
        
           |  |  | 893 |             return null;
 | 
        
           |  |  | 894 |         }
 | 
        
           |  |  | 895 |   | 
        
           |  |  | 896 |         $rawname = clean_param($tagname, PARAM_TAG);
 | 
        
           |  |  | 897 |         $normalisedname = core_text::strtolower($rawname);
 | 
        
           |  |  | 898 |         $tagcollid = core_tag_area::get_collection($component, $itemtype);
 | 
        
           |  |  | 899 |   | 
        
           |  |  | 900 |         $usersql = $tiuserid ? " AND ti.tiuserid = :tiuserid " : "";
 | 
        
           |  |  | 901 |         $sql = 'SELECT t.*, ti.id AS taginstanceid
 | 
        
           |  |  | 902 |                 FROM {tag} t
 | 
        
           |  |  | 903 |                 LEFT JOIN {tag_instance} ti ON ti.tagid = t.id AND ti.itemtype = :itemtype '.
 | 
        
           |  |  | 904 |                 $usersql .
 | 
        
           |  |  | 905 |                 'AND ti.itemid = :itemid AND ti.component = :component
 | 
        
           |  |  | 906 |                 WHERE t.name = :name AND t.tagcollid = :tagcollid';
 | 
        
           |  |  | 907 |         $params = array('name' => $normalisedname, 'tagcollid' => $tagcollid, 'itemtype' => $itemtype,
 | 
        
           |  |  | 908 |             'itemid' => $itemid, 'component' => $component, 'tiuserid' => $tiuserid);
 | 
        
           |  |  | 909 |         $record = $DB->get_record_sql($sql, $params);
 | 
        
           |  |  | 910 |         if ($record) {
 | 
        
           |  |  | 911 |             if ($record->taginstanceid) {
 | 
        
           |  |  | 912 |                 // Tag was already added to the item, nothing to do here.
 | 
        
           |  |  | 913 |                 return $record->taginstanceid;
 | 
        
           |  |  | 914 |             }
 | 
        
           |  |  | 915 |             $tag = new static($record);
 | 
        
           |  |  | 916 |         } else {
 | 
        
           |  |  | 917 |             // The tag does not exist yet, create it.
 | 
        
           |  |  | 918 |             $tags = static::add($tagcollid, array($tagname));
 | 
        
           |  |  | 919 |             $tag = reset($tags);
 | 
        
           |  |  | 920 |         }
 | 
        
           |  |  | 921 |   | 
        
           |  |  | 922 |         $ordering = $DB->get_field_sql('SELECT MAX(ordering) FROM {tag_instance} ti
 | 
        
           |  |  | 923 |                 WHERE ti.itemtype = :itemtype AND ti.itemid = :itemid AND
 | 
        
           |  |  | 924 |                 ti.component = :component' . $usersql, $params);
 | 
        
           |  |  | 925 |   | 
        
           |  |  | 926 |         return $tag->add_instance($component, $itemtype, $itemid, $context, $ordering + 1, $tiuserid);
 | 
        
           |  |  | 927 |     }
 | 
        
           |  |  | 928 |   | 
        
           |  |  | 929 |     /**
 | 
        
           |  |  | 930 |      * Removes the tag from an item without changing the other tags
 | 
        
           |  |  | 931 |      *
 | 
        
           |  |  | 932 |      * @param string $component the component that was tagged
 | 
        
           |  |  | 933 |      * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)
 | 
        
           |  |  | 934 |      * @param int $itemid the id of the record to tag
 | 
        
           |  |  | 935 |      * @param string $tagname the tag to remove
 | 
        
           |  |  | 936 |      * @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)
 | 
        
           |  |  | 937 |      */
 | 
        
           |  |  | 938 |     public static function remove_item_tag($component, $itemtype, $itemid, $tagname, $tiuserid = 0) {
 | 
        
           |  |  | 939 |         global $DB;
 | 
        
           |  |  | 940 |   | 
        
           |  |  | 941 |         if (static::is_enabled($component, $itemtype) === false) {
 | 
        
           |  |  | 942 |             // Tagging area is properly defined but not enabled - do nothing.
 | 
        
           |  |  | 943 |             return array();
 | 
        
           |  |  | 944 |         }
 | 
        
           |  |  | 945 |   | 
        
           |  |  | 946 |         $rawname = clean_param($tagname, PARAM_TAG);
 | 
        
           |  |  | 947 |         $normalisedname = core_text::strtolower($rawname);
 | 
        
           |  |  | 948 |   | 
        
           |  |  | 949 |         $usersql = $tiuserid ? " AND tiuserid = :tiuserid " : "";
 | 
        
           |  |  | 950 |         $componentsql = $component ? " AND ti.component = :component " : "";
 | 
        
           |  |  | 951 |         $sql = 'SELECT t.*, ti.id AS taginstanceid, ti.contextid AS taginstancecontextid, ti.ordering
 | 
        
           |  |  | 952 |                 FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id ' . $usersql . '
 | 
        
           |  |  | 953 |                 WHERE t.name = :name AND ti.itemtype = :itemtype
 | 
        
           |  |  | 954 |                 AND ti.itemid = :itemid ' . $componentsql;
 | 
        
           |  |  | 955 |         $params = array('name' => $normalisedname,
 | 
        
           |  |  | 956 |             'itemtype' => $itemtype, 'itemid' => $itemid, 'component' => $component,
 | 
        
           |  |  | 957 |             'tiuserid' => $tiuserid);
 | 
        
           |  |  | 958 |         if ($record = $DB->get_record_sql($sql, $params)) {
 | 
        
           |  |  | 959 |             $taginstance = (object)array('id' => $record->taginstanceid,
 | 
        
           |  |  | 960 |                 'itemtype' => $itemtype, 'itemid' => $itemid,
 | 
        
           |  |  | 961 |                 'contextid' => $record->taginstancecontextid, 'tiuserid' => $tiuserid);
 | 
        
           |  |  | 962 |             $tag = new static($record);
 | 
        
           |  |  | 963 |             $tag->delete_instance_as_record($taginstance, false);
 | 
        
           |  |  | 964 |             $componentsql = $component ? " AND component = :component " : "";
 | 
        
           |  |  | 965 |             $sql = "UPDATE {tag_instance} SET ordering = ordering - 1
 | 
        
           |  |  | 966 |                     WHERE itemtype = :itemtype
 | 
        
           |  |  | 967 |                 AND itemid = :itemid $componentsql $usersql
 | 
        
           |  |  | 968 |                 AND ordering > :ordering";
 | 
        
           |  |  | 969 |             $params['ordering'] = $record->ordering;
 | 
        
           |  |  | 970 |             $DB->execute($sql, $params);
 | 
        
           |  |  | 971 |         }
 | 
        
           |  |  | 972 |     }
 | 
        
           |  |  | 973 |   | 
        
           |  |  | 974 |     /**
 | 
        
           |  |  | 975 |      * Allows to move all tag instances from one context to another
 | 
        
           |  |  | 976 |      *
 | 
        
           |  |  | 977 |      * @param string $component the component that was tagged
 | 
        
           |  |  | 978 |      * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)
 | 
        
           |  |  | 979 |      * @param context $oldcontext
 | 
        
           |  |  | 980 |      * @param context $newcontext
 | 
        
           |  |  | 981 |      */
 | 
        
           |  |  | 982 |     public static function move_context($component, $itemtype, $oldcontext, $newcontext) {
 | 
        
           |  |  | 983 |         global $DB;
 | 
        
           |  |  | 984 |         if ($oldcontext instanceof context) {
 | 
        
           |  |  | 985 |             $oldcontext = $oldcontext->id;
 | 
        
           |  |  | 986 |         }
 | 
        
           |  |  | 987 |         if ($newcontext instanceof context) {
 | 
        
           |  |  | 988 |             $newcontext = $newcontext->id;
 | 
        
           |  |  | 989 |         }
 | 
        
           |  |  | 990 |         $DB->set_field('tag_instance', 'contextid', $newcontext,
 | 
        
           |  |  | 991 |                 array('component' => $component, 'itemtype' => $itemtype, 'contextid' => $oldcontext));
 | 
        
           |  |  | 992 |     }
 | 
        
           |  |  | 993 |   | 
        
           |  |  | 994 |     /**
 | 
        
           |  |  | 995 |      * Moves all tags of the specified items to the new context
 | 
        
           |  |  | 996 |      *
 | 
        
           |  |  | 997 |      * @param string $component the component that was tagged
 | 
        
           |  |  | 998 |      * @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)
 | 
        
           |  |  | 999 |      * @param array $itemids
 | 
        
           |  |  | 1000 |      * @param context|int $newcontext target context to move tags to
 | 
        
           |  |  | 1001 |      */
 | 
        
           |  |  | 1002 |     public static function change_items_context($component, $itemtype, $itemids, $newcontext) {
 | 
        
           |  |  | 1003 |         global $DB;
 | 
        
           |  |  | 1004 |         if (empty($itemids)) {
 | 
        
           |  |  | 1005 |             return;
 | 
        
           |  |  | 1006 |         }
 | 
        
           |  |  | 1007 |         if (!is_array($itemids)) {
 | 
        
           |  |  | 1008 |             $itemids = array($itemids);
 | 
        
           |  |  | 1009 |         }
 | 
        
           |  |  | 1010 |         list($sql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
 | 
        
           |  |  | 1011 |         $params['component'] = $component;
 | 
        
           |  |  | 1012 |         $params['itemtype'] = $itemtype;
 | 
        
           |  |  | 1013 |         if ($newcontext instanceof context) {
 | 
        
           |  |  | 1014 |             $newcontext = $newcontext->id;
 | 
        
           |  |  | 1015 |         }
 | 
        
           |  |  | 1016 |   | 
        
           |  |  | 1017 |         $DB->set_field_select('tag_instance', 'contextid', $newcontext,
 | 
        
           |  |  | 1018 |             'component = :component AND itemtype = :itemtype AND itemid ' . $sql, $params);
 | 
        
           |  |  | 1019 |     }
 | 
        
           |  |  | 1020 |   | 
        
           |  |  | 1021 |     /**
 | 
        
           |  |  | 1022 |      * Moves all of the specified tag instances into a new context.
 | 
        
           |  |  | 1023 |      *
 | 
        
           |  |  | 1024 |      * @param array $taginstanceids The list of tag instance ids that should be moved
 | 
        
           |  |  | 1025 |      * @param context $newcontext The context to move the tag instances into
 | 
        
           |  |  | 1026 |      */
 | 
        
           |  |  | 1027 |     public static function change_instances_context(array $taginstanceids, context $newcontext) {
 | 
        
           |  |  | 1028 |         global $DB;
 | 
        
           |  |  | 1029 |   | 
        
           |  |  | 1030 |         if (empty($taginstanceids)) {
 | 
        
           |  |  | 1031 |             return;
 | 
        
           |  |  | 1032 |         }
 | 
        
           |  |  | 1033 |   | 
        
           |  |  | 1034 |         list($sql, $params) = $DB->get_in_or_equal($taginstanceids);
 | 
        
           |  |  | 1035 |         $DB->set_field_select('tag_instance', 'contextid', $newcontext->id, "id {$sql}", $params);
 | 
        
           |  |  | 1036 |     }
 | 
        
           |  |  | 1037 |   | 
        
           |  |  | 1038 |     /**
 | 
        
           |  |  | 1039 |      * Updates the information about the tag
 | 
        
           |  |  | 1040 |      *
 | 
        
           |  |  | 1041 |      * @param array|stdClass $data data to update, may contain: isstandard, description, descriptionformat, rawname
 | 
        
           |  |  | 1042 |      * @return bool whether the tag was updated. False may be returned if: all new values match the existing,
 | 
        
           |  |  | 1043 |      *         or it was attempted to rename the tag to the name that is already used.
 | 
        
           |  |  | 1044 |      */
 | 
        
           |  |  | 1045 |     public function update($data) {
 | 
        
           |  |  | 1046 |         global $DB, $COURSE;
 | 
        
           |  |  | 1047 |   | 
        
           |  |  | 1048 |         $allowedfields = array('isstandard', 'description', 'descriptionformat', 'rawname');
 | 
        
           |  |  | 1049 |   | 
        
           |  |  | 1050 |         $data = (array)$data;
 | 
        
           |  |  | 1051 |         if ($extrafields = array_diff(array_keys($data), $allowedfields)) {
 | 
        
           |  |  | 1052 |             debugging('The field(s) '.join(', ', $extrafields).' will be ignored when updating the tag',
 | 
        
           |  |  | 1053 |                     DEBUG_DEVELOPER);
 | 
        
           |  |  | 1054 |         }
 | 
        
           |  |  | 1055 |         $data = array_intersect_key($data, array_fill_keys($allowedfields, 1));
 | 
        
           |  |  | 1056 |         $this->ensure_fields_exist(array_merge(array('tagcollid', 'userid', 'name', 'rawname'), array_keys($data)), 'update');
 | 
        
           |  |  | 1057 |   | 
        
           |  |  | 1058 |         // Validate the tag name.
 | 
        
           |  |  | 1059 |         if (array_key_exists('rawname', $data)) {
 | 
        
           |  |  | 1060 |             $data['rawname'] = clean_param($data['rawname'], PARAM_TAG);
 | 
        
           |  |  | 1061 |             $name = core_text::strtolower($data['rawname']);
 | 
        
           |  |  | 1062 |   | 
        
           |  |  | 1063 |             if (!$name || $data['rawname'] === $this->rawname) {
 | 
        
           |  |  | 1064 |                 unset($data['rawname']);
 | 
        
           |  |  | 1065 |             } else if ($existing = static::get_by_name($this->tagcollid, $name, 'id')) {
 | 
        
           |  |  | 1066 |                 // Prevent the rename if a tag with that name already exists.
 | 
        
           |  |  | 1067 |                 if ($existing->id != $this->id) {
 | 
        
           |  |  | 1068 |                     throw new moodle_exception('namesalreadybeeingused', 'core_tag');
 | 
        
           |  |  | 1069 |                 }
 | 
        
           |  |  | 1070 |             }
 | 
        
           |  |  | 1071 |             if (isset($data['rawname'])) {
 | 
        
           |  |  | 1072 |                 $data['name'] = $name;
 | 
        
           |  |  | 1073 |             }
 | 
        
           |  |  | 1074 |         }
 | 
        
           |  |  | 1075 |   | 
        
           |  |  | 1076 |         // Validate the tag type.
 | 
        
           |  |  | 1077 |         if (array_key_exists('isstandard', $data)) {
 | 
        
           |  |  | 1078 |             $data['isstandard'] = $data['isstandard'] ? 1 : 0;
 | 
        
           |  |  | 1079 |         }
 | 
        
           |  |  | 1080 |   | 
        
           |  |  | 1081 |         // Find only the attributes that need to be changed.
 | 
        
           |  |  | 1082 |         $originalname = $this->name;
 | 
        
           |  |  | 1083 |         foreach ($data as $key => $value) {
 | 
        
           |  |  | 1084 |             if ($this->record->$key !== $value) {
 | 
        
           |  |  | 1085 |                 $this->record->$key = $value;
 | 
        
           |  |  | 1086 |             } else {
 | 
        
           |  |  | 1087 |                 unset($data[$key]);
 | 
        
           |  |  | 1088 |             }
 | 
        
           |  |  | 1089 |         }
 | 
        
           |  |  | 1090 |         if (empty($data)) {
 | 
        
           |  |  | 1091 |             return false;
 | 
        
           |  |  | 1092 |         }
 | 
        
           |  |  | 1093 |   | 
        
           |  |  | 1094 |         $data['id'] = $this->id;
 | 
        
           |  |  | 1095 |         $data['timemodified'] = time();
 | 
        
           |  |  | 1096 |         $DB->update_record('tag', $data);
 | 
        
           |  |  | 1097 |   | 
        
           |  |  | 1098 |         $event = \core\event\tag_updated::create(array(
 | 
        
           |  |  | 1099 |             'objectid' => $this->id,
 | 
        
           |  |  | 1100 |             'relateduserid' => $this->userid,
 | 
        
           |  |  | 1101 |             'context' => context_system::instance(),
 | 
        
           |  |  | 1102 |             'other' => array(
 | 
        
           |  |  | 1103 |                 'name' => $this->name,
 | 
        
           |  |  | 1104 |                 'rawname' => $this->rawname
 | 
        
           |  |  | 1105 |             )
 | 
        
           |  |  | 1106 |         ));
 | 
        
           |  |  | 1107 |         $event->trigger();
 | 
        
           |  |  | 1108 |         return true;
 | 
        
           |  |  | 1109 |     }
 | 
        
           |  |  | 1110 |   | 
        
           |  |  | 1111 |     /**
 | 
        
           |  |  | 1112 |      * Flag a tag as inappropriate
 | 
        
           |  |  | 1113 |      */
 | 
        
           |  |  | 1114 |     public function flag() {
 | 
        
           |  |  | 1115 |         global $DB;
 | 
        
           |  |  | 1116 |   | 
        
           |  |  | 1117 |         $this->ensure_fields_exist(array('name', 'userid', 'rawname', 'flag'), 'flag');
 | 
        
           |  |  | 1118 |   | 
        
           |  |  | 1119 |         // Update all the tags to flagged.
 | 
        
           |  |  | 1120 |         $this->timemodified = time();
 | 
        
           |  |  | 1121 |         $this->flag++;
 | 
        
           |  |  | 1122 |         $DB->update_record('tag', array('timemodified' => $this->timemodified,
 | 
        
           |  |  | 1123 |             'flag' => $this->flag, 'id' => $this->id));
 | 
        
           |  |  | 1124 |   | 
        
           |  |  | 1125 |         $event = \core\event\tag_flagged::create(array(
 | 
        
           |  |  | 1126 |             'objectid' => $this->id,
 | 
        
           |  |  | 1127 |             'relateduserid' => $this->userid,
 | 
        
           |  |  | 1128 |             'context' => context_system::instance(),
 | 
        
           |  |  | 1129 |             'other' => array(
 | 
        
           |  |  | 1130 |                 'name' => $this->name,
 | 
        
           |  |  | 1131 |                 'rawname' => $this->rawname
 | 
        
           |  |  | 1132 |             )
 | 
        
           |  |  | 1133 |   | 
        
           |  |  | 1134 |         ));
 | 
        
           |  |  | 1135 |         $event->trigger();
 | 
        
           |  |  | 1136 |     }
 | 
        
           |  |  | 1137 |   | 
        
           |  |  | 1138 |     /**
 | 
        
           |  |  | 1139 |      * Remove the inappropriate flag on a tag.
 | 
        
           |  |  | 1140 |      */
 | 
        
           |  |  | 1141 |     public function reset_flag() {
 | 
        
           |  |  | 1142 |         global $DB;
 | 
        
           |  |  | 1143 |   | 
        
           |  |  | 1144 |         $this->ensure_fields_exist(array('name', 'userid', 'rawname', 'flag'), 'flag');
 | 
        
           |  |  | 1145 |   | 
        
           |  |  | 1146 |         if (!$this->flag) {
 | 
        
           |  |  | 1147 |             // Nothing to do.
 | 
        
           |  |  | 1148 |             return false;
 | 
        
           |  |  | 1149 |         }
 | 
        
           |  |  | 1150 |   | 
        
           |  |  | 1151 |         $this->timemodified = time();
 | 
        
           |  |  | 1152 |         $this->flag = 0;
 | 
        
           |  |  | 1153 |         $DB->update_record('tag', array('timemodified' => $this->timemodified,
 | 
        
           |  |  | 1154 |             'flag' => 0, 'id' => $this->id));
 | 
        
           |  |  | 1155 |   | 
        
           |  |  | 1156 |         $event = \core\event\tag_unflagged::create(array(
 | 
        
           |  |  | 1157 |             'objectid' => $this->id,
 | 
        
           |  |  | 1158 |             'relateduserid' => $this->userid,
 | 
        
           |  |  | 1159 |             'context' => context_system::instance(),
 | 
        
           |  |  | 1160 |             'other' => array(
 | 
        
           |  |  | 1161 |                 'name' => $this->name,
 | 
        
           |  |  | 1162 |                 'rawname' => $this->rawname
 | 
        
           |  |  | 1163 |             )
 | 
        
           |  |  | 1164 |         ));
 | 
        
           |  |  | 1165 |         $event->trigger();
 | 
        
           |  |  | 1166 |     }
 | 
        
           |  |  | 1167 |   | 
        
           |  |  | 1168 |     /**
 | 
        
           |  |  | 1169 |      * Sets the list of tags related to this one.
 | 
        
           |  |  | 1170 |      *
 | 
        
           |  |  | 1171 |      * Tag relations are recorded by two instances linking two tags to each other.
 | 
        
           |  |  | 1172 |      * For tag relations ordering is not used and may be random.
 | 
        
           |  |  | 1173 |      *
 | 
        
           |  |  | 1174 |      * @param array $tagnames
 | 
        
           |  |  | 1175 |      */
 | 
        
           |  |  | 1176 |     public function set_related_tags($tagnames) {
 | 
        
           |  |  | 1177 |         $context = context_system::instance();
 | 
        
           |  |  | 1178 |         $tagobjects = $tagnames ? static::create_if_missing($this->tagcollid, $tagnames) : array();
 | 
        
           |  |  | 1179 |         unset($tagobjects[$this->name]); // Never link to itself.
 | 
        
           |  |  | 1180 |   | 
        
           |  |  | 1181 |         $currenttags = static::get_item_tags('core', 'tag', $this->id);
 | 
        
           |  |  | 1182 |   | 
        
           |  |  | 1183 |         // For data coherence reasons, it's better to remove deleted tags
 | 
        
           |  |  | 1184 |         // before adding new data: ordering could be duplicated.
 | 
        
           |  |  | 1185 |         foreach ($currenttags as $currenttag) {
 | 
        
           |  |  | 1186 |             if (!array_key_exists($currenttag->name, $tagobjects)) {
 | 
        
           |  |  | 1187 |                 $taginstance = (object)array('id' => $currenttag->taginstanceid,
 | 
        
           |  |  | 1188 |                     'itemtype' => 'tag', 'itemid' => $this->id,
 | 
        
           |  |  | 1189 |                     'contextid' => $context->id);
 | 
        
           |  |  | 1190 |                 $currenttag->delete_instance_as_record($taginstance, false);
 | 
        
           |  |  | 1191 |                 $this->delete_instance('core', 'tag', $currenttag->id);
 | 
        
           |  |  | 1192 |             }
 | 
        
           |  |  | 1193 |         }
 | 
        
           |  |  | 1194 |   | 
        
           |  |  | 1195 |         foreach ($tagobjects as $name => $tag) {
 | 
        
           |  |  | 1196 |             foreach ($currenttags as $currenttag) {
 | 
        
           |  |  | 1197 |                 if ($currenttag->name === $name) {
 | 
        
           |  |  | 1198 |                     continue 2;
 | 
        
           |  |  | 1199 |                 }
 | 
        
           |  |  | 1200 |             }
 | 
        
           |  |  | 1201 |             $this->add_instance('core', 'tag', $tag->id, $context, 0);
 | 
        
           |  |  | 1202 |             $tag->add_instance('core', 'tag', $this->id, $context, 0);
 | 
        
           |  |  | 1203 |             $currenttags[] = $tag;
 | 
        
           |  |  | 1204 |         }
 | 
        
           |  |  | 1205 |     }
 | 
        
           |  |  | 1206 |   | 
        
           |  |  | 1207 |     /**
 | 
        
           |  |  | 1208 |      * Adds to the list of related tags without removing existing
 | 
        
           |  |  | 1209 |      *
 | 
        
           |  |  | 1210 |      * Tag relations are recorded by two instances linking two tags to each other.
 | 
        
           |  |  | 1211 |      * For tag relations ordering is not used and may be random.
 | 
        
           |  |  | 1212 |      *
 | 
        
           |  |  | 1213 |      * @param array $tagnames
 | 
        
           |  |  | 1214 |      */
 | 
        
           |  |  | 1215 |     public function add_related_tags($tagnames) {
 | 
        
           |  |  | 1216 |         $context = context_system::instance();
 | 
        
           |  |  | 1217 |         $tagobjects = static::create_if_missing($this->tagcollid, $tagnames);
 | 
        
           |  |  | 1218 |   | 
        
           |  |  | 1219 |         $currenttags = static::get_item_tags('core', 'tag', $this->id);
 | 
        
           |  |  | 1220 |   | 
        
           |  |  | 1221 |         foreach ($tagobjects as $name => $tag) {
 | 
        
           |  |  | 1222 |             foreach ($currenttags as $currenttag) {
 | 
        
           |  |  | 1223 |                 if ($currenttag->name === $name) {
 | 
        
           |  |  | 1224 |                     continue 2;
 | 
        
           |  |  | 1225 |                 }
 | 
        
           |  |  | 1226 |             }
 | 
        
           |  |  | 1227 |             $this->add_instance('core', 'tag', $tag->id, $context, 0);
 | 
        
           |  |  | 1228 |             $tag->add_instance('core', 'tag', $this->id, $context, 0);
 | 
        
           |  |  | 1229 |             $currenttags[] = $tag;
 | 
        
           |  |  | 1230 |         }
 | 
        
           |  |  | 1231 |     }
 | 
        
           |  |  | 1232 |   | 
        
           |  |  | 1233 |     /**
 | 
        
           |  |  | 1234 |      * Returns the correlated tags of a tag, retrieved from the tag_correlation table.
 | 
        
           |  |  | 1235 |      *
 | 
        
           |  |  | 1236 |      * Correlated tags are calculated in cron based on existing tag instances.
 | 
        
           |  |  | 1237 |      *
 | 
        
           |  |  | 1238 |      * @param bool $keepduplicates if true, will return one record for each existing
 | 
        
           |  |  | 1239 |      *      tag instance which may result in duplicates of the actual tags
 | 
        
           |  |  | 1240 |      * @return core_tag_tag[] an array of tag objects
 | 
        
           |  |  | 1241 |      */
 | 
        
           |  |  | 1242 |     public function get_correlated_tags($keepduplicates = false) {
 | 
        
           |  |  | 1243 |         global $DB;
 | 
        
           |  |  | 1244 |   | 
        
           |  |  | 1245 |         $correlated = $DB->get_field('tag_correlation', 'correlatedtags', array('tagid' => $this->id));
 | 
        
           |  |  | 1246 |   | 
        
           |  |  | 1247 |         if (!$correlated) {
 | 
        
           |  |  | 1248 |             return array();
 | 
        
           |  |  | 1249 |         }
 | 
        
           |  |  | 1250 |         $correlated = preg_split('/\s*,\s*/', trim($correlated), -1, PREG_SPLIT_NO_EMPTY);
 | 
        
           |  |  | 1251 |         list($query, $params) = $DB->get_in_or_equal($correlated);
 | 
        
           |  |  | 1252 |   | 
        
           |  |  | 1253 |         // This is (and has to) return the same fields as the query in core_tag_tag::get_item_tags().
 | 
        
           |  |  | 1254 |         $sql = "SELECT ti.id AS taginstanceid, tg.id, tg.isstandard, tg.name, tg.rawname, tg.flag,
 | 
        
           |  |  | 1255 |                 tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid, ti.itemid
 | 
        
           |  |  | 1256 |               FROM {tag} tg
 | 
        
           |  |  | 1257 |         INNER JOIN {tag_instance} ti ON tg.id = ti.tagid
 | 
        
           |  |  | 1258 |              WHERE tg.id $query AND tg.id <> ? AND tg.tagcollid = ?
 | 
        
           |  |  | 1259 |           ORDER BY ti.ordering ASC, ti.id";
 | 
        
           |  |  | 1260 |         $params[] = $this->id;
 | 
        
           |  |  | 1261 |         $params[] = $this->tagcollid;
 | 
        
           |  |  | 1262 |         $records = $DB->get_records_sql($sql, $params);
 | 
        
           |  |  | 1263 |         $seen = array();
 | 
        
           |  |  | 1264 |         $result = array();
 | 
        
           |  |  | 1265 |         foreach ($records as $id => $record) {
 | 
        
           |  |  | 1266 |             if (!$keepduplicates && !empty($seen[$record->id])) {
 | 
        
           |  |  | 1267 |                 continue;
 | 
        
           |  |  | 1268 |             }
 | 
        
           |  |  | 1269 |             $result[$id] = new static($record);
 | 
        
           |  |  | 1270 |             $seen[$record->id] = true;
 | 
        
           |  |  | 1271 |         }
 | 
        
           |  |  | 1272 |         return $result;
 | 
        
           |  |  | 1273 |     }
 | 
        
           |  |  | 1274 |   | 
        
           |  |  | 1275 |     /**
 | 
        
           |  |  | 1276 |      * Returns tags that this tag was manually set as related to
 | 
        
           |  |  | 1277 |      *
 | 
        
           |  |  | 1278 |      * @return core_tag_tag[]
 | 
        
           |  |  | 1279 |      */
 | 
        
           |  |  | 1280 |     public function get_manual_related_tags() {
 | 
        
           |  |  | 1281 |         return self::get_item_tags('core', 'tag', $this->id);
 | 
        
           |  |  | 1282 |     }
 | 
        
           |  |  | 1283 |   | 
        
           |  |  | 1284 |     /**
 | 
        
           |  |  | 1285 |      * Returns tags related to a tag
 | 
        
           |  |  | 1286 |      *
 | 
        
           |  |  | 1287 |      * Related tags of a tag come from two sources:
 | 
        
           |  |  | 1288 |      *   - manually added related tags, which are tag_instance entries for that tag
 | 
        
           |  |  | 1289 |      *   - correlated tags, which are calculated
 | 
        
           |  |  | 1290 |      *
 | 
        
           |  |  | 1291 |      * @return core_tag_tag[] an array of tag objects
 | 
        
           |  |  | 1292 |      */
 | 
        
           |  |  | 1293 |     public function get_related_tags() {
 | 
        
           |  |  | 1294 |         $manual = $this->get_manual_related_tags();
 | 
        
           |  |  | 1295 |         $automatic = $this->get_correlated_tags();
 | 
        
           |  |  | 1296 |         $relatedtags = array_merge($manual, $automatic);
 | 
        
           |  |  | 1297 |   | 
        
           |  |  | 1298 |         // Remove duplicated tags (multiple instances of the same tag).
 | 
        
           |  |  | 1299 |         $seen = array();
 | 
        
           |  |  | 1300 |         foreach ($relatedtags as $instance => $tag) {
 | 
        
           |  |  | 1301 |             if (isset($seen[$tag->id])) {
 | 
        
           |  |  | 1302 |                 unset($relatedtags[$instance]);
 | 
        
           |  |  | 1303 |             } else {
 | 
        
           |  |  | 1304 |                 $seen[$tag->id] = 1;
 | 
        
           |  |  | 1305 |             }
 | 
        
           |  |  | 1306 |         }
 | 
        
           |  |  | 1307 |   | 
        
           |  |  | 1308 |         return $relatedtags;
 | 
        
           |  |  | 1309 |     }
 | 
        
           |  |  | 1310 |   | 
        
           |  |  | 1311 |     /**
 | 
        
           |  |  | 1312 |      * Find all items tagged with a tag of a given type ('post', 'user', etc.)
 | 
        
           |  |  | 1313 |      *
 | 
        
           |  |  | 1314 |      * @param    string   $component component responsible for tagging. For BC it can be empty but in this case the
 | 
        
           |  |  | 1315 |      *                    query will be slow because DB index will not be used.
 | 
        
           |  |  | 1316 |      * @param    string   $itemtype  type to restrict search to
 | 
        
           |  |  | 1317 |      * @param    int      $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point.
 | 
        
           |  |  | 1318 |      * @param    int      $limitnum  (optional, required if $limitfrom is set) return a subset comprising this many records.
 | 
        
           |  |  | 1319 |      * @param    string   $subquery additional query to be appended to WHERE clause, refer to the itemtable as 'it'
 | 
        
           |  |  | 1320 |      * @param    array    $params additional parameters for the DB query
 | 
        
           |  |  | 1321 |      * @return   array of matching objects, indexed by record id, from the table containing the type requested
 | 
        
           |  |  | 1322 |      */
 | 
        
           |  |  | 1323 |     public function get_tagged_items($component, $itemtype, $limitfrom = '', $limitnum = '', $subquery = '', $params = array()) {
 | 
        
           |  |  | 1324 |         global $DB;
 | 
        
           |  |  | 1325 |   | 
        
           |  |  | 1326 |         if (empty($itemtype) || !$DB->get_manager()->table_exists($itemtype)) {
 | 
        
           |  |  | 1327 |             return array();
 | 
        
           |  |  | 1328 |         }
 | 
        
           |  |  | 1329 |         $params = $params ? $params : array();
 | 
        
           |  |  | 1330 |   | 
        
           |  |  | 1331 |         $query = "SELECT it.*
 | 
        
           |  |  | 1332 |                     FROM {".$itemtype."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid
 | 
        
           |  |  | 1333 |                    WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid";
 | 
        
           |  |  | 1334 |         $params['itemtype'] = $itemtype;
 | 
        
           |  |  | 1335 |         $params['tagid'] = $this->id;
 | 
        
           |  |  | 1336 |         if ($component) {
 | 
        
           |  |  | 1337 |             $query .= ' AND tt.component = :component';
 | 
        
           |  |  | 1338 |             $params['component'] = $component;
 | 
        
           |  |  | 1339 |         }
 | 
        
           |  |  | 1340 |         if ($subquery) {
 | 
        
           |  |  | 1341 |             $query .= ' AND ' . $subquery;
 | 
        
           |  |  | 1342 |         }
 | 
        
           |  |  | 1343 |         $query .= ' ORDER BY it.id';
 | 
        
           |  |  | 1344 |   | 
        
           |  |  | 1345 |         return $DB->get_records_sql($query, $params, $limitfrom, $limitnum);
 | 
        
           |  |  | 1346 |     }
 | 
        
           |  |  | 1347 |   | 
        
           |  |  | 1348 |     /**
 | 
        
           |  |  | 1349 |      * Count how many items are tagged with a specific tag.
 | 
        
           |  |  | 1350 |      *
 | 
        
           |  |  | 1351 |      * @param    string   $component component responsible for tagging. For BC it can be empty but in this case the
 | 
        
           |  |  | 1352 |      *                    query will be slow because DB index will not be used.
 | 
        
           |  |  | 1353 |      * @param    string   $itemtype  type to restrict search to
 | 
        
           |  |  | 1354 |      * @param    string   $subquery additional query to be appended to WHERE clause, refer to the itemtable as 'it'
 | 
        
           |  |  | 1355 |      * @param    array    $params additional parameters for the DB query
 | 
        
           |  |  | 1356 |      * @return   int      number of mathing tags.
 | 
        
           |  |  | 1357 |      */
 | 
        
           |  |  | 1358 |     public function count_tagged_items($component, $itemtype, $subquery = '', $params = array()) {
 | 
        
           |  |  | 1359 |         global $DB;
 | 
        
           |  |  | 1360 |   | 
        
           |  |  | 1361 |         if (empty($itemtype) || !$DB->get_manager()->table_exists($itemtype)) {
 | 
        
           |  |  | 1362 |             return 0;
 | 
        
           |  |  | 1363 |         }
 | 
        
           |  |  | 1364 |         $params = $params ? $params : array();
 | 
        
           |  |  | 1365 |   | 
        
           |  |  | 1366 |         $query = "SELECT COUNT(it.id)
 | 
        
           |  |  | 1367 |                     FROM {".$itemtype."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid
 | 
        
           |  |  | 1368 |                    WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid";
 | 
        
           |  |  | 1369 |         $params['itemtype'] = $itemtype;
 | 
        
           |  |  | 1370 |         $params['tagid'] = $this->id;
 | 
        
           |  |  | 1371 |         if ($component) {
 | 
        
           |  |  | 1372 |             $query .= ' AND tt.component = :component';
 | 
        
           |  |  | 1373 |             $params['component'] = $component;
 | 
        
           |  |  | 1374 |         }
 | 
        
           |  |  | 1375 |         if ($subquery) {
 | 
        
           |  |  | 1376 |             $query .= ' AND ' . $subquery;
 | 
        
           |  |  | 1377 |         }
 | 
        
           |  |  | 1378 |   | 
        
           |  |  | 1379 |         return $DB->get_field_sql($query, $params);
 | 
        
           |  |  | 1380 |     }
 | 
        
           |  |  | 1381 |   | 
        
           |  |  | 1382 |     /**
 | 
        
           |  |  | 1383 |      * Determine if an item is tagged with a specific tag
 | 
        
           |  |  | 1384 |      *
 | 
        
           |  |  | 1385 |      * Note that this is a static method and not a method of core_tag object because the tag might not exist yet,
 | 
        
           |  |  | 1386 |      * for example user searches for "php" and we offer him to add "php" to his interests.
 | 
        
           |  |  | 1387 |      *
 | 
        
           |  |  | 1388 |      * @param   string   $component component responsible for tagging. For BC it can be empty but in this case the
 | 
        
           |  |  | 1389 |      *                   query will be slow because DB index will not be used.
 | 
        
           |  |  | 1390 |      * @param   string   $itemtype    the record type to look for
 | 
        
           |  |  | 1391 |      * @param   int      $itemid      the record id to look for
 | 
        
           |  |  | 1392 |      * @param   string   $tagname     a tag name
 | 
        
           |  |  | 1393 |      * @return  int                   1 if it is tagged, 0 otherwise
 | 
        
           |  |  | 1394 |      */
 | 
        
           |  |  | 1395 |     public static function is_item_tagged_with($component, $itemtype, $itemid, $tagname) {
 | 
        
           |  |  | 1396 |         global $DB;
 | 
        
           |  |  | 1397 |         $tagcollid = core_tag_area::get_collection($component, $itemtype);
 | 
        
           |  |  | 1398 |         $query = 'SELECT 1 FROM {tag} t
 | 
        
           |  |  | 1399 |                     JOIN {tag_instance} ti ON ti.tagid = t.id
 | 
        
           |  |  | 1400 |                     WHERE t.name = ? AND t.tagcollid = ? AND ti.itemtype = ? AND ti.itemid = ?';
 | 
        
           |  |  | 1401 |         $cleanname = core_text::strtolower(clean_param($tagname, PARAM_TAG));
 | 
        
           |  |  | 1402 |         $params = array($cleanname, $tagcollid, $itemtype, $itemid);
 | 
        
           |  |  | 1403 |         if ($component) {
 | 
        
           |  |  | 1404 |             $query .= ' AND ti.component = ?';
 | 
        
           |  |  | 1405 |             $params[] = $component;
 | 
        
           |  |  | 1406 |         }
 | 
        
           |  |  | 1407 |         return $DB->record_exists_sql($query, $params) ? 1 : 0;
 | 
        
           |  |  | 1408 |     }
 | 
        
           |  |  | 1409 |   | 
        
           |  |  | 1410 |     /**
 | 
        
           |  |  | 1411 |      * Returns whether the tag area is enabled
 | 
        
           |  |  | 1412 |      *
 | 
        
           |  |  | 1413 |      * @param string $component component responsible for tagging
 | 
        
           |  |  | 1414 |      * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc.
 | 
        
           |  |  | 1415 |      * @return bool|null
 | 
        
           |  |  | 1416 |      */
 | 
        
           |  |  | 1417 |     public static function is_enabled($component, $itemtype) {
 | 
        
           |  |  | 1418 |         return core_tag_area::is_enabled($component, $itemtype);
 | 
        
           |  |  | 1419 |     }
 | 
        
           |  |  | 1420 |   | 
        
           |  |  | 1421 |     /**
 | 
        
           |  |  | 1422 |      * Retrieves contents of tag area for the tag/index.php page
 | 
        
           |  |  | 1423 |      *
 | 
        
           |  |  | 1424 |      * @param stdClass $tagarea
 | 
        
           |  |  | 1425 |      * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
 | 
        
           |  |  | 1426 |      *             are displayed on the page and the per-page limit may be bigger
 | 
        
           |  |  | 1427 |      * @param int $fromctx context id where the link was displayed, may be used by callbacks
 | 
        
           |  |  | 1428 |      *            to display items in the same context first
 | 
        
           |  |  | 1429 |      * @param int $ctx context id where to search for records
 | 
        
           |  |  | 1430 |      * @param bool $rec search in subcontexts as well
 | 
        
           |  |  | 1431 |      * @param int $page 0-based number of page being displayed
 | 
        
           |  |  | 1432 |      * @return \core_tag\output\tagindex
 | 
        
           |  |  | 1433 |      */
 | 
        
           |  |  | 1434 |     public function get_tag_index($tagarea, $exclusivemode, $fromctx, $ctx, $rec, $page = 0) {
 | 
        
           |  |  | 1435 |         global $CFG;
 | 
        
           |  |  | 1436 |         if (!empty($tagarea->callback)) {
 | 
        
           |  |  | 1437 |             if (!empty($tagarea->callbackfile)) {
 | 
        
           |  |  | 1438 |                 require_once($CFG->dirroot . '/' . ltrim($tagarea->callbackfile, '/'));
 | 
        
           |  |  | 1439 |             }
 | 
        
           |  |  | 1440 |             $callback = $tagarea->callback;
 | 
        
           |  |  | 1441 |             return call_user_func_array($callback, [$this, $exclusivemode, $fromctx, $ctx, $rec, $page]);
 | 
        
           |  |  | 1442 |         }
 | 
        
           |  |  | 1443 |         return null;
 | 
        
           |  |  | 1444 |     }
 | 
        
           |  |  | 1445 |   | 
        
           |  |  | 1446 |     /**
 | 
        
           |  |  | 1447 |      * Returns formatted description of the tag
 | 
        
           |  |  | 1448 |      *
 | 
        
           |  |  | 1449 |      * @param array $options
 | 
        
           |  |  | 1450 |      * @return string
 | 
        
           |  |  | 1451 |      */
 | 
        
           |  |  | 1452 |     public function get_formatted_description($options = array()) {
 | 
        
           |  |  | 1453 |         $options = empty($options) ? array() : (array)$options;
 | 
        
           |  |  | 1454 |         $options += array('para' => false, 'overflowdiv' => true);
 | 
        
           |  |  | 1455 |         $description = file_rewrite_pluginfile_urls($this->description, 'pluginfile.php',
 | 
        
           |  |  | 1456 |                 context_system::instance()->id, 'tag', 'description', $this->id);
 | 
        
           |  |  | 1457 |         return format_text($description, $this->descriptionformat, $options);
 | 
        
           |  |  | 1458 |     }
 | 
        
           |  |  | 1459 |   | 
        
           |  |  | 1460 |     /**
 | 
        
           |  |  | 1461 |      * Returns the list of tag links available for the current user (edit, flag, etc.)
 | 
        
           |  |  | 1462 |      *
 | 
        
           |  |  | 1463 |      * @return array
 | 
        
           |  |  | 1464 |      */
 | 
        
           |  |  | 1465 |     public function get_links() {
 | 
        
           |  |  | 1466 |         global $USER;
 | 
        
           |  |  | 1467 |         $links = array();
 | 
        
           |  |  | 1468 |   | 
        
           |  |  | 1469 |         if (!isloggedin() || isguestuser()) {
 | 
        
           |  |  | 1470 |             return $links;
 | 
        
           |  |  | 1471 |         }
 | 
        
           |  |  | 1472 |   | 
        
           |  |  | 1473 |         $tagname = $this->get_display_name();
 | 
        
           |  |  | 1474 |         $systemcontext = context_system::instance();
 | 
        
           |  |  | 1475 |   | 
        
           |  |  | 1476 |         // Add a link for users to add/remove this from their interests.
 | 
        
           |  |  | 1477 |         if (static::is_enabled('core', 'user') && core_tag_area::get_collection('core', 'user') == $this->tagcollid) {
 | 
        
           |  |  | 1478 |             if (static::is_item_tagged_with('core', 'user', $USER->id, $this->name)) {
 | 
        
           |  |  | 1479 |                 $url = new moodle_url('/tag/user.php', array('action' => 'removeinterest',
 | 
        
           |  |  | 1480 |                     'sesskey' => sesskey(), 'tag' => $this->rawname));
 | 
        
           |  |  | 1481 |                 $links[] = html_writer::link($url, get_string('removetagfrommyinterests', 'tag', $tagname),
 | 
        
           |  |  | 1482 |                         array('class' => 'removefrommyinterests'));
 | 
        
           |  |  | 1483 |             } else {
 | 
        
           |  |  | 1484 |                 $url = new moodle_url('/tag/user.php', array('action' => 'addinterest',
 | 
        
           |  |  | 1485 |                     'sesskey' => sesskey(), 'tag' => $this->rawname));
 | 
        
           |  |  | 1486 |                 $links[] = html_writer::link($url, get_string('addtagtomyinterests', 'tag', $tagname),
 | 
        
           |  |  | 1487 |                         array('class' => 'addtomyinterests'));
 | 
        
           |  |  | 1488 |             }
 | 
        
           |  |  | 1489 |         }
 | 
        
           |  |  | 1490 |   | 
        
           |  |  | 1491 |         // Flag as inappropriate link.  Only people with moodle/tag:flag capability.
 | 
        
           |  |  | 1492 |         if (has_capability('moodle/tag:flag', $systemcontext)) {
 | 
        
           |  |  | 1493 |             $url = new moodle_url('/tag/user.php', array('action' => 'flaginappropriate',
 | 
        
           |  |  | 1494 |                 'sesskey' => sesskey(), 'id' => $this->id));
 | 
        
           |  |  | 1495 |             $links[] = html_writer::link($url, get_string('flagasinappropriate', 'tag', $tagname),
 | 
        
           |  |  | 1496 |                         array('class' => 'flagasinappropriate'));
 | 
        
           |  |  | 1497 |         }
 | 
        
           |  |  | 1498 |   | 
        
           |  |  | 1499 |         // Edit tag: Only people with moodle/tag:edit capability who either have it as an interest or can manage tags.
 | 
        
           |  |  | 1500 |         if (has_capability('moodle/tag:edit', $systemcontext) ||
 | 
        
           |  |  | 1501 |                 has_capability('moodle/tag:manage', $systemcontext)) {
 | 
        
           |  |  | 1502 |             $url = new moodle_url('/tag/edit.php', array('id' => $this->id));
 | 
        
           |  |  | 1503 |             $links[] = html_writer::link($url, get_string('edittag', 'tag'),
 | 
        
           |  |  | 1504 |                         array('class' => 'edittag'));
 | 
        
           |  |  | 1505 |         }
 | 
        
           |  |  | 1506 |   | 
        
           |  |  | 1507 |         return $links;
 | 
        
           |  |  | 1508 |     }
 | 
        
           |  |  | 1509 |   | 
        
           |  |  | 1510 |     /**
 | 
        
           |  |  | 1511 |      * Delete one or more tag, and all their instances if there are any left.
 | 
        
           |  |  | 1512 |      *
 | 
        
           |  |  | 1513 |      * @param    int|array    $tagids one tagid (int), or one array of tagids to delete
 | 
        
           |  |  | 1514 |      * @return   bool     true on success, false otherwise
 | 
        
           |  |  | 1515 |      */
 | 
        
           |  |  | 1516 |     public static function delete_tags($tagids) {
 | 
        
           |  |  | 1517 |         global $DB;
 | 
        
           |  |  | 1518 |   | 
        
           |  |  | 1519 |         if (!is_array($tagids)) {
 | 
        
           |  |  | 1520 |             $tagids = array($tagids);
 | 
        
           |  |  | 1521 |         }
 | 
        
           |  |  | 1522 |         if (empty($tagids)) {
 | 
        
           |  |  | 1523 |             return;
 | 
        
           |  |  | 1524 |         }
 | 
        
           |  |  | 1525 |   | 
        
           |  |  | 1526 |         // Use the tagids to create a select statement to be used later.
 | 
        
           |  |  | 1527 |         list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids);
 | 
        
           |  |  | 1528 |   | 
        
           |  |  | 1529 |         // Store the tags and tag instances we are going to delete.
 | 
        
           |  |  | 1530 |         $tags = $DB->get_records_select('tag', 'id ' . $tagsql, $tagparams);
 | 
        
           |  |  | 1531 |         $taginstances = $DB->get_records_select('tag_instance', 'tagid ' . $tagsql, $tagparams);
 | 
        
           |  |  | 1532 |   | 
        
           |  |  | 1533 |         // Delete all the tag instances.
 | 
        
           |  |  | 1534 |         $select = 'WHERE tagid ' . $tagsql;
 | 
        
           |  |  | 1535 |         $sql = "DELETE FROM {tag_instance} $select";
 | 
        
           |  |  | 1536 |         $DB->execute($sql, $tagparams);
 | 
        
           |  |  | 1537 |   | 
        
           |  |  | 1538 |         // Delete all the tag correlations.
 | 
        
           |  |  | 1539 |         $sql = "DELETE FROM {tag_correlation} $select";
 | 
        
           |  |  | 1540 |         $DB->execute($sql, $tagparams);
 | 
        
           |  |  | 1541 |   | 
        
           |  |  | 1542 |         // Delete all the tags.
 | 
        
           |  |  | 1543 |         $select = 'WHERE id ' . $tagsql;
 | 
        
           |  |  | 1544 |         $sql = "DELETE FROM {tag} $select";
 | 
        
           |  |  | 1545 |         $DB->execute($sql, $tagparams);
 | 
        
           |  |  | 1546 |   | 
        
           |  |  | 1547 |         // Fire an event that these items were untagged.
 | 
        
           |  |  | 1548 |         if ($taginstances) {
 | 
        
           |  |  | 1549 |             // Save the system context in case the 'contextid' column in the 'tag_instance' table is null.
 | 
        
           |  |  | 1550 |             $syscontextid = context_system::instance()->id;
 | 
        
           |  |  | 1551 |             // Loop through the tag instances and fire a 'tag_removed'' event.
 | 
        
           |  |  | 1552 |             foreach ($taginstances as $taginstance) {
 | 
        
           |  |  | 1553 |                 // We can not fire an event with 'null' as the contextid.
 | 
        
           |  |  | 1554 |                 if (is_null($taginstance->contextid)) {
 | 
        
           |  |  | 1555 |                     $taginstance->contextid = $syscontextid;
 | 
        
           |  |  | 1556 |                 }
 | 
        
           |  |  | 1557 |   | 
        
           |  |  | 1558 |                 // Trigger tag removed event.
 | 
        
           |  |  | 1559 |                 \core\event\tag_removed::create_from_tag_instance($taginstance,
 | 
        
           |  |  | 1560 |                     $tags[$taginstance->tagid]->name, $tags[$taginstance->tagid]->rawname,
 | 
        
           |  |  | 1561 |                     true)->trigger();
 | 
        
           |  |  | 1562 |             }
 | 
        
           |  |  | 1563 |         }
 | 
        
           |  |  | 1564 |   | 
        
           |  |  | 1565 |         // Fire an event that these tags were deleted.
 | 
        
           |  |  | 1566 |         if ($tags) {
 | 
        
           |  |  | 1567 |             $context = context_system::instance();
 | 
        
           |  |  | 1568 |             foreach ($tags as $tag) {
 | 
        
           |  |  | 1569 |                 // Delete all files associated with this tag.
 | 
        
           |  |  | 1570 |                 $fs = get_file_storage();
 | 
        
           |  |  | 1571 |                 $files = $fs->get_area_files($context->id, 'tag', 'description', $tag->id);
 | 
        
           |  |  | 1572 |                 foreach ($files as $file) {
 | 
        
           |  |  | 1573 |                     $file->delete();
 | 
        
           |  |  | 1574 |                 }
 | 
        
           |  |  | 1575 |   | 
        
           |  |  | 1576 |                 // Trigger an event for deleting this tag.
 | 
        
           |  |  | 1577 |                 $event = \core\event\tag_deleted::create(array(
 | 
        
           |  |  | 1578 |                     'objectid' => $tag->id,
 | 
        
           |  |  | 1579 |                     'relateduserid' => $tag->userid,
 | 
        
           |  |  | 1580 |                     'context' => $context,
 | 
        
           |  |  | 1581 |                     'other' => array(
 | 
        
           |  |  | 1582 |                         'name' => $tag->name,
 | 
        
           |  |  | 1583 |                         'rawname' => $tag->rawname
 | 
        
           |  |  | 1584 |                     )
 | 
        
           |  |  | 1585 |                 ));
 | 
        
           |  |  | 1586 |                 $event->add_record_snapshot('tag', $tag);
 | 
        
           |  |  | 1587 |                 $event->trigger();
 | 
        
           |  |  | 1588 |             }
 | 
        
           |  |  | 1589 |         }
 | 
        
           |  |  | 1590 |   | 
        
           |  |  | 1591 |         return true;
 | 
        
           |  |  | 1592 |     }
 | 
        
           |  |  | 1593 |   | 
        
           |  |  | 1594 |     /**
 | 
        
           |  |  | 1595 |      * Combine together correlated tags of several tags
 | 
        
           |  |  | 1596 |      *
 | 
        
           |  |  | 1597 |      * This is a help method for method combine_tags()
 | 
        
           |  |  | 1598 |      *
 | 
        
           |  |  | 1599 |      * @param core_tag_tag[] $tags
 | 
        
           |  |  | 1600 |      */
 | 
        
           |  |  | 1601 |     protected function combine_correlated_tags($tags) {
 | 
        
           |  |  | 1602 |         global $DB;
 | 
        
           |  |  | 1603 |         $ids = array_map(function($t) {
 | 
        
           |  |  | 1604 |             return $t->id;
 | 
        
           |  |  | 1605 |         }, $tags);
 | 
        
           |  |  | 1606 |   | 
        
           |  |  | 1607 |         // Retrieve the correlated tags of this tag and correlated tags of all tags to be merged in one query
 | 
        
           |  |  | 1608 |         // but store them separately. Calculate the list of correlated tags that need to be added to the current.
 | 
        
           |  |  | 1609 |         list($sql, $params) = $DB->get_in_or_equal($ids);
 | 
        
           |  |  | 1610 |         $params[] = $this->id;
 | 
        
           |  |  | 1611 |         $records = $DB->get_records_select('tag_correlation', 'tagid '.$sql.' OR tagid = ?',
 | 
        
           |  |  | 1612 |             $params, '', 'tagid, id, correlatedtags');
 | 
        
           |  |  | 1613 |         $correlated = array();
 | 
        
           |  |  | 1614 |         $mycorrelated = array();
 | 
        
           |  |  | 1615 |         foreach ($records as $record) {
 | 
        
           |  |  | 1616 |             $taglist = preg_split('/\s*,\s*/', trim($record->correlatedtags), -1, PREG_SPLIT_NO_EMPTY);
 | 
        
           |  |  | 1617 |             if ($record->tagid == $this->id) {
 | 
        
           |  |  | 1618 |                 $mycorrelated = $taglist;
 | 
        
           |  |  | 1619 |             } else {
 | 
        
           |  |  | 1620 |                 $correlated = array_merge($correlated, $taglist);
 | 
        
           |  |  | 1621 |             }
 | 
        
           |  |  | 1622 |         }
 | 
        
           |  |  | 1623 |         array_unique($correlated);
 | 
        
           |  |  | 1624 |         // Strip out from $correlated the ids of the tags that are already in $mycorrelated
 | 
        
           |  |  | 1625 |         // or are one of the tags that are going to be combined.
 | 
        
           |  |  | 1626 |         $correlated = array_diff($correlated, [$this->id], $ids, $mycorrelated);
 | 
        
           |  |  | 1627 |   | 
        
           |  |  | 1628 |         if (empty($correlated)) {
 | 
        
           |  |  | 1629 |             // Nothing to do, ignore situation when current tag is correlated to one of the merged tags - they will
 | 
        
           |  |  | 1630 |             // be deleted later and get_tag_correlation() will not return them. Next cron will clean everything up.
 | 
        
           |  |  | 1631 |             return;
 | 
        
           |  |  | 1632 |         }
 | 
        
           |  |  | 1633 |   | 
        
           |  |  | 1634 |         // Update correlated tags of this tag.
 | 
        
           |  |  | 1635 |         $newcorrelatedlist = join(',', array_merge($mycorrelated, $correlated));
 | 
        
           |  |  | 1636 |         if (isset($records[$this->id])) {
 | 
        
           |  |  | 1637 |             $DB->update_record('tag_correlation', array('id' => $records[$this->id]->id, 'correlatedtags' => $newcorrelatedlist));
 | 
        
           |  |  | 1638 |         } else {
 | 
        
           |  |  | 1639 |             $DB->insert_record('tag_correlation', array('tagid' => $this->id, 'correlatedtags' => $newcorrelatedlist));
 | 
        
           |  |  | 1640 |         }
 | 
        
           |  |  | 1641 |   | 
        
           |  |  | 1642 |         // Add this tag to the list of correlated tags of each tag in $correlated.
 | 
        
           |  |  | 1643 |         list($sql, $params) = $DB->get_in_or_equal($correlated);
 | 
        
           |  |  | 1644 |         $records = $DB->get_records_select('tag_correlation', 'tagid '.$sql, $params, '', 'tagid, id, correlatedtags');
 | 
        
           |  |  | 1645 |         foreach ($correlated as $tagid) {
 | 
        
           |  |  | 1646 |             if (isset($records[$tagid])) {
 | 
        
           |  |  | 1647 |                 $newcorrelatedlist = $records[$tagid]->correlatedtags . ',' . $this->id;
 | 
        
           |  |  | 1648 |                 $DB->update_record('tag_correlation', array('id' => $records[$tagid]->id, 'correlatedtags' => $newcorrelatedlist));
 | 
        
           |  |  | 1649 |             } else {
 | 
        
           |  |  | 1650 |                 $DB->insert_record('tag_correlation', array('tagid' => $tagid, 'correlatedtags' => '' . $this->id));
 | 
        
           |  |  | 1651 |             }
 | 
        
           |  |  | 1652 |         }
 | 
        
           |  |  | 1653 |     }
 | 
        
           |  |  | 1654 |   | 
        
           |  |  | 1655 |     /**
 | 
        
           |  |  | 1656 |      * Combines several other tags into this one
 | 
        
           |  |  | 1657 |      *
 | 
        
           |  |  | 1658 |      * Combining rules:
 | 
        
           |  |  | 1659 |      * - current tag becomes the "main" one, all instances
 | 
        
           |  |  | 1660 |      *   pointing to other tags are changed to point to it.
 | 
        
           |  |  | 1661 |      * - if any of the tags is standard, the "main" tag becomes standard too
 | 
        
           |  |  | 1662 |      * - all tags except for the current ("main") are deleted, even when they are standard
 | 
        
           |  |  | 1663 |      *
 | 
        
           |  |  | 1664 |      * @param core_tag_tag[] $tags tags to combine into this one
 | 
        
           |  |  | 1665 |      */
 | 
        
           |  |  | 1666 |     public function combine_tags($tags) {
 | 
        
           |  |  | 1667 |         global $DB;
 | 
        
           |  |  | 1668 |   | 
        
           |  |  | 1669 |         $this->ensure_fields_exist(array('id', 'tagcollid', 'isstandard', 'name', 'rawname'), 'combine_tags');
 | 
        
           |  |  | 1670 |   | 
        
           |  |  | 1671 |         // Retrieve all tag objects, find if there are any standard tags in the set.
 | 
        
           |  |  | 1672 |         $isstandard = false;
 | 
        
           |  |  | 1673 |         $tagstocombine = array();
 | 
        
           |  |  | 1674 |         $ids = array();
 | 
        
           |  |  | 1675 |         $relatedtags = $this->get_manual_related_tags();
 | 
        
           |  |  | 1676 |         foreach ($tags as $tag) {
 | 
        
           |  |  | 1677 |             $tag->ensure_fields_exist(array('id', 'tagcollid', 'isstandard', 'tagcollid', 'name', 'rawname'), 'combine_tags');
 | 
        
           |  |  | 1678 |             if ($tag && $tag->id != $this->id && $tag->tagcollid == $this->tagcollid) {
 | 
        
           |  |  | 1679 |                 $isstandard = $isstandard || $tag->isstandard;
 | 
        
           |  |  | 1680 |                 $tagstocombine[$tag->name] = $tag;
 | 
        
           |  |  | 1681 |                 $ids[] = $tag->id;
 | 
        
           |  |  | 1682 |                 $relatedtags = array_merge($relatedtags, $tag->get_manual_related_tags());
 | 
        
           |  |  | 1683 |             }
 | 
        
           |  |  | 1684 |         }
 | 
        
           |  |  | 1685 |   | 
        
           |  |  | 1686 |         if (empty($tagstocombine)) {
 | 
        
           |  |  | 1687 |             // Nothing to do.
 | 
        
           |  |  | 1688 |             return;
 | 
        
           |  |  | 1689 |         }
 | 
        
           |  |  | 1690 |   | 
        
           |  |  | 1691 |         // Combine all manually set related tags, exclude itself all the tags it is about to be combined with.
 | 
        
           |  |  | 1692 |         if ($relatedtags) {
 | 
        
           |  |  | 1693 |             $relatedtags = array_map(function($t) {
 | 
        
           |  |  | 1694 |                 return $t->name;
 | 
        
           |  |  | 1695 |             }, $relatedtags);
 | 
        
           |  |  | 1696 |             array_unique($relatedtags);
 | 
        
           |  |  | 1697 |             $relatedtags = array_diff($relatedtags, [$this->name], array_keys($tagstocombine));
 | 
        
           |  |  | 1698 |         }
 | 
        
           |  |  | 1699 |         $this->set_related_tags($relatedtags);
 | 
        
           |  |  | 1700 |   | 
        
           |  |  | 1701 |         // Combine all correlated tags, exclude itself all the tags it is about to be combined with.
 | 
        
           |  |  | 1702 |         $this->combine_correlated_tags($tagstocombine);
 | 
        
           |  |  | 1703 |   | 
        
           |  |  | 1704 |         // If any of the duplicate tags are standard, mark this one as standard too.
 | 
        
           |  |  | 1705 |         if ($isstandard && !$this->isstandard) {
 | 
        
           |  |  | 1706 |             $this->update(array('isstandard' => 1));
 | 
        
           |  |  | 1707 |         }
 | 
        
           |  |  | 1708 |   | 
        
           |  |  | 1709 |         // Go through all instances of each tag that needs to be combined and make them point to this tag instead.
 | 
        
           |  |  | 1710 |         // We go though the list one by one because otherwise looking-for-duplicates logic would be too complicated.
 | 
        
           |  |  | 1711 |         foreach ($tagstocombine as $tag) {
 | 
        
           |  |  | 1712 |             $params = array('tagid' => $tag->id, 'mainid' => $this->id);
 | 
        
           |  |  | 1713 |             $mainsql = 'SELECT ti.*, t.name, t.rawname, tim.id AS alreadyhasmaintag '
 | 
        
           |  |  | 1714 |                     . 'FROM {tag_instance} ti '
 | 
        
           |  |  | 1715 |                     . 'LEFT JOIN {tag} t ON t.id = ti.tagid '
 | 
        
           |  |  | 1716 |                     . 'LEFT JOIN {tag_instance} tim ON ti.component = tim.component AND '
 | 
        
           |  |  | 1717 |                     . '    ti.itemtype = tim.itemtype AND ti.itemid = tim.itemid AND '
 | 
        
           |  |  | 1718 |                     . '    ti.tiuserid = tim.tiuserid AND tim.tagid = :mainid '
 | 
        
           |  |  | 1719 |                     . 'WHERE ti.tagid = :tagid';
 | 
        
           |  |  | 1720 |   | 
        
           |  |  | 1721 |             $records = $DB->get_records_sql($mainsql, $params);
 | 
        
           |  |  | 1722 |             foreach ($records as $record) {
 | 
        
           |  |  | 1723 |                 if ($record->alreadyhasmaintag) {
 | 
        
           |  |  | 1724 |                     // Item is tagged with both main tag and the duplicate tag.
 | 
        
           |  |  | 1725 |                     // Remove instance pointing to the duplicate tag.
 | 
        
           |  |  | 1726 |                     $tag->delete_instance_as_record($record, false);
 | 
        
           |  |  | 1727 |                     $sql = "UPDATE {tag_instance} SET ordering = ordering - 1
 | 
        
           |  |  | 1728 |                             WHERE itemtype = :itemtype
 | 
        
           |  |  | 1729 |                         AND itemid = :itemid AND component = :component AND tiuserid = :tiuserid
 | 
        
           |  |  | 1730 |                         AND ordering > :ordering";
 | 
        
           |  |  | 1731 |                     $DB->execute($sql, (array)$record);
 | 
        
           |  |  | 1732 |                 } else {
 | 
        
           |  |  | 1733 |                     // Item is tagged only with duplicate tag but not the main tag.
 | 
        
           |  |  | 1734 |                     // Replace tagid in the instance pointing to the duplicate tag with this tag.
 | 
        
           |  |  | 1735 |                     $DB->update_record('tag_instance', array('id' => $record->id, 'tagid' => $this->id));
 | 
        
           |  |  | 1736 |                     \core\event\tag_removed::create_from_tag_instance($record, $record->name, $record->rawname)->trigger();
 | 
        
           |  |  | 1737 |                     $record->tagid = $this->id;
 | 
        
           |  |  | 1738 |                     \core\event\tag_added::create_from_tag_instance($record, $this->name, $this->rawname)->trigger();
 | 
        
           |  |  | 1739 |                 }
 | 
        
           |  |  | 1740 |             }
 | 
        
           |  |  | 1741 |         }
 | 
        
           |  |  | 1742 |   | 
        
           |  |  | 1743 |         // Finally delete all tags that we combined into the current one.
 | 
        
           |  |  | 1744 |         self::delete_tags($ids);
 | 
        
           |  |  | 1745 |     }
 | 
        
           |  |  | 1746 |   | 
        
           |  |  | 1747 |     /**
 | 
        
           |  |  | 1748 |      * Retrieve a list of tags that have been used to tag the given $component
 | 
        
           |  |  | 1749 |      * and $itemtype in the provided $contexts.
 | 
        
           |  |  | 1750 |      *
 | 
        
           |  |  | 1751 |      * @param string $component The tag instance component
 | 
        
           |  |  | 1752 |      * @param string $itemtype The tag instance item type
 | 
        
           |  |  | 1753 |      * @param context[] $contexts The list of contexts to look for tag instances in
 | 
        
           |  |  | 1754 |      * @return core_tag_tag[]
 | 
        
           |  |  | 1755 |      */
 | 
        
           |  |  | 1756 |     public static function get_tags_by_area_in_contexts($component, $itemtype, array $contexts) {
 | 
        
           |  |  | 1757 |         global $DB;
 | 
        
           |  |  | 1758 |   | 
        
           |  |  | 1759 |         $params = [$component, $itemtype];
 | 
        
           |  |  | 1760 |         $contextids = array_map(function($context) {
 | 
        
           |  |  | 1761 |             return $context->id;
 | 
        
           |  |  | 1762 |         }, $contexts);
 | 
        
           |  |  | 1763 |         list($contextsql, $contextsqlparams) = $DB->get_in_or_equal($contextids);
 | 
        
           |  |  | 1764 |         $params = array_merge($params, $contextsqlparams);
 | 
        
           |  |  | 1765 |   | 
        
           | 1441 | ariadna | 1766 |         $subsql = "SELECT DISTINCT ti.tagid
 | 
        
           |  |  | 1767 |                      FROM {tag_instance} ti
 | 
        
           |  |  | 1768 |                     WHERE ti.component = ?
 | 
        
           |  |  | 1769 |                             AND ti.itemtype = ?
 | 
        
           |  |  | 1770 |                             AND ti.contextid {$contextsql}";
 | 
        
           | 1 | efrain | 1771 |   | 
        
           |  |  | 1772 |         $sql = "SELECT tt.*
 | 
        
           | 1441 | ariadna | 1773 |                   FROM ($subsql) tv
 | 
        
           |  |  | 1774 |                   JOIN {tag} tt ON tt.id = tv.tagid";
 | 
        
           | 1 | efrain | 1775 |   | 
        
           |  |  | 1776 |         return array_map(function($record) {
 | 
        
           |  |  | 1777 |             return new core_tag_tag($record);
 | 
        
           |  |  | 1778 |         }, $DB->get_records_sql($sql, $params));
 | 
        
           |  |  | 1779 |     }
 | 
        
           |  |  | 1780 | }
 |