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 * Class core_tag_area for managing tag areas 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 * Class to manage tag areas 29 * 30 * @package core_tag 31 * @copyright 2015 Marina Glancy 32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 */ 34 class core_tag_area { 35 36 /** 37 * Returns the list of areas indexed by itemtype and component 38 * 39 * @param int $tagcollid return only areas in this tag collection 40 * @param bool $enabledonly return only enabled tag areas 41 * @return array itemtype=>component=>tagarea object 42 */ 43 public static function get_areas($tagcollid = null, $enabledonly = false) { 44 global $DB; 45 $cache = cache::make('core', 'tags'); 46 if (($itemtypes = $cache->get('tag_area')) === false) { 47 $colls = core_tag_collection::get_collections(); 48 $defaultcoll = reset($colls); 49 $itemtypes = array(); 50 $areas = $DB->get_records('tag_area', array(), 'component,itemtype'); 51 foreach ($areas as $area) { 52 if ($colls[$area->tagcollid]->component) { 53 $area->locked = true; 54 } 55 $itemtypes[$area->itemtype][$area->component] = $area; 56 } 57 $cache->set('tag_area', $itemtypes); 58 } 59 if ($tagcollid || $enabledonly) { 60 $rv = array(); 61 foreach ($itemtypes as $itemtype => $it) { 62 foreach ($it as $component => $v) { 63 if (($v->tagcollid == $tagcollid || !$tagcollid) && (!$enabledonly || $v->enabled)) { 64 $rv[$itemtype][$component] = $v; 65 } 66 } 67 } 68 return $rv; 69 } 70 return $itemtypes; 71 } 72 73 /** 74 * Retrieves info about one tag area 75 * 76 * @param int $tagareaid 77 * @return stdClass 78 */ 79 public static function get_by_id($tagareaid) { 80 $tagareas = self::get_areas(); 81 foreach ($tagareas as $itemtype => $it) { 82 foreach ($it as $component => $v) { 83 if ($v->id == $tagareaid) { 84 return $v; 85 } 86 } 87 } 88 return null; 89 } 90 91 /** 92 * Returns the display name for this area 93 * 94 * @param string $component 95 * @param string $itemtype 96 * @return lang_string 97 */ 98 public static function display_name($component, $itemtype) { 99 $identifier = 'tagarea_' . clean_param($itemtype, PARAM_STRINGID); 100 if ($component === 'core') { 101 $component = 'tag'; 102 } 103 return new lang_string($identifier, $component); 104 } 105 106 /** 107 * Returns whether the tag area is enabled 108 * 109 * @param string $component component responsible for tagging 110 * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. 111 * @return bool|null 112 */ 113 public static function is_enabled($component, $itemtype) { 114 global $CFG; 115 if (empty($CFG->usetags)) { 116 return false; 117 } 118 $itemtypes = self::get_areas(); 119 if (isset($itemtypes[$itemtype][$component])) { 120 return $itemtypes[$itemtype][$component]->enabled ? true : false; 121 } 122 return null; 123 } 124 125 /** 126 * Checks if the tag area allows items to be tagged in multiple different contexts. 127 * 128 * If true then it indicates that not all tag instance contexts must match the 129 * context of the item they are tagging. If false then all tag instance should 130 * match the context of the item they are tagging. 131 * 132 * Example use case for multi-context tagging: 133 * A question that exists in a course category context may be used by multiple 134 * child courses. The question tag area can allow tag instances to be created in 135 * multiple contexts which allows the tag API to tag the question at the course 136 * category context and then seperately in each of the child course contexts. 137 * 138 * @param string $component component responsible for tagging 139 * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. 140 * @return bool 141 */ 142 public static function allows_tagging_in_multiple_contexts($component, $itemtype) { 143 $itemtypes = self::get_areas(); 144 if (isset($itemtypes[$itemtype][$component])) { 145 $config = $itemtypes[$itemtype][$component]; 146 return isset($config->multiplecontexts) ? $config->multiplecontexts : false; 147 } 148 return false; 149 } 150 151 /** 152 * Returns the id of the tag collection that should be used for storing tags of this itemtype 153 * 154 * @param string $component component responsible for tagging 155 * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. 156 * @return int 157 */ 158 public static function get_collection($component, $itemtype) { 159 $itemtypes = self::get_areas(); 160 if (array_key_exists($itemtype, $itemtypes)) { 161 if (!array_key_exists($component, $itemtypes[$itemtype])) { 162 $component = key($itemtypes[$itemtype]); 163 } 164 return $itemtypes[$itemtype][$component]->tagcollid; 165 } 166 return core_tag_collection::get_default(); 167 } 168 169 /** 170 * Returns wether this tag area should display or not standard tags when user edits it. 171 * 172 * @param string $component component responsible for tagging 173 * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. 174 * @return int 175 */ 176 public static function get_showstandard($component, $itemtype) { 177 $itemtypes = self::get_areas(); 178 if (array_key_exists($itemtype, $itemtypes)) { 179 if (!array_key_exists($component, $itemtypes[$itemtype])) { 180 $component = key($itemtypes[$itemtype]); 181 } 182 return $itemtypes[$itemtype][$component]->showstandard; 183 } 184 return core_tag_tag::BOTH_STANDARD_AND_NOT; 185 } 186 187 /** 188 * Returns all tag areas and collections that are currently cached in DB for this component 189 * 190 * @param string $componentname 191 * @return array first element is the list of areas and the second list of collections 192 */ 193 protected static function get_definitions_for_component($componentname) { 194 global $DB; 195 list($a, $b) = core_component::normalize_component($componentname); 196 $component = $b ? ($a . '_' . $b) : $a; 197 $sql = 'component = :component'; 198 $params = array('component' => $component); 199 if ($component === 'core') { 200 $sql .= ' OR component LIKE :coreprefix'; 201 $params['coreprefix'] = 'core_%'; 202 } 203 $fields = $DB->sql_concat_join("':'", array('itemtype', 'component')); 204 $existingareas = $DB->get_records_sql( 205 "SELECT $fields AS returnkey, a.* FROM {tag_area} a WHERE $sql", $params); 206 $fields = $DB->sql_concat_join("':'", array('name', 'component')); 207 $existingcolls = $DB->get_records_sql( 208 "SELECT $fields AS returnkey, t.* FROM {tag_coll} t WHERE $sql", $params); 209 return array($existingareas, $existingcolls); 210 211 } 212 213 /** 214 * Completely delete a tag area and all instances inside it 215 * 216 * @param stdClass $record 217 */ 218 protected static function delete($record) { 219 global $DB; 220 221 core_tag_tag::delete_instances($record->component, $record->itemtype); 222 223 $DB->delete_records('tag_area', 224 array('itemtype' => $record->itemtype, 225 'component' => $record->component)); 226 227 // Reset cache. 228 cache::make('core', 'tags')->delete('tag_area'); 229 } 230 231 /** 232 * Create a new tag area 233 * 234 * @param stdClass $record 235 */ 236 protected static function create($record) { 237 global $DB; 238 if (empty($record->tagcollid)) { 239 $record->tagcollid = core_tag_collection::get_default(); 240 } 241 $DB->insert_record('tag_area', array('component' => $record->component, 242 'itemtype' => $record->itemtype, 243 'tagcollid' => $record->tagcollid, 244 'callback' => $record->callback, 245 'callbackfile' => $record->callbackfile, 246 'showstandard' => isset($record->showstandard) ? $record->showstandard : core_tag_tag::BOTH_STANDARD_AND_NOT, 247 'multiplecontexts' => isset($record->multiplecontexts) ? $record->multiplecontexts : 0)); 248 249 // Reset cache. 250 cache::make('core', 'tags')->delete('tag_area'); 251 } 252 253 /** 254 * Update the tag area 255 * 256 * @param stdClass $existing current record from DB table tag_area 257 * @param array|stdClass $data fields that need updating 258 */ 259 public static function update($existing, $data) { 260 global $DB; 261 $data = array_intersect_key((array)$data, 262 array('enabled' => 1, 'tagcollid' => 1, 263 'callback' => 1, 'callbackfile' => 1, 'showstandard' => 1, 264 'multiplecontexts' => 1)); 265 foreach ($data as $key => $value) { 266 if ($existing->$key == $value) { 267 unset($data[$key]); 268 } 269 } 270 if (!$data) { 271 return; 272 } 273 274 if (!empty($data['tagcollid'])) { 275 self::move_tags($existing->component, $existing->itemtype, $data['tagcollid']); 276 } 277 278 $data['id'] = $existing->id; 279 $DB->update_record('tag_area', $data); 280 281 // Reset cache. 282 cache::make('core', 'tags')->delete('tag_area'); 283 } 284 285 /** 286 * Update the database to contain a list of tagged areas for a component. 287 * The list of tagged areas is read from [plugindir]/db/tag.php 288 * 289 * @param string $componentname - The frankenstyle component name. 290 */ 291 public static function reset_definitions_for_component($componentname) { 292 global $DB; 293 $dir = core_component::get_component_directory($componentname); 294 $file = $dir . '/db/tag.php'; 295 $tagareas = null; 296 if (file_exists($file)) { 297 require_once($file); 298 } 299 300 list($a, $b) = core_component::normalize_component($componentname); 301 $component = $b ? ($a . '_' . $b) : $a; 302 303 list($existingareas, $existingcolls) = self::get_definitions_for_component($componentname); 304 305 $itemtypes = array(); 306 $collections = array(); 307 $needcleanup = false; 308 if ($tagareas) { 309 foreach ($tagareas as $tagarea) { 310 $record = (object)$tagarea; 311 if ($component !== 'core' || empty($record->component)) { 312 if (isset($record->component) && $record->component !== $component) { 313 debugging("Item type {$record->itemtype} has illegal component {$record->component}", DEBUG_DEVELOPER); 314 } 315 $record->component = $component; 316 } 317 unset($record->tagcollid); 318 if (!empty($record->collection)) { 319 // Create collection if it does not exist, or update 'searchable' and/or 'customurl' if needed. 320 $key = $record->collection . ':' . $record->component; 321 $collectiondata = array_intersect_key((array)$record, 322 array('component' => 1, 'searchable' => 1, 'customurl' => 1)); 323 $collectiondata['name'] = $record->collection; 324 if (!array_key_exists($key, $existingcolls)) { 325 $existingcolls[$key] = core_tag_collection::create($collectiondata); 326 } else { 327 core_tag_collection::update($existingcolls[$key], $collectiondata); 328 } 329 $record->tagcollid = $existingcolls[$key]->id; 330 $collections[$key] = $existingcolls[$key]; 331 unset($record->collection); 332 } 333 unset($record->searchable); 334 unset($record->customurl); 335 if (!isset($record->callback)) { 336 $record->callback = null; 337 } 338 if (!isset($record->callbackfile)) { 339 $record->callbackfile = null; 340 } 341 if (!isset($record->multiplecontexts)) { 342 $record->multiplecontexts = false; 343 } 344 $itemtypes[$record->itemtype . ':' . $record->component] = $record; 345 } 346 } 347 $todeletearea = array_diff_key($existingareas, $itemtypes); 348 $todeletecoll = array_diff_key($existingcolls, $collections); 349 350 // Delete tag areas that are no longer needed. 351 foreach ($todeletearea as $key => $record) { 352 self::delete($record); 353 } 354 355 // Update tag areas if changed. 356 $toupdatearea = array_intersect_key($existingareas, $itemtypes); 357 foreach ($toupdatearea as $key => $tagarea) { 358 if (!isset($itemtypes[$key]->tagcollid)) { 359 foreach ($todeletecoll as $tagcoll) { 360 if ($tagcoll->id == $tagarea->tagcollid) { 361 $itemtypes[$key]->tagcollid = core_tag_collection::get_default(); 362 } 363 } 364 } 365 unset($itemtypes[$key]->showstandard); // Do not override value that was already changed by admin with the default. 366 self::update($tagarea, $itemtypes[$key]); 367 } 368 369 // Create new tag areas. 370 $toaddarea = array_diff_key($itemtypes, $existingareas); 371 foreach ($toaddarea as $record) { 372 self::create($record); 373 } 374 375 // Delete tag collections that are no longer needed. 376 foreach ($todeletecoll as $key => $tagcoll) { 377 core_tag_collection::delete($tagcoll); 378 } 379 } 380 381 /** 382 * Deletes all tag areas, collections and instances associated with the plugin. 383 * 384 * @param string $pluginname 385 */ 386 public static function uninstall($pluginname) { 387 global $DB; 388 389 list($a, $b) = core_component::normalize_component($pluginname); 390 if (empty($b) || $a === 'core') { 391 throw new coding_exception('Core component can not be uninstalled'); 392 } 393 $component = $a . '_' . $b; 394 395 core_tag_tag::delete_instances($component); 396 397 $DB->delete_records('tag_area', array('component' => $component)); 398 $DB->delete_records('tag_coll', array('component' => $component)); 399 cache::make('core', 'tags')->delete_many(array('tag_area', 'tag_coll')); 400 } 401 402 /** 403 * Moves existing tags associated with an item type to another tag collection 404 * 405 * @param string $component 406 * @param string $itemtype 407 * @param int $tagcollid 408 */ 409 public static function move_tags($component, $itemtype, $tagcollid) { 410 global $DB; 411 $params = array('itemtype1' => $itemtype, 'component1' => $component, 412 'itemtype2' => $itemtype, 'component2' => $component, 413 'tagcollid1' => $tagcollid, 'tagcollid2' => $tagcollid); 414 415 // Find all collections that need to be cleaned later. 416 $sql = "SELECT DISTINCT t.tagcollid " . 417 "FROM {tag_instance} ti " . 418 "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 " . 419 "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 "; 420 $cleanupcollections = $DB->get_fieldset_sql($sql, $params); 421 422 // Find all tags that are related to the tags being moved and make sure they are present in the target tagcoll. 423 // This query is a little complicated because Oracle does not allow to run SELECT DISTINCT on CLOB fields. 424 $sql = "SELECT name, rawname, description, descriptionformat, userid, isstandard, flag, timemodified ". 425 "FROM {tag} WHERE id IN ". 426 "(SELECT r.id ". 427 "FROM {tag_instance} ti ". // Instances that need moving. 428 "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 ". // Tags that need moving. 429 "JOIN {tag_instance} tr ON tr.itemtype = 'tag' and tr.component = 'core' AND tr.itemid = t.id ". 430 "JOIN {tag} r ON r.id = tr.tagid ". // Tags related to the tags that need moving. 431 "LEFT JOIN {tag} re ON re.name = r.name AND re.tagcollid = :tagcollid2 ". // Existing tags in the target tagcoll with the same name as related tags. 432 "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 ". 433 " AND re.id IS NULL)"; // We need related tags that ARE NOT present in the target tagcoll. 434 $result = $DB->get_records_sql($sql, $params); 435 foreach ($result as $tag) { 436 $tag->tagcollid = $tagcollid; 437 $tag->id = $DB->insert_record('tag', $tag); 438 \core\event\tag_created::create_from_tag($tag); 439 } 440 441 // Find all tags that need moving and have related tags, remember their related tags. 442 $sql = "SELECT t.name AS tagname, r.rawname AS relatedtag ". 443 "FROM {tag_instance} ti ". // Instances that need moving. 444 "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 ". // Tags that need moving. 445 "JOIN {tag_instance} tr ON t.id = tr.tagid AND tr.itemtype = 'tag' and tr.component = 'core' ". 446 "JOIN {tag} r ON r.id = tr.itemid ". // Tags related to the tags that need moving. 447 "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 ". 448 "ORDER BY t.id, tr.ordering "; 449 $relatedtags = array(); 450 $result = $DB->get_recordset_sql($sql, $params); 451 foreach ($result as $record) { 452 $relatedtags[$record->tagname][] = $record->relatedtag; 453 } 454 $result->close(); 455 456 // Find all tags that are used for this itemtype/component and are not present in the target tag collection. 457 // This query is a little complicated because Oracle does not allow to run SELECT DISTINCT on CLOB fields. 458 $sql = "SELECT id, name, rawname, description, descriptionformat, userid, isstandard, flag, timemodified 459 FROM {tag} WHERE id IN 460 (SELECT t.id 461 FROM {tag_instance} ti 462 JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 463 LEFT JOIN {tag} tt ON tt.name = t.name AND tt.tagcollid = :tagcollid2 464 WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 465 AND tt.id IS NULL)"; 466 $movedtags = array(); // Keep track of moved tags so we don't hit DB index violation. 467 $result = $DB->get_records_sql($sql, $params); 468 foreach ($result as $tag) { 469 $originaltagid = $tag->id; 470 if (array_key_exists($tag->name, $movedtags)) { 471 // Case of corrupted data when the same tag was in several collections. 472 $tag->id = $movedtags[$tag->name]; 473 } else { 474 // Copy the tag into the new collection. 475 unset($tag->id); 476 $tag->tagcollid = $tagcollid; 477 $tag->id = $DB->insert_record('tag', $tag); 478 \core\event\tag_created::create_from_tag($tag); 479 $movedtags[$tag->name] = $tag->id; 480 } 481 $DB->execute("UPDATE {tag_instance} SET tagid = ? WHERE tagid = ? AND itemtype = ? AND component = ?", 482 array($tag->id, $originaltagid, $itemtype, $component)); 483 } 484 485 // Find all tags that are used for this itemtype/component and are already present in the target tag collection. 486 $sql = "SELECT DISTINCT t.id, tt.id AS targettagid 487 FROM {tag_instance} ti 488 JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 489 JOIN {tag} tt ON tt.name = t.name AND tt.tagcollid = :tagcollid2 490 WHERE ti.itemtype = :itemtype2 AND ti.component = :component2"; 491 $result = $DB->get_records_sql($sql, $params); 492 foreach ($result as $tag) { 493 $DB->execute("UPDATE {tag_instance} SET tagid = ? WHERE tagid = ? AND itemtype = ? AND component = ?", 494 array($tag->targettagid, $tag->id, $itemtype, $component)); 495 } 496 497 // Add related tags to the moved tags. 498 if ($relatedtags) { 499 $tags = core_tag_tag::get_by_name_bulk($tagcollid, array_keys($relatedtags)); 500 foreach ($tags as $tag) { 501 $tag->add_related_tags($relatedtags[$tag->name]); 502 } 503 } 504 505 if ($cleanupcollections) { 506 core_tag_collection::cleanup_unused_tags($cleanupcollections); 507 } 508 509 // Reset caches. 510 cache::make('core', 'tags')->delete('tag_area'); 511 } 512 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body