weekly release 3.4dev
[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     // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
168     // See MDL-9367.
170     return array();
173 /**
174  * No cron in book.
175  *
176  * @return bool
177  */
178 function book_cron () {
179     return true;
182 /**
183  * No grading in book.
184  *
185  * @param int $bookid
186  * @return null
187  */
188 function book_grades($bookid) {
189     return null;
192 /**
193  * This function returns if a scale is being used by one book
194  * it it has support for grading and scales. Commented code should be
195  * modified if necessary. See book, glossary or journal modules
196  * as reference.
197  *
198  * @param int $bookid
199  * @param int $scaleid
200  * @return boolean True if the scale is used by any journal
201  */
202 function book_scale_used($bookid, $scaleid) {
203     return false;
206 /**
207  * Checks if scale is being used by any instance of book
208  *
209  * This is used to find out if scale used anywhere
210  *
211  * @param int $scaleid
212  * @return bool true if the scale is used by any book
213  */
214 function book_scale_used_anywhere($scaleid) {
215     return false;
218 /**
219  * Return read actions.
220  *
221  * Note: This is not used by new logging system. Event with
222  *       crud = 'r' and edulevel = LEVEL_PARTICIPATING will
223  *       be considered as view action.
224  *
225  * @return array
226  */
227 function book_get_view_actions() {
228     global $CFG; // necessary for includes
230     $return = array('view', 'view all');
232     $plugins = core_component::get_plugin_list('booktool');
233     foreach ($plugins as $plugin => $dir) {
234         if (file_exists("$dir/lib.php")) {
235             require_once("$dir/lib.php");
236         }
237         $function = 'booktool_'.$plugin.'_get_view_actions';
238         if (function_exists($function)) {
239             if ($actions = $function()) {
240                 $return = array_merge($return, $actions);
241             }
242         }
243     }
245     return $return;
248 /**
249  * Return write actions.
250  *
251  * Note: This is not used by new logging system. Event with
252  *       crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING
253  *       will be considered as post action.
254  *
255  * @return array
256  */
257 function book_get_post_actions() {
258     global $CFG; // necessary for includes
260     $return = array('update');
262     $plugins = core_component::get_plugin_list('booktool');
263     foreach ($plugins as $plugin => $dir) {
264         if (file_exists("$dir/lib.php")) {
265             require_once("$dir/lib.php");
266         }
267         $function = 'booktool_'.$plugin.'_get_post_actions';
268         if (function_exists($function)) {
269             if ($actions = $function()) {
270                 $return = array_merge($return, $actions);
271             }
272         }
273     }
275     return $return;
278 /**
279  * Supported features
280  *
281  * @param string $feature FEATURE_xx constant for requested feature
282  * @return mixed True if module supports feature, false if not, null if doesn't know
283  */
284 function book_supports($feature) {
285     switch($feature) {
286         case FEATURE_MOD_ARCHETYPE:           return MOD_ARCHETYPE_RESOURCE;
287         case FEATURE_GROUPS:                  return false;
288         case FEATURE_GROUPINGS:               return false;
289         case FEATURE_MOD_INTRO:               return true;
290         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
291         case FEATURE_GRADE_HAS_GRADE:         return false;
292         case FEATURE_GRADE_OUTCOMES:          return false;
293         case FEATURE_BACKUP_MOODLE2:          return true;
294         case FEATURE_SHOW_DESCRIPTION:        return true;
296         default: return null;
297     }
300 /**
301  * Adds module specific settings to the settings block
302  *
303  * @param settings_navigation $settingsnav The settings navigation object
304  * @param navigation_node $booknode The node to add module settings to
305  * @return void
306  */
307 function book_extend_settings_navigation(settings_navigation $settingsnav, navigation_node $booknode) {
308     global $USER, $PAGE, $OUTPUT;
310     if ($booknode->children->count() > 0) {
311         $firstkey = $booknode->children->get_key_list()[0];
312     } else {
313         $firstkey = null;
314     }
316     $params = $PAGE->url->params();
318     if ($PAGE->cm->modname === 'book' and !empty($params['id']) and !empty($params['chapterid'])
319             and has_capability('mod/book:edit', $PAGE->cm->context)) {
320         if (!empty($USER->editing)) {
321             $string = get_string("turneditingoff");
322             $edit = '0';
323         } else {
324             $string = get_string("turneditingon");
325             $edit = '1';
326         }
327         $url = new moodle_url('/mod/book/view.php', array('id'=>$params['id'], 'chapterid'=>$params['chapterid'], 'edit'=>$edit, 'sesskey'=>sesskey()));
328         $editnode = navigation_node::create($string, $url, navigation_node::TYPE_SETTING);
329         $booknode->add_node($editnode, $firstkey);
330         $PAGE->set_button($OUTPUT->single_button($url, $string));
331     }
333     $plugins = core_component::get_plugin_list('booktool');
334     foreach ($plugins as $plugin => $dir) {
335         if (file_exists("$dir/lib.php")) {
336             require_once("$dir/lib.php");
337         }
338         $function = 'booktool_'.$plugin.'_extend_settings_navigation';
339         if (function_exists($function)) {
340             $function($settingsnav, $booknode);
341         }
342     }
346 /**
347  * Lists all browsable file areas
348  * @param object $course
349  * @param object $cm
350  * @param object $context
351  * @return array
352  */
353 function book_get_file_areas($course, $cm, $context) {
354     $areas = array();
355     $areas['chapter'] = get_string('chapters', 'mod_book');
356     return $areas;
359 /**
360  * File browsing support for book module chapter area.
361  * @param object $browser
362  * @param object $areas
363  * @param object $course
364  * @param object $cm
365  * @param object $context
366  * @param string $filearea
367  * @param int $itemid
368  * @param string $filepath
369  * @param string $filename
370  * @return object file_info instance or null if not found
371  */
372 function book_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
373     global $CFG, $DB;
375     // note: 'intro' area is handled in file_browser automatically
377     if (!has_capability('mod/book:read', $context)) {
378         return null;
379     }
381     if ($filearea !== 'chapter') {
382         return null;
383     }
385     require_once(__DIR__.'/locallib.php');
387     if (is_null($itemid)) {
388         return new book_file_info($browser, $course, $cm, $context, $areas, $filearea);
389     }
391     $fs = get_file_storage();
392     $filepath = is_null($filepath) ? '/' : $filepath;
393     $filename = is_null($filename) ? '.' : $filename;
394     if (!$storedfile = $fs->get_file($context->id, 'mod_book', $filearea, $itemid, $filepath, $filename)) {
395         return null;
396     }
398     // modifications may be tricky - may cause caching problems
399     $canwrite = has_capability('mod/book:edit', $context);
401     $chaptername = $DB->get_field('book_chapters', 'title', array('bookid'=>$cm->instance, 'id'=>$itemid));
402     $chaptername = format_string($chaptername, true, array('context'=>$context));
404     $urlbase = $CFG->wwwroot.'/pluginfile.php';
405     return new file_info_stored($browser, $context, $storedfile, $urlbase, $chaptername, true, true, $canwrite, false);
408 /**
409  * Serves the book attachments. Implements needed access control ;-)
410  *
411  * @param stdClass $course course object
412  * @param cm_info $cm course module object
413  * @param context $context context object
414  * @param string $filearea file area
415  * @param array $args extra arguments
416  * @param bool $forcedownload whether or not force download
417  * @param array $options additional options affecting the file serving
418  * @return bool false if file not found, does not return if found - just send the file
419  */
420 function book_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
421     global $CFG, $DB;
423     if ($context->contextlevel != CONTEXT_MODULE) {
424         return false;
425     }
427     require_course_login($course, true, $cm);
429     if ($filearea !== 'chapter') {
430         return false;
431     }
433     if (!has_capability('mod/book:read', $context)) {
434         return false;
435     }
437     $chid = (int)array_shift($args);
439     if (!$book = $DB->get_record('book', array('id'=>$cm->instance))) {
440         return false;
441     }
443     if (!$chapter = $DB->get_record('book_chapters', array('id'=>$chid, 'bookid'=>$book->id))) {
444         return false;
445     }
447     if ($chapter->hidden and !has_capability('mod/book:viewhiddenchapters', $context)) {
448         return false;
449     }
451     // Download the contents of a chapter as an html file.
452     if ($args[0] == 'index.html') {
453         $filename = "index.html";
455         // We need to rewrite the pluginfile URLs so the media filters can work.
456         $content = file_rewrite_pluginfile_urls($chapter->content, 'webservice/pluginfile.php', $context->id, 'mod_book', 'chapter',
457                                                 $chapter->id);
458         $formatoptions = new stdClass;
459         $formatoptions->noclean = true;
460         $formatoptions->overflowdiv = true;
461         $formatoptions->context = $context;
463         $content = format_text($content, $chapter->contentformat, $formatoptions);
465         // Remove @@PLUGINFILE@@/.
466         $options = array('reverse' => true);
467         $content = file_rewrite_pluginfile_urls($content, 'webservice/pluginfile.php', $context->id, 'mod_book', 'chapter',
468                                                 $chapter->id, $options);
469         $content = str_replace('@@PLUGINFILE@@/', '', $content);
471         $titles = "";
472         // Format the chapter titles.
473         if (!$book->customtitles) {
474             require_once(__DIR__.'/locallib.php');
475             $chapters = book_preload_chapters($book);
477             if (!$chapter->subchapter) {
478                 $currtitle = book_get_chapter_title($chapter->id, $chapters, $book, $context);
479                 // Note that we can't use the $OUTPUT->heading() in WS_SERVER mode.
480                 $titles = "<h3>$currtitle</h3>";
481             } else {
482                 $currtitle = book_get_chapter_title($chapters[$chapter->id]->parent, $chapters, $book, $context);
483                 $currsubtitle = book_get_chapter_title($chapter->id, $chapters, $book, $context);
484                 // Note that we can't use the $OUTPUT->heading() in WS_SERVER mode.
485                 $titles = "<h3>$currtitle</h3>";
486                 $titles .= "<h4>$currsubtitle</h4>";
487             }
488         }
490         $content = $titles . $content;
492         send_file($content, $filename, 0, 0, true, true);
493     } else {
494         $fs = get_file_storage();
495         $relativepath = implode('/', $args);
496         $fullpath = "/$context->id/mod_book/chapter/$chid/$relativepath";
497         if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
498             return false;
499         }
501         // Nasty hack because we do not have file revisions in book yet.
502         $lifetime = $CFG->filelifetime;
503         if ($lifetime > 60 * 10) {
504             $lifetime = 60 * 10;
505         }
507         // Finally send the file.
508         send_stored_file($file, $lifetime, 0, $forcedownload, $options);
509     }
512 /**
513  * Return a list of page types
514  *
515  * @param string $pagetype current page type
516  * @param stdClass $parentcontext Block's parent context
517  * @param stdClass $currentcontext Current context of block
518  * @return array
519  */
520 function book_page_type_list($pagetype, $parentcontext, $currentcontext) {
521     $module_pagetype = array('mod-book-*'=>get_string('page-mod-book-x', 'mod_book'));
522     return $module_pagetype;
525 /**
526  * Export book resource contents
527  *
528  * @param  stdClass $cm     Course module object
529  * @param  string $baseurl  Base URL for file downloads
530  * @return array of file content
531  */
532 function book_export_contents($cm, $baseurl) {
533     global $DB;
535     $contents = array();
536     $context = context_module::instance($cm->id);
538     $book = $DB->get_record('book', array('id' => $cm->instance), '*', MUST_EXIST);
540     $fs = get_file_storage();
542     $chapters = $DB->get_records('book_chapters', array('bookid' => $book->id), 'pagenum');
544     $structure = array();
545     $currentchapter = 0;
547     foreach ($chapters as $chapter) {
548         if ($chapter->hidden) {
549             continue;
550         }
552         // Generate the book structure.
553         $thischapter = array(
554             "title"     => format_string($chapter->title, true, array('context' => $context)),
555             "href"      => $chapter->id . "/index.html",
556             "level"     => 0,
557             "subitems"  => array()
558         );
560         // Main chapter.
561         if (!$chapter->subchapter) {
562             $currentchapter = $chapter->pagenum;
563             $structure[$currentchapter] = $thischapter;
564         } else {
565             // Subchapter.
566             $thischapter['level'] = 1;
567             $structure[$currentchapter]["subitems"][] = $thischapter;
568         }
570         // Export the chapter contents.
572         // Main content (html).
573         $filename = 'index.html';
574         $chapterindexfile = array();
575         $chapterindexfile['type']         = 'file';
576         $chapterindexfile['filename']     = $filename;
577         // Each chapter in a subdirectory.
578         $chapterindexfile['filepath']     = "/{$chapter->id}/";
579         $chapterindexfile['filesize']     = 0;
580         $chapterindexfile['fileurl']      = moodle_url::make_webservice_pluginfile_url(
581                     $context->id, 'mod_book', 'chapter', $chapter->id, '/', 'index.html')->out(false);
582         $chapterindexfile['timecreated']  = $book->timecreated;
583         $chapterindexfile['timemodified'] = $book->timemodified;
584         $chapterindexfile['content']      = format_string($chapter->title, true, array('context' => $context));
585         $chapterindexfile['sortorder']    = 0;
586         $chapterindexfile['userid']       = null;
587         $chapterindexfile['author']       = null;
588         $chapterindexfile['license']      = null;
589         $contents[] = $chapterindexfile;
591         // Chapter files (images usually).
592         $files = $fs->get_area_files($context->id, 'mod_book', 'chapter', $chapter->id, 'sortorder DESC, id ASC', false);
593         foreach ($files as $fileinfo) {
594             $file = array();
595             $file['type']         = 'file';
596             $file['filename']     = $fileinfo->get_filename();
597             $file['filepath']     = "/{$chapter->id}" . $fileinfo->get_filepath();
598             $file['filesize']     = $fileinfo->get_filesize();
599             $file['fileurl']      = moodle_url::make_webservice_pluginfile_url(
600                                         $context->id, 'mod_book', 'chapter', $chapter->id,
601                                         $fileinfo->get_filepath(), $fileinfo->get_filename())->out(false);
602             $file['timecreated']  = $fileinfo->get_timecreated();
603             $file['timemodified'] = $fileinfo->get_timemodified();
604             $file['sortorder']    = $fileinfo->get_sortorder();
605             $file['userid']       = $fileinfo->get_userid();
606             $file['author']       = $fileinfo->get_author();
607             $file['license']      = $fileinfo->get_license();
608             $file['mimetype']     = $fileinfo->get_mimetype();
609             $file['isexternalfile'] = $fileinfo->is_external_file();
610             if ($file['isexternalfile']) {
611                 $file['repositorytype'] = $fileinfo->get_repository_type();
612             }
613             $contents[] = $file;
614         }
615     }
617     // First content is the structure in encoded JSON format.
618     $structurefile = array();
619     $structurefile['type']         = 'content';
620     $structurefile['filename']     = 'structure';
621     $structurefile['filepath']     = "/";
622     $structurefile['filesize']     = 0;
623     $structurefile['fileurl']      = null;
624     $structurefile['timecreated']  = $book->timecreated;
625     $structurefile['timemodified'] = $book->timemodified;
626     $structurefile['content']      = json_encode(array_values($structure));
627     $structurefile['sortorder']    = 0;
628     $structurefile['userid']       = null;
629     $structurefile['author']       = null;
630     $structurefile['license']      = null;
632     // Add it as first element.
633     array_unshift($contents, $structurefile);
635     return $contents;
638 /**
639  * Mark the activity completed (if required) and trigger the course_module_viewed event.
640  *
641  * @param  stdClass $book       book object
642  * @param  stdClass $chapter    chapter object
643  * @param  bool $islaschapter   is the las chapter of the book?
644  * @param  stdClass $course     course object
645  * @param  stdClass $cm         course module object
646  * @param  stdClass $context    context object
647  * @since Moodle 3.0
648  */
649 function book_view($book, $chapter, $islastchapter, $course, $cm, $context) {
651     // First case, we are just opening the book.
652     if (empty($chapter)) {
653         \mod_book\event\course_module_viewed::create_from_book($book, $context)->trigger();
655     } else {
656         \mod_book\event\chapter_viewed::create_from_chapter($book, $context, $chapter)->trigger();
658         if ($islastchapter) {
659             // We cheat a bit here in assuming that viewing the last page means the user viewed the whole book.
660             $completion = new completion_info($course);
661             $completion->set_module_viewed($cm);
662         }
663     }
666 /**
667  * Check if the module has any update that affects the current user since a given time.
668  *
669  * @param  cm_info $cm course module data
670  * @param  int $from the time to check updates from
671  * @param  array $filter  if we need to check only specific updates
672  * @return stdClass an object with the different type of areas indicating if they were updated or not
673  * @since Moodle 3.2
674  */
675 function book_check_updates_since(cm_info $cm, $from, $filter = array()) {
676     global $DB;
678     $context = $cm->context;
679     $updates = new stdClass();
680     if (!has_capability('mod/book:read', $context)) {
681         return $updates;
682     }
683     $updates = course_check_module_updates_since($cm, $from, array('content'), $filter);
685     $select = 'bookid = :id AND (timecreated > :since1 OR timemodified > :since2)';
686     $params = array('id' => $cm->instance, 'since1' => $from, 'since2' => $from);
687     if (!has_capability('mod/book:viewhiddenchapters', $context)) {
688         $select .= ' AND hidden = 0';
689     }
690     $updates->entries = (object) array('updated' => false);
691     $entries = $DB->get_records_select('book_chapters', $select, $params, '', 'id');
692     if (!empty($entries)) {
693         $updates->entries->updated = true;
694         $updates->entries->itemids = array_keys($entries);
695     }
697     return $updates;
700 /**
701  * Get icon mapping for font-awesome.
702  */
703 function mod_book_get_fontawesome_icon_map() {
704     return [
705         'mod_book:chapter' => 'fa-bookmark-o',
706         'mod_book:nav_prev' => 'fa-arrow-left',
707         'mod_book:nav_prev_dis' => 'fa-angle-left',
708         'mod_book:nav_sep' => 'fa-minus',
709         'mod_book:add' => 'fa-plus',
710         'mod_book:nav_next' => 'fa-arrow-right',
711         'mod_book:nav_next_dis' => 'fa-angle-right',
712         'mod_book:nav_exit' => 'fa-arrow-up',
713     ];
716 /**
717  * This function receives a calendar event and returns the action associated with it, or null if there is none.
718  *
719  * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
720  * is not displayed on the block.
721  *
722  * @param calendar_event $event
723  * @param \core_calendar\action_factory $factory
724  * @return \core_calendar\local\event\entities\action_interface|null
725  */
726 function mod_book_core_calendar_provide_event_action(calendar_event $event,
727                                                      \core_calendar\action_factory $factory) {
728     $cm = get_fast_modinfo($event->courseid)->instances['book'][$event->instance];
729     $context = context_module::instance($cm->id);
731     if (!has_capability('mod/book:read', $context)) {
732         return null;
733     }
735     $completion = new \completion_info($cm->get_course());
737     $completiondata = $completion->get_data($cm, false);
739     if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
740         return null;
741     }
743     return $factory->create_instance(
744         get_string('view'),
745         new \moodle_url('/mod/book/view.php', ['id' => $cm->id]),
746         1,
747         true
748     );