[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/tag/classes/ -> tag.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1