forked from moodle/moodle
-
Notifications
You must be signed in to change notification settings - Fork 0
/
lib.php
1227 lines (1080 loc) · 44.5 KB
/
lib.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Moodle tag library
*
* Tag strings : you can use any character in tags, except the comma (which is the separator) and
* the '\' (backslash). Note that many spaces (or other blank characters) will get "compressed"
* into one. A tag string is always a rawurlencode'd string. This is the same behavior as
* http://del.icio.us.
*
* A "record" is a php array (note that an object will work too) that contains the following
* variables :
* - type: The database table containing the record that we are tagging (eg: for a blog, this is
* the table named 'post', and for a user it is the table name 'user')
* - id: The id of the record
*
* BASIC INSTRUCTIONS :
* - to "tag a blog post" (for example):
* tag_set('post', $blog_post->id, $array_of_tags);
*
* - to "remove all the tags on a blog post":
* tag_set('post', $blog_post->id, array());
*
* Tag set will create tags that need to be created.
*
* @package core_tag
* @category tag
* @todo MDL-31090 turn this into a full-fledged categorization system. This could start by
* modifying (removing, probably) the 'tag type' to use another table describing the
* relationship between tags (parents, sibling, etc.), which could then be merged with
* the 'course categorization' system.
* @see http://www.php.net/manual/en/function.urlencode.php
* @copyright 2007 Luiz Cruz <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Used to require that the return value from a function is an array.
* @see tag_set()
*/
define('TAG_RETURN_ARRAY', 0);
/**
* Used to require that the return value from a function is an object.
* @see tag_set()
*/
define('TAG_RETURN_OBJECT', 1);
/**
* Use to specify that HTML free text is expected to be returned from a function.
* @see tag_display_name()
*/
define('TAG_RETURN_TEXT', 2);
/**
* Use to specify that encoded HTML is expected to be returned from a function.
* @see tag_display_name()
*/
define('TAG_RETURN_HTML', 3);
/**
* Used to specify that we wish a lowercased string to be returned
* @see tag_normal()
*/
define('TAG_CASE_LOWER', 0);
/**
* Used to specify that we do not wish the case of the returned string to change
* @see tag_normal()
*/
define('TAG_CASE_ORIGINAL', 1);
/**
* Used to specify that we want all related tags returned, no matter how they are related.
* @see tag_get_related_tags()
*/
define('TAG_RELATED_ALL', 0);
/**
* Used to specify that we only want back tags that were manually related.
* @see tag_get_related_tags()
*/
define('TAG_RELATED_MANUAL', 1);
/**
* Used to specify that we only want back tags where the relationship was automatically correlated.
* @see tag_get_related_tags()
*/
define('TAG_RELATED_CORRELATED', 2);
///////////////////////////////////////////////////////
/////////////////// PUBLIC TAG API ////////////////////
/// Functions for settings tags //////////////////////
/**
* Set the tags assigned to a record. This overwrites the current tags.
*
* This function is meant to be fed the string coming up from the user interface, which contains all tags assigned to a record.
*
* @package core_tag
* @category tag
* @access public
* @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, 'tag' for tags, etc.)
* @param int $record_id the id of the record to tag
* @param array $tags the array of tags to set on the record. If given an empty array, all tags will be removed.
* @return bool|null
*/
function tag_set($record_type, $record_id, $tags) {
static $in_recursion_semaphore = false; // this is to prevent loops when tagging a tag
if ( $record_type == 'tag' && !$in_recursion_semaphore) {
$current_tagged_tag_name = tag_get_name($record_id);
}
$tags_ids = tag_get_id($tags, TAG_RETURN_ARRAY); // force an array, even if we only have one tag.
$cleaned_tags = tag_normalize($tags);
//echo 'tags-in-tag_set'; var_dump($tags); var_dump($tags_ids); var_dump($cleaned_tags);
$current_ids = tag_get_tags_ids($record_type, $record_id);
//var_dump($current_ids);
// for data coherence reasons, it's better to remove deleted tags
// before adding new data: ordering could be duplicated.
foreach($current_ids as $current_id) {
if (!in_array($current_id, $tags_ids)) {
tag_delete_instance($record_type, $record_id, $current_id);
if ( $record_type == 'tag' && !$in_recursion_semaphore) {
// if we are removing a tag-on-a-tag (manually related tag),
// we need to remove the opposite relationship as well.
tag_delete_instance('tag', $current_id, $record_id);
}
}
}
if (empty($tags)) {
return true;
}
foreach($tags as $ordering => $tag) {
$tag = trim($tag);
if (!$tag) {
continue;
}
$clean_tag = $cleaned_tags[$tag];
$tag_current_id = $tags_ids[$clean_tag];
if ( is_null($tag_current_id) ) {
// create new tags
//echo "call to add tag $tag\n";
$new_tag = tag_add($tag);
$tag_current_id = $new_tag[$clean_tag];
}
tag_assign($record_type, $record_id, $tag_current_id, $ordering);
// if we are tagging a tag (adding a manually-assigned related tag), we
// need to create the opposite relationship as well.
if ( $record_type == 'tag' && !$in_recursion_semaphore) {
$in_recursion_semaphore = true;
tag_set_add('tag', $tag_current_id, $current_tagged_tag_name);
$in_recursion_semaphore = false;
}
}
}
/**
* Adds a tag to a record, without overwriting the current tags.
*
* @package core_tag
* @category tag
* @access public
* @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, etc.)
* @param int $record_id the id of the record to tag
* @param string $tag the tag to add
*/
function tag_set_add($record_type, $record_id, $tag) {
$new_tags = array();
foreach( tag_get_tags($record_type, $record_id) as $current_tag ) {
$new_tags[] = $current_tag->rawname;
}
$new_tags[] = $tag;
return tag_set($record_type, $record_id, $new_tags);
}
/**
* Removes a tag from a record, without overwriting other current tags.
*
* @package core_tag
* @category tag
* @access public
* @param string $record_type the type of record to tag ('post' for blogs, 'user' for users, etc.)
* @param int $record_id the id of the record to tag
* @param string $tag the tag to delete
*/
function tag_set_delete($record_type, $record_id, $tag) {
$new_tags = array();
foreach( tag_get_tags($record_type, $record_id) as $current_tag ) {
if ($current_tag->name != $tag) { // Keep all tags but the one specified
$new_tags[] = $current_tag->name;
}
}
return tag_set($record_type, $record_id, $new_tags);
}
/**
* Set the type of a tag. At this time (version 2.2) the possible values are 'default' or 'official'. Official tags will be
* displayed separately "at tagging time" (while selecting the tags to apply to a record).
*
* @package core_tag
* @category tag
* @access public
* @param string $tagid tagid to modify
* @param string $type either 'default' or 'official'
* @return bool true on success, false otherwise
*/
function tag_type_set($tagid, $type) {
global $DB;
if ($tag = $DB->get_record('tag', array('id'=>$tagid), 'id')) {
$tag->tagtype = $type;
$tag->timemodified = time();
return $DB->update_record('tag', $tag);
}
return false;
}
/**
* Set the description of a tag
*
* @package core_tag
* @category tag
* @access public
* @param int $tagid the id of the tag
* @param string $description the tag's description string to be set
* @param int $descriptionformat the moodle text format of the description
* {@link http://docs.moodle.org/dev/Text_formats_2.0#Database_structure}
* @return bool true on success, false otherwise
*/
function tag_description_set($tagid, $description, $descriptionformat) {
global $DB;
if ($tag = $DB->get_record('tag', array('id'=>$tagid),'id')) {
$tag->description = $description;
$tag->descriptionformat = $descriptionformat;
$tag->timemodified = time();
return $DB->update_record('tag', $tag);
}
return false;
}
/// Functions for getting information about tags //////
/**
* Simple function to just return a single tag object when you know the name or something
*
* @package core_tag
* @category tag
* @access public
* @param string $field which field do we use to identify the tag: id, name or rawname
* @param string $value the required value of the aforementioned field
* @param string $returnfields which fields do we want returned. This is a comma seperated string containing any combination of
* 'id', 'name', 'rawname' or '*' to include all fields.
* @return mixed tag object
*/
function tag_get($field, $value, $returnfields='id, name, rawname') {
global $DB;
if ($field == 'name') {
$value = textlib::strtolower($value); // To cope with input that might just be wrong case
}
return $DB->get_record('tag', array($field=>$value), $returnfields);
}
/**
* Get the array of db record of tags associated to a record (instances). Use {@see tag_get_tags_csv()} if you wish to get the same
* data in a comma-separated string, for instances such as needing to simply display a list of tags to the end user. This should
* really be called tag_get_tag_instances().
*
* @package core_tag
* @category tag
* @access public
* @param string $record_type the record type for which we want to get the tags
* @param int $record_id the record id for which we want to get the tags
* @param string $type the tag type (either 'default' or 'official'). By default, all tags are returned.
* @param int $userid (optional) only required for course tagging
* @return array the array of tags
*/
function tag_get_tags($record_type, $record_id, $type=null, $userid=0) {
global $CFG, $DB;
$params = array();
if ($type) {
$sql_type = "AND tg.tagtype = :type";
$params['type'] = $type;
} else {
$sql_type = '';
}
$u = null;
if ($userid) {
$u = "AND ti.tiuserid = :userid ";
$params['userid'] = $userid;
}
$sql = "SELECT tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering
FROM {tag_instance} ti
JOIN {tag} tg ON tg.id = ti.tagid
WHERE ti.itemtype = :recordtype AND ti.itemid = :recordid $u $sql_type
ORDER BY ti.ordering ASC";
$params['recordtype'] = $record_type;
$params['recordid'] = $record_id;
// if the fields in this query are changed, you need to do the same changes in tag_get_correlated_tags
return $DB->get_records_sql($sql, $params);
// This version of the query, reversing the ON clause, "correctly" returns
// a row with NULL values for instances that are still in the DB even though
// the tag has been deleted. This shouldn't happen, but if it did, using
// this query could help "clean it up". This causes bugs at this time.
//$tags = $DB->get_records_sql("SELECT ti.tagid, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ".
// "FROM {tag_instance} ti LEFT JOIN {tag} tg ON ti.tagid = tg.id ".
// "WHERE ti.itemtype = '{$record_type}' AND ti.itemid = '{$record_id}' {$type} ".
// "ORDER BY ti.ordering ASC");
}
/**
* Get the array of tags display names, indexed by id.
*
* @package core_tag
* @category tag
* @access public
* @param string $record_type the record type for which we want to get the tags
* @param int $record_id the record id for which we want to get the tags
* @param string $type the tag type (either 'default' or 'official'). By default, all tags are returned.
* @return array the array of tags (with the value returned by tag_display_name), indexed by id
*/
function tag_get_tags_array($record_type, $record_id, $type=null) {
$tags = array();
foreach(tag_get_tags($record_type, $record_id, $type) as $tag) {
$tags[$tag->id] = tag_display_name($tag);
}
return $tags;
}
/**
* Get a comma-separated string of tags associated to a record. Use {@see tag_get_tags()} to get the same information in an array.
*
* @package core_tag
* @category tag
* @access public
* @param string $record_type the record type for which we want to get the tags
* @param int $record_id the record id for which we want to get the tags
* @param int $html either TAG_RETURN_HTML or TAG_RETURN_TEXT, depending on the type of output desired
* @param string $type either 'official' or 'default', if null, all tags are returned
* @return string the comma-separated list of tags.
*/
function tag_get_tags_csv($record_type, $record_id, $html=TAG_RETURN_HTML, $type=null) {
global $CFG;
$tags_names = array();
foreach(tag_get_tags($record_type, $record_id, $type) as $tag) {
if ($html == TAG_RETURN_TEXT) {
$tags_names[] = tag_display_name($tag, TAG_RETURN_TEXT);
} else { // TAG_RETURN_HTML
$tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
}
}
return implode(', ', $tags_names);
}
/**
* Get an array of tag ids associated to a record.
*
* @package core_tag
* @category tag
* @access public
* @todo MDL-31150 Update ordering property
* @param string $record_type the record type for which we want to get the tags
* @param int $record_id the record id for which we want to get the tags
* @return array tag ids, indexed and sorted by 'ordering'
*/
function tag_get_tags_ids($record_type, $record_id) {
$tag_ids = array();
foreach (tag_get_tags($record_type, $record_id) as $tag) {
if ( array_key_exists($tag->ordering, $tag_ids) ) {
// until we can add a unique constraint, in table tag_instance,
// on (itemtype, itemid, ordering), this is needed to prevent a bug
// TODO MDL-31150 modify database in 2.0
$tag->ordering++;
}
$tag_ids[$tag->ordering] = $tag->id;
}
ksort($tag_ids);
return $tag_ids;
}
/**
* Returns the database ID of a set of tags.
*
* @package core_tag
* @category tag
* @access public
* @todo MDL-31152 Test the commented MDL-31152 todo in this function to see if it helps performance
* without breaking anything.
* @param mixed $tags one tag, or array of tags, to look for.
* @param bool $return_value specify the type of the returned value. Either TAG_RETURN_OBJECT, or TAG_RETURN_ARRAY (default).
* If TAG_RETURN_ARRAY is specified, an array will be returned even if only one tag was passed in $tags.
* @return mixed tag-indexed array of ids (or objects, if second parameter is TAG_RETURN_OBJECT), or only an int, if only one tag
* is given *and* the second parameter is null. No value for a key means the tag wasn't found.
*/
function tag_get_id($tags, $return_value=null) {
global $CFG, $DB;
static $tag_id_cache = array();
$return_an_int = false;
if (!is_array($tags)) {
if(is_null($return_value) || $return_value == TAG_RETURN_OBJECT) {
$return_an_int = true;
}
$tags = array($tags);
}
$result = array();
//TODO MDL-31152 test this and see if it helps performance without breaking anything
//foreach($tags as $key => $tag) {
// $clean_tag = textlib::strtolower($tag);
// if ( array_key_exists($clean_tag), $tag_id_cache) ) {
// $result[$clean_tag] = $tag_id_cache[$clean_tag];
// $tags[$key] = ''; // prevent further processing for this one.
// }
//}
$tags = array_values(tag_normalize($tags));
foreach($tags as $key => $tag) {
$tags[$key] = textlib::strtolower($tag);
$result[textlib::strtolower($tag)] = null; // key must exists : no value for a key means the tag wasn't found.
}
if (empty($tags)) {
return array();
}
list($tag_string, $params) = $DB->get_in_or_equal($tags);
$rs = $DB->get_recordset_sql("SELECT * FROM {tag} WHERE name $tag_string ORDER BY name", $params);
foreach ($rs as $record) {
if ($return_value == TAG_RETURN_OBJECT) {
$result[$record->name] = $record;
} else { // TAG_RETURN_ARRAY
$result[$record->name] = $record->id;
}
}
$rs->close();
if ($return_an_int) {
return array_pop($result);
}
return $result;
}
/**
* Returns tags related to a tag
*
* Related tags of a tag come from two sources:
* - manually added related tags, which are tag_instance entries for that tag
* - correlated tags, which are calculated
*
* @package core_tag
* @category tag
* @access public
* @param string $tagid is a single **normalized** tag name or the id of a tag
* @param int $type the function will return either manually (TAG_RELATED_MANUAL) related tags or correlated
* (TAG_RELATED_CORRELATED) tags. Default is TAG_RELATED_ALL, which returns everything.
* @param int $limitnum (optional) return a subset comprising this many records, the default is 10
* @return array an array of tag objects
*/
function tag_get_related_tags($tagid, $type=TAG_RELATED_ALL, $limitnum=10) {
$related_tags = array();
if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_MANUAL) {
//gets the manually added related tags
$related_tags = tag_get_tags('tag', $tagid);
}
if ( $type == TAG_RELATED_ALL || $type == TAG_RELATED_CORRELATED ) {
//gets the correlated tags
$automatic_related_tags = tag_get_correlated($tagid, $limitnum);
if (is_array($automatic_related_tags)) {
$related_tags = array_merge($related_tags, $automatic_related_tags);
}
}
return array_slice(object_array_unique($related_tags), 0 , $limitnum);
}
/**
* Get a comma-separated list of tags related to another tag.
*
* @package core_tag
* @category tag
* @access public
* @param array $related_tags the array returned by tag_get_related_tags
* @param int $html either TAG_RETURN_HTML (default) or TAG_RETURN_TEXT : return html links, or just text.
* @return string comma-separated list
*/
function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) {
global $CFG;
$tags_names = array();
foreach($related_tags as $tag) {
if ( $html == TAG_RETURN_TEXT) {
$tags_names[] = tag_display_name($tag, TAG_RETURN_TEXT);
} else {
// TAG_RETURN_HTML
$tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
}
}
return implode(', ', $tags_names);
}
/**
* Change the "value" of a tag, and update the associated 'name'.
*
* @package core_tag
* @category tag
* @access public
* @param int $tagid the id of the tag to modify
* @param string $newrawname the new rawname
* @return bool true on success, false otherwise
*/
function tag_rename($tagid, $newrawname) {
global $DB;
$norm = tag_normalize($newrawname, TAG_CASE_ORIGINAL);
if (! $newrawname_clean = array_shift($norm) ) {
return false;
}
if (! $newname_clean = textlib::strtolower($newrawname_clean)) {
return false;
}
// Prevent the rename if a tag with that name already exists
if ($existing = tag_get('name', $newname_clean, 'id, name, rawname')) {
if ($existing->id != $tagid) { // Another tag already exists with this name
return false;
}
}
if ($tag = tag_get('id', $tagid, 'id, name, rawname')) {
$tag->rawname = $newrawname_clean;
$tag->name = $newname_clean;
$tag->timemodified = time();
return $DB->update_record('tag', $tag);
}
return false;
}
/**
* Delete one or more tag, and all their instances if there are any left.
*
* @package core_tag
* @category tag
* @access public
* @param mixed $tagids one tagid (int), or one array of tagids to delete
* @return bool true on success, false otherwise
*/
function tag_delete($tagids) {
global $DB;
if (!is_array($tagids)) {
$tagids = array($tagids);
}
$success = true;
$context = context_system::instance();
foreach ($tagids as $tagid) {
if (is_null($tagid)) { // can happen if tag doesn't exists
continue;
}
// only delete the main entry if there were no problems deleting all the
// instances - that (and the fact we won't often delete lots of tags)
// is the reason for not using $DB->delete_records_select()
if ($DB->delete_records('tag_instance', array('tagid'=>$tagid)) && $DB->delete_records('tag_correlation', array('tagid' => $tagid))) {
$success &= (bool) $DB->delete_records('tag', array('id'=>$tagid));
// Delete all files associated with this tag
$fs = get_file_storage();
$files = $fs->get_area_files($context->id, 'tag', 'description', $tagid);
foreach ($files as $file) {
$file->delete();
}
}
}
return $success;
}
/**
* Delete one instance of a tag. If the last instance was deleted, it will also delete the tag, unless its type is 'official'.
*
* @package core_tag
* @category tag
* @access public
* @param string $record_type the type of the record for which to remove the instance
* @param int $record_id the id of the record for which to remove the instance
* @param int $tagid the tagid that needs to be removed
* @return bool true on success, false otherwise
*/
function tag_delete_instance($record_type, $record_id, $tagid) {
global $CFG, $DB;
if ($DB->delete_records('tag_instance', array('tagid'=>$tagid, 'itemtype'=>$record_type, 'itemid'=>$record_id))) {
if (!$DB->record_exists_sql("SELECT * ".
"FROM {tag} tg ".
"WHERE tg.id = ? AND ( tg.tagtype = 'official' OR ".
"EXISTS (SELECT 1
FROM {tag_instance} ti
WHERE ti.tagid = ?) )",
array($tagid, $tagid))) {
return tag_delete($tagid);
}
} else {
return false;
}
return true;
}
/**
* Function that returns the name that should be displayed for a specific tag
*
* @package core_tag
* @category tag
* @access public
* @param object $tagobject a line out of tag table, as returned by the adobd functions
* @param int $html TAG_RETURN_HTML (default) will return htmlspecialchars encoded string, TAG_RETURN_TEXT will not encode.
* @return string
*/
function tag_display_name($tagobject, $html=TAG_RETURN_HTML) {
global $CFG;
if (!isset($tagobject->name)) {
return '';
}
if (empty($CFG->keeptagnamecase)) {
//this is the normalized tag name
$tagname = textlib::strtotitle($tagobject->name);
} else {
//original casing of the tag name
$tagname = $tagobject->rawname;
}
// clean up a bit just in case the rules change again
$tagname = clean_param($tagname, PARAM_TAG);
if ($html == TAG_RETURN_TEXT) {
return $tagname;
} else { // TAG_RETURN_HTML
return htmlspecialchars($tagname);
}
}
/**
* Find all records tagged with a tag of a given type ('post', 'user', etc.)
*
* @package core_tag
* @category tag
* @access public
* @param string $tag tag to look for
* @param string $type type to restrict search to. If null, every matching record will be returned
* @param int $limitfrom (optional, required if $limitnum is set) return a subset of records, starting at this point.
* @param int $limitnum (optional, required if $limitfrom is set) return a subset comprising this many records.
* @return array of matching objects, indexed by record id, from the table containing the type requested
*/
function tag_find_records($tag, $type, $limitfrom='', $limitnum='') {
global $CFG, $DB;
if (!$tag || !$type) {
return array();
}
$tagid = tag_get_id($tag);
$query = "SELECT it.*
FROM {".$type."} it INNER JOIN {tag_instance} tt ON it.id = tt.itemid
WHERE tt.itemtype = ? AND tt.tagid = ?";
$params = array($type, $tagid);
return $DB->get_records_sql($query, $params, $limitfrom, $limitnum);
}
///////////////////////////////////////////////////////
/////////////////// PRIVATE TAG API ///////////////////
/**
* Adds one or more tag in the database. This function should not be called directly : you should
* use tag_set.
*
* @package core_tag
* @access private
* @param mixed $tags one tag, or an array of tags, to be created
* @param string $type type of tag to be created ("default" is the default value and "official" is the only other supported
* value at this time). An official tag is kept even if there are no records tagged with it.
* @return array $tags ids indexed by their lowercase normalized names. Any boolean false in the array indicates an error while
* adding the tag.
*/
function tag_add($tags, $type="default") {
global $USER, $DB;
if (!is_array($tags)) {
$tags = array($tags);
}
$tag_object = new StdClass;
$tag_object->tagtype = $type;
$tag_object->userid = $USER->id;
$tag_object->timemodified = time();
$clean_tags = tag_normalize($tags, TAG_CASE_ORIGINAL);
$tags_ids = array();
foreach($clean_tags as $tag) {
$tag = trim($tag);
if (!$tag) {
$tags_ids[$tag] = false;
} else {
// note that the difference between rawname and name is only
// capitalization : the rawname is NOT the same at the rawtag.
$tag_object->rawname = $tag;
$tag_name_lc = textlib::strtolower($tag);
$tag_object->name = $tag_name_lc;
//var_dump($tag_object);
$tags_ids[$tag_name_lc] = $DB->insert_record('tag', $tag_object);
}
}
return $tags_ids;
}
/**
* Assigns a tag to a record; if the record already exists, the time and ordering will be updated.
*
* @package core_tag
* @access private
* @param string $record_type the type of the record that will be tagged
* @param int $record_id the id of the record that will be tagged
* @param string $tagid the tag id to set on the record.
* @param int $ordering the order of the instance for this record
* @param int $userid (optional) only required for course tagging
* @return bool true on success, false otherwise
*/
function tag_assign($record_type, $record_id, $tagid, $ordering, $userid = 0) {
global $DB;
if ( $tag_instance_object = $DB->get_record('tag_instance', array('tagid'=>$tagid, 'itemtype'=>$record_type, 'itemid'=>$record_id, 'tiuserid'=>$userid), 'id')) {
$tag_instance_object->ordering = $ordering;
$tag_instance_object->timemodified = time();
return $DB->update_record('tag_instance', $tag_instance_object);
} else {
$tag_instance_object = new StdClass;
$tag_instance_object->tagid = $tagid;
$tag_instance_object->itemid = $record_id;
$tag_instance_object->itemtype = $record_type;
$tag_instance_object->ordering = $ordering;
$tag_instance_object->timemodified = time();
$tag_instance_object->tiuserid = $userid;
return $DB->insert_record('tag_instance', $tag_instance_object);
}
}
/**
* Function that returns tags that start with some text, for use by the autocomplete feature
*
* @package core_tag
* @access private
* @param string $text string that the tag names will be matched against
* @return mixed an array of objects, or false if no records were found or an error occured.
*/
function tag_autocomplete($text) {
global $DB;
return $DB->get_records_sql("SELECT tg.id, tg.name, tg.rawname
FROM {tag} tg
WHERE tg.name LIKE ?", array(textlib::strtolower($text)."%"));
}
/**
* Clean up the tag tables, making sure all tagged object still exists.
*
* This should normally not be necessary, but in case related tags are not deleted when the tagged record is removed, this should be
* done once in a while, perhaps on an occasional cron run. On a site with lots of tags, this could become an expensive function to
* call: don't run at peak time.
*
* @package core_tag
* @access private
* @todo MDL-31212 Update tag cleanup sql so that it supports multiple types of tags
*/
function tag_cleanup() {
global $DB;
$instances = $DB->get_recordset('tag_instance');
// cleanup tag instances
foreach ($instances as $instance) {
$delete = false;
if (!$DB->record_exists('tag', array('id'=>$instance->tagid))) {
// if the tag has been removed, instance should be deleted.
$delete = true;
} else {
switch ($instance->itemtype) {
case 'user': // users are marked as deleted, but not actually deleted
if ($DB->record_exists('user', array('id'=>$instance->itemid, 'deleted'=>1))) {
$delete = true;
}
break;
default: // anything else, if the instance is not there, delete.
if (!$DB->record_exists($instance->itemtype, array('id'=>$instance->itemid))) {
$delete = true;
}
break;
}
}
if ($delete) {
tag_delete_instance($instance->itemtype, $instance->itemid, $instance->tagid);
//debugging('deleting tag_instance #'. $instance->id .', linked to tag id #'. $instance->tagid, DEBUG_DEVELOPER);
}
}
$instances->close();
// TODO MDL-31212 this will only clean tags of type 'default'. This is good as
// it won't delete 'official' tags, but the day we get more than two
// types, we need to fix this.
$unused_tags = $DB->get_recordset_sql("SELECT tg.id
FROM {tag} tg
WHERE tg.tagtype = 'default'
AND NOT EXISTS (
SELECT 'x'
FROM {tag_instance} ti
WHERE ti.tagid = tg.id
)");
// cleanup tags
foreach ($unused_tags as $unused_tag) {
tag_delete($unused_tag->id);
//debugging('deleting unused tag #'. $unused_tag->id, DEBUG_DEVELOPER);
}
$unused_tags->close();
}
/**
* Calculates and stores the correlated tags of all tags. The correlations are stored in the 'tag_correlation' table.
*
* Two tags are correlated if they appear together a lot. Ex.: Users tagged with "computers" will probably also be tagged with "algorithms".
*
* The rationale for the 'tag_correlation' table is performance. It works as a cache for a potentially heavy load query done at the
* 'tag_instance' table. So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table.
*
* @package core_tag
* @access private
* @param int $mincorrelation Only tags with more than $mincorrelation correlations will be identified.
*/
function tag_compute_correlations($mincorrelation = 2) {
global $DB;
// This mighty one line query fetches a row from the database for every
// individual tag correlation. We then need to process the rows collecting
// the correlations for each tag id.
// The fields used by this query are as follows:
// tagid : This is the tag id, there should be at least $mincorrelation
// rows for each tag id.
// correlation : This is the tag id that correlates to the above tagid field.
// correlationid : This is the id of the row in the tag_correlation table that
// relates to the tagid field and will be NULL if there are no
// existing correlations
$sql = 'SELECT pairs.tagid, pairs.correlation, pairs.ocurrences, co.id AS correlationid
FROM (
SELECT ta.tagid, tb.tagid AS correlation, COUNT(*) AS ocurrences
FROM {tag_instance} ta
JOIN {tag_instance} tb ON (ta.itemtype = tb.itemtype AND ta.itemid = tb.itemid AND ta.tagid <> tb.tagid)
GROUP BY ta.tagid, tb.tagid
HAVING COUNT(*) > :mincorrelation
) pairs
LEFT JOIN {tag_correlation} co ON co.tagid = pairs.tagid
ORDER BY pairs.tagid ASC, pairs.ocurrences DESC, pairs.correlation ASC';
$rs = $DB->get_recordset_sql($sql, array('mincorrelation' => $mincorrelation));
// Set up an empty tag correlation object
$tagcorrelation = new stdClass;
$tagcorrelation->id = null;
$tagcorrelation->tagid = null;
$tagcorrelation->correlatedtags = array();
// We store each correlation id in this array so we can remove any correlations
// that no longer exist.
$correlations = array();
// Iterate each row of the result set and build them into tag correlations.
// We add all of a tag's correlations to $tagcorrelation->correlatedtags[]
// then save the $tagcorrelation object
foreach ($rs as $row) {
if ($row->tagid != $tagcorrelation->tagid) {
// The tag id has changed so we have all of the correlations for this tag
$tagcorrelationid = tag_process_computed_correlation($tagcorrelation);
if ($tagcorrelationid) {
$correlations[] = $tagcorrelationid;
}
// Now we reset the tag correlation object so we can reuse it and set it
// up for the current record.
$tagcorrelation = new stdClass;
$tagcorrelation->id = $row->correlationid;
$tagcorrelation->tagid = $row->tagid;
$tagcorrelation->correlatedtags = array();
}
//Save the correlation on the tag correlation object
$tagcorrelation->correlatedtags[] = $row->correlation;
}
// Update the current correlation after the last record.
$tagcorrelationid = tag_process_computed_correlation($tagcorrelation);
if ($tagcorrelationid) {
$correlations[] = $tagcorrelationid;
}
// Close the recordset
$rs->close();
// Remove any correlations that weren't just identified
if (empty($correlations)) {
//there are no tag correlations
$DB->delete_records('tag_correlation');
} else {
list($sql, $params) = $DB->get_in_or_equal($correlations, SQL_PARAMS_NAMED, 'param0000', false);
$DB->delete_records_select('tag_correlation', 'id '.$sql, $params);
}
}
/**
* This function processes a tag correlation and makes changes in the database as required.
*
* The tag correlation object needs have both a tagid property and a correlatedtags property that is an array.
*
* @package core_tag
* @access private
* @param stdClass $tagcorrelation
* @return int/bool The id of the tag correlation that was just processed or false.
*/
function tag_process_computed_correlation(stdClass $tagcorrelation) {
global $DB;
// You must provide a tagid and correlatedtags must be set and be an array
if (empty($tagcorrelation->tagid) || !isset($tagcorrelation->correlatedtags) || !is_array($tagcorrelation->correlatedtags)) {
return false;
}
$tagcorrelation->correlatedtags = join(',', $tagcorrelation->correlatedtags);
if (!empty($tagcorrelation->id)) {
// The tag correlation already exists so update it
$DB->update_record('tag_correlation', $tagcorrelation);
} else {
// This is a new correlation to insert
$tagcorrelation->id = $DB->insert_record('tag_correlation', $tagcorrelation);
}
return $tagcorrelation->id;
}
/**
* Tasks that should be performed at cron time