[ Index ]

PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008]

title

Body

[close]

/tag/ -> lib.php (source)

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


Generated: Wed Jan 14 11:33:29 2009 Cross-referenced by PHPXref 0.7