MDL-13623 fixed regression in PHP < 5.2.0; merged from MOODLE_19_STABLE
[moodle.git] / tag / lib.php
CommitLineData
db32b054 1<?php // $Id$
4d4dac81 2
c933a060 3/**
db32b054 4 * lib.php - moodle tag library
d4be9d6d 5 *
db32b054 6 * @version: $Id$
7 * @licence http://www.gnu.org/copyleft/gpl.html GNU Public License
8 * @package moodlecore
9 *
10 * A "tag string" is always a rawurlencode'd string. This is the same behavior
11 * as http://del.icio.us
12 * @see http://www.php.net/manual/en/function.urlencode.php
13 *
14 * Tag strings : you can use any character in tags, except the comma (which is
15 * the separator) and the '\' (backslash). Note that many spaces (or other
16 * blank characters) will get "compressed" into one.
17 *
18 * A "record" is a php array (note that an object will work too) that contains
19 * the following variables :
20 * - type: the table containing the record that we are tagging (eg: for a
21 * blog, this is table 'post', and for a user it is 'user')
22 * - id: the id of the record
d4be9d6d 23 *
db32b054 24 * TODO: turn this into a full-fledged categorization system. This could start
25 * by modifying (removing, probably) the 'tag type' to use another table
26 * describing the relationship between tags (parents, sibling, etc.), which
27 * could then be merged with the 'course categorization' system...
c933a060 28 *
db32b054 29 * BASIC INSTRUCTIONS :
30 * - to "tag a blog post" (for example):
31 * tag_set('post', $blog_post->id, $array_of_tags);
d4be9d6d 32 *
db32b054 33 * - to "remove all the tags on a blog post":
34 * tag_set('post', $blog_post->id, array());
d4be9d6d 35 *
db32b054 36 * Tag set will create tags that need to be created.
c933a060 37 */
38
db32b054 39define('TAG_RETURN_ARRAY', 0);
40define('TAG_RETURN_OBJECT', 1);
41define('TAG_RETURN_TEXT', 2);
42define('TAG_RETURN_HTML', 3);
c933a060 43
db32b054 44define('TAG_CASE_LOWER', 0);
45define('TAG_CASE_ORIGINAL', 1);
d4be9d6d 46
bb062447 47require_once($CFG->dirroot .'/tag/locallib.php');
48
db32b054 49///////////////////////////////////////////////////////
50/////////////////// PUBLIC TAG API ////////////////////
c933a060 51
52/**
db32b054 53 * Delete one or more tag, and all their instances if there are any left.
54 *
55 * @param mixed $tagids one tagid (int), or one array of tagids to delete
56 * @return bool true on success, false otherwise
c933a060 57 */
db32b054 58function tag_delete($tagids) {
3d436d8a 59
db32b054 60 if (!is_array($tagids)) {
61 $tagids = array($tagids);
c933a060 62 }
3d436d8a 63
db32b054 64 $success = true;
65 foreach( $tagids as $tagid ) {
66 if (is_null($tagid)) { // can happen if tag doesn't exists
67 continue;
68 }
69 // only delete the main entry if there were no problems deleting all the
70 // instances - that (and the fact we won't often delete lots of tags)
71 // is the reason for not using delete_records_select()
72 if ( delete_records('tag_instance', 'tagid', $tagid) ) {
73 $success &= (bool) delete_records('tag', 'id', $tagid);
74 }
75 }
3d436d8a 76
db32b054 77 return $success;
c933a060 78}
79
80/**
db32b054 81 * Delete one instance of a tag. If the last instance was deleted, it will
82 * also delete the tag, unless it's type is 'official'.
d4be9d6d 83 *
db32b054 84 * @param array $record the record for which to remove the instance
85 * @param int $tagid the tagid that needs to be removed
86 * @return bool true on success, false otherwise
c933a060 87 */
db32b054 88function tag_delete_instance($record, $tagid) {
89 global $CFG;
70e9b59d 90
db32b054 91 if ( delete_records('tag_instance', 'tagid', $tagid, 'itemtype', $record['type'], 'itemid', $record['id']) ) {
2f9ba531 92 if ( !record_exists_sql("SELECT * FROM {$CFG->prefix}tag tg, {$CFG->prefix}tag_instance ti ".
93 "WHERE (tg.id = ti.tagid AND ti.tagid = {$tagid} ) OR ".
94 "(tg.id = {$tagid} AND tg.tagtype = 'official')") ) {
db32b054 95 return tag_delete($tagid);
96 }
70e9b59d 97 } else {
db32b054 98 return false;
c933a060 99 }
100}
101
102/**
db32b054 103 * Function that returns the name that should be displayed for a specific tag
c933a060 104 *
db32b054 105 * @param object $tag_object a line out of tag table, as returned by the adobd functions
106 * @return string
c933a060 107 */
db32b054 108function tag_display_name($tag_object) {
3d436d8a 109
6361e02a 110 global $CFG;
111
db32b054 112 if(!isset($tag_object->name)) {
113 return '';
4f81c51d 114 }
db32b054 115
116 if( empty($CFG->keeptagnamecase) ) {
117 //this is the normalized tag name
118 $textlib = textlib_get_instance();
119 return htmlspecialchars($textlib->strtotitle($tag_object->name));
120 }
121 else {
122 //original casing of the tag name
123 return htmlspecialchars($tag_object->rawname);
3d436d8a 124 }
c933a060 125}
126
127/**
db32b054 128 * Find all records tagged with a tag of a given type ('post', 'user', etc.)
d4be9d6d 129 *
db32b054 130 * @param string $tag tag to look for
131 * @param string $type type to restrict search to. If null, every matching
132 * record will be returned
2dc038ff 133 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
134 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
db32b054 135 * @return array of matching objects, indexed by record id, from the table containing the type requested
c933a060 136 */
2dc038ff 137function tag_find_records($tag, $type, $limitfrom='', $limitnum='') {
db32b054 138
139 global $CFG;
70e9b59d 140
db32b054 141 if (!$tag || !$type) {
142 return array();
c933a060 143 }
144
db32b054 145 $tagid = tag_get_id($tag);
146
147 $query = "SELECT it.* ".
148 "FROM {$CFG->prefix}{$type} it INNER JOIN {$CFG->prefix}tag_instance tt ON it.id = tt.itemid ".
149 "WHERE tt.itemtype = '{$type}' AND tt.tagid = '{$tagid}'";
150
2dc038ff 151 return get_records_sql($query, $limitfrom, $limitnum);
c933a060 152}
153
154/**
db32b054 155 * Get the array of db record of tags associated to a record (instances). Use
156 * tag_get_tags_csv to get the same information in a comma-separated string.
c933a060 157 *
db32b054 158 * @param array $record the record for which we want to get the tags
159 * @param string $type the tag type (either 'default' or 'official'). By default,
160 * all tags are returned.
161 * @return array the array of tags
c933a060 162 */
db32b054 163function tag_get_tags($record, $type=null) {
164
165 global $CFG;
3d436d8a 166
db32b054 167 if ($type) {
168 $type = "AND tg.tagtype = '$type'";
169 }
170
2f9ba531 171 $tags = get_records_sql("SELECT tg.id, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ".
172 "FROM {$CFG->prefix}tag_instance ti INNER JOIN {$CFG->prefix}tag tg ON tg.id = ti.tagid ".
173 "WHERE ti.itemtype = '{$record['type']}' AND ti.itemid = '{$record['id']}' {$type} ".
174 "ORDER BY ti.ordering ASC");
db32b054 175 // This version of the query, reversing the ON clause, "correctly" returns
176 // a row with NULL values for instances that are still in the DB even though
177 // the tag has been deleted. This shouldn't happen, but if it did, using
178 // this query could help "clean it up". This causes bugs at this time.
2f9ba531 179 //$tags = get_records_sql("SELECT ti.tagid, tg.tagtype, tg.name, tg.rawname, tg.flag, ti.ordering ".
180 // "FROM {$CFG->prefix}tag_instance ti LEFT JOIN {$CFG->prefix}tag tg ON ti.tagid = tg.id ".
181 // "WHERE ti.itemtype = '{$record['type']}' AND ti.itemid = '{$record['id']}' {$type} ".
182 // "ORDER BY ti.ordering ASC");
db32b054 183
184 if (!$tags) {
185 return array();
186 } else {
187 return $tags;
4f81c51d 188 }
db32b054 189}
190
191/**
192 * Get the array of tags display names, indexed by id.
193 *
194 * @param array $record the record for which we want to get the tags
195 * @param string $type the tag type (either 'default' or 'official'). By default,
196 * all tags are returned.
197 * @return array the array of tags (with the value returned by tag_display_name), indexed by id
198 */
199function tag_get_tags_array($record, $type=null) {
200 $tags = array();
201 foreach(tag_get_tags($record, $type) as $tag) {
202 $tags[$tag->id] = tag_display_name($tag);
4f81c51d 203 }
db32b054 204 return $tags;
c933a060 205}
206
207/**
db32b054 208 * Get a comma-separated string of tags associated to a record. Use tag_get_tags
209 * to get the same information in an array.
c933a060 210 *
db32b054 211 * @param array $record the record for which we want to get the tags
212 * @param int $html either TAG_RETURN_HTML or TAG_RETURN_TEXT, depending
213 * on the type of output desired
214 * @param string $type either 'official' or 'default', if null, all tags are
215 * returned
216 * @return string the comma-separated list of tags.
c933a060 217 */
db32b054 218function tag_get_tags_csv($record, $html=TAG_RETURN_HTML, $type=null) {
219 global $CFG;
c933a060 220
221 $tags_names = array();
db32b054 222 foreach( tag_get_tags($record, $type) as $tag ) {
223 if ($html == TAG_RETURN_TEXT) {
224 $tags_names[] = tag_display_name($tag);
225 } else { // TAG_RETURN_HTML
226 $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
227 }
c933a060 228 }
db32b054 229 return implode(', ', $tags_names);
c933a060 230}
231
4f81c51d 232/**
db32b054 233 * Get an array of tag ids associated to a record.
d4be9d6d 234 *
db32b054 235 * @param array $record the record for which we want to get the tags
236 * @return array of tag ids, indexed and sorted by 'ordering'
4f81c51d 237 */
db32b054 238function tag_get_tags_ids($record) {
239
240 $tag_ids = array();
241 foreach( tag_get_tags($record) as $tag ) {
242 $tag_ids[$tag->ordering] = $tag->id;
243 }
244 ksort($tag_ids);
245 return $tag_ids;
246}
3d436d8a 247
db32b054 248/**
249 * Returns the database ID of a set of tags.
250 *
251 * @param mixed $tags one tag, or array of tags, to look for.
dee71a16 252 * @param bool $return_value specify the type of the returned value. Either
253 * TAG_RETURN_OBJECT, or TAG_RETURN_ARRAY (default). If TAG_RETURN_ARRAY
254 * is specified, an array will be returned even if only one tag was
255 * passed in $tags.
256 * @return mixed tag-indexed array of ids (or objects, if second parameter is
257 * TAG_RETURN_OBJECT), or only an int, if only one tag is given *and* the
258 * second parameter is null. No value for a key means the tag wasn't found.
db32b054 259 */
260function tag_get_id($tags, $return_value=null) {
4f81c51d 261 global $CFG;
db32b054 262 static $tag_id_cache = array();
3d436d8a 263
db32b054 264 $return_an_int = false;
265 if (!is_array($tags)) {
266 if(is_null($return_value) || $return_value == TAG_RETURN_OBJECT) {
267 $return_an_int = true;
268 }
269 $tags = array($tags);
270 }
271
272 $result = array();
273
274 //TODO: test this and see if it helps performance without breaking anything
275 //foreach($tags as $key => $tag) {
276 // $clean_tag = moodle_strtolower($tag);
277 // if ( array_key_exists($clean_tag), $tag_id_cache) ) {
278 // $result[$clean_tag] = $tag_id_cache[$clean_tag];
279 // $tags[$key] = ''; // prevent further processing for this one.
280 // }
281 //}
282
283 $tags = array_values(tag_normalize($tags));
284 foreach($tags as $key => $tag) {
285 $tags[$key] = addslashes(moodle_strtolower($tag));
286 $result[moodle_strtolower($tag)] = null; // key must exists : no value for a key means the tag wasn't found.
287 }
288 $tag_string = "'". implode("', '", $tags) ."'";
289
2f9ba531 290 if ($rs = get_recordset_sql("SELECT * FROM {$CFG->prefix}tag WHERE name in ({$tag_string}) order by name")) {
db32b054 291 while ($record = rs_fetch_next_record($rs)) {
292 if ($return_value == TAG_RETURN_OBJECT) {
293 $result[$record->name] = $record;
294 } else { // TAG_RETURN_ARRAY
295 $result[$record->name] = $record->id;
296 }
297 }
4f81c51d 298 }
3d436d8a 299
db32b054 300 if ($return_an_int) {
301 return array_pop($result);
302 }
3d436d8a 303
db32b054 304 return $result;
c933a060 305}
306
307/**
db32b054 308 * Get a tag as an object (line) returned by get_recordset_sql
c933a060 309 *
db32b054 310 * @param int $tagid a tag id
311 * @return object a line returned from get_recordset_sql, or false
c933a060 312 */
db32b054 313function tag_get_tag_by_id($tagid) {
314 global $CFG;
2f9ba531 315 $rs = get_recordset_sql("SELECT * FROM {$CFG->prefix}tag WHERE id = $tagid");
db32b054 316 return rs_fetch_next_record($rs);
c933a060 317}
318
319/**
db32b054 320 * Returns tags related to a tag
d4be9d6d 321 *
db32b054 322 * Related tags of a tag come from two sources:
323 * - manually added related tags, which are tag_instance entries for that tag
324 * - correlated tags, which are a calculated
d4be9d6d 325 *
db32b054 326 * @param string $tag_name_or_id is a single **normalized** tag name or the id of a tag
327 * @param int $limitnum return a subset comprising this many records (optional, default is 10)
328 * @return array an array of tag objects
c933a060 329 */
db32b054 330function tag_get_related_tags($tagid, $limitnum=10) {
c933a060 331
db32b054 332 //gets the manually added related tags
333 if (!$related_tags = tag_get_tags(array('type'=>'tag', 'id'=>$tagid))) {
334 $related_tags = array();
c933a060 335 }
336
db32b054 337 //gets the correlated tags
338 $automatic_related_tags = tag_get_correlated($tagid, $limitnum);
339 if (is_array($automatic_related_tags)) {
340 $related_tags = array_merge($related_tags, $automatic_related_tags);
341 }
3d436d8a 342
db32b054 343 return array_slice(object_array_unique($related_tags), 0 , $limitnum);
c933a060 344}
345
db32b054 346/**
347 * Get a comma-separated list of tags related to another tag.
d4be9d6d 348 *
db32b054 349 * @param array $related_tags the array returned by tag_get_related_tags
350 * @param int $html either TAG_RETURN_HTML (default) or TAG_RETURN_TEXT : return html links, or just text.
351 * @return string comma-separated list
c933a060 352 */
db32b054 353function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) {
354 global $CFG;
c933a060 355
db32b054 356 $tags_names = array();
357 foreach($related_tags as $tag) {
358 if ( $html == TAG_RETURN_TEXT) {
0f039283 359 $tags_names[] = tag_display_name($tag);
db32b054 360 } else {
361 // TAG_RETURN_HTML
9fccb889 362 $tags_names[] = '<a href="'. $CFG->wwwroot .'/tag/index.php?tag='. rawurlencode($tag->name) .'">'. tag_display_name($tag) .'</a>';
c933a060 363 }
c933a060 364 }
db32b054 365 return implode(', ', $tags_names);
c933a060 366}
367
3a597ace 368/**
db32b054 369 * Change the "value" of a tag, and update the associated 'name'.
3a597ace 370 *
db32b054 371 * @param int $tagid the id of the tag to modify
372 * @param string $newtag the new name
373 * @return bool true on success, false otherwise
3a597ace 374 */
db32b054 375function tag_rename($tagid, $newtag) {
3a597ace 376
db32b054 377 if (! $newtag_clean = array_shift(tag_normalize($newtag, TAG_CASE_ORIGINAL)) ) {
378 return false;
379 }
3a597ace 380
db32b054 381 if ( tag_get_id($newtag_clean) ) {
382 // 'newtag' already exists and merging tags is not yet supported.
383 return false;
384 }
3a597ace 385
db32b054 386 if ($tag = get_record('tag', 'id', $tagid)) {
387 $tag->rawname = addslashes($newtag_clean);
388 $tag->name = addslashes(moodle_strtolower($newtag_clean));
389 $tag->timemodified = time();
390 return update_record('tag', $tag);
391 }
392 return false;
3a597ace 393}
394
c933a060 395/**
db32b054 396 * Set the tags assigned to a record. This overwrites the current tags.
397 *
398 * This function is meant to be fed the string coming up from the user
399 * interface, which contains all tags assigned to a record.
d4be9d6d 400 *
db32b054 401 * @param string $record_type the type of record to tag ('post' for blogs,
402 * 'user' for users, 'tag' for tags, etc.
403 * @param int $record_id the id of the record to tag
404 * @param array $tags the array of tags to set on the record. If
405 * given an empty array, all tags will be removed.
406 * @return void
c933a060 407 */
db32b054 408function tag_set($record_type, $record_id, $tags) {
409 global $db;
c933a060 410
db32b054 411 $record = array('type' => $record_type, 'id' => $record_id);
3a597ace 412
db32b054 413 $tags_ids = tag_get_id($tags, TAG_RETURN_ARRAY); // force an array, even if we only have one tag.
414 $cleaned_tags = tag_normalize($tags);
415 //echo 'tags-in-tag_set'; var_dump($tags); var_dump($tags_ids); var_dump($cleaned_tags);
3a597ace 416
db32b054 417 $current_ids = tag_get_tags_ids($record);
418 //var_dump($current_ids);
419 $tags_to_assign = array();
3d436d8a 420
db32b054 421 // for data coherence reasons, it's better to remove deleted tags
422 // before adding new data: ordering could be duplicated.
423 foreach($current_ids as $current_id) {
424 if (!in_array($current_id, $tags_ids)) {
425 tag_delete_instance($record, $current_id);
426 }
427 }
3d436d8a 428
db32b054 429 foreach($tags as $ordering => $tag) {
430 $tag = trim($tag);
431 if (!$tag) {
432 continue;
26d8649f 433 }
db32b054 434
435 $clean_tag = $cleaned_tags[$tag];
436 $tag_current_id = $tags_ids[$clean_tag];
437
438 if ( is_null($tag_current_id) ) {
439 // create new tags
440 //echo "call to add tag $tag\n";
441 $new_tag = tag_add($tag);
442 tag_assign($record, $new_tag[$clean_tag], $ordering);
443 }
444 elseif ( empty($current_ids) || !in_array($tag_current_id, $current_ids) ) {
445 // assign existing tags
446 tag_assign($record, $tag_current_id, $ordering);
447 }
448 elseif ( isset($current_ids[$ordering]) && $current_ids[$ordering] != $tag_current_id ) {
449 // this actually checks if the ordering number points to the same tag
450 //recompute ordering, if necessary
451 //echo 'ordering changed for ', $tag, ':', $ordering, "\n";
452 tag_assign($record, $tag_current_id, $ordering);
1acd661d 453 }
c933a060 454 }
db32b054 455}
c933a060 456
db32b054 457/**
458 * Adds a tag to a record, without overwriting the current tags.
459 *
460 * @param string $record_type the type of record to tag ('post' for blogs,
461 * 'user' for users, etc.
462 * @param int $record_id the id of the record to tag
463 * @param string $tag the tag to add
464 * @return void
465 */
466function tag_set_add($record_type, $record_id, $tag) {
3d436d8a 467
db32b054 468 $record = array('type' => $record_type, 'id' => $record_id);
469
470 $new_tags = array();
471 foreach( tag_get_tags($record) as $current_tag ) {
472 $new_tags[] = $current_tag->rawname;
473 }
474 $new_tags[] = $tag;
475
476 return tag_set($record_type, $record_id, $new_tags);
477}
c933a060 478
3627b0e3 479/**
480 * Removes a tag from a record, without overwriting other current tags.
481 *
482 * @param string $record_type the type of record to tag ('post' for blogs,
483 * 'user' for users, etc.
484 * @param int $record_id the id of the record to tag
485 * @param string $tag the tag to delete
486 * @return void
487 */
488function tag_set_delete($record_type, $record_id, $tag) {
489
490 $record = array('type' => $record_type, 'id' => $record_id);
491
492 $new_tags = array();
493 foreach( tag_get_tags($record) as $current_tag ) {
494 if ($current_tag->name != $tag) { // Keep all tags but the one specified
495 $new_tags[] = $current_tag->name;
496 }
497 }
498
499 return tag_set($record_type, $record_id, $new_tags);
500}
501
db32b054 502/**
503 * Set the type of a tag. At this time (version 1.9) the possible values
504 * are 'default' or 'official'. Official tags will be displayed separately "at
505 * tagging time" (while selecting the tags to apply to a record).
506 *
507 * @param string $tagid tagid to modify
508 * @param string $type either 'default' or 'official'
509 * @return true on success, false otherwise
510 */
511function tag_type_set($tagid, $type) {
512 if ($tag = get_record('tag', 'id', $tagid)) {
513 $tag->tagtype = $type;
514 $tag->timemodified = time();
515 return update_record('tag', $tag);
516 }
517 return false;
c933a060 518}
519
db32b054 520///////////////////////////////////////////////////////
521/////////////////// PRIVATE TAG API ///////////////////
c933a060 522
523/**
db32b054 524 * A * @param array $record the record that will be tagged
525 * @param string $tags the comma-separated tags to set on the record. If
526 * given an empty array, all tags will be removed.
527dds one or more tag in the database. This function should not be called
528 * directly : you should use tag_set.
d4be9d6d 529 *
db32b054 530 * @param mixed $tags one tag, or an array of tags, to be created
531 * @param string $tag_type type of tag to be created ("default" is the default
532 * value and "official" is the only other supported value at this time). An
533 * official tag is kept even if there are no records tagged with it.
534 * @return an array of tags ids, indexed by their lowercase normalized names.
535 * Any boolean false in the array indicates an error while adding the tag.
c933a060 536 */
db32b054 537function tag_add($tags, $type="default") {
538 global $USER;
c933a060 539
db32b054 540 require_capability('moodle/tag:create', get_context_instance(CONTEXT_SYSTEM));
3d436d8a 541
db32b054 542 if (!is_array($tags)) {
543 $tags = array($tags);
c933a060 544 }
3d436d8a 545
db32b054 546 $tag_object = new StdClass;
547 $tag_object->tagtype = $type;
548 $tag_object->userid = $USER->id;
549 $tag_object->timemodified = time();
c933a060 550
db32b054 551 $clean_tags = tag_normalize($tags, TAG_CASE_ORIGINAL);
c933a060 552
db32b054 553 $tags_ids = array();
554 foreach($clean_tags as $tag) {
555 $tag = trim($tag);
556 if (!$tag) {
557 $tags_ids[$tag] = false;
558 } else {
559 // note that the difference between rawname and name is only
560 // capitalization : the rawname is NOT the same at the rawtag.
561 $tag_object->rawname = addslashes($tag);
562 $tag_name_lc = moodle_strtolower($tag);
563 $tag_object->name = addslashes($tag_name_lc);
564 //var_dump($tag_object);
565 $tags_ids[$tag_name_lc] = insert_record('tag', $tag_object);
566 }
567 }
c933a060 568
db32b054 569 return $tags_ids;
c933a060 570}
571
572/**
db32b054 573 * Assigns a tag to a record: if the record already exists, the time and
574 * ordering will be updated.
575 *
576 * @param array $record the record that will be tagged
577 * @param string $tagid the tag id to set on the record.
578 * @param int $ordering the order of the instance for this record
579 * @return bool true on success, false otherwise
c933a060 580 */
db32b054 581function tag_assign($record, $tagid, $ordering) {
c933a060 582
db32b054 583 require_capability('moodle/tag:create', get_context_instance(CONTEXT_SYSTEM));
c933a060 584
db32b054 585 if ( $tag_instance_object = get_record('tag_instance', 'tagid', $tagid, 'itemtype', $record['type'], 'itemid', $record['id']) ) {
586 $tag_instance_object->ordering = $ordering;
587 $tag_instance_object->timemodified = time();
588 return update_record('tag_instance', $tag_instance_object);
589 } else {
590 $tag_instance_object = new StdClass;
591 $tag_instance_object->tagid = $tagid;
592 $tag_instance_object->itemid = $record['id'];
593 $tag_instance_object->itemtype = $record['type'];
594 $tag_instance_object->ordering = $ordering;
595 $tag_instance_object->timemodified = time();
596 return insert_record('tag_instance', $tag_instance_object);
c933a060 597 }
c933a060 598}
599
600/**
db32b054 601 * Function that returns tags that start with some text, for use by the autocomplete feature
d4be9d6d 602 *
db32b054 603 * @param string $text string that the tag names will be matched against
c933a060 604 * @return mixed an array of objects, or false if no records were found or an error occured.
605 */
db32b054 606function tag_autocomplete($text) {
c933a060 607 global $CFG;
2f9ba531 608 return get_records_sql("SELECT tg.id, tg.name, tg.rawname FROM {$CFG->prefix}tag tg WHERE tg.name LIKE '". moodle_strtolower($text) ."%'");
c933a060 609}
610
c933a060 611/**
db32b054 612 * Calculates and stores the correlated tags of all tags.
613 * The correlations are stored in the 'tag_correlation' table.
614 *
615 * Two tags are correlated if they appear together a lot.
616 * Ex.: Users tagged with "computers" will probably also be tagged with "algorithms".
d4be9d6d 617 *
db32b054 618 * The rationale for the 'tag_correlation' table is performance.
619 * It works as a cache for a potentially heavy load query done at the 'tag_instance' table.
620 * So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table.
d4be9d6d 621 *
db32b054 622 * @param number $min_correlation cutoff percentage (optional, default is 2)
c933a060 623 */
db32b054 624function tag_compute_correlations($min_correlation=2) {
3d436d8a 625
c933a060 626 global $CFG;
3d436d8a 627
db32b054 628 $all_tags = get_records_list('tag');
629
630
631 $tag_correlation_obj = new object();
632 foreach($all_tags as $tag) {
633
634 // query that counts how many times any tag appears together in items
635 // with the tag passed as argument ($tag_id)
636 $query = "SELECT tb.tagid , COUNT(*) AS nr ".
637 "FROM {$CFG->prefix}tag_instance ta INNER JOIN {$CFG->prefix}tag_instance tb ON ta.itemid = tb.itemid ".
638 "WHERE ta.tagid = {$tag->id} AND tb.tagid != {$tag->id} ".
639 "GROUP BY tb.tagid ".
640 "HAVING nr > $min_correlation ".
641 "ORDER BY nr DESC"; // todo: find out if it's necessary to order.
642
643 $correlated = array();
644
645 // Correlated tags happen when they appear together in more occasions
646 // than $min_correlation.
647 if ($tag_correlations = get_records_sql($query)) {
648 foreach($tag_correlations as $correlation) {
2f9ba531 649 // commented out - now done in query. kept here in case it breaks on some db
db32b054 650 // if($correlation->nr >= $min_correlation){
651 $correlated[] = $correlation->tagid;
652 // }
653 }
654 }
c933a060 655
db32b054 656 $correlated = implode(',', $correlated);
657 //var_dump($correlated);
658
659 //saves correlation info in the caching table
660 if ($tag_correlation_obj = get_record('tag_correlation', 'tagid', $tag->id)) {
661 $tag_correlation_obj->correlatedtags = $correlated;
662 update_record('tag_correlation', $tag_correlation_obj);
663 } else {
664 $tag_correlation_obj->tagid = $tag->id;
665 $tag_correlation_obj->correlatedtags = $correlated;
666 insert_record('tag_correlation', $tag_correlation_obj);
667 }
3d436d8a 668 }
db32b054 669}
670
671/**
672 * Tasks that should be performed at cron time
673 */
674function tag_cron() {
675 tag_compute_correlations();
676}
677
0f039283 678/**
679 * Search for tags with names that match some text
680 *
681 * @param string $text escaped string that the tag names will be matched against
682 * @param boolean $ordered If true, tags are ordered by their popularity. If false, no ordering.
683 * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
684 * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
685 * @return mixed an array of objects, or false if no records were found or an error occured.
686 */
687function tag_find_tags($text, $ordered=true, $limitfrom='', $limitnum='') {
688
689 global $CFG;
690
691 $text = array_shift(tag_normalize($text, TAG_CASE_LOWER));
692
693 if ($ordered) {
694 $query = "SELECT tg.id, tg.name, tg.rawname, COUNT(ti.id) AS count ".
695 "FROM {$CFG->prefix}tag tg LEFT JOIN {$CFG->prefix}tag_instance ti ON tg.id = ti.tagid ".
696 "WHERE tg.name LIKE '%{$text}%' ".
697 "GROUP BY tg.id, tg.name, tg.rawname ".
698 "ORDER BY count DESC";
699 } else {
700 $query = "SELECT tg.id, tg.name, tg.rawname ".
701 "FROM {$CFG->prefix}tag tg ".
702 "WHERE tg.name LIKE '%{$text}%'";
703 }
704 return get_records_sql($query, $limitfrom , $limitnum);
705}
706
db32b054 707/**
708 * Get the name of a tag
709 *
710 * @param mixed $tagids the id of the tag, or an array of ids
711 * @return mixed string name of one tag, or id-indexed array of strings
712 */
713function tag_get_name($tagids) {
3d436d8a 714
db32b054 715 $return_a_string = false;
716 if ( !is_array($tagids) ) {
717 $return_a_string = true;
718 $tagids = array($tagids);
719 }
c933a060 720
db32b054 721 $tag_names = array();
722 foreach(get_records_list('tag', 'id', implode(',', $tagids)) as $tag) {
723 $tag_names[$tag->id] = $tag->name;
724 }
c933a060 725
db32b054 726 if ($return_a_string) {
727 return array_pop($tag_names);
728 }
c933a060 729
db32b054 730 return $tag_names;
c933a060 731}
732
733/**
db32b054 734 * Returns the correlated tags of a tag, retrieved from the tag_correlation
735 * table. Make sure cron runs, otherwise the table will be empty and this
736 * function won't return anything.
c933a060 737 *
db32b054 738 * @param int $tag_id is a single tag id
739 * @return array an array of tag objects, empty if no correlated tags are found
c933a060 740 */
db32b054 741function tag_get_correlated($tag_id, $limitnum=null) {
3d436d8a 742
db32b054 743 $tag_correlation = get_record('tag_correlation', 'tagid', $tag_id);
c933a060 744
db32b054 745 if (!$tag_correlation || empty($tag_correlation->correlatedtags)) {
746 return array();
c933a060 747 }
db32b054 748
749 if (!$result = get_records_select('tag', "id IN ({$tag_correlation->correlatedtags})", '', '*', 0, $limitnum)) {
750 return array();
c933a060 751 }
752
db32b054 753 return $result;
c933a060 754}
755
c933a060 756/**
db32b054 757 * Function that normalizes a list of tag names.
c933a060 758 *
db32b054 759 * @param mixed $tags array of tags, or a single tag.
0f039283 760 * @param int $case case to use for returned value (default: lower case).
761 * Either TAG_CASE_LOWER (default) or TAG_CASE_ORIGINAL
762 * @return array of lowercased normalized tags, indexed by the normalized tag,
763 * in the same order as the original array. (Eg: 'Banana' => 'banana').
c933a060 764 */
db32b054 765function tag_normalize($rawtags, $case = TAG_CASE_LOWER) {
766
0f039283 767 // cache normalized tags, to prevent costly repeated calls to clean_param
db32b054 768 static $cleaned_tags_lc = array(); // lower case - use for comparison
769 static $cleaned_tags_mc = array(); // mixed case - use for saving to database
c933a060 770
db32b054 771 if ( !is_array($rawtags) ) {
772 $rawtags = array($rawtags);
773 }
c933a060 774
db32b054 775 $result = array();
776 foreach($rawtags as $rawtag) {
777 $rawtag = trim($rawtag);
778 if (!$rawtag) {
779 continue;
780 }
781 if ( !array_key_exists($rawtag, $cleaned_tags_lc) ) {
782 $cleaned_tags_lc[$rawtag] = moodle_strtolower( clean_param($rawtag, PARAM_TAG) );
783 $cleaned_tags_mc[$rawtag] = clean_param($rawtag, PARAM_TAG);
784 }
785 if ( $case == TAG_CASE_LOWER ) {
786 $result[$rawtag] = $cleaned_tags_lc[$rawtag];
787 } else { // TAG_CASE_ORIGINAL
788 $result[$rawtag] = $cleaned_tags_mc[$rawtag];
789 }
790 }
791
792 return $result;
c933a060 793}
794
795/**
0f039283 796 * Count how many records are tagged with a specific tag,
c933a060 797 *
0f039283 798 * @param string $record record to look for ('post', 'user', etc.)
799 * @param int $tag is a single tag id
800 * @return int number of mathing tags.
c933a060 801 */
0f039283 802function tag_record_count($record_type, $tagid) {
803 return count_records('tag_instance', 'itemtype', $record_type, 'tagid', $tagid);
804}
c933a060 805
0f039283 806/**
807 * Determine if a record is tagged with a specific tag
808 *
809 * @param string $record_type the record type to look for
810 * @param int $record_id the record id to look for
811 * @param string $tag a tag name
812 * @return bool true if it is tagged, false otherwise
813 */
814function tag_record_tagged_with($record_type, $record_id, $tag) {
815 if ($tagid = tag_get_id($tag)) {
816 return count_records('tag_instance', 'itemtype', $record_type, 'itemid', $record_id, 'tagid', $tagid);
c933a060 817 } else {
0f039283 818 return 0; // tag doesn't exist
c933a060 819 }
c933a060 820}
821
822/**
db32b054 823 * Flag a tag as inapropriate
824 *
825 * @param mixed $tagids one (int) tagid, or an array of tagids
826 * @return void
c933a060 827 */
db32b054 828function tag_set_flag($tagids) {
829 if ( !is_array($tagids) ) {
830 $tagids = array($tagids);
1e550d2f 831 }
db32b054 832 foreach ($tagids as $tagid) {
833 $tag = get_record('tag', 'id', $tagid);
c933a060 834 $tag->flag++;
835 $tag->timemodified = time();
c933a060 836 update_record('tag', $tag);
837 }
838}
839
db32b054 840/**
841 * Remove the inapropriate flag on a tag
842 *
843 * @param mixed $tagids one (int) tagid, or an array of tagids
844 * @return bool true if function succeeds, false otherwise
c933a060 845 */
db32b054 846function tag_unset_flag($tagids) {
c933a060 847 global $CFG;
d4be9d6d 848
db32b054 849 require_capability('moodle/tag:manage', get_context_instance(CONTEXT_SYSTEM));
c933a060 850
db32b054 851 if ( is_array($tagids) ) {
852 $tagids = implode(',', $tagids);
ab86d9c7 853 }
db32b054 854 $timemodified = time();
2f9ba531 855 return execute_sql("UPDATE {$CFG->prefix}tag tg SET tg.flag = 0, tg.timemodified = $timemodified WHERE tg.id IN ($tagids)", false);
c933a060 856}
857
c933a060 858?>