1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17
18/**
19 * Library of functions and constants for module glossary
20 * outside of what is required for the core moodle api
21 *
22 * @package   mod_glossary
23 * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
24 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
27require_once($CFG->libdir . '/portfolio/caller.php');
28require_once($CFG->libdir . '/filelib.php');
29
30/**
31 * class to handle exporting an entire glossary database
32 */
33class glossary_full_portfolio_caller extends portfolio_module_caller_base {
34
35    private $glossary;
36    private $exportdata;
37    private $keyedfiles = array(); // keyed on entry
38
39    /**
40     * return array of expected call back arguments
41     * and whether they are required or not
42     *
43     * @return array
44     */
45    public static function expected_callbackargs() {
46        return array(
47            'id' => true,
48        );
49    }
50
51    /**
52     * load up all data required for this export.
53     *
54     * @return void
55     */
56    public function load_data() {
57        global $DB;
58        if (!$this->cm = get_coursemodule_from_id('glossary', $this->id)) {
59            throw new portfolio_caller_exception('invalidid', 'glossary');
60        }
61        if (!$this->glossary = $DB->get_record('glossary', array('id' => $this->cm->instance))) {
62            throw new portfolio_caller_exception('invalidid', 'glossary');
63        }
64        $entries = $DB->get_records('glossary_entries', array('glossaryid' => $this->glossary->id));
65        list($where, $params) = $DB->get_in_or_equal(array_keys($entries));
66
67        $aliases = $DB->get_records_select('glossary_alias', 'entryid ' . $where, $params);
68        $categoryentries = $DB->get_records_sql('SELECT ec.entryid, c.name FROM {glossary_entries_categories} ec
69            JOIN {glossary_categories} c
70            ON c.id = ec.categoryid
71            WHERE ec.entryid ' . $where, $params);
72
73        $this->exportdata = array('entries' => $entries, 'aliases' => $aliases, 'categoryentries' => $categoryentries);
74        $fs = get_file_storage();
75        $context = context_module::instance($this->cm->id);
76        $this->multifiles = array();
77        foreach (array_keys($entries) as $entry) {
78            $this->keyedfiles[$entry] = array_merge(
79                $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry, "timemodified", false),
80                $fs->get_area_files($context->id, 'mod_glossary', 'entry', $entry, "timemodified", false)
81            );
82            $this->multifiles = array_merge($this->multifiles, $this->keyedfiles[$entry]);
83        }
84    }
85
86    /**
87     * how long might we expect this export to take
88     *
89     * @return constant one of PORTFOLIO_TIME_XX
90     */
91    public function expected_time() {
92        $filetime = portfolio_expected_time_file($this->multifiles);
93        $dbtime   = portfolio_expected_time_db(count($this->exportdata['entries']));
94        return ($filetime > $dbtime) ? $filetime : $dbtime;
95    }
96
97    /**
98     * return the sha1 of this content
99     *
100     * @return string
101     */
102    public function get_sha1() {
103        $file = '';
104        if ($this->multifiles) {
105            $file = $this->get_sha1_file();
106        }
107        return sha1(serialize($this->exportdata) . $file);
108    }
109
110    /**
111     * prepare the package ready to be passed off to the portfolio plugin
112     *
113     * @return void
114     */
115    public function prepare_package() {
116        $entries = $this->exportdata['entries'];
117        $aliases = array();
118        $categories = array();
119        if (is_array($this->exportdata['aliases'])) {
120            foreach ($this->exportdata['aliases'] as $alias) {
121                if (!array_key_exists($alias->entryid, $aliases)) {
122                    $aliases[$alias->entryid] = array();
123                }
124                $aliases[$alias->entryid][] = $alias->alias;
125            }
126        }
127        if (is_array($this->exportdata['categoryentries'])) {
128            foreach ($this->exportdata['categoryentries'] as $cat) {
129                if (!array_key_exists($cat->entryid, $categories)) {
130                    $categories[$cat->entryid] = array();
131                }
132                $categories[$cat->entryid][] = $cat->name;
133            }
134        }
135        if ($this->get('exporter')->get('formatclass') == PORTFOLIO_FORMAT_SPREADSHEET) {
136            $csv = glossary_generate_export_csv($entries, $aliases, $categories);
137            $this->exporter->write_new_file($csv, clean_filename($this->cm->name) . '.csv', false);
138            return;
139        } else if ($this->get('exporter')->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
140            $ids = array(); // keep track of these to make into a selection later
141            global $USER, $DB;
142            $writer = $this->get('exporter')->get('format')->leap2a_writer($USER);
143            $format = $this->exporter->get('format');
144            $filename = $this->get('exporter')->get('format')->manifest_name();
145            foreach ($entries as $e) {
146                $content = glossary_entry_portfolio_caller::entry_content(
147                    $this->course,
148                    $this->cm,
149                    $this->glossary,
150                    $e,
151                    (array_key_exists($e->id, $aliases) ? $aliases[$e->id] : array()),
152                    $format
153                );
154                $entry = new portfolio_format_leap2a_entry('glossaryentry' . $e->id, $e->concept, 'entry', $content);
155                $entry->author    = $DB->get_record('user', array('id' => $e->userid), 'id,firstname,lastname,email');
156                $entry->published = $e->timecreated;
157                $entry->updated   = $e->timemodified;
158                if (!empty($this->keyedfiles[$e->id])) {
159                    $writer->link_files($entry, $this->keyedfiles[$e->id], 'glossaryentry' . $e->id . 'file');
160                    foreach ($this->keyedfiles[$e->id] as $file) {
161                        $this->exporter->copy_existing_file($file);
162                    }
163                }
164                if (!empty($categories[$e->id])) {
165                    foreach ($categories[$e->id] as $cat) {
166                        // this essentially treats them as plain tags
167                        // leap has the idea of category schemes
168                        // but I think this is overkill here
169                        $entry->add_category($cat);
170                    }
171                }
172                $writer->add_entry($entry);
173                $ids[] = $entry->id;
174            }
175            $selection = new portfolio_format_leap2a_entry('wholeglossary' . $this->glossary->id, get_string('modulename', 'glossary'), 'selection');
176            $writer->add_entry($selection);
177            $writer->make_selection($selection, $ids, 'Grouping');
178            $content = $writer->to_xml();
179        }
180        $this->exporter->write_new_file($content, $filename, true);
181    }
182
183    /**
184     * make sure that the current user is allowed to do this
185     *
186     * @return boolean
187     */
188    public function check_permissions() {
189        return has_capability('mod/glossary:export', context_module::instance($this->cm->id));
190    }
191
192    /**
193     * return a nice name to be displayed about this export location
194     *
195     * @return string
196     */
197    public static function display_name() {
198        return get_string('modulename', 'glossary');
199    }
200
201    /**
202     * what formats this function *generally* supports
203     *
204     * @return array
205     */
206    public static function base_supported_formats() {
207        return array(PORTFOLIO_FORMAT_SPREADSHEET, PORTFOLIO_FORMAT_LEAP2A);
208    }
209}
210
211/**
212 * class to export a single glossary entry
213 *
214 * @package   mod_glossary
215 * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
216 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
217 */
218class glossary_entry_portfolio_caller extends portfolio_module_caller_base {
219
220    private $glossary;
221    private $entry;
222    protected $entryid;
223    /*
224     * @return array
225     */
226    public static function expected_callbackargs() {
227        return array(
228            'entryid' => true,
229            'id'      => true,
230        );
231    }
232
233    /**
234     * load up all data required for this export.
235     *
236     * @return void
237     */
238    public function load_data() {
239        global $DB;
240        if (!$this->cm = get_coursemodule_from_id('glossary', $this->id)) {
241            throw new portfolio_caller_exception('invalidid', 'glossary');
242        }
243        if (!$this->glossary = $DB->get_record('glossary', array('id' => $this->cm->instance))) {
244            throw new portfolio_caller_exception('invalidid', 'glossary');
245        }
246        if ($this->entryid) {
247            if (!$this->entry = $DB->get_record('glossary_entries', array('id' => $this->entryid))) {
248                throw new portfolio_caller_exception('noentry', 'glossary');
249            }
250            // in case we don't have USER this will make the entry be printed
251            $this->entry->approved = true;
252        }
253        $this->categories = $DB->get_records_sql('SELECT ec.entryid, c.name FROM {glossary_entries_categories} ec
254            JOIN {glossary_categories} c
255            ON c.id = ec.categoryid
256            WHERE ec.entryid = ?', array($this->entryid));
257        $context = context_module::instance($this->cm->id);
258        if ($this->entry->sourceglossaryid == $this->cm->instance) {
259            if ($maincm = get_coursemodule_from_instance('glossary', $this->entry->glossaryid)) {
260                $context = context_module::instance($maincm->id);
261            }
262        }
263        $this->aliases = $DB->get_record('glossary_alias', array('entryid'=>$this->entryid));
264        $fs = get_file_storage();
265        $this->multifiles = array_merge(
266            $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $this->entry->id, "timemodified", false),
267            $fs->get_area_files($context->id, 'mod_glossary', 'entry', $this->entry->id, "timemodified", false)
268        );
269
270        if (!empty($this->multifiles)) {
271            $this->add_format(PORTFOLIO_FORMAT_RICHHTML);
272        } else {
273            $this->add_format(PORTFOLIO_FORMAT_PLAINHTML);
274        }
275    }
276
277    /**
278     * how long might we expect this export to take
279     *
280     * @return constant one of PORTFOLIO_TIME_XX
281     */
282    public function expected_time() {
283        return PORTFOLIO_TIME_LOW;
284    }
285
286    /**
287     * make sure that the current user is allowed to do this
288     *
289     * @return boolean
290     */
291    public function check_permissions() {
292        $context = context_module::instance($this->cm->id);
293        return has_capability('mod/glossary:exportentry', $context)
294            || ($this->entry->userid == $this->user->id && has_capability('mod/glossary:exportownentry', $context));
295    }
296
297    /**
298     * return a nice name to be displayed about this export location
299     *
300     * @return string
301     */
302    public static function display_name() {
303        return get_string('modulename', 'glossary');
304    }
305
306    /**
307     * prepare the package ready to be passed off to the portfolio plugin
308     *
309     * @return void
310     */
311    public function prepare_package() {
312        global $DB;
313
314        $format = $this->exporter->get('format');
315        $content = self::entry_content($this->course, $this->cm, $this->glossary, $this->entry, $this->aliases, $format);
316
317        if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_PLAINHTML) {
318            $filename = clean_filename($this->entry->concept) . '.html';
319            $this->exporter->write_new_file($content, $filename);
320
321        } else if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_RICHHTML) {
322            if ($this->multifiles) {
323                foreach ($this->multifiles as $file) {
324                    $this->exporter->copy_existing_file($file);
325                }
326            }
327            $filename = clean_filename($this->entry->concept) . '.html';
328            $this->exporter->write_new_file($content, $filename);
329
330        } else if ($this->exporter->get('formatclass') === PORTFOLIO_FORMAT_LEAP2A) {
331            $writer = $this->get('exporter')->get('format')->leap2a_writer();
332            $entry = new portfolio_format_leap2a_entry('glossaryentry' . $this->entry->id, $this->entry->concept, 'entry', $content);
333            $entry->author = $DB->get_record('user', array('id' => $this->entry->userid), 'id,firstname,lastname,email');
334            $entry->published = $this->entry->timecreated;
335            $entry->updated = $this->entry->timemodified;
336            if ($this->multifiles) {
337                $writer->link_files($entry, $this->multifiles);
338                foreach ($this->multifiles as $file) {
339                    $this->exporter->copy_existing_file($file);
340                }
341            }
342            if ($this->categories) {
343                foreach ($this->categories as $cat) {
344                    // this essentially treats them as plain tags
345                    // leap has the idea of category schemes
346                    // but I think this is overkill here
347                    $entry->add_category($cat->name);
348                }
349            }
350            $writer->add_entry($entry);
351            $content = $writer->to_xml();
352            $filename = $this->get('exporter')->get('format')->manifest_name();
353            $this->exporter->write_new_file($content, $filename);
354
355        } else {
356            throw new portfolio_caller_exception('unexpected_format_class', 'glossary');
357        }
358    }
359
360    /**
361     * return the sha1 of this content
362     *
363     * @return string
364     */
365    public function get_sha1() {
366        if ($this->multifiles) {
367            return sha1(serialize($this->entry) . $this->get_sha1_file());
368        }
369        return sha1(serialize($this->entry));
370    }
371
372    /**
373     * what formats this function *generally* supports
374     *
375     * @return array
376     */
377    public static function base_supported_formats() {
378        return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A);
379    }
380
381    /**
382     * helper function to get the html content of an entry
383     * for both this class and the full glossary exporter
384     * this is a very simplified version of the dictionary format output,
385     * but with its 500 levels of indirection removed
386     * and file rewriting handled by the portfolio export format.
387     *
388     * @param stdclass $course
389     * @param stdclass $cm
390     * @param stdclass $glossary
391     * @param stdclass $entry
392     *
393     * @return string
394     */
395    public static function entry_content($course, $cm, $glossary, $entry, $aliases, $format) {
396        global $OUTPUT, $DB;
397        $entry = clone $entry;
398        $context = context_module::instance($cm->id);
399        $options = portfolio_format_text_options();
400        $options->trusted = $entry->definitiontrust;
401        $options->context = $context;
402
403        $output = '<table class="glossarypost dictionary" cellspacing="0">' . "\n";
404        $output .= '<tr valign="top">' . "\n";
405        $output .= '<td class="entry">' . "\n";
406
407        $output .= '<div class="concept">';
408        $output .= format_text($OUTPUT->heading($entry->concept, 3), FORMAT_MOODLE, $options);
409        $output .= '</div> ' . "\n";
410
411        $entry->definition = format_text($entry->definition, $entry->definitionformat, $options);
412        $output .= portfolio_rewrite_pluginfile_urls($entry->definition, $context->id, 'mod_glossary', 'entry', $entry->id, $format);
413
414        if (isset($entry->footer)) {
415            $output .= $entry->footer;
416        }
417
418        $output .= '</td></tr>' . "\n";
419
420        if (!empty($aliases)) {
421            $aliases = explode(',', $aliases->alias);
422            $output .= '<tr valign="top"><td class="entrylowersection">';
423            $key = (count($aliases) == 1) ? 'alias' : 'aliases';
424            $output .= get_string($key, 'glossary') . ': ';
425            foreach ($aliases as $alias) {
426                $output .= s($alias) . ',';
427            }
428            $output = substr($output, 0, -1);
429            $output .= '</td></tr>' . "\n";
430        }
431
432        if ($entry->sourceglossaryid == $cm->instance) {
433            if (!$maincm = get_coursemodule_from_instance('glossary', $entry->glossaryid)) {
434                return '';
435            }
436            $filecontext = context_module::instance($maincm->id);
437
438        } else {
439            $filecontext = $context;
440        }
441        $fs = get_file_storage();
442        if ($files = $fs->get_area_files($filecontext->id, 'mod_glossary', 'attachment', $entry->id, "timemodified", false)) {
443            $output .= '<table border="0" width="100%"><tr><td>' . "\n";
444
445            foreach ($files as $file) {
446                $output .= $format->file_output($file);
447            }
448            $output .= '</td></tr></table>' . "\n";
449        }
450
451        $output .= '</table>' . "\n";
452
453        return $output;
454    }
455}
456
457
458/**
459 * Class representing the virtual node with all itemids in the file browser
460 *
461 * @category  files
462 * @copyright 2012 David Mudrak <david@moodle.com>
463 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
464 */
465class glossary_file_info_container extends file_info {
466    /** @var file_browser */
467    protected $browser;
468    /** @var stdClass */
469    protected $course;
470    /** @var stdClass */
471    protected $cm;
472    /** @var string */
473    protected $component;
474    /** @var stdClass */
475    protected $context;
476    /** @var array */
477    protected $areas;
478    /** @var string */
479    protected $filearea;
480
481    /**
482     * Constructor (in case you did not realize it ;-)
483     *
484     * @param file_browser $browser
485     * @param stdClass $course
486     * @param stdClass $cm
487     * @param stdClass $context
488     * @param array $areas
489     * @param string $filearea
490     */
491    public function __construct($browser, $course, $cm, $context, $areas, $filearea) {
492        parent::__construct($browser, $context);
493        $this->browser = $browser;
494        $this->course = $course;
495        $this->cm = $cm;
496        $this->component = 'mod_glossary';
497        $this->context = $context;
498        $this->areas = $areas;
499        $this->filearea = $filearea;
500    }
501
502    /**
503     * @return array with keys contextid, filearea, itemid, filepath and filename
504     */
505    public function get_params() {
506        return array(
507            'contextid' => $this->context->id,
508            'component' => $this->component,
509            'filearea' => $this->filearea,
510            'itemid' => null,
511            'filepath' => null,
512            'filename' => null,
513        );
514    }
515
516    /**
517     * Can new files or directories be added via the file browser
518     *
519     * @return bool
520     */
521    public function is_writable() {
522        return false;
523    }
524
525    /**
526     * Should this node be considered as a folder in the file browser
527     *
528     * @return bool
529     */
530    public function is_directory() {
531        return true;
532    }
533
534    /**
535     * Returns localised visible name of this node
536     *
537     * @return string
538     */
539    public function get_visible_name() {
540        return $this->areas[$this->filearea];
541    }
542
543    /**
544     * Returns list of children nodes
545     *
546     * @return array of file_info instances
547     */
548    public function get_children() {
549        return $this->get_filtered_children('*', false, true);
550    }
551
552    /**
553     * Help function to return files matching extensions or their count
554     *
555     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
556     * @param bool|int $countonly if false returns the children, if an int returns just the
557     *    count of children but stops counting when $countonly number of children is reached
558     * @param bool $returnemptyfolders if true returns items that don't have matching files inside
559     * @return array|int array of file_info instances or the count
560     */
561    private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
562        global $DB;
563        $sql = 'SELECT DISTINCT f.itemid, ge.concept
564                  FROM {files} f
565                  JOIN {modules} m ON (m.name = :modulename AND m.visible = 1)
566                  JOIN {course_modules} cm ON (cm.module = m.id AND cm.id = :instanceid)
567                  JOIN {glossary} g ON g.id = cm.instance
568                  JOIN {glossary_entries} ge ON (ge.glossaryid = g.id AND ge.id = f.itemid)
569                 WHERE f.contextid = :contextid
570                  AND f.component = :component
571                  AND f.filearea = :filearea';
572        $params = array(
573            'modulename' => 'glossary',
574            'instanceid' => $this->context->instanceid,
575            'contextid' => $this->context->id,
576            'component' => $this->component,
577            'filearea' => $this->filearea);
578        if (!$returnemptyfolders) {
579            $sql .= ' AND f.filename <> :emptyfilename';
580            $params['emptyfilename'] = '.';
581        }
582        list($sql2, $params2) = $this->build_search_files_sql($extensions, 'f');
583        $sql .= ' '.$sql2;
584        $params = array_merge($params, $params2);
585        if ($countonly !== false) {
586            $sql .= ' ORDER BY ge.concept, f.itemid';
587        }
588
589        $rs = $DB->get_recordset_sql($sql, $params);
590        $children = array();
591        foreach ($rs as $record) {
592            if ($child = $this->browser->get_file_info($this->context, 'mod_glossary', $this->filearea, $record->itemid)) {
593                $children[] = $child;
594            }
595            if ($countonly !== false && count($children) >= $countonly) {
596                break;
597            }
598        }
599        $rs->close();
600        if ($countonly !== false) {
601            return count($children);
602        }
603        return $children;
604    }
605
606    /**
607     * Returns list of children which are either files matching the specified extensions
608     * or folders that contain at least one such file.
609     *
610     * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
611     * @return array of file_info instances
612     */
613    public function get_non_empty_children($extensions = '*') {
614        return $this->get_filtered_children($extensions, false);
615    }
616
617    /**
618     * Returns the number of children which are either files matching the specified extensions
619     * or folders containing at least one such file.
620     *
621     * @param string|array $extensions, for example '*' or array('.gif','.jpg')
622     * @param int $limit stop counting after at least $limit non-empty children are found
623     * @return int
624     */
625    public function count_non_empty_children($extensions = '*', $limit = 1) {
626        return $this->get_filtered_children($extensions, $limit);
627    }
628
629    /**
630     * Returns parent file_info instance
631     *
632     * @return file_info or null for root
633     */
634    public function get_parent() {
635        return $this->browser->get_file_info($this->context);
636    }
637}
638
639/**
640 * Returns glossary entries tagged with a specified tag.
641 *
642 * This is a callback used by the tag area mod_glossary/glossary_entries to search for glossary entries
643 * tagged with a specific tag.
644 *
645 * @param core_tag_tag $tag
646 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
647 *             are displayed on the page and the per-page limit may be bigger
648 * @param int $fromctx context id where the link was displayed, may be used by callbacks
649 *            to display items in the same context first
650 * @param int $ctx context id where to search for records
651 * @param bool $rec search in subcontexts as well
652 * @param int $page 0-based number of page being displayed
653 * @return \core_tag\output\tagindex
654 */
655function mod_glossary_get_tagged_entries($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
656    global $OUTPUT;
657    $perpage = $exclusivemode ? 20 : 5;
658
659    // Build the SQL query.
660    $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
661    $query = "SELECT ge.id, ge.concept, ge.glossaryid, ge.approved, ge.userid,
662                    cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect
663                FROM {glossary_entries} ge
664                JOIN {glossary} g ON g.id = ge.glossaryid
665                JOIN {modules} m ON m.name='glossary'
666                JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = g.id
667                JOIN {tag_instance} tt ON ge.id = tt.itemid
668                JOIN {course} c ON cm.course = c.id
669                JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
670               WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
671                 AND cm.deletioninprogress = 0
672                 AND ge.id %ITEMFILTER% AND c.id %COURSEFILTER%";
673
674    $params = array('itemtype' => 'glossary_entries', 'tagid' => $tag->id, 'component' => 'mod_glossary',
675                    'coursemodulecontextlevel' => CONTEXT_MODULE);
676
677    if ($ctx) {
678        $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
679        $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
680        $params['contextid'] = $context->id;
681        $params['path'] = $context->path.'/%';
682    }
683
684    $query .= " ORDER BY ";
685    if ($fromctx) {
686        // In order-clause specify that modules from inside "fromctx" context should be returned first.
687        $fromcontext = context::instance_by_id($fromctx);
688        $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
689        $params['fromcontextid'] = $fromcontext->id;
690        $params['frompath'] = $fromcontext->path.'/%';
691    }
692    $query .= ' c.sortorder, cm.id, ge.id';
693
694    $totalpages = $page + 1;
695
696    // Use core_tag_index_builder to build and filter the list of items.
697    $builder = new core_tag_index_builder('mod_glossary', 'glossary_entries', $query, $params, $page * $perpage, $perpage + 1);
698    while ($item = $builder->has_item_that_needs_access_check()) {
699        context_helper::preload_from_record($item);
700        $courseid = $item->courseid;
701        if (!$builder->can_access_course($courseid)) {
702            $builder->set_accessible($item, false);
703            continue;
704        }
705        $modinfo = get_fast_modinfo($builder->get_course($courseid));
706        // Set accessibility of this item and all other items in the same course.
707        $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) {
708            global $USER;
709            if ($taggeditem->courseid == $courseid) {
710                $accessible = false;
711                if (($cm = $modinfo->get_cm($taggeditem->cmid)) && $cm->uservisible) {
712                    if ($taggeditem->approved) {
713                        $accessible = true;
714                    } else if ($taggeditem->userid == $USER->id) {
715                        $accessible = true;
716                    } else {
717                        $accessible = has_capability('mod/glossary:approve', context_module::instance($cm->id));
718                    }
719                }
720                $builder->set_accessible($taggeditem, $accessible);
721            }
722        });
723    }
724
725    $items = $builder->get_items();
726    if (count($items) > $perpage) {
727        $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
728        array_pop($items);
729    }
730
731    // Build the display contents.
732    if ($items) {
733        $tagfeed = new core_tag\output\tagfeed();
734        foreach ($items as $item) {
735            context_helper::preload_from_record($item);
736            $modinfo = get_fast_modinfo($item->courseid);
737            $cm = $modinfo->get_cm($item->cmid);
738            $pageurl = new moodle_url('/mod/glossary/showentry.php', array('eid' => $item->id, 'displayformat' => 'dictionary'));
739            $pagename = format_string($item->concept, true, array('context' => context_module::instance($item->cmid)));
740            $pagename = html_writer::link($pageurl, $pagename);
741            $courseurl = course_get_url($item->courseid, $cm->sectionnum);
742            $cmname = html_writer::link($cm->url, $cm->get_formatted_name());
743            $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
744            $coursename = html_writer::link($courseurl, $coursename);
745            $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
746
747            $approved = "";
748            if (!$item->approved) {
749                $approved = '<br>'. html_writer::span(get_string('entrynotapproved', 'mod_glossary'), 'badge badge-warning');
750            }
751            $tagfeed->add($icon, $pagename, $cmname.'<br>'.$coursename.$approved);
752        }
753
754        $content = $OUTPUT->render_from_template('core_tag/tagfeed',
755            $tagfeed->export_for_template($OUTPUT));
756
757        return new core_tag\output\tagindex($tag, 'mod_glossary', 'glossary_entries', $content,
758            $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
759    }
760}
761