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