Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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