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