Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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   * This filter provides automatic linking to
  19   * glossary entries, aliases and categories when
  20   * found inside every Moodle text.
  21   *
  22   * @package    filter
  23   * @subpackage glossary
  24   * @copyright  2004 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  25   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26   */
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  /**
  31   * Glossary linking filter class.
  32   *
  33   * NOTE: multilang glossary entries are not compatible with this filter.
  34   */
  35  class filter_glossary extends moodle_text_filter {
  36      /** @var null|cache_store cache used to store the terms for this course. */
  37      protected $cache = null;
  38  
  39      public function setup($page, $context) {
  40          if ($page->requires->should_create_one_time_item_now('filter_glossary_autolinker')) {
  41              $page->requires->yui_module(
  42                      'moodle-filter_glossary-autolinker',
  43                      'M.filter_glossary.init_filter_autolinking',
  44                      array(array('courseid' => 0)));
  45              $page->requires->strings_for_js(array('ok'), 'moodle');
  46          }
  47      }
  48  
  49      /**
  50       * Get all the concepts for this context.
  51       * @return filterobject[] the concepts, and filterobjects.
  52       */
  53      protected function get_all_concepts() {
  54          global $USER;
  55  
  56          if ($this->cache === null) {
  57              $this->cache = cache::make_from_params(cache_store::MODE_REQUEST, 'filter', 'glossary');
  58          }
  59  
  60          // Try to get current course.
  61          $coursectx = $this->context->get_course_context(false);
  62          if (!$coursectx) {
  63              // Only global glossaries will be linked.
  64              $courseid = 0;
  65          } else {
  66              $courseid = $coursectx->instanceid;
  67          }
  68  
  69          $cached = $this->cache->get('concepts');
  70          if ($cached !== false && ($cached->cachecourseid != $courseid || $cached->cacheuserid != $USER->id)) {
  71              // Invalidate the page cache.
  72              $cached = false;
  73          }
  74  
  75          if ($cached !== false && is_array($cached->cacheconceptlist)) {
  76              return $cached->cacheconceptlist;
  77          }
  78  
  79          list($glossaries, $allconcepts) = \mod_glossary\local\concept_cache::get_concepts($courseid);
  80  
  81          if (!$allconcepts) {
  82              $tocache = new stdClass();
  83              $tocache->cacheuserid = $USER->id;
  84              $tocache->cachecourseid = $courseid;
  85              $tocache->cacheconceptlist = [];
  86              $this->cache->set('concepts', $tocache);
  87              return [];
  88          }
  89  
  90          $conceptlist = array();
  91  
  92          foreach ($allconcepts as $concepts) {
  93              foreach ($concepts as $concept) {
  94                  $conceptlist[] = new filterobject($concept->concept, null, null,
  95                          $concept->casesensitive, $concept->fullmatch, null,
  96                          [$this, 'filterobject_prepare_replacement_callback'], [$concept, $glossaries]);
  97              }
  98          }
  99  
 100          // We sort longest first, so that when we replace the terms,
 101          // the longest ones are replaced first. This does the right thing
 102          // when you have two terms like 'Moodle' and 'Moodle 3.5'. You want the longest match.
 103          usort($conceptlist, [$this, 'sort_entries_by_length']);
 104  
 105          $conceptlist = filter_prepare_phrases_for_filtering($conceptlist);
 106  
 107          $tocache = new stdClass();
 108          $tocache->cacheuserid = $USER->id;
 109          $tocache->cachecourseid = $courseid;
 110          $tocache->cacheconceptlist = $conceptlist;
 111          $this->cache->set('concepts', $tocache);
 112  
 113          return $conceptlist;
 114      }
 115  
 116      /**
 117       * Callback used by filterobject / filter_phrases.
 118       *
 119       * @param object $concept the concept that is being replaced (from get_all_concepts).
 120       * @param array $glossaries the list of glossary titles (from get_all_concepts).
 121       * @return array [$hreftagbegin, $hreftagend, $replacementphrase] for filterobject.
 122       */
 123      public function filterobject_prepare_replacement_callback($concept, $glossaries) {
 124          global $CFG;
 125  
 126          if ($concept->category) { // Link to a category.
 127              $title = get_string('glossarycategory', 'filter_glossary',
 128                      ['glossary' => $glossaries[$concept->glossaryid], 'category' => $concept->concept]);
 129              $link = new moodle_url('/mod/glossary/view.php',
 130                      ['g' => $concept->glossaryid, 'mode' => 'cat', 'hook' => $concept->id]);
 131              $attributes = array(
 132                      'href'  => $link,
 133                      'title' => $title,
 134                      'class' => 'glossary autolink category glossaryid' . $concept->glossaryid);
 135  
 136          } else { // Link to entry or alias.
 137              $title = get_string('glossaryconcept', 'filter_glossary',
 138                      ['glossary' => $glossaries[$concept->glossaryid], 'concept' => $concept->concept]);
 139              // Hardcoding dictionary format in the URL rather than defaulting
 140              // to the current glossary format which may not work in a popup.
 141              // for example "entry list" means the popup would only contain
 142              // a link that opens another popup.
 143              $link = new moodle_url('/mod/glossary/showentry.php',
 144                      ['eid' => $concept->id, 'displayformat' => 'dictionary']);
 145              $attributes = array(
 146                      'href'  => $link,
 147                      'title' => str_replace('&amp;', '&', $title), // Undo the s() mangling.
 148                      'class' => 'glossary autolink concept glossaryid' . $concept->glossaryid);
 149          }
 150  
 151          // This flag is optionally set by resource_pluginfile()
 152          // if processing an embedded file use target to prevent getting nested Moodles.
 153          if (!empty($CFG->embeddedsoforcelinktarget)) {
 154              $attributes['target'] = '_top';
 155          }
 156  
 157          return [html_writer::start_tag('a', $attributes), '</a>', null];
 158      }
 159  
 160      public function filter($text, array $options = array()) {
 161          global $GLOSSARY_EXCLUDEENTRY;
 162  
 163          $conceptlist = $this->get_all_concepts();
 164  
 165          if (empty($conceptlist)) {
 166              return $text;
 167          }
 168  
 169          if (!empty($GLOSSARY_EXCLUDEENTRY)) {
 170              foreach ($conceptlist as $key => $filterobj) {
 171                  // The original concept object was stored here in when $filterobj was constructed in
 172                  // get_all_concepts(). Get it back out now so we can check to see if it is excluded.
 173                  $concept = $filterobj->replacementcallbackdata[0];
 174                  if (!$concept->category && $concept->id == $GLOSSARY_EXCLUDEENTRY) {
 175                      unset($conceptlist[$key]);
 176                  }
 177              }
 178          }
 179  
 180          if (empty($conceptlist)) {
 181              return $text;
 182          }
 183  
 184          return filter_phrases($text, $conceptlist, null, null, false, true);
 185      }
 186  
 187      /**
 188       * usort helper used in get_all_concepts above.
 189       * @param filterobject $filterobject0 first item to compare.
 190       * @param filterobject $filterobject1 second item to compare.
 191       * @return int -1, 0 or 1.
 192       */
 193      private function sort_entries_by_length($filterobject0, $filterobject1) {
 194          return strlen($filterobject1->phrase) <=> strlen($filterobject0->phrase);
 195      }
 196  }