Rev 1 | AutorÃa | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Contains class core_tag_tag** @package core_tag* @copyright 2015 Marina Glancy* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();/*** Represents one tag and also contains lots of useful tag-related methods as static functions.** Tags can be added to any database records.* $itemtype refers to the DB table name* $itemid refers to id field in this DB table* $component is the component that is responsible for the tag instance* $context is the affected context** BASIC INSTRUCTIONS :* - to "tag a blog post" (for example):* core_tag_tag::set_item_tags('post', 'core', $blogpost->id, $context, $arrayoftags);** - to "remove all the tags on a blog post":* core_tag_tag::remove_all_item_tags('post', 'core', $blogpost->id);** set_item_tags() will create tags that do not exist yet.** @property-read int $id* @property-read string $name* @property-read string $rawname* @property-read int $tagcollid* @property-read int $userid* @property-read int $isstandard* @property-read string $description* @property-read int $descriptionformat* @property-read int $flag 0 if not flagged or positive integer if flagged* @property-read int $timemodified** @package core_tag* @copyright 2015 Marina Glancy* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class core_tag_tag {/** @var stdClass data about the tag */protected $record = null;/** @var int indicates that both standard and not standard tags can be used (or should be returned) */const BOTH_STANDARD_AND_NOT = 0;/** @var int indicates that only standard tags can be used (or must be returned) */const STANDARD_ONLY = 1;/** @var int indicates that only non-standard tags should be returned - this does not really have use cases, left for BC */const NOT_STANDARD_ONLY = -1;/** @var int option to hide standard tags when editing item tags */const HIDE_STANDARD = 2;/** @var int|null tag context ID. */public $taginstancecontextid;/** @var int|null time modification. */public $timemodified;/** @var int|null 0 if not flagged or positive integer if flagged. */public $flag;/*** Constructor. Use functions get(), get_by_name(), etc.** @param stdClass $record*/protected function __construct($record) {if (empty($record->id)) {throw new coding_exception("Record must contain at least field 'id'");}// The following three variables must be added because the database ($record) does not contain them.$this->taginstancecontextid = $record->taginstancecontextid ?? null;$this->flag = $record->flag ?? null;$this->record = $record;}/*** Magic getter** @param string $name* @return mixed*/public function __get($name) {return $this->record->$name;}/*** Magic isset method** @param string $name* @return bool*/public function __isset($name) {return isset($this->record->$name);}/*** Converts to object** @return stdClass*/public function to_object() {return fullclone($this->record);}/*** Returns tag name ready to be displayed** @param bool $ashtml (default true) if true will return htmlspecialchars encoded string* @return string*/public function get_display_name($ashtml = true) {return static::make_display_name($this->record, $ashtml);}/*** Prepares tag name ready to be displayed** @param stdClass|core_tag_tag $tag record from db table tag, must contain properties name and rawname* @param bool $ashtml (default true) if true will return htmlspecialchars encoded string* @return string*/public static function make_display_name($tag, $ashtml = true) {global $CFG;if (empty($CFG->keeptagnamecase)) {// This is the normalized tag name.$tagname = core_text::strtotitle($tag->name);} else {// Original casing of the tag name.$tagname = $tag->rawname;}// Clean up a bit just in case the rules change again.$tagname = clean_param($tagname, PARAM_TAG);return $ashtml ? htmlspecialchars($tagname, ENT_COMPAT) : $tagname;}/*** Adds one or more tag in the database. This function should not be called directly : you should* use tag_set.** @param int $tagcollid* @param string|array $tags one tag, or an array of tags, to be created* @param bool $isstandard type of tag to be created. A standard tag is kept even if there are no records tagged with it.* @return array tag objects indexed by their lowercase normalized names. Any boolean false in the array* indicates an error while adding the tag.*/protected static function add($tagcollid, $tags, $isstandard = false) {global $USER, $DB;$tagobject = new stdClass();$tagobject->isstandard = $isstandard ? 1 : 0;$tagobject->userid = $USER->id;$tagobject->timemodified = time();$tagobject->tagcollid = $tagcollid;$rv = array();foreach ($tags as $veryrawname) {$rawname = clean_param($veryrawname, PARAM_TAG);if (!$rawname) {$rv[$rawname] = false;} else {$obj = (object)(array)$tagobject;$obj->rawname = $rawname;$obj->name = core_text::strtolower($rawname);$obj->id = $DB->insert_record('tag', $obj);$rv[$obj->name] = new static($obj);\core\event\tag_created::create_from_tag($rv[$obj->name])->trigger();}}return $rv;}/*** Simple function to just return a single tag object by its id** @param int $id* @param string $returnfields which fields do we want returned from table {tag}.* Default value is 'id,name,rawname,tagcollid',* specify '*' to include all fields.* @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;* IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);* MUST_EXIST means throw exception if no record or multiple records found* @return core_tag_tag|false tag object*/public static function get($id, $returnfields = 'id, name, rawname, tagcollid', $strictness = IGNORE_MISSING) {global $DB;$record = $DB->get_record('tag', array('id' => $id), $returnfields, $strictness);if ($record) {return new static($record);}return false;}/*** Simple function to just return an array of tag objects by their ids** @param int[] $ids* @param string $returnfields which fields do we want returned from table {tag}.* Default value is 'id,name,rawname,tagcollid',* specify '*' to include all fields.* @return core_tag_tag[] array of retrieved tags*/public static function get_bulk($ids, $returnfields = 'id, name, rawname, tagcollid') {global $DB;$result = array();if (empty($ids)) {return $result;}list($sql, $params) = $DB->get_in_or_equal($ids);$records = $DB->get_records_select('tag', 'id '.$sql, $params, '', $returnfields);foreach ($records as $record) {$result[$record->id] = new static($record);}return $result;}/*** Simple function to just return a single tag object by tagcollid and name** @param int $tagcollid tag collection to use,* if 0 is given we will try to guess the tag collection and return the first match* @param string $name tag name* @param string $returnfields which fields do we want returned. This is a comma separated string* containing any combination of 'id', 'name', 'rawname', 'tagcollid' or '*' to include all fields.* @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;* IGNORE_MULTIPLE means return first, ignore multiple records found(not recommended);* MUST_EXIST means throw exception if no record or multiple records found* @return core_tag_tag|false tag object*/public static function get_by_name($tagcollid, $name, $returnfields='id, name, rawname, tagcollid',$strictness = IGNORE_MISSING) {global $DB;if ($tagcollid == 0) {$tags = static::guess_by_name($name, $returnfields);if ($tags) {$tag = reset($tags);return $tag;} else if ($strictness == MUST_EXIST) {throw new dml_missing_record_exception('tag', 'name=?', array($name));}return false;}$name = core_text::strtolower($name); // To cope with input that might just be wrong case.$params = array('name' => $name, 'tagcollid' => $tagcollid);$record = $DB->get_record('tag', $params, $returnfields, $strictness);if ($record) {return new static($record);}return false;}/*** Looking in all tag collections for the tag with the given name** @param string $name tag name* @param string $returnfields* @return array array of core_tag_tag instances*/public static function guess_by_name($name, $returnfields='id, name, rawname, tagcollid') {global $DB;if (empty($name)) {return array();}$tagcolls = core_tag_collection::get_collections();list($sql, $params) = $DB->get_in_or_equal(array_keys($tagcolls), SQL_PARAMS_NAMED);$params['name'] = core_text::strtolower($name);$tags = $DB->get_records_select('tag', 'name = :name AND tagcollid ' . $sql, $params, '', $returnfields);if (count($tags) > 1) {// Sort in the same order as tag collections.$tagcolls = core_tag_collection::get_collections();uasort($tags, function($a, $b) use ($tagcolls) {return $tagcolls[$a->tagcollid]->sortorder < $tagcolls[$b->tagcollid]->sortorder ? -1 : 1;});}$rv = array();foreach ($tags as $id => $tag) {$rv[$id] = new static($tag);}return $rv;}/*** Returns the list of tag objects by tag collection id and the list of tag names** @param int $tagcollid* @param array $tags array of tags to look for* @param string $returnfields list of DB fields to return, must contain 'id', 'name' and 'rawname'* @return array tag-indexed array of objects. No value for a key means the tag wasn't found.*/public static function get_by_name_bulk($tagcollid, $tags, $returnfields = 'id, name, rawname, tagcollid') {global $DB;if (empty($tags)) {return array();}$cleantags = self::normalize(self::normalize($tags, false)); // Format: rawname => normalised name.list($namesql, $params) = $DB->get_in_or_equal(array_values($cleantags));array_unshift($params, $tagcollid);$recordset = $DB->get_recordset_sql("SELECT $returnfields FROM {tag} WHERE tagcollid = ? AND name $namesql", $params);$result = array_fill_keys($cleantags, null);foreach ($recordset as $record) {$result[$record->name] = new static($record);}$recordset->close();return $result;}/*** Function that normalizes a list of tag names.** @param array $rawtags array of tags* @param bool $tolowercase convert to lower case?* @return array lowercased normalized tags, indexed by the normalized tag, in the same order as the original array.* (Eg: 'Banana' => 'banana').*/public static function normalize($rawtags, $tolowercase = true) {$result = array();foreach ($rawtags as $rawtag) {$rawtag = trim($rawtag);if (strval($rawtag) !== '') {$clean = clean_param($rawtag, PARAM_TAG);if ($tolowercase) {$result[$rawtag] = core_text::strtolower($clean);} else {$result[$rawtag] = $clean;}}}return $result;}/*** Retrieves tags and/or creates them if do not exist yet** @param int $tagcollid* @param array $tags array of raw tag names, do not have to be normalised* @param bool $isstandard create as standard tag (default false)* @return core_tag_tag[] array of tag objects indexed with lowercase normalised tag name*/public static function create_if_missing($tagcollid, $tags, $isstandard = false) {$cleantags = self::normalize(array_filter(self::normalize($tags, false))); // Array rawname => normalised name .$result = static::get_by_name_bulk($tagcollid, $tags, '*');$existing = array_filter($result);$missing = array_diff_key(array_flip($cleantags), $existing); // Array normalised name => rawname.if ($missing) {$newtags = static::add($tagcollid, array_values($missing), $isstandard);foreach ($newtags as $tag) {$result[$tag->name] = $tag;}}return $result;}/*** Creates a URL to view a tag** @param int $tagcollid* @param string $name* @param int $exclusivemode* @param int $fromctx context id where this tag cloud is displayed* @param int $ctx context id for tag view link* @param int $rec recursive argument for tag view link* @return \moodle_url*/public static function make_url($tagcollid, $name, $exclusivemode = 0, $fromctx = 0, $ctx = 0, $rec = 1) {$coll = core_tag_collection::get_by_id($tagcollid);if (!empty($coll->customurl)) {$url = '/' . ltrim(trim($coll->customurl), '/');} else {$url = '/tag/index.php';}$params = array('tc' => $tagcollid, 'tag' => $name);if ($exclusivemode) {$params['excl'] = 1;}if ($fromctx) {$params['from'] = $fromctx;}if ($ctx) {$params['ctx'] = $ctx;}if (!$rec) {$params['rec'] = 0;}return new moodle_url($url, $params);}/*** Returns URL to view the tag** @param int $exclusivemode* @param int $fromctx context id where this tag cloud is displayed* @param int $ctx context id for tag view link* @param int $rec recursive argument for tag view link* @return \moodle_url*/public function get_view_url($exclusivemode = 0, $fromctx = 0, $ctx = 0, $rec = 1) {return static::make_url($this->record->tagcollid, $this->record->rawname,$exclusivemode, $fromctx, $ctx, $rec);}/*** Validates that the required fields were retrieved and retrieves them if missing** @param array $list array of the fields that need to be validated* @param string $caller name of the function that requested it, for the debugging message*/protected function ensure_fields_exist($list, $caller) {global $DB;$missing = array_diff($list, array_keys((array)$this->record));if ($missing) {debugging('core_tag_tag::' . $caller . '() must be called on fully retrieved tag object. Missing fields: '.join(', ', $missing), DEBUG_DEVELOPER);$this->record = $DB->get_record('tag', array('id' => $this->record->id), '*', MUST_EXIST);}}/*** Deletes the tag instance given the record from tag_instance DB table** @param stdClass $taginstance* @param bool $fullobject whether $taginstance contains all fields from DB table tag_instance* (in this case it is safe to add a record snapshot to the event)* @return bool*/protected function delete_instance_as_record($taginstance, $fullobject = false) {global $DB;$this->ensure_fields_exist(array('name', 'rawname', 'isstandard'), 'delete_instance_as_record');$DB->delete_records('tag_instance', array('id' => $taginstance->id));// We can not fire an event with 'null' as the contextid.if (is_null($taginstance->contextid)) {$taginstance->contextid = context_system::instance()->id;}// Trigger tag removed event.$taginstance->tagid = $this->id;\core\event\tag_removed::create_from_tag_instance($taginstance, $this->name, $this->rawname, $fullobject)->trigger();// If there are no other instances of the tag then consider deleting the tag as well.if (!$this->isstandard) {if (!$DB->record_exists('tag_instance', array('tagid' => $this->id))) {self::delete_tags($this->id);}}return true;}/*** Delete one instance of a tag. If the last instance was deleted, it will also delete the tag, unless it is standard.** @param string $component component responsible for tagging. For BC it can be empty but in this case the* query will be slow because DB index will not be used.* @param string $itemtype the type of the record for which to remove the instance* @param int $itemid the id of the record for which to remove the instance* @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)*/protected function delete_instance($component, $itemtype, $itemid, $tiuserid = 0) {global $DB;$params = array('tagid' => $this->id,'itemtype' => $itemtype, 'itemid' => $itemid);if ($tiuserid) {$params['tiuserid'] = $tiuserid;}if ($component) {$params['component'] = $component;}$taginstance = $DB->get_record('tag_instance', $params);if (!$taginstance) {return;}$this->delete_instance_as_record($taginstance, true);}/*** Bulk delete all tag instances.** @param stdClass[] $taginstances A list of tag_instance records to delete. Each* record must also contain the name and rawname* columns from the related tag record.*/public static function delete_instances_as_record(array $taginstances) {global $DB;if (empty($taginstances)) {return;}$taginstanceids = array_map(function($taginstance) {return $taginstance->id;}, $taginstances);// Now remove all the tag instances.$DB->delete_records_list('tag_instance', 'id', $taginstanceids);// Save the system context in case the 'contextid' column in the 'tag_instance' table is null.$syscontextid = context_system::instance()->id;// Loop through the tag instances and fire an 'tag_removed' event.foreach ($taginstances as $taginstance) {// We can not fire an event with 'null' as the contextid.if (is_null($taginstance->contextid)) {$taginstance->contextid = $syscontextid;}// Trigger tag removed event.\core\event\tag_removed::create_from_tag_instance($taginstance, $taginstance->name,$taginstance->rawname, true)->trigger();}}/*** Bulk delete all tag instances by tag id.** @param int[] $taginstanceids List of tag instance ids to be deleted.*/public static function delete_instances_by_id(array $taginstanceids) {global $DB;if (empty($taginstanceids)) {return;}list($idsql, $params) = $DB->get_in_or_equal($taginstanceids);$sql = "SELECT ti.*, t.name, t.rawname, t.isstandardFROM {tag_instance} tiJOIN {tag} tON ti.tagid = t.idWHERE ti.id {$idsql}";if ($taginstances = $DB->get_records_sql($sql, $params)) {static::delete_instances_as_record($taginstances);}}/*** Bulk delete all tag instances for a component or tag area** @param string $component* @param string $itemtype (optional)* @param int $contextid (optional)*/public static function delete_instances($component, $itemtype = null, $contextid = null) {global $DB;$sql = "SELECT ti.*, t.name, t.rawname, t.isstandardFROM {tag_instance} tiJOIN {tag} tON ti.tagid = t.idWHERE ti.component = :component";$params = array('component' => $component);if (!is_null($contextid)) {$sql .= " AND ti.contextid = :contextid";$params['contextid'] = $contextid;}if (!is_null($itemtype)) {$sql .= " AND ti.itemtype = :itemtype";$params['itemtype'] = $itemtype;}if ($taginstances = $DB->get_records_sql($sql, $params)) {static::delete_instances_as_record($taginstances);}}/*** Adds a tag instance** @param string $component* @param string $itemtype* @param string $itemid* @param context $context* @param int $ordering* @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)* @return int id of tag_instance*/protected function add_instance($component, $itemtype, $itemid, context $context, $ordering, $tiuserid = 0) {global $DB;$this->ensure_fields_exist(array('name', 'rawname'), 'add_instance');$taginstance = new stdClass;$taginstance->tagid = $this->id;$taginstance->component = $component ? $component : '';$taginstance->itemid = $itemid;$taginstance->itemtype = $itemtype;$taginstance->contextid = $context->id;$taginstance->ordering = $ordering;$taginstance->timecreated = time();$taginstance->timemodified = $taginstance->timecreated;$taginstance->tiuserid = $tiuserid;$taginstance->id = $DB->insert_record('tag_instance', $taginstance);// Trigger tag added event.\core\event\tag_added::create_from_tag_instance($taginstance, $this->name, $this->rawname, true)->trigger();return $taginstance->id;}/*** Updates the ordering on tag instance** @param int $instanceid* @param int $ordering*/protected function update_instance_ordering($instanceid, $ordering) {global $DB;$data = new stdClass();$data->id = $instanceid;$data->ordering = $ordering;$data->timemodified = time();$DB->update_record('tag_instance', $data);}/*** Get the array of core_tag_tag objects associated with a list of items.** Use {@link core_tag_tag::get_item_tags_array()} if you wish to get the same data as simple array.** @param string $component component responsible for tagging. For BC it can be empty but in this case the* query will be slow because DB index will not be used.* @param string $itemtype type of the tagged item* @param int[] $itemids* @param int $standardonly wether to return only standard tags or any* @param int $tiuserid tag instance user id, only needed for tag areas with user tagging* @return core_tag_tag[][] first array key is itemid. For each itemid,* an array tagid => tag object with additional fields taginstanceid, taginstancecontextid and ordering*/public static function get_items_tags($component, $itemtype, $itemids, $standardonly = self::BOTH_STANDARD_AND_NOT,$tiuserid = 0) {global $DB;if (static::is_enabled($component, $itemtype) === false) {// Tagging area is properly defined but not enabled - return empty array.return array();}if (empty($itemids)) {return array();}$standardonly = (int)$standardonly; // In case somebody passed bool.list($idsql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);// Note: if the fields in this query are changed, you need to do the same changes in core_tag_tag::get_correlated_tags().$sql = "SELECT ti.id AS taginstanceid, tg.id, tg.isstandard, tg.name, tg.rawname, tg.flag,tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid, ti.itemidFROM {tag_instance} tiJOIN {tag} tg ON tg.id = ti.tagidWHERE ti.itemtype = :itemtype AND ti.itemid $idsql ".($component ? "AND ti.component = :component " : "").($tiuserid ? "AND ti.tiuserid = :tiuserid " : "").(($standardonly == self::STANDARD_ONLY) ? "AND tg.isstandard = 1 " : "").(($standardonly == self::NOT_STANDARD_ONLY) ? "AND tg.isstandard = 0 " : "")."ORDER BY ti.ordering ASC, ti.id";$params['itemtype'] = $itemtype;$params['component'] = $component;$params['tiuserid'] = $tiuserid;$records = $DB->get_records_sql($sql, $params);$result = array();foreach ($itemids as $itemid) {$result[$itemid] = [];}foreach ($records as $id => $record) {$result[$record->itemid][$id] = new static($record);}return $result;}/*** Get the array of core_tag_tag objects associated with an item (instances).** Use {@link core_tag_tag::get_item_tags_array()} if you wish to get the same data as simple array.** @param string $component component responsible for tagging. For BC it can be empty but in this case the* query will be slow because DB index will not be used.* @param string $itemtype type of the tagged item* @param int $itemid* @param int $standardonly wether to return only standard tags or any* @param int $tiuserid tag instance user id, only needed for tag areas with user tagging* @return core_tag_tag[] each object contains additional fields taginstanceid, taginstancecontextid and ordering*/public static function get_item_tags($component, $itemtype, $itemid, $standardonly = self::BOTH_STANDARD_AND_NOT,$tiuserid = 0) {$tagobjects = static::get_items_tags($component, $itemtype, [$itemid], $standardonly, $tiuserid);return empty($tagobjects) ? [] : $tagobjects[$itemid];}/*** Returns the list of display names of the tags that are associated with an item** This method is usually used to prefill the form data for the 'tags' form element** @param string $component component responsible for tagging. For BC it can be empty but in this case the* query will be slow because DB index will not be used.* @param string $itemtype type of the tagged item* @param int $itemid* @param int $standardonly wether to return only standard tags or any* @param int $tiuserid tag instance user id, only needed for tag areas with user tagging* @param bool $ashtml (default true) if true will return htmlspecialchars encoded tag names* @return string[] array of tags display names*/public static function get_item_tags_array($component, $itemtype, $itemid, $standardonly = self::BOTH_STANDARD_AND_NOT,$tiuserid = 0, $ashtml = true) {$tags = array();foreach (static::get_item_tags($component, $itemtype, $itemid, $standardonly, $tiuserid) as $tag) {$tags[$tag->id] = $tag->get_display_name($ashtml);}return $tags;}/*** Sets the list of tag instances for one item (table record).** Extra exsisting instances are removed, new ones are added. New tags are created if needed.** This method can not be used for setting tags relations, please use set_related_tags()** @param string $component component responsible for tagging* @param string $itemtype type of the tagged item* @param int $itemid* @param context $context* @param array $tagnames* @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)*/public static function set_item_tags($component, $itemtype, $itemid, context $context, $tagnames, $tiuserid = 0) {if ($itemtype === 'tag') {if ($tiuserid) {throw new coding_exception('Related tags can not have tag instance userid');}debugging('You can not use set_item_tags() for tagging a tag, please use set_related_tags()', DEBUG_DEVELOPER);static::get($itemid, '*', MUST_EXIST)->set_related_tags($tagnames);return;}if ($tagnames !== null && static::is_enabled($component, $itemtype) === false) {// Tagging area is properly defined but not enabled - do nothing.// Unless we are deleting the item tags ($tagnames === null), in which case proceed with deleting.return;}// Apply clean_param() to all tags.if ($tagnames) {$tagcollid = core_tag_area::get_collection($component, $itemtype);$tagobjects = static::create_if_missing($tagcollid, $tagnames);} else {$tagobjects = array();}$allowmultiplecontexts = core_tag_area::allows_tagging_in_multiple_contexts($component, $itemtype);$currenttags = static::get_item_tags($component, $itemtype, $itemid, self::BOTH_STANDARD_AND_NOT, $tiuserid);$taginstanceidstomovecontext = [];// For data coherence reasons, it's better to remove deleted tags// before adding new data: ordering could be duplicated.foreach ($currenttags as $currenttag) {$hasbeenrequested = array_key_exists($currenttag->name, $tagobjects);$issamecontext = $currenttag->taginstancecontextid == $context->id;if ($allowmultiplecontexts) {// If the tag area allows multiple contexts then we should only be// managing tags in the given $context. All other tags can be ignored.$shoulddelete = $issamecontext && !$hasbeenrequested;} else {// If the tag area only allows tag instances in a single context then// all tags that aren't in the requested tags should be deleted, regardless// of their context, if they are not part of the new set of tags.$shoulddelete = !$hasbeenrequested;// If the tag instance isn't in the correct context (legacy data)// then we should take this opportunity to update it with the correct// context id.if (!$shoulddelete && !$issamecontext) {$currenttag->taginstancecontextid = $context->id;$taginstanceidstomovecontext[] = $currenttag->taginstanceid;}}if ($shoulddelete) {$taginstance = (object)array('id' => $currenttag->taginstanceid,'itemtype' => $itemtype, 'itemid' => $itemid,'contextid' => $currenttag->taginstancecontextid, 'tiuserid' => $tiuserid);$currenttag->delete_instance_as_record($taginstance, false);}}if (!empty($taginstanceidstomovecontext)) {static::change_instances_context($taginstanceidstomovecontext, $context);}$ordering = -1;foreach ($tagobjects as $name => $tag) {$ordering++;foreach ($currenttags as $currenttag) {$namesmatch = strval($currenttag->name) === strval($name);if ($allowmultiplecontexts) {// If the tag area allows multiple contexts then we should only// skip adding a new instance if the existing one is in the correct// context.$contextsmatch = $currenttag->taginstancecontextid == $context->id;$shouldskipinstance = $namesmatch && $contextsmatch;} else {// The existing behaviour for single context tag areas is to// skip adding a new instance regardless of whether the existing// instance is in the same context as the provided $context.$shouldskipinstance = $namesmatch;}if ($shouldskipinstance) {if ($currenttag->ordering != $ordering) {$currenttag->update_instance_ordering($currenttag->taginstanceid, $ordering);}continue 2;}}$tag->add_instance($component, $itemtype, $itemid, $context, $ordering, $tiuserid);}}/*** Removes all tags from an item.** All tags will be removed even if tagging is disabled in this area. This is* usually called when the item itself has been deleted.** @param string $component component responsible for tagging* @param string $itemtype type of the tagged item* @param int $itemid* @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)*/public static function remove_all_item_tags($component, $itemtype, $itemid, $tiuserid = 0) {$context = context_system::instance(); // Context will not be used.static::set_item_tags($component, $itemtype, $itemid, $context, null, $tiuserid);}/*** Adds a tag to an item, without overwriting the current tags.** If the tag has already been added to the record, no changes are made.** @param string $component the component that was tagged* @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)* @param int $itemid the id of the record to tag* @param context $context the context of where this tag was assigned* @param string $tagname the tag to add* @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)* @return int id of tag_instance that was either created or already existed or null if tagging is not enabled*/public static function add_item_tag($component, $itemtype, $itemid, context $context, $tagname, $tiuserid = 0) {global $DB;if (static::is_enabled($component, $itemtype) === false) {// Tagging area is properly defined but not enabled - do nothing.return null;}$rawname = clean_param($tagname, PARAM_TAG);$normalisedname = core_text::strtolower($rawname);$tagcollid = core_tag_area::get_collection($component, $itemtype);$usersql = $tiuserid ? " AND ti.tiuserid = :tiuserid " : "";$sql = 'SELECT t.*, ti.id AS taginstanceidFROM {tag} tLEFT JOIN {tag_instance} ti ON ti.tagid = t.id AND ti.itemtype = :itemtype '.$usersql .'AND ti.itemid = :itemid AND ti.component = :componentWHERE t.name = :name AND t.tagcollid = :tagcollid';$params = array('name' => $normalisedname, 'tagcollid' => $tagcollid, 'itemtype' => $itemtype,'itemid' => $itemid, 'component' => $component, 'tiuserid' => $tiuserid);$record = $DB->get_record_sql($sql, $params);if ($record) {if ($record->taginstanceid) {// Tag was already added to the item, nothing to do here.return $record->taginstanceid;}$tag = new static($record);} else {// The tag does not exist yet, create it.$tags = static::add($tagcollid, array($tagname));$tag = reset($tags);}$ordering = $DB->get_field_sql('SELECT MAX(ordering) FROM {tag_instance} tiWHERE ti.itemtype = :itemtype AND ti.itemid = :itemid ANDti.component = :component' . $usersql, $params);return $tag->add_instance($component, $itemtype, $itemid, $context, $ordering + 1, $tiuserid);}/*** Removes the tag from an item without changing the other tags** @param string $component the component that was tagged* @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)* @param int $itemid the id of the record to tag* @param string $tagname the tag to remove* @param int $tiuserid tag instance user id, only needed for tag areas with user tagging (such as core/course)*/public static function remove_item_tag($component, $itemtype, $itemid, $tagname, $tiuserid = 0) {global $DB;if (static::is_enabled($component, $itemtype) === false) {// Tagging area is properly defined but not enabled - do nothing.return array();}$rawname = clean_param($tagname, PARAM_TAG);$normalisedname = core_text::strtolower($rawname);$usersql = $tiuserid ? " AND tiuserid = :tiuserid " : "";$componentsql = $component ? " AND ti.component = :component " : "";$sql = 'SELECT t.*, ti.id AS taginstanceid, ti.contextid AS taginstancecontextid, ti.orderingFROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id ' . $usersql . 'WHERE t.name = :name AND ti.itemtype = :itemtypeAND ti.itemid = :itemid ' . $componentsql;$params = array('name' => $normalisedname,'itemtype' => $itemtype, 'itemid' => $itemid, 'component' => $component,'tiuserid' => $tiuserid);if ($record = $DB->get_record_sql($sql, $params)) {$taginstance = (object)array('id' => $record->taginstanceid,'itemtype' => $itemtype, 'itemid' => $itemid,'contextid' => $record->taginstancecontextid, 'tiuserid' => $tiuserid);$tag = new static($record);$tag->delete_instance_as_record($taginstance, false);$componentsql = $component ? " AND component = :component " : "";$sql = "UPDATE {tag_instance} SET ordering = ordering - 1WHERE itemtype = :itemtypeAND itemid = :itemid $componentsql $usersqlAND ordering > :ordering";$params['ordering'] = $record->ordering;$DB->execute($sql, $params);}}/*** Allows to move all tag instances from one context to another** @param string $component the component that was tagged* @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)* @param context $oldcontext* @param context $newcontext*/public static function move_context($component, $itemtype, $oldcontext, $newcontext) {global $DB;if ($oldcontext instanceof context) {$oldcontext = $oldcontext->id;}if ($newcontext instanceof context) {$newcontext = $newcontext->id;}$DB->set_field('tag_instance', 'contextid', $newcontext,array('component' => $component, 'itemtype' => $itemtype, 'contextid' => $oldcontext));}/*** Moves all tags of the specified items to the new context** @param string $component the component that was tagged* @param string $itemtype the type of record to tag ('post' for blogs, 'user' for users, etc.)* @param array $itemids* @param context|int $newcontext target context to move tags to*/public static function change_items_context($component, $itemtype, $itemids, $newcontext) {global $DB;if (empty($itemids)) {return;}if (!is_array($itemids)) {$itemids = array($itemids);}list($sql, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);$params['component'] = $component;$params['itemtype'] = $itemtype;if ($newcontext instanceof context) {$newcontext = $newcontext->id;}$DB->set_field_select('tag_instance', 'contextid', $newcontext,'component = :component AND itemtype = :itemtype AND itemid ' . $sql, $params);}/*** Moves all of the specified tag instances into a new context.** @param array $taginstanceids The list of tag instance ids that should be moved* @param context $newcontext The context to move the tag instances into*/public static function change_instances_context(array $taginstanceids, context $newcontext) {global $DB;if (empty($taginstanceids)) {return;}list($sql, $params) = $DB->get_in_or_equal($taginstanceids);$DB->set_field_select('tag_instance', 'contextid', $newcontext->id, "id {$sql}", $params);}/*** Updates the information about the tag** @param array|stdClass $data data to update, may contain: isstandard, description, descriptionformat, rawname* @return bool whether the tag was updated. False may be returned if: all new values match the existing,* or it was attempted to rename the tag to the name that is already used.*/public function update($data) {global $DB, $COURSE;$allowedfields = array('isstandard', 'description', 'descriptionformat', 'rawname');$data = (array)$data;if ($extrafields = array_diff(array_keys($data), $allowedfields)) {debugging('The field(s) '.join(', ', $extrafields).' will be ignored when updating the tag',DEBUG_DEVELOPER);}$data = array_intersect_key($data, array_fill_keys($allowedfields, 1));$this->ensure_fields_exist(array_merge(array('tagcollid', 'userid', 'name', 'rawname'), array_keys($data)), 'update');// Validate the tag name.if (array_key_exists('rawname', $data)) {$data['rawname'] = clean_param($data['rawname'], PARAM_TAG);$name = core_text::strtolower($data['rawname']);if (!$name || $data['rawname'] === $this->rawname) {unset($data['rawname']);} else if ($existing = static::get_by_name($this->tagcollid, $name, 'id')) {// Prevent the rename if a tag with that name already exists.if ($existing->id != $this->id) {throw new moodle_exception('namesalreadybeeingused', 'core_tag');}}if (isset($data['rawname'])) {$data['name'] = $name;}}// Validate the tag type.if (array_key_exists('isstandard', $data)) {$data['isstandard'] = $data['isstandard'] ? 1 : 0;}// Find only the attributes that need to be changed.$originalname = $this->name;foreach ($data as $key => $value) {if ($this->record->$key !== $value) {$this->record->$key = $value;} else {unset($data[$key]);}}if (empty($data)) {return false;}$data['id'] = $this->id;$data['timemodified'] = time();$DB->update_record('tag', $data);$event = \core\event\tag_updated::create(array('objectid' => $this->id,'relateduserid' => $this->userid,'context' => context_system::instance(),'other' => array('name' => $this->name,'rawname' => $this->rawname)));$event->trigger();return true;}/*** Flag a tag as inappropriate*/public function flag() {global $DB;$this->ensure_fields_exist(array('name', 'userid', 'rawname', 'flag'), 'flag');// Update all the tags to flagged.$this->timemodified = time();$this->flag++;$DB->update_record('tag', array('timemodified' => $this->timemodified,'flag' => $this->flag, 'id' => $this->id));$event = \core\event\tag_flagged::create(array('objectid' => $this->id,'relateduserid' => $this->userid,'context' => context_system::instance(),'other' => array('name' => $this->name,'rawname' => $this->rawname)));$event->trigger();}/*** Remove the inappropriate flag on a tag.*/public function reset_flag() {global $DB;$this->ensure_fields_exist(array('name', 'userid', 'rawname', 'flag'), 'flag');if (!$this->flag) {// Nothing to do.return false;}$this->timemodified = time();$this->flag = 0;$DB->update_record('tag', array('timemodified' => $this->timemodified,'flag' => 0, 'id' => $this->id));$event = \core\event\tag_unflagged::create(array('objectid' => $this->id,'relateduserid' => $this->userid,'context' => context_system::instance(),'other' => array('name' => $this->name,'rawname' => $this->rawname)));$event->trigger();}/*** Sets the list of tags related to this one.** Tag relations are recorded by two instances linking two tags to each other.* For tag relations ordering is not used and may be random.** @param array $tagnames*/public function set_related_tags($tagnames) {$context = context_system::instance();$tagobjects = $tagnames ? static::create_if_missing($this->tagcollid, $tagnames) : array();unset($tagobjects[$this->name]); // Never link to itself.$currenttags = static::get_item_tags('core', 'tag', $this->id);// For data coherence reasons, it's better to remove deleted tags// before adding new data: ordering could be duplicated.foreach ($currenttags as $currenttag) {if (!array_key_exists($currenttag->name, $tagobjects)) {$taginstance = (object)array('id' => $currenttag->taginstanceid,'itemtype' => 'tag', 'itemid' => $this->id,'contextid' => $context->id);$currenttag->delete_instance_as_record($taginstance, false);$this->delete_instance('core', 'tag', $currenttag->id);}}foreach ($tagobjects as $name => $tag) {foreach ($currenttags as $currenttag) {if ($currenttag->name === $name) {continue 2;}}$this->add_instance('core', 'tag', $tag->id, $context, 0);$tag->add_instance('core', 'tag', $this->id, $context, 0);$currenttags[] = $tag;}}/*** Adds to the list of related tags without removing existing** Tag relations are recorded by two instances linking two tags to each other.* For tag relations ordering is not used and may be random.** @param array $tagnames*/public function add_related_tags($tagnames) {$context = context_system::instance();$tagobjects = static::create_if_missing($this->tagcollid, $tagnames);$currenttags = static::get_item_tags('core', 'tag', $this->id);foreach ($tagobjects as $name => $tag) {foreach ($currenttags as $currenttag) {if ($currenttag->name === $name) {continue 2;}}$this->add_instance('core', 'tag', $tag->id, $context, 0);$tag->add_instance('core', 'tag', $this->id, $context, 0);$currenttags[] = $tag;}}/*** Returns the correlated tags of a tag, retrieved from the tag_correlation table.** Correlated tags are calculated in cron based on existing tag instances.** @param bool $keepduplicates if true, will return one record for each existing* tag instance which may result in duplicates of the actual tags* @return core_tag_tag[] an array of tag objects*/public function get_correlated_tags($keepduplicates = false) {global $DB;$correlated = $DB->get_field('tag_correlation', 'correlatedtags', array('tagid' => $this->id));if (!$correlated) {return array();}$correlated = preg_split('/\s*,\s*/', trim($correlated), -1, PREG_SPLIT_NO_EMPTY);list($query, $params) = $DB->get_in_or_equal($correlated);// This is (and has to) return the same fields as the query in core_tag_tag::get_item_tags().$sql = "SELECT ti.id AS taginstanceid, tg.id, tg.isstandard, tg.name, tg.rawname, tg.flag,tg.tagcollid, ti.ordering, ti.contextid AS taginstancecontextid, ti.itemidFROM {tag} tgINNER JOIN {tag_instance} ti ON tg.id = ti.tagidWHERE tg.id $query AND tg.id <> ? AND tg.tagcollid = ?ORDER BY ti.ordering ASC, ti.id";$params[] = $this->id;$params[] = $this->tagcollid;$records = $DB->get_records_sql($sql, $params);$seen = array();$result = array();foreach ($records as $id => $record) {if (!$keepduplicates && !empty($seen[$record->id])) {continue;}$result[$id] = new static($record);$seen[$record->id] = true;}return $result;}/*** Returns tags that this tag was manually set as related to** @return core_tag_tag[]*/public function get_manual_related_tags() {return self::get_item_tags('core', 'tag', $this->id);}/*** Returns tags related to a tag** Related tags of a tag come from two sources:* - manually added related tags, which are tag_instance entries for that tag* - correlated tags, which are calculated** @return core_tag_tag[] an array of tag objects*/public function get_related_tags() {$manual = $this->get_manual_related_tags();$automatic = $this->get_correlated_tags();$relatedtags = array_merge($manual, $automatic);// Remove duplicated tags (multiple instances of the same tag).$seen = array();foreach ($relatedtags as $instance => $tag) {if (isset($seen[$tag->id])) {unset($relatedtags[$instance]);} else {$seen[$tag->id] = 1;}}return $relatedtags;}/*** Find all items tagged with a tag of a given type ('post', 'user', etc.)** @param string $component component responsible for tagging. For BC it can be empty but in this case the* query will be slow because DB index will not be used.* @param string $itemtype type to restrict search to* @param int $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point.* @param int $limitnum (optional, required if $limitfrom is set) return a subset comprising this many records.* @param string $subquery additional query to be appended to WHERE clause, refer to the itemtable as 'it'* @param array $params additional parameters for the DB query* @return array of matching objects, indexed by record id, from the table containing the type requested*/public function get_tagged_items($component, $itemtype, $limitfrom = '', $limitnum = '', $subquery = '', $params = array()) {global $DB;if (empty($itemtype) || !$DB->get_manager()->table_exists($itemtype)) {return array();}$params = $params ? $params : array();$query = "SELECT it.*FROM {".$itemtype."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemidWHERE tt.itemtype = :itemtype AND tt.tagid = :tagid";$params['itemtype'] = $itemtype;$params['tagid'] = $this->id;if ($component) {$query .= ' AND tt.component = :component';$params['component'] = $component;}if ($subquery) {$query .= ' AND ' . $subquery;}$query .= ' ORDER BY it.id';return $DB->get_records_sql($query, $params, $limitfrom, $limitnum);}/*** Count how many items are tagged with a specific tag.** @param string $component component responsible for tagging. For BC it can be empty but in this case the* query will be slow because DB index will not be used.* @param string $itemtype type to restrict search to* @param string $subquery additional query to be appended to WHERE clause, refer to the itemtable as 'it'* @param array $params additional parameters for the DB query* @return int number of mathing tags.*/public function count_tagged_items($component, $itemtype, $subquery = '', $params = array()) {global $DB;if (empty($itemtype) || !$DB->get_manager()->table_exists($itemtype)) {return 0;}$params = $params ? $params : array();$query = "SELECT COUNT(it.id)FROM {".$itemtype."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemidWHERE tt.itemtype = :itemtype AND tt.tagid = :tagid";$params['itemtype'] = $itemtype;$params['tagid'] = $this->id;if ($component) {$query .= ' AND tt.component = :component';$params['component'] = $component;}if ($subquery) {$query .= ' AND ' . $subquery;}return $DB->get_field_sql($query, $params);}/*** Determine if an item is tagged with a specific tag** Note that this is a static method and not a method of core_tag object because the tag might not exist yet,* for example user searches for "php" and we offer him to add "php" to his interests.** @param string $component component responsible for tagging. For BC it can be empty but in this case the* query will be slow because DB index will not be used.* @param string $itemtype the record type to look for* @param int $itemid the record id to look for* @param string $tagname a tag name* @return int 1 if it is tagged, 0 otherwise*/public static function is_item_tagged_with($component, $itemtype, $itemid, $tagname) {global $DB;$tagcollid = core_tag_area::get_collection($component, $itemtype);$query = 'SELECT 1 FROM {tag} tJOIN {tag_instance} ti ON ti.tagid = t.idWHERE t.name = ? AND t.tagcollid = ? AND ti.itemtype = ? AND ti.itemid = ?';$cleanname = core_text::strtolower(clean_param($tagname, PARAM_TAG));$params = array($cleanname, $tagcollid, $itemtype, $itemid);if ($component) {$query .= ' AND ti.component = ?';$params[] = $component;}return $DB->record_exists_sql($query, $params) ? 1 : 0;}/*** Returns whether the tag area is enabled** @param string $component component responsible for tagging* @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc.* @return bool|null*/public static function is_enabled($component, $itemtype) {return core_tag_area::is_enabled($component, $itemtype);}/*** Retrieves contents of tag area for the tag/index.php page** @param stdClass $tagarea* @param bool $exclusivemode if set to true it means that no other entities tagged with this tag* are displayed on the page and the per-page limit may be bigger* @param int $fromctx context id where the link was displayed, may be used by callbacks* to display items in the same context first* @param int $ctx context id where to search for records* @param bool $rec search in subcontexts as well* @param int $page 0-based number of page being displayed* @return \core_tag\output\tagindex*/public function get_tag_index($tagarea, $exclusivemode, $fromctx, $ctx, $rec, $page = 0) {global $CFG;if (!empty($tagarea->callback)) {if (!empty($tagarea->callbackfile)) {require_once($CFG->dirroot . '/' . ltrim($tagarea->callbackfile, '/'));}$callback = $tagarea->callback;return call_user_func_array($callback, [$this, $exclusivemode, $fromctx, $ctx, $rec, $page]);}return null;}/*** Returns formatted description of the tag** @param array $options* @return string*/public function get_formatted_description($options = array()) {$options = empty($options) ? array() : (array)$options;$options += array('para' => false, 'overflowdiv' => true);$description = file_rewrite_pluginfile_urls($this->description, 'pluginfile.php',context_system::instance()->id, 'tag', 'description', $this->id);return format_text($description, $this->descriptionformat, $options);}/*** Returns the list of tag links available for the current user (edit, flag, etc.)** @return array*/public function get_links() {global $USER;$links = array();if (!isloggedin() || isguestuser()) {return $links;}$tagname = $this->get_display_name();$systemcontext = context_system::instance();// Add a link for users to add/remove this from their interests.if (static::is_enabled('core', 'user') && core_tag_area::get_collection('core', 'user') == $this->tagcollid) {if (static::is_item_tagged_with('core', 'user', $USER->id, $this->name)) {$url = new moodle_url('/tag/user.php', array('action' => 'removeinterest','sesskey' => sesskey(), 'tag' => $this->rawname));$links[] = html_writer::link($url, get_string('removetagfrommyinterests', 'tag', $tagname),array('class' => 'removefrommyinterests'));} else {$url = new moodle_url('/tag/user.php', array('action' => 'addinterest','sesskey' => sesskey(), 'tag' => $this->rawname));$links[] = html_writer::link($url, get_string('addtagtomyinterests', 'tag', $tagname),array('class' => 'addtomyinterests'));}}// Flag as inappropriate link. Only people with moodle/tag:flag capability.if (has_capability('moodle/tag:flag', $systemcontext)) {$url = new moodle_url('/tag/user.php', array('action' => 'flaginappropriate','sesskey' => sesskey(), 'id' => $this->id));$links[] = html_writer::link($url, get_string('flagasinappropriate', 'tag', $tagname),array('class' => 'flagasinappropriate'));}// Edit tag: Only people with moodle/tag:edit capability who either have it as an interest or can manage tags.if (has_capability('moodle/tag:edit', $systemcontext) ||has_capability('moodle/tag:manage', $systemcontext)) {$url = new moodle_url('/tag/edit.php', array('id' => $this->id));$links[] = html_writer::link($url, get_string('edittag', 'tag'),array('class' => 'edittag'));}return $links;}/*** Delete one or more tag, and all their instances if there are any left.** @param int|array $tagids one tagid (int), or one array of tagids to delete* @return bool true on success, false otherwise*/public static function delete_tags($tagids) {global $DB;if (!is_array($tagids)) {$tagids = array($tagids);}if (empty($tagids)) {return;}// Use the tagids to create a select statement to be used later.list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids);// Store the tags and tag instances we are going to delete.$tags = $DB->get_records_select('tag', 'id ' . $tagsql, $tagparams);$taginstances = $DB->get_records_select('tag_instance', 'tagid ' . $tagsql, $tagparams);// Delete all the tag instances.$select = 'WHERE tagid ' . $tagsql;$sql = "DELETE FROM {tag_instance} $select";$DB->execute($sql, $tagparams);// Delete all the tag correlations.$sql = "DELETE FROM {tag_correlation} $select";$DB->execute($sql, $tagparams);// Delete all the tags.$select = 'WHERE id ' . $tagsql;$sql = "DELETE FROM {tag} $select";$DB->execute($sql, $tagparams);// Fire an event that these items were untagged.if ($taginstances) {// Save the system context in case the 'contextid' column in the 'tag_instance' table is null.$syscontextid = context_system::instance()->id;// Loop through the tag instances and fire a 'tag_removed'' event.foreach ($taginstances as $taginstance) {// We can not fire an event with 'null' as the contextid.if (is_null($taginstance->contextid)) {$taginstance->contextid = $syscontextid;}// Trigger tag removed event.\core\event\tag_removed::create_from_tag_instance($taginstance,$tags[$taginstance->tagid]->name, $tags[$taginstance->tagid]->rawname,true)->trigger();}}// Fire an event that these tags were deleted.if ($tags) {$context = context_system::instance();foreach ($tags as $tag) {// Delete all files associated with this tag.$fs = get_file_storage();$files = $fs->get_area_files($context->id, 'tag', 'description', $tag->id);foreach ($files as $file) {$file->delete();}// Trigger an event for deleting this tag.$event = \core\event\tag_deleted::create(array('objectid' => $tag->id,'relateduserid' => $tag->userid,'context' => $context,'other' => array('name' => $tag->name,'rawname' => $tag->rawname)));$event->add_record_snapshot('tag', $tag);$event->trigger();}}return true;}/*** Combine together correlated tags of several tags** This is a help method for method combine_tags()** @param core_tag_tag[] $tags*/protected function combine_correlated_tags($tags) {global $DB;$ids = array_map(function($t) {return $t->id;}, $tags);// Retrieve the correlated tags of this tag and correlated tags of all tags to be merged in one query// but store them separately. Calculate the list of correlated tags that need to be added to the current.list($sql, $params) = $DB->get_in_or_equal($ids);$params[] = $this->id;$records = $DB->get_records_select('tag_correlation', 'tagid '.$sql.' OR tagid = ?',$params, '', 'tagid, id, correlatedtags');$correlated = array();$mycorrelated = array();foreach ($records as $record) {$taglist = preg_split('/\s*,\s*/', trim($record->correlatedtags), -1, PREG_SPLIT_NO_EMPTY);if ($record->tagid == $this->id) {$mycorrelated = $taglist;} else {$correlated = array_merge($correlated, $taglist);}}array_unique($correlated);// Strip out from $correlated the ids of the tags that are already in $mycorrelated// or are one of the tags that are going to be combined.$correlated = array_diff($correlated, [$this->id], $ids, $mycorrelated);if (empty($correlated)) {// Nothing to do, ignore situation when current tag is correlated to one of the merged tags - they will// be deleted later and get_tag_correlation() will not return them. Next cron will clean everything up.return;}// Update correlated tags of this tag.$newcorrelatedlist = join(',', array_merge($mycorrelated, $correlated));if (isset($records[$this->id])) {$DB->update_record('tag_correlation', array('id' => $records[$this->id]->id, 'correlatedtags' => $newcorrelatedlist));} else {$DB->insert_record('tag_correlation', array('tagid' => $this->id, 'correlatedtags' => $newcorrelatedlist));}// Add this tag to the list of correlated tags of each tag in $correlated.list($sql, $params) = $DB->get_in_or_equal($correlated);$records = $DB->get_records_select('tag_correlation', 'tagid '.$sql, $params, '', 'tagid, id, correlatedtags');foreach ($correlated as $tagid) {if (isset($records[$tagid])) {$newcorrelatedlist = $records[$tagid]->correlatedtags . ',' . $this->id;$DB->update_record('tag_correlation', array('id' => $records[$tagid]->id, 'correlatedtags' => $newcorrelatedlist));} else {$DB->insert_record('tag_correlation', array('tagid' => $tagid, 'correlatedtags' => '' . $this->id));}}}/*** Combines several other tags into this one** Combining rules:* - current tag becomes the "main" one, all instances* pointing to other tags are changed to point to it.* - if any of the tags is standard, the "main" tag becomes standard too* - all tags except for the current ("main") are deleted, even when they are standard** @param core_tag_tag[] $tags tags to combine into this one*/public function combine_tags($tags) {global $DB;$this->ensure_fields_exist(array('id', 'tagcollid', 'isstandard', 'name', 'rawname'), 'combine_tags');// Retrieve all tag objects, find if there are any standard tags in the set.$isstandard = false;$tagstocombine = array();$ids = array();$relatedtags = $this->get_manual_related_tags();foreach ($tags as $tag) {$tag->ensure_fields_exist(array('id', 'tagcollid', 'isstandard', 'tagcollid', 'name', 'rawname'), 'combine_tags');if ($tag && $tag->id != $this->id && $tag->tagcollid == $this->tagcollid) {$isstandard = $isstandard || $tag->isstandard;$tagstocombine[$tag->name] = $tag;$ids[] = $tag->id;$relatedtags = array_merge($relatedtags, $tag->get_manual_related_tags());}}if (empty($tagstocombine)) {// Nothing to do.return;}// Combine all manually set related tags, exclude itself all the tags it is about to be combined with.if ($relatedtags) {$relatedtags = array_map(function($t) {return $t->name;}, $relatedtags);array_unique($relatedtags);$relatedtags = array_diff($relatedtags, [$this->name], array_keys($tagstocombine));}$this->set_related_tags($relatedtags);// Combine all correlated tags, exclude itself all the tags it is about to be combined with.$this->combine_correlated_tags($tagstocombine);// If any of the duplicate tags are standard, mark this one as standard too.if ($isstandard && !$this->isstandard) {$this->update(array('isstandard' => 1));}// Go through all instances of each tag that needs to be combined and make them point to this tag instead.// We go though the list one by one because otherwise looking-for-duplicates logic would be too complicated.foreach ($tagstocombine as $tag) {$params = array('tagid' => $tag->id, 'mainid' => $this->id);$mainsql = 'SELECT ti.*, t.name, t.rawname, tim.id AS alreadyhasmaintag '. 'FROM {tag_instance} ti '. 'LEFT JOIN {tag} t ON t.id = ti.tagid '. 'LEFT JOIN {tag_instance} tim ON ti.component = tim.component AND '. ' ti.itemtype = tim.itemtype AND ti.itemid = tim.itemid AND '. ' ti.tiuserid = tim.tiuserid AND tim.tagid = :mainid '. 'WHERE ti.tagid = :tagid';$records = $DB->get_records_sql($mainsql, $params);foreach ($records as $record) {if ($record->alreadyhasmaintag) {// Item is tagged with both main tag and the duplicate tag.// Remove instance pointing to the duplicate tag.$tag->delete_instance_as_record($record, false);$sql = "UPDATE {tag_instance} SET ordering = ordering - 1WHERE itemtype = :itemtypeAND itemid = :itemid AND component = :component AND tiuserid = :tiuseridAND ordering > :ordering";$DB->execute($sql, (array)$record);} else {// Item is tagged only with duplicate tag but not the main tag.// Replace tagid in the instance pointing to the duplicate tag with this tag.$DB->update_record('tag_instance', array('id' => $record->id, 'tagid' => $this->id));\core\event\tag_removed::create_from_tag_instance($record, $record->name, $record->rawname)->trigger();$record->tagid = $this->id;\core\event\tag_added::create_from_tag_instance($record, $this->name, $this->rawname)->trigger();}}}// Finally delete all tags that we combined into the current one.self::delete_tags($ids);}/*** Retrieve a list of tags that have been used to tag the given $component* and $itemtype in the provided $contexts.** @param string $component The tag instance component* @param string $itemtype The tag instance item type* @param context[] $contexts The list of contexts to look for tag instances in* @return core_tag_tag[]*/public static function get_tags_by_area_in_contexts($component, $itemtype, array $contexts) {global $DB;$params = [$component, $itemtype];$contextids = array_map(function($context) {return $context->id;}, $contexts);list($contextsql, $contextsqlparams) = $DB->get_in_or_equal($contextids);$params = array_merge($params, $contextsqlparams);$subsql = "SELECT DISTINCT ti.tagidFROM {tag_instance} tiWHERE ti.component = ?AND ti.itemtype = ?AND ti.contextid {$contextsql}";$sql = "SELECT tt.*FROM ($subsql) tvJOIN {tag} tt ON tt.id = tv.tagid";return array_map(function($record) {return new core_tag_tag($record);}, $DB->get_records_sql($sql, $params));}}