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