6d5fa5d07dfbc9e502924577cd3d2110dbf17d0f
[moodle.git] / mod / book / lib.php
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/>.
17 /**
18  * Book module core interaction API
19  *
20  * @package    mod_book
21  * @copyright  2004-2011 Petr Skoda {@link http://skodak.org}
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die;
27 /**
28  * Returns list of available numbering types
29  * @return array
30  */
31 function book_get_numbering_types() {
32     global $CFG; // required for the include
34     require_once(__DIR__.'/locallib.php');
36     return array (
37         BOOK_NUM_NONE       => get_string('numbering0', 'mod_book'),
38         BOOK_NUM_NUMBERS    => get_string('numbering1', 'mod_book'),
39         BOOK_NUM_BULLETS    => get_string('numbering2', 'mod_book'),
40         BOOK_NUM_INDENTED   => get_string('numbering3', 'mod_book')
41     );
42 }
44 /**
45  * Returns list of available navigation link types.
46  * @return array
47  */
48 function book_get_nav_types() {
49     require_once(__DIR__.'/locallib.php');
51     return array (
52         BOOK_LINK_TOCONLY   => get_string('navtoc', 'mod_book'),
53         BOOK_LINK_IMAGE     => get_string('navimages', 'mod_book'),
54         BOOK_LINK_TEXT      => get_string('navtext', 'mod_book'),
55     );
56 }
58 /**
59  * Returns list of available navigation link CSS classes.
60  * @return array
61  */
62 function book_get_nav_classes() {
63     return array ('navtoc', 'navimages', 'navtext');
64 }
66 /**
67  * Returns all other caps used in module
68  * @return array
69  */
70 function book_get_extra_capabilities() {
71     // used for group-members-only
72     return array('moodle/site:accessallgroups');
73 }
75 /**
76  * Add book instance.
77  *
78  * @param stdClass $data
79  * @param stdClass $mform
80  * @return int new book instance id
81  */
82 function book_add_instance($data, $mform) {
83     global $DB;
85     $data->timecreated = time();
86     $data->timemodified = $data->timecreated;
87     if (!isset($data->customtitles)) {
88         $data->customtitles = 0;
89     }
91     $id = $DB->insert_record('book', $data);
93     $completiontimeexpected = !empty($data->completionexpected) ? $data->completionexpected : null;
94     \core_completion\api::update_completion_date_event($data->coursemodule, 'book', $id, $completiontimeexpected);
96     return $id;
97 }
99 /**
100  * Update book instance.
101  *
102  * @param stdClass $data
103  * @param stdClass $mform
104  * @return bool true
105  */
106 function book_update_instance($data, $mform) {
107     global $DB;
109     $data->timemodified = time();
110     $data->id = $data->instance;
111     if (!isset($data->customtitles)) {
112         $data->customtitles = 0;
113     }
115     $DB->update_record('book', $data);
117     $book = $DB->get_record('book', array('id'=>$data->id));
118     $DB->set_field('book', 'revision', $book->revision+1, array('id'=>$book->id));
120     $completiontimeexpected = !empty($data->completionexpected) ? $data->completionexpected : null;
121     \core_completion\api::update_completion_date_event($data->coursemodule, 'book', $book->id, $completiontimeexpected);
123     return true;
126 /**
127  * Delete book instance by activity id
128  *
129  * @param int $id
130  * @return bool success
131  */
132 function book_delete_instance($id) {
133     global $DB;
135     if (!$book = $DB->get_record('book', array('id'=>$id))) {
136         return false;
137     }
139     $cm = get_coursemodule_from_instance('book', $id);
140     \core_completion\api::update_completion_date_event($cm->id, 'book', $id, null);
142     $DB->delete_records('book_chapters', array('bookid'=>$book->id));
143     $DB->delete_records('book', array('id'=>$book->id));
145     return true;
148 /**
149  * Given a course and a time, this module should find recent activity
150  * that has occurred in book activities and print it out.
151  *
152  * @param stdClass $course
153  * @param bool $viewfullnames
154  * @param int $timestart
155  * @return bool true if there was output, or false is there was none
156  */
157 function book_print_recent_activity($course, $viewfullnames, $timestart) {
158     return false;  //  True if anything was printed, otherwise false
161 /**
162  * This function is used by the reset_course_userdata function in moodlelib.
163  * @param $data the data submitted from the reset course.
164  * @return array status array
165  */
166 function book_reset_userdata($data) {
167     global $DB;
168     // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
169     // See MDL-9367.
171     $status = [];
173     if (!empty($data->reset_book_tags)) {
174         // Loop through the books and remove the tags from the chapters.
175         if ($books = $DB->get_records('book', array('course' => $data->courseid))) {
176             foreach ($books as $book) {
177                 if (!$cm = get_coursemodule_from_instance('book', $book->id)) {
178                     continue;
179                 }
181                 $context = context_module::instance($cm->id);
182                 core_tag_tag::delete_instances('mod_book', null, $context->id);
183             }
184         }
187         $status[] = [
188             'component' => get_string('modulenameplural', 'book'),
189             'item' => get_string('tagsdeleted', 'book'),
190             'error' => false
191         ];
192     }
194     return $status;
197 /**
198  * The elements to add the course reset form.
199  *
200  * @param moodleform $mform
201  */
202 function book_reset_course_form_definition(&$mform) {
203     $mform->addElement('header', 'bookheader', get_string('modulenameplural', 'book'));
204     $mform->addElement('checkbox', 'reset_book_tags', get_string('removeallbooktags', 'book'));
207 /**
208  * No cron in book.
209  *
210  * @return bool
211  */
212 function book_cron () {
213     return true;
216 /**
217  * No grading in book.
218  *
219  * @param int $bookid
220  * @return null
221  */
222 function book_grades($bookid) {
223     return null;
226 /**
227  * This function returns if a scale is being used by one book
228  * it it has support for grading and scales. Commented code should be
229  * modified if necessary. See book, glossary or journal modules
230  * as reference.
231  *
232  * @param int $bookid
233  * @param int $scaleid
234  * @return boolean True if the scale is used by any journal
235  */
236 function book_scale_used($bookid, $scaleid) {
237     return false;
240 /**
241  * Checks if scale is being used by any instance of book
242  *
243  * This is used to find out if scale used anywhere
244  *
245  * @param int $scaleid
246  * @return bool true if the scale is used by any book
247  */
248 function book_scale_used_anywhere($scaleid) {
249     return false;
252 /**
253  * Return read actions.
254  *
255  * Note: This is not used by new logging system. Event with
256  *       crud = 'r' and edulevel = LEVEL_PARTICIPATING will
257  *       be considered as view action.
258  *
259  * @return array
260  */
261 function book_get_view_actions() {
262     global $CFG; // necessary for includes
264     $return = array('view', 'view all');
266     $plugins = core_component::get_plugin_list('booktool');
267     foreach ($plugins as $plugin => $dir) {
268         if (file_exists("$dir/lib.php")) {
269             require_once("$dir/lib.php");
270         }
271         $function = 'booktool_'.$plugin.'_get_view_actions';
272         if (function_exists($function)) {
273             if ($actions = $function()) {
274                 $return = array_merge($return, $actions);
275             }
276         }
277     }
279     return $return;
282 /**
283  * Return write actions.
284  *
285  * Note: This is not used by new logging system. Event with
286  *       crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING
287  *       will be considered as post action.
288  *
289  * @return array
290  */
291 function book_get_post_actions() {
292     global $CFG; // necessary for includes
294     $return = array('update');
296     $plugins = core_component::get_plugin_list('booktool');
297     foreach ($plugins as $plugin => $dir) {
298         if (file_exists("$dir/lib.php")) {
299             require_once("$dir/lib.php");
300         }
301         $function = 'booktool_'.$plugin.'_get_post_actions';
302         if (function_exists($function)) {
303             if ($actions = $function()) {
304                 $return = array_merge($return, $actions);
305             }
306         }
307     }
309     return $return;
312 /**
313  * Supported features
314  *
315  * @param string $feature FEATURE_xx constant for requested feature
316  * @return mixed True if module supports feature, false if not, null if doesn't know
317  */
318 function book_supports($feature) {
319     switch($feature) {
320         case FEATURE_MOD_ARCHETYPE:           return MOD_ARCHETYPE_RESOURCE;
321         case FEATURE_GROUPS:                  return false;
322         case FEATURE_GROUPINGS:               return false;
323         case FEATURE_MOD_INTRO:               return true;
324         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
325         case FEATURE_GRADE_HAS_GRADE:         return false;
326         case FEATURE_GRADE_OUTCOMES:          return false;
327         case FEATURE_BACKUP_MOODLE2:          return true;
328         case FEATURE_SHOW_DESCRIPTION:        return true;
330         default: return null;
331     }
334 /**
335  * Adds module specific settings to the settings block
336  *
337  * @param settings_navigation $settingsnav The settings navigation object
338  * @param navigation_node $booknode The node to add module settings to
339  * @return void
340  */
341 function book_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $booknode) {
342     global $USER, $PAGE, $OUTPUT;
344     if ($booknode->children->count() > 0) {
345         $firstkey = $booknode->children->get_key_list()[0];
346     } else {
347         $firstkey = null;
348     }
350     $params = $PAGE->url->params();
352     if ($PAGE->cm->modname === 'book' and !empty($params['id']) and !empty($params['chapterid'])
353             and has_capability('mod/book:edit', $PAGE->cm->context)) {
354         if (!empty($USER->editing)) {
355             $string = get_string("turneditingoff");
356             $edit = '0';
357         } else {
358             $string = get_string("turneditingon");
359             $edit = '1';
360         }
361         $url = new moodle_url('/mod/book/view.php', array('id'=>$params['id'], 'chapterid'=>$params['chapterid'], 'edit'=>$edit, 'sesskey'=>sesskey()));
362         $editnode = navigation_node::create($string, $url, navigation_node::TYPE_SETTING);
363         $booknode->add_node($editnode, $firstkey);
364         $PAGE->set_button($OUTPUT->single_button($url, $string));
365     }
367     $plugins = core_component::get_plugin_list('booktool');
368     foreach ($plugins as $plugin => $dir) {
369         if (file_exists("$dir/lib.php")) {
370             require_once("$dir/lib.php");
371         }
372         $function = 'booktool_'.$plugin.'_extend_settings_navigation';
373         if (function_exists($function)) {
374             $function($settingsnav, $booknode);
375         }
376     }
380 /**
381  * Lists all browsable file areas
382  * @param object $course
383  * @param object $cm
384  * @param object $context
385  * @return array
386  */
387 function book_get_file_areas($course, $cm, $context) {
388     $areas = array();
389     $areas['chapter'] = get_string('chapters', 'mod_book');
390     return $areas;
393 /**
394  * File browsing support for book module chapter area.
395  * @param object $browser
396  * @param object $areas
397  * @param object $course
398  * @param object $cm
399  * @param object $context
400  * @param string $filearea
401  * @param int $itemid
402  * @param string $filepath
403  * @param string $filename
404  * @return object file_info instance or null if not found
405  */
406 function book_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
407     global $CFG, $DB;
409     // note: 'intro' area is handled in file_browser automatically
411     if (!has_capability('mod/book:read', $context)) {
412         return null;
413     }
415     if ($filearea !== 'chapter') {
416         return null;
417     }
419     require_once(__DIR__.'/locallib.php');
421     if (is_null($itemid)) {
422         return new book_file_info($browser, $course, $cm, $context, $areas, $filearea);
423     }
425     $fs = get_file_storage();
426     $filepath = is_null($filepath) ? '/' : $filepath;
427     $filename = is_null($filename) ? '.' : $filename;
428     if (!$storedfile = $fs->get_file($context->id, 'mod_book', $filearea, $itemid, $filepath, $filename)) {
429         return null;
430     }
432     // modifications may be tricky - may cause caching problems
433     $canwrite = has_capability('mod/book:edit', $context);
435     $chaptername = $DB->get_field('book_chapters', 'title', array('bookid'=>$cm->instance, 'id'=>$itemid));
436     $chaptername = format_string($chaptername, true, array('context'=>$context));
438     $urlbase = $CFG->wwwroot.'/pluginfile.php';
439     return new file_info_stored($browser, $context, $storedfile, $urlbase, $chaptername, true, true, $canwrite, false);
442 /**
443  * Serves the book attachments. Implements needed access control ;-)
444  *
445  * @param stdClass $course course object
446  * @param cm_info $cm course module object
447  * @param context $context context object
448  * @param string $filearea file area
449  * @param array $args extra arguments
450  * @param bool $forcedownload whether or not force download
451  * @param array $options additional options affecting the file serving
452  * @return bool false if file not found, does not return if found - just send the file
453  */
454 function book_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
455     global $CFG, $DB;
457     if ($context->contextlevel != CONTEXT_MODULE) {
458         return false;
459     }
461     require_course_login($course, true, $cm);
463     if ($filearea !== 'chapter') {
464         return false;
465     }
467     if (!has_capability('mod/book:read', $context)) {
468         return false;
469     }
471     $chid = (int)array_shift($args);
473     if (!$book = $DB->get_record('book', array('id'=>$cm->instance))) {
474         return false;
475     }
477     if (!$chapter = $DB->get_record('book_chapters', array('id'=>$chid, 'bookid'=>$book->id))) {
478         return false;
479     }
481     if ($chapter->hidden and !has_capability('mod/book:viewhiddenchapters', $context)) {
482         return false;
483     }
485     // Download the contents of a chapter as an html file.
486     if ($args[0] == 'index.html') {
487         $filename = "index.html";
489         // We need to rewrite the pluginfile URLs so the media filters can work.
490         $content = file_rewrite_pluginfile_urls($chapter->content, 'webservice/pluginfile.php', $context->id, 'mod_book', 'chapter',
491                                                 $chapter->id);
492         $formatoptions = new stdClass;
493         $formatoptions->noclean = true;
494         $formatoptions->overflowdiv = true;
495         $formatoptions->context = $context;
497         $content = format_text($content, $chapter->contentformat, $formatoptions);
499         // Remove @@PLUGINFILE@@/.
500         $options = array('reverse' => true);
501         $content = file_rewrite_pluginfile_urls($content, 'webservice/pluginfile.php', $context->id, 'mod_book', 'chapter',
502                                                 $chapter->id, $options);
503         $content = str_replace('@@PLUGINFILE@@/', '', $content);
505         $titles = "";
506         // Format the chapter titles.
507         if (!$book->customtitles) {
508             require_once(__DIR__.'/locallib.php');
509             $chapters = book_preload_chapters($book);
511             if (!$chapter->subchapter) {
512                 $currtitle = book_get_chapter_title($chapter->id, $chapters, $book, $context);
513                 // Note that we can't use the $OUTPUT->heading() in WS_SERVER mode.
514                 $titles = "<h3>$currtitle</h3>";
515             } else {
516                 $currtitle = book_get_chapter_title($chapters[$chapter->id]->parent, $chapters, $book, $context);
517                 $currsubtitle = book_get_chapter_title($chapter->id, $chapters, $book, $context);
518                 // Note that we can't use the $OUTPUT->heading() in WS_SERVER mode.
519                 $titles = "<h3>$currtitle</h3>";
520                 $titles .= "<h4>$currsubtitle</h4>";
521             }
522         }
524         $content = $titles . $content;
526         send_file($content, $filename, 0, 0, true, true);
527     } else {
528         $fs = get_file_storage();
529         $relativepath = implode('/', $args);
530         $fullpath = "/$context->id/mod_book/chapter/$chid/$relativepath";
531         if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
532             return false;
533         }
535         // Nasty hack because we do not have file revisions in book yet.
536         $lifetime = $CFG->filelifetime;
537         if ($lifetime > 60 * 10) {
538             $lifetime = 60 * 10;
539         }
541         // Finally send the file.
542         send_stored_file($file, $lifetime, 0, $forcedownload, $options);
543     }
546 /**
547  * Return a list of page types
548  *
549  * @param string $pagetype current page type
550  * @param stdClass $parentcontext Block's parent context
551  * @param stdClass $currentcontext Current context of block
552  * @return array
553  */
554 function book_page_type_list($pagetype, $parentcontext, $currentcontext) {
555     $module_pagetype = array('mod-book-*'=>get_string('page-mod-book-x', 'mod_book'));
556     return $module_pagetype;
559 /**
560  * Export book resource contents
561  *
562  * @param  stdClass $cm     Course module object
563  * @param  string $baseurl  Base URL for file downloads
564  * @return array of file content
565  */
566 function book_export_contents($cm, $baseurl) {
567     global $DB;
569     $contents = array();
570     $context = context_module::instance($cm->id);
572     $book = $DB->get_record('book', array('id' => $cm->instance), '*', MUST_EXIST);
574     $fs = get_file_storage();
576     $chapters = $DB->get_records('book_chapters', array('bookid' => $book->id), 'pagenum');
578     $structure = array();
579     $currentchapter = 0;
581     foreach ($chapters as $chapter) {
582         if ($chapter->hidden) {
583             continue;
584         }
586         // Generate the book structure.
587         $thischapter = array(
588             "title"     => format_string($chapter->title, true, array('context' => $context)),
589             "href"      => $chapter->id . "/index.html",
590             "level"     => 0,
591             "subitems"  => array()
592         );
594         // Main chapter.
595         if (!$chapter->subchapter) {
596             $currentchapter = $chapter->pagenum;
597             $structure[$currentchapter] = $thischapter;
598         } else {
599             // Subchapter.
600             $thischapter['level'] = 1;
601             $structure[$currentchapter]["subitems"][] = $thischapter;
602         }
604         // Export the chapter contents.
606         // Main content (html).
607         $filename = 'index.html';
608         $chapterindexfile = array();
609         $chapterindexfile['type']         = 'file';
610         $chapterindexfile['filename']     = $filename;
611         // Each chapter in a subdirectory.
612         $chapterindexfile['filepath']     = "/{$chapter->id}/";
613         $chapterindexfile['filesize']     = 0;
614         $chapterindexfile['fileurl']      = moodle_url::make_webservice_pluginfile_url(
615                     $context->id, 'mod_book', 'chapter', $chapter->id, '/', 'index.html')->out(false);
616         $chapterindexfile['timecreated']  = $chapter->timecreated;
617         $chapterindexfile['timemodified'] = $chapter->timemodified;
618         $chapterindexfile['content']      = format_string($chapter->title, true, array('context' => $context));
619         $chapterindexfile['sortorder']    = 0;
620         $chapterindexfile['userid']       = null;
621         $chapterindexfile['author']       = null;
622         $chapterindexfile['license']      = null;
623         $contents[] = $chapterindexfile;
625         // Chapter files (images usually).
626         $files = $fs->get_area_files($context->id, 'mod_book', 'chapter', $chapter->id, 'sortorder DESC, id ASC', false);
627         foreach ($files as $fileinfo) {
628             $file = array();
629             $file['type']         = 'file';
630             $file['filename']     = $fileinfo->get_filename();
631             $file['filepath']     = "/{$chapter->id}" . $fileinfo->get_filepath();
632             $file['filesize']     = $fileinfo->get_filesize();
633             $file['fileurl']      = moodle_url::make_webservice_pluginfile_url(
634                                         $context->id, 'mod_book', 'chapter', $chapter->id,
635                                         $fileinfo->get_filepath(), $fileinfo->get_filename())->out(false);
636             $file['timecreated']  = $fileinfo->get_timecreated();
637             $file['timemodified'] = $fileinfo->get_timemodified();
638             $file['sortorder']    = $fileinfo->get_sortorder();
639             $file['userid']       = $fileinfo->get_userid();
640             $file['author']       = $fileinfo->get_author();
641             $file['license']      = $fileinfo->get_license();
642             $file['mimetype']     = $fileinfo->get_mimetype();
643             $file['isexternalfile'] = $fileinfo->is_external_file();
644             if ($file['isexternalfile']) {
645                 $file['repositorytype'] = $fileinfo->get_repository_type();
646             }
647             $contents[] = $file;
648         }
649     }
651     // First content is the structure in encoded JSON format.
652     $structurefile = array();
653     $structurefile['type']         = 'content';
654     $structurefile['filename']     = 'structure';
655     $structurefile['filepath']     = "/";
656     $structurefile['filesize']     = 0;
657     $structurefile['fileurl']      = null;
658     $structurefile['timecreated']  = $book->timecreated;
659     $structurefile['timemodified'] = $book->timemodified;
660     $structurefile['content']      = json_encode(array_values($structure));
661     $structurefile['sortorder']    = 0;
662     $structurefile['userid']       = null;
663     $structurefile['author']       = null;
664     $structurefile['license']      = null;
666     // Add it as first element.
667     array_unshift($contents, $structurefile);
669     return $contents;
672 /**
673  * Mark the activity completed (if required) and trigger the course_module_viewed event.
674  *
675  * @param  stdClass $book       book object
676  * @param  stdClass $chapter    chapter object
677  * @param  bool $islaschapter   is the las chapter of the book?
678  * @param  stdClass $course     course object
679  * @param  stdClass $cm         course module object
680  * @param  stdClass $context    context object
681  * @since Moodle 3.0
682  */
683 function book_view($book, $chapter, $islastchapter, $course, $cm, $context) {
685     // First case, we are just opening the book.
686     if (empty($chapter)) {
687         \mod_book\event\course_module_viewed::create_from_book($book, $context)->trigger();
689     } else {
690         \mod_book\event\chapter_viewed::create_from_chapter($book, $context, $chapter)->trigger();
692         if ($islastchapter) {
693             // We cheat a bit here in assuming that viewing the last page means the user viewed the whole book.
694             $completion = new completion_info($course);
695             $completion->set_module_viewed($cm);
696         }
697     }
700 /**
701  * Check if the module has any update that affects the current user since a given time.
702  *
703  * @param  cm_info $cm course module data
704  * @param  int $from the time to check updates from
705  * @param  array $filter  if we need to check only specific updates
706  * @return stdClass an object with the different type of areas indicating if they were updated or not
707  * @since Moodle 3.2
708  */
709 function book_check_updates_since(cm_info $cm, $from, $filter = array()) {
710     global $DB;
712     $context = $cm->context;
713     $updates = new stdClass();
714     if (!has_capability('mod/book:read', $context)) {
715         return $updates;
716     }
717     $updates = course_check_module_updates_since($cm, $from, array('content'), $filter);
719     $select = 'bookid = :id AND (timecreated > :since1 OR timemodified > :since2)';
720     $params = array('id' => $cm->instance, 'since1' => $from, 'since2' => $from);
721     if (!has_capability('mod/book:viewhiddenchapters', $context)) {
722         $select .= ' AND hidden = 0';
723     }
724     $updates->entries = (object) array('updated' => false);
725     $entries = $DB->get_records_select('book_chapters', $select, $params, '', 'id');
726     if (!empty($entries)) {
727         $updates->entries->updated = true;
728         $updates->entries->itemids = array_keys($entries);
729     }
731     return $updates;
734 /**
735  * Get icon mapping for font-awesome.
736  */
737 function mod_book_get_fontawesome_icon_map() {
738     return [
739         'mod_book:chapter' => 'fa-bookmark-o',
740         'mod_book:nav_prev' => 'fa-arrow-left',
741         'mod_book:nav_prev_dis' => 'fa-angle-left',
742         'mod_book:nav_sep' => 'fa-minus',
743         'mod_book:add' => 'fa-plus',
744         'mod_book:nav_next' => 'fa-arrow-right',
745         'mod_book:nav_next_dis' => 'fa-angle-right',
746         'mod_book:nav_exit' => 'fa-arrow-up',
747     ];
750 /**
751  * This function receives a calendar event and returns the action associated with it, or null if there is none.
752  *
753  * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
754  * is not displayed on the block.
755  *
756  * @param calendar_event $event
757  * @param \core_calendar\action_factory $factory
758  * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
759  * @return \core_calendar\local\event\entities\action_interface|null
760  */
761 function mod_book_core_calendar_provide_event_action(calendar_event $event,
762                                                      \core_calendar\action_factory $factory,
763                                                      int $userid = 0) {
764     global $USER;
766     if (empty($userid)) {
767         $userid = $USER->id;
768     }
770     $cm = get_fast_modinfo($event->courseid, $userid)->instances['book'][$event->instance];
771     $context = context_module::instance($cm->id);
773     if (!has_capability('mod/book:read', $context, $userid)) {
774         return null;
775     }
777     $completion = new \completion_info($cm->get_course());
779     $completiondata = $completion->get_data($cm, false, $userid);
781     if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
782         return null;
783     }
785     return $factory->create_instance(
786         get_string('view'),
787         new \moodle_url('/mod/book/view.php', ['id' => $cm->id]),
788         1,
789         true
790     );