Commit | Line | Data |
---|---|---|
c026a28d MG |
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 | * Returns the id of the tag collection that should be used for storing tags of this itemtype | |
127 | * | |
128 | * @param string $component component responsible for tagging | |
129 | * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. | |
130 | * @return int | |
131 | */ | |
132 | public static function get_collection($component, $itemtype) { | |
133 | $itemtypes = self::get_areas(); | |
134 | if (array_key_exists($itemtype, $itemtypes)) { | |
135 | if (!array_key_exists($component, $itemtypes[$itemtype])) { | |
136 | $component = key($itemtypes[$itemtype]); | |
137 | } | |
138 | return $itemtypes[$itemtype][$component]->tagcollid; | |
139 | } | |
140 | return core_tag_collection::get_default(); | |
141 | } | |
142 | ||
4be9c7ad MG |
143 | /** |
144 | * Returns wether this tag area should display or not standard tags when user edits it. | |
145 | * | |
146 | * @param string $component component responsible for tagging | |
147 | * @param string $itemtype what is being tagged, for example, 'post', 'course', 'user', etc. | |
148 | * @return int | |
149 | */ | |
150 | public static function get_showstandard($component, $itemtype) { | |
151 | $itemtypes = self::get_areas(); | |
152 | if (array_key_exists($itemtype, $itemtypes)) { | |
153 | if (!array_key_exists($component, $itemtypes[$itemtype])) { | |
154 | $component = key($itemtypes[$itemtype]); | |
155 | } | |
156 | return $itemtypes[$itemtype][$component]->showstandard; | |
157 | } | |
158 | return core_tag_tag::BOTH_STANDARD_AND_NOT; | |
159 | } | |
160 | ||
c026a28d MG |
161 | /** |
162 | * Returns all tag areas and collections that are currently cached in DB for this component | |
163 | * | |
164 | * @param string $componentname | |
165 | * @return array first element is the list of areas and the second list of collections | |
166 | */ | |
167 | protected static function get_definitions_for_component($componentname) { | |
168 | global $DB; | |
169 | list($a, $b) = core_component::normalize_component($componentname); | |
170 | $component = $b ? ($a . '_' . $b) : $a; | |
171 | $sql = 'component = :component'; | |
172 | $params = array('component' => $component); | |
173 | if ($component === 'core') { | |
174 | $sql .= ' OR component LIKE :coreprefix'; | |
175 | $params['coreprefix'] = 'core_%'; | |
176 | } | |
177 | $fields = $DB->sql_concat_join("':'", array('itemtype', 'component')); | |
178 | $existingareas = $DB->get_records_sql( | |
179 | "SELECT $fields AS returnkey, a.* FROM {tag_area} a WHERE $sql", $params); | |
180 | $fields = $DB->sql_concat_join("':'", array('name', 'component')); | |
181 | $existingcolls = $DB->get_records_sql( | |
182 | "SELECT $fields AS returnkey, t.* FROM {tag_coll} t WHERE $sql", $params); | |
183 | return array($existingareas, $existingcolls); | |
184 | ||
185 | } | |
186 | ||
187 | /** | |
188 | * Completely delete a tag area and all instances inside it | |
189 | * | |
190 | * @param stdClass $record | |
191 | */ | |
192 | protected static function delete($record) { | |
193 | global $DB; | |
194 | ||
195 | core_tag_tag::delete_instances($record->component, $record->itemtype); | |
196 | ||
197 | $DB->delete_records('tag_area', | |
198 | array('itemtype' => $record->itemtype, | |
199 | 'component' => $record->component)); | |
200 | ||
201 | // Reset cache. | |
202 | cache::make('core', 'tags')->delete('tag_area'); | |
203 | } | |
204 | ||
205 | /** | |
206 | * Create a new tag area | |
207 | * | |
208 | * @param stdClass $record | |
209 | */ | |
210 | protected static function create($record) { | |
211 | global $DB; | |
212 | if (empty($record->tagcollid)) { | |
213 | $record->tagcollid = core_tag_collection::get_default(); | |
214 | } | |
215 | $DB->insert_record('tag_area', array('component' => $record->component, | |
216 | 'itemtype' => $record->itemtype, | |
217 | 'tagcollid' => $record->tagcollid, | |
218 | 'callback' => $record->callback, | |
4be9c7ad MG |
219 | 'callbackfile' => $record->callbackfile, |
220 | 'showstandard' => isset($record->showstandard) ? $record->showstandard : core_tag_tag::BOTH_STANDARD_AND_NOT)); | |
c026a28d MG |
221 | |
222 | // Reset cache. | |
223 | cache::make('core', 'tags')->delete('tag_area'); | |
224 | } | |
225 | ||
226 | /** | |
227 | * Update the tag area | |
228 | * | |
229 | * @param stdClass $existing current record from DB table tag_area | |
230 | * @param array|stdClass $data fields that need updating | |
231 | */ | |
232 | public static function update($existing, $data) { | |
233 | global $DB; | |
234 | $data = array_intersect_key((array)$data, | |
235 | array('enabled' => 1, 'tagcollid' => 1, | |
4be9c7ad | 236 | 'callback' => 1, 'callbackfile' => 1, 'showstandard' => 1)); |
c026a28d MG |
237 | foreach ($data as $key => $value) { |
238 | if ($existing->$key == $value) { | |
239 | unset($data[$key]); | |
240 | } | |
241 | } | |
242 | if (!$data) { | |
243 | return; | |
244 | } | |
245 | ||
246 | if (!empty($data['tagcollid'])) { | |
247 | self::move_tags($existing->component, $existing->itemtype, $data['tagcollid']); | |
248 | } | |
249 | ||
250 | $data['id'] = $existing->id; | |
251 | $DB->update_record('tag_area', $data); | |
252 | ||
253 | // Reset cache. | |
254 | cache::make('core', 'tags')->delete('tag_area'); | |
255 | } | |
256 | ||
257 | /** | |
258 | * Update the database to contain a list of tagged areas for a component. | |
259 | * The list of tagged areas is read from [plugindir]/db/tag.php | |
260 | * | |
261 | * @param string $componentname - The frankenstyle component name. | |
262 | */ | |
263 | public static function reset_definitions_for_component($componentname) { | |
264 | global $DB; | |
265 | $dir = core_component::get_component_directory($componentname); | |
266 | $file = $dir . '/db/tag.php'; | |
267 | $tagareas = null; | |
268 | if (file_exists($file)) { | |
269 | require_once($file); | |
270 | } | |
271 | ||
272 | list($a, $b) = core_component::normalize_component($componentname); | |
273 | $component = $b ? ($a . '_' . $b) : $a; | |
274 | ||
275 | list($existingareas, $existingcolls) = self::get_definitions_for_component($componentname); | |
276 | ||
277 | $itemtypes = array(); | |
278 | $collections = array(); | |
279 | $needcleanup = false; | |
280 | if ($tagareas) { | |
281 | foreach ($tagareas as $tagarea) { | |
282 | $record = (object)$tagarea; | |
283 | if ($component !== 'core' || empty($record->component)) { | |
284 | if (isset($record->component) && $record->component !== $component) { | |
285 | debugging("Item type {$record->itemtype} has illegal component {$record->component}", DEBUG_DEVELOPER); | |
286 | } | |
287 | $record->component = $component; | |
288 | } | |
289 | unset($record->tagcollid); | |
290 | if (!empty($record->collection)) { | |
291 | // Create collection if it does not exist, or update 'searchable' and/or 'customurl' if needed. | |
292 | $key = $record->collection . ':' . $record->component; | |
293 | $collectiondata = array_intersect_key((array)$record, | |
294 | array('component' => 1, 'searchable' => 1, 'customurl' => 1)); | |
295 | $collectiondata['name'] = $record->collection; | |
296 | if (!array_key_exists($key, $existingcolls)) { | |
297 | $existingcolls[$key] = core_tag_collection::create($collectiondata); | |
298 | } else { | |
299 | core_tag_collection::update($existingcolls[$key], $collectiondata); | |
300 | } | |
301 | $record->tagcollid = $existingcolls[$key]->id; | |
302 | $collections[$key] = $existingcolls[$key]; | |
303 | unset($record->collection); | |
304 | } | |
305 | unset($record->searchable); | |
306 | unset($record->customurl); | |
307 | if (!isset($record->callback)) { | |
308 | $record->callback = null; | |
309 | } | |
310 | if (!isset($record->callbackfile)) { | |
311 | $record->callbackfile = null; | |
312 | } | |
313 | $itemtypes[$record->itemtype . ':' . $record->component] = $record; | |
314 | } | |
315 | } | |
316 | $todeletearea = array_diff_key($existingareas, $itemtypes); | |
317 | $todeletecoll = array_diff_key($existingcolls, $collections); | |
318 | ||
319 | // Delete tag areas that are no longer needed. | |
320 | foreach ($todeletearea as $key => $record) { | |
321 | self::delete($record); | |
322 | } | |
323 | ||
324 | // Update tag areas if changed. | |
325 | $toupdatearea = array_intersect_key($existingareas, $itemtypes); | |
326 | foreach ($toupdatearea as $key => $tagarea) { | |
327 | if (!isset($itemtypes[$key]->tagcollid)) { | |
328 | foreach ($todeletecoll as $tagcoll) { | |
329 | if ($tagcoll->id == $tagarea->tagcollid) { | |
330 | $itemtypes[$key]->tagcollid = core_tag_collection::get_default(); | |
331 | } | |
332 | } | |
333 | } | |
4be9c7ad | 334 | unset($itemtypes[$key]->showstandard); // Do not override value that was already changed by admin with the default. |
c026a28d MG |
335 | self::update($tagarea, $itemtypes[$key]); |
336 | } | |
337 | ||
338 | // Create new tag areas. | |
339 | $toaddarea = array_diff_key($itemtypes, $existingareas); | |
340 | foreach ($toaddarea as $record) { | |
341 | self::create($record); | |
342 | } | |
343 | ||
344 | // Delete tag collections that are no longer needed. | |
345 | foreach ($todeletecoll as $key => $tagcoll) { | |
346 | core_tag_collection::delete($tagcoll); | |
347 | } | |
348 | } | |
349 | ||
350 | /** | |
351 | * Deletes all tag areas, collections and instances associated with the plugin. | |
352 | * | |
353 | * @param string $pluginname | |
354 | */ | |
355 | public static function uninstall($pluginname) { | |
356 | global $DB; | |
357 | ||
358 | list($a, $b) = core_component::normalize_component($pluginname); | |
359 | if (empty($b) || $a === 'core') { | |
360 | throw new coding_exception('Core component can not be uninstalled'); | |
361 | } | |
362 | $component = $a . '_' . $b; | |
363 | ||
364 | core_tag_tag::delete_instances($component); | |
365 | ||
366 | $DB->delete_records('tag_area', array('component' => $component)); | |
367 | $DB->delete_records('tag_coll', array('component' => $component)); | |
368 | cache::make('core', 'tags')->delete_many(array('tag_area', 'tag_coll')); | |
369 | } | |
370 | ||
371 | /** | |
372 | * Moves existing tags associated with an item type to another tag collection | |
373 | * | |
374 | * @param string $component | |
375 | * @param string $itemtype | |
376 | * @param int $tagcollid | |
377 | */ | |
378 | public static function move_tags($component, $itemtype, $tagcollid) { | |
379 | global $DB; | |
380 | $params = array('itemtype1' => $itemtype, 'component1' => $component, | |
381 | 'itemtype2' => $itemtype, 'component2' => $component, | |
382 | 'tagcollid1' => $tagcollid, 'tagcollid2' => $tagcollid); | |
383 | ||
384 | // Find all collections that need to be cleaned later. | |
385 | $sql = "SELECT DISTINCT t.tagcollid " . | |
386 | "FROM {tag_instance} ti " . | |
387 | "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 " . | |
388 | "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 "; | |
389 | $cleanupcollections = $DB->get_fieldset_sql($sql, $params); | |
390 | ||
391 | // Find all tags that are related to the tags being moved and make sure they are present in the target tagcoll. | |
721d6659 | 392 | // This query is a little complicated because Oracle does not allow to run SELECT DISTINCT on CLOB fields. |
e11d7380 | 393 | $sql = "SELECT name, rawname, description, descriptionformat, userid, isstandard, flag ". |
721d6659 MG |
394 | "FROM {tag} WHERE id IN ". |
395 | "(SELECT r.id ". | |
c026a28d MG |
396 | "FROM {tag_instance} ti ". // Instances that need moving. |
397 | "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 ". // Tags that need moving. | |
398 | "JOIN {tag_instance} tr ON tr.itemtype = 'tag' and tr.component = 'core' AND tr.itemid = t.id ". | |
399 | "JOIN {tag} r ON r.id = tr.tagid ". // Tags related to the tags that need moving. | |
400 | "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. | |
401 | "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 ". | |
721d6659 | 402 | " AND re.id IS NULL)"; // We need related tags that ARE NOT present in the target tagcoll. |
c026a28d MG |
403 | $result = $DB->get_records_sql($sql, $params); |
404 | foreach ($result as $tag) { | |
405 | $tag->tagcollid = $tagcollid; | |
406 | $tag->id = $DB->insert_record('tag', $tag); | |
407 | \core\event\tag_created::create_from_tag($tag); | |
408 | } | |
409 | ||
410 | // Find all tags that need moving and have related tags, remember their related tags. | |
411 | $sql = "SELECT t.name AS tagname, r.rawname AS relatedtag ". | |
412 | "FROM {tag_instance} ti ". // Instances that need moving. | |
413 | "JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 ". // Tags that need moving. | |
414 | "JOIN {tag_instance} tr ON t.id = tr.tagid AND tr.itemtype = 'tag' and tr.component = 'core' ". | |
415 | "JOIN {tag} r ON r.id = tr.itemid ". // Tags related to the tags that need moving. | |
416 | "WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 ". | |
417 | "ORDER BY t.id, tr.ordering "; | |
418 | $relatedtags = array(); | |
419 | $result = $DB->get_recordset_sql($sql, $params); | |
420 | foreach ($result as $record) { | |
421 | $relatedtags[$record->tagname][] = $record->relatedtag; | |
422 | } | |
423 | $result->close(); | |
424 | ||
425 | // Find all tags that are used for this itemtype/component and are not present in the target tag collection. | |
25430f75 | 426 | // This query is a little complicated because Oracle does not allow to run SELECT DISTINCT on CLOB fields. |
e11d7380 | 427 | $sql = "SELECT id, name, rawname, description, descriptionformat, userid, isstandard, flag |
25430f75 MG |
428 | FROM {tag} WHERE id IN |
429 | (SELECT t.id | |
c026a28d MG |
430 | FROM {tag_instance} ti |
431 | JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 | |
432 | LEFT JOIN {tag} tt ON tt.name = t.name AND tt.tagcollid = :tagcollid2 | |
433 | WHERE ti.itemtype = :itemtype2 AND ti.component = :component2 | |
25430f75 | 434 | AND tt.id IS NULL)"; |
71da0356 | 435 | $movedtags = array(); // Keep track of moved tags so we don't hit DB index violation. |
c026a28d MG |
436 | $result = $DB->get_records_sql($sql, $params); |
437 | foreach ($result as $tag) { | |
438 | $originaltagid = $tag->id; | |
71da0356 MG |
439 | if (array_key_exists($tag->name, $movedtags)) { |
440 | // Case of corrupted data when the same tag was in several collections. | |
441 | $tag->id = $movedtags[$tag->name]; | |
442 | } else { | |
443 | // Copy the tag into the new collection. | |
444 | unset($tag->id); | |
445 | $tag->tagcollid = $tagcollid; | |
446 | $tag->id = $DB->insert_record('tag', $tag); | |
447 | \core\event\tag_created::create_from_tag($tag); | |
448 | $movedtags[$tag->name] = $tag->id; | |
449 | } | |
c026a28d MG |
450 | $DB->execute("UPDATE {tag_instance} SET tagid = ? WHERE tagid = ? AND itemtype = ? AND component = ?", |
451 | array($tag->id, $originaltagid, $itemtype, $component)); | |
452 | } | |
453 | ||
454 | // Find all tags that are used for this itemtype/component and are already present in the target tag collection. | |
455 | $sql = "SELECT DISTINCT t.id, tt.id AS targettagid | |
456 | FROM {tag_instance} ti | |
457 | JOIN {tag} t ON t.id = ti.tagid AND t.tagcollid <> :tagcollid1 | |
458 | JOIN {tag} tt ON tt.name = t.name AND tt.tagcollid = :tagcollid2 | |
459 | WHERE ti.itemtype = :itemtype2 AND ti.component = :component2"; | |
460 | $result = $DB->get_records_sql($sql, $params); | |
461 | foreach ($result as $tag) { | |
462 | $DB->execute("UPDATE {tag_instance} SET tagid = ? WHERE tagid = ? AND itemtype = ? AND component = ?", | |
463 | array($tag->targettagid, $tag->id, $itemtype, $component)); | |
464 | } | |
465 | ||
466 | // Add related tags to the moved tags. | |
467 | if ($relatedtags) { | |
468 | $tags = core_tag_tag::get_by_name_bulk($tagcollid, array_keys($relatedtags)); | |
469 | foreach ($tags as $tag) { | |
470 | $tag->add_related_tags($relatedtags[$tag->name]); | |
471 | } | |
472 | } | |
473 | ||
474 | if ($cleanupcollections) { | |
475 | core_tag_collection::cleanup_unused_tags($cleanupcollections); | |
476 | } | |
477 | ||
478 | // Reset caches. | |
479 | cache::make('core', 'tags')->delete('tag_area'); | |
480 | } | |
481 | } |