Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   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  
1766          $subsql = "SELECT DISTINCT t.id
1767                      FROM {tag} t
1768                      JOIN {tag_instance} ti ON t.id = ti.tagid
1769                     WHERE component = ?
1770                     AND itemtype = ?
1771                     AND contextid {$contextsql}";
1772  
1773          $sql = "SELECT tt.*
1774                  FROM ($subsql) tv
1775                  JOIN {tag} tt ON tt.id = tv.id";
1776  
1777          return array_map(function($record) {
1778              return new core_tag_tag($record);
1779          }, $DB->get_records_sql($sql, $params));
1780      }
1781  }