Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Class to manage tag collections
  19   *
  20   * @package   core_tag
  21   * @copyright 2015 Marina Glancy
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  /**
  28   * Class to manage tag collections
  29   *
  30   * @package   core_tag
  31   * @copyright 2015 Marina Glancy
  32   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class core_tag_collection {
  35  
  36      /** @var string used for function cloud_sort() */
  37      public static $cloudsortfield = 'name';
  38  
  39      /**
  40       * Returns the list of tag collections defined in the system.
  41       *
  42       * @param bool $onlysearchable only return collections that can be searched.
  43       * @return array array of objects where each object has properties: id, name, isdefault, itemtypes, sortorder
  44       */
  45      public static function get_collections($onlysearchable = false) {
  46          global $DB;
  47          $cache = cache::make('core', 'tags');
  48          if (($tagcolls = $cache->get('tag_coll')) === false) {
  49              // Retrieve records from DB and create a default one if it is not present.
  50              $tagcolls = $DB->get_records('tag_coll', null, 'isdefault DESC, sortorder, id');
  51              if (empty($tagcolls)) {
  52                  // When this method is called for the first time it automatically creates the default tag collection.
  53                  $DB->insert_record('tag_coll', array('isdefault' => 1, 'sortorder' => 0));
  54                  $tagcolls = $DB->get_records('tag_coll');
  55              } else {
  56                  // Make sure sortorder is correct.
  57                  $idx = 0;
  58                  foreach ($tagcolls as $id => $tagcoll) {
  59                      if ($tagcoll->sortorder != $idx) {
  60                          $DB->update_record('tag_coll', array('sortorder' => $idx, 'id' => $id));
  61                          $tagcolls[$id]->sortorder = $idx;
  62                      }
  63                      $idx++;
  64                  }
  65              }
  66              $cache->set('tag_coll', $tagcolls);
  67          }
  68          if ($onlysearchable) {
  69              $rv = array();
  70              foreach ($tagcolls as $id => $tagcoll) {
  71                  if ($tagcoll->searchable) {
  72                      $rv[$id] = $tagcoll;
  73                  }
  74              }
  75              return $rv;
  76          }
  77          return $tagcolls;
  78      }
  79  
  80      /**
  81       * Returns the tag collection object
  82       *
  83       * @param int $tagcollid
  84       * @return stdClass
  85       */
  86      public static function get_by_id($tagcollid) {
  87          $tagcolls = self::get_collections();
  88          if (array_key_exists($tagcollid, $tagcolls)) {
  89              return $tagcolls[$tagcollid];
  90          }
  91          return null;
  92      }
  93  
  94      /**
  95       * Returns the list of existing tag collections as id=>name
  96       *
  97       * @param bool $unlockedonly
  98       * @param bool $onlysearchable
  99       * @param string $selectalllabel
 100       * @return array
 101       */
 102      public static function get_collections_menu($unlockedonly = false, $onlysearchable = false,
 103              $selectalllabel = null) {
 104          $tagcolls = self::get_collections($onlysearchable);
 105          $options = array();
 106          foreach ($tagcolls as $id => $tagcoll) {
 107              if (!$unlockedonly || empty($tagcoll->component)) {
 108                  $options[$id] = self::display_name($tagcoll);
 109              }
 110          }
 111          if (count($options) > 1 && $selectalllabel) {
 112              $options = array(0 => $selectalllabel) + $options;
 113          }
 114          return $options;
 115      }
 116  
 117      /**
 118       * Returns id of the default tag collection
 119       *
 120       * @return int
 121       */
 122      public static function get_default() {
 123          $collections = self::get_collections();
 124          $keys = array_keys($collections);
 125          return $keys[0];
 126      }
 127  
 128      /**
 129       * Returns formatted name of the tag collection
 130       *
 131       * @param stdClass $record record from DB table tag_coll
 132       * @return string
 133       */
 134      public static function display_name($record) {
 135          $syscontext = context_system::instance();
 136          if (!empty($record->component)) {
 137              $identifier = 'tagcollection_' .
 138                      clean_param($record->name, PARAM_STRINGID);
 139              $component = $record->component;
 140              if ($component === 'core') {
 141                  $component = 'tag';
 142              }
 143              return get_string($identifier, $component);
 144          }
 145          if (!empty($record->name)) {
 146              return format_string($record->name, true, array('context' => $syscontext));
 147          } else if ($record->isdefault) {
 148              return get_string('defautltagcoll', 'tag');
 149          } else {
 150              return $record->id;
 151          }
 152      }
 153  
 154      /**
 155       * Returns all tag areas in the given tag collection
 156       *
 157       * @param int $tagcollid
 158       * @return array
 159       */
 160      public static function get_areas($tagcollid) {
 161          $allitemtypes = core_tag_area::get_areas($tagcollid, true);
 162          $itemtypes = array();
 163          foreach ($allitemtypes as $itemtype => $it) {
 164              foreach ($it as $component => $v) {
 165                  $itemtypes[$v->id] = $v;
 166              }
 167          }
 168          return $itemtypes;
 169      }
 170  
 171      /**
 172       * Returns the list of names of areas (enabled only) that are in this collection.
 173       *
 174       * @param int $tagcollid
 175       * @return array
 176       */
 177      public static function get_areas_names($tagcollid, $enabledonly = true) {
 178          $allitemtypes = core_tag_area::get_areas($tagcollid, $enabledonly);
 179          $itemtypes = array();
 180          foreach ($allitemtypes as $itemtype => $it) {
 181              foreach ($it as $component => $v) {
 182                  $itemtypes[$v->id] = core_tag_area::display_name($component, $itemtype);
 183              }
 184          }
 185          return $itemtypes;
 186      }
 187  
 188      /**
 189       * Creates a new tag collection
 190       *
 191       * @param stdClass $data data from form core_tag_collection_form
 192       * @return int|false id of created tag collection or false if failed
 193       */
 194      public static function create($data) {
 195          global $DB;
 196          $data = (object)$data;
 197          $tagcolls = self::get_collections();
 198          $tagcoll = (object)array(
 199              'name' => $data->name,
 200              'isdefault' => 0,
 201              'component' => !empty($data->component) ? $data->component : null,
 202              'sortorder' => count($tagcolls),
 203              'searchable' => isset($data->searchable) ? (int)(bool)$data->searchable : 1,
 204              'customurl' => !empty($data->customurl) ? $data->customurl : null,
 205          );
 206          $tagcoll->id = $DB->insert_record('tag_coll', $tagcoll);
 207  
 208          // Reset cache.
 209          cache::make('core', 'tags')->delete('tag_coll');
 210  
 211          \core\event\tag_collection_created::create_from_record($tagcoll)->trigger();
 212          return $tagcoll;
 213      }
 214  
 215      /**
 216       * Updates the tag collection information
 217       *
 218       * @param stdClass $tagcoll existing record in DB table tag_coll
 219       * @param stdClass $data data to update
 220       * @return bool wether the record was updated
 221       */
 222      public static function update($tagcoll, $data) {
 223          global $DB;
 224          $defaulttagcollid = self::get_default();
 225          $allowedfields = array('name', 'searchable', 'customurl');
 226          if ($tagcoll->id == $defaulttagcollid) {
 227              $allowedfields = array('name');
 228          }
 229  
 230          $updatedata = array();
 231          $data = (array)$data;
 232          foreach ($allowedfields as $key) {
 233              if (array_key_exists($key, $data) && $data[$key] !== $tagcoll->$key) {
 234                  $updatedata[$key] = $data[$key];
 235              }
 236          }
 237  
 238          if (!$updatedata) {
 239              // Nothing to update.
 240              return false;
 241          }
 242  
 243          if (isset($updatedata['searchable'])) {
 244              $updatedata['searchable'] = (int)(bool)$updatedata['searchable'];
 245          }
 246          foreach ($updatedata as $key => $value) {
 247              $tagcoll->$key = $value;
 248          }
 249          $updatedata['id'] = $tagcoll->id;
 250          $DB->update_record('tag_coll', $updatedata);
 251  
 252          // Reset cache.
 253          cache::make('core', 'tags')->delete('tag_coll');
 254  
 255          \core\event\tag_collection_updated::create_from_record($tagcoll)->trigger();
 256  
 257          return true;
 258      }
 259  
 260      /**
 261       * Deletes a custom tag collection
 262       *
 263       * @param stdClass $tagcoll existing record in DB table tag_coll
 264       * @return bool wether the tag collection was deleted
 265       */
 266      public static function delete($tagcoll) {
 267          global $DB, $CFG;
 268  
 269          $defaulttagcollid = self::get_default();
 270          if ($tagcoll->id == $defaulttagcollid) {
 271              return false;
 272          }
 273  
 274          // Move all tags from this tag collection to the default one.
 275          $allitemtypes = core_tag_area::get_areas($tagcoll->id);
 276          foreach ($allitemtypes as $it) {
 277              foreach ($it as $v) {
 278                  core_tag_area::update($v, array('tagcollid' => $defaulttagcollid));
 279              }
 280          }
 281  
 282          // Delete tags from this tag_coll.
 283          core_tag_tag::delete_tags($DB->get_fieldset_select('tag', 'id', 'tagcollid = ?', array($tagcoll->id)));
 284  
 285          // Delete the tag collection.
 286          $DB->delete_records('tag_coll', array('id' => $tagcoll->id));
 287  
 288          // Reset cache.
 289          cache::make('core', 'tags')->delete('tag_coll');
 290  
 291          \core\event\tag_collection_deleted::create_from_record($tagcoll)->trigger();
 292  
 293          return true;
 294      }
 295  
 296      /**
 297       * Moves the tag collection in the list one position up or down
 298       *
 299       * @param stdClass $tagcoll existing record in DB table tag_coll
 300       * @param int $direction move direction: +1 or -1
 301       * @return bool
 302       */
 303      public static function change_sortorder($tagcoll, $direction) {
 304          global $DB;
 305          if ($direction != -1 && $direction != 1) {
 306              throw new coding_exception('Second argument in tag_coll_change_sortorder() can be only 1 or -1');
 307          }
 308          $tagcolls = self::get_collections();
 309          $keys = array_keys($tagcolls);
 310          $idx = array_search($tagcoll->id, $keys);
 311          if ($idx === false || $idx == 0 || $idx + $direction < 1 || $idx + $direction >= count($tagcolls)) {
 312              return false;
 313          }
 314          $otherid = $keys[$idx + $direction];
 315          $DB->update_record('tag_coll', array('id' => $tagcoll->id, 'sortorder' => $idx + $direction));
 316          $DB->update_record('tag_coll', array('id' => $otherid, 'sortorder' => $idx));
 317          // Reset cache.
 318          cache::make('core', 'tags')->delete('tag_coll');
 319          return true;
 320      }
 321  
 322      /**
 323       * Permanently deletes all non-standard tags that no longer have any instances pointing to them
 324       *
 325       * @param array $collections optional list of tag collections ids to cleanup
 326       */
 327      public static function cleanup_unused_tags($collections = null) {
 328          global $DB, $CFG;
 329  
 330          $params = array();
 331          $sql = "SELECT tg.id FROM {tag} tg LEFT OUTER JOIN {tag_instance} ti ON ti.tagid = tg.id
 332                  WHERE ti.id IS NULL AND tg.isstandard = 0";
 333          if ($collections) {
 334              list($sqlcoll, $params) = $DB->get_in_or_equal($collections, SQL_PARAMS_NAMED);
 335              $sql .= " AND tg.tagcollid " . $sqlcoll;
 336          }
 337          if ($unusedtags = $DB->get_fieldset_sql($sql, $params)) {
 338              core_tag_tag::delete_tags($unusedtags);
 339          }
 340      }
 341  
 342      /**
 343       * Returns the list of tags with number of items tagged
 344       *
 345       * @param int $tagcollid
 346       * @param null|bool $isstandard return only standard tags
 347       * @param int $limit maximum number of tags to retrieve, tags are sorted by the instance count
 348       *            descending here regardless of $sort parameter
 349       * @param string $sort sort order for display, default 'name' - tags will be sorted after they are retrieved
 350       * @param string $search search string
 351       * @param int $fromctx context id where this tag cloud is displayed
 352       * @param int $ctx only retrieve tag instances in this context
 353       * @param int $rec retrieve tag instances in the $ctx context and it's children (default 1)
 354       * @return \core_tag\output\tagcloud
 355       */
 356      public static function get_tag_cloud($tagcollid, $isstandard = false, $limit = 150, $sort = 'name',
 357              $search = '', $fromctx = 0, $ctx = 0, $rec = 1) {
 358          global $DB;
 359  
 360          $fromclause = 'FROM {tag_instance} ti JOIN {tag} tg ON tg.id = ti.tagid';
 361          $whereclause = 'WHERE ti.itemtype <> \'tag\'';
 362          list($sql, $params) = $DB->get_in_or_equal($tagcollid ? array($tagcollid) :
 363              array_keys(self::get_collections(true)));
 364          $whereclause .= ' AND tg.tagcollid ' . $sql;
 365          if ($isstandard) {
 366              $whereclause .= ' AND tg.isstandard = 1';
 367          }
 368          $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
 369          if ($rec && $context->contextlevel != CONTEXT_SYSTEM) {
 370              $fromclause .= ' JOIN {context} ctx ON ctx.id = ti.contextid ';
 371              $whereclause .= ' AND ctx.path LIKE ?';
 372              $params[] = $context->path . '%';
 373          } else if (!$rec) {
 374              $whereclause .= ' AND ti.contextid = ?';
 375              $params[] = $context->id;
 376          }
 377          if (strval($search) !== '') {
 378              $whereclause .= ' AND tg.name LIKE ?';
 379              $params[] = '%' . core_text::strtolower($search) . '%';
 380          }
 381          $tagsincloud = $DB->get_records_sql(
 382                  "SELECT tg.id, tg.rawname, tg.name, tg.isstandard, COUNT(ti.id) AS count, tg.flag, tg.tagcollid
 383                  $fromclause
 384                  $whereclause
 385                  GROUP BY tg.id, tg.rawname, tg.name, tg.flag, tg.isstandard, tg.tagcollid
 386                  ORDER BY count DESC, tg.name ASC",
 387              $params, 0, $limit);
 388  
 389          $tagscount = count($tagsincloud);
 390          if ($tagscount == $limit) {
 391              $tagscount = $DB->get_field_sql("SELECT COUNT(DISTINCT tg.id) $fromclause $whereclause", $params);
 392          }
 393  
 394          self::$cloudsortfield = $sort;
 395          usort($tagsincloud, self::class . "::cloud_sort");
 396  
 397          return new core_tag\output\tagcloud($tagsincloud, $tagscount, $fromctx, $ctx, $rec);
 398      }
 399  
 400      /**
 401       * This function is used to sort the tags in the cloud.
 402       *
 403       * @param   string $a Tag name to compare against $b
 404       * @param   string $b Tag name to compare against $a
 405       * @return  int    The result of the comparison/validation 1, 0 or -1
 406       */
 407      public static function cloud_sort($a, $b) {
 408          $tagsort = self::$cloudsortfield ?: 'name';
 409  
 410          if (is_numeric($a->$tagsort)) {
 411              return (($a->$tagsort == $b->$tagsort) ? 0 : ($a->$tagsort > $b->$tagsort)) ? 1 : -1;
 412          } else if (is_string($a->$tagsort)) {
 413              return strcmp($a->$tagsort, $b->$tagsort);
 414          } else {
 415              return 0;
 416          }
 417      }
 418  }