Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Book module local lib functions
  19   *
  20   * @package    mod_book
  21   * @copyright  2010-2011 Petr Skoda {@link http://skodak.org}
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die;
  26  
  27  require_once (__DIR__.'/lib.php');
  28  require_once($CFG->libdir.'/filelib.php');
  29  
  30  /**
  31   * The following defines are used to define how the chapters and subchapters of a book should be displayed in that table of contents.
  32   * BOOK_NUM_NONE        No special styling will applied and the editor will be able to do what ever thay want in the title
  33   * BOOK_NUM_NUMBERS     Chapters and subchapters are numbered (1, 1.1, 1.2, 2, ...)
  34   * BOOK_NUM_BULLETS     Subchapters are indented and displayed with bullets
  35   * BOOK_NUM_INDENTED    Subchapters are indented
  36   */
  37  define('BOOK_NUM_NONE',     '0');
  38  define('BOOK_NUM_NUMBERS',  '1');
  39  define('BOOK_NUM_BULLETS',  '2');
  40  define('BOOK_NUM_INDENTED', '3');
  41  
  42  /**
  43   * The following defines are used to define the navigation style used within a book.
  44   * BOOK_LINK_TOCONLY    Only the table of contents is shown, in a side region.
  45   * BOOK_LINK_IMAGE      Arrows link to previous/next/exit pages, in addition to the TOC.
  46   * BOOK_LINK_TEXT       Page names and arrows link to previous/next/exit pages, in addition to the TOC.
  47   */
  48  define ('BOOK_LINK_TOCONLY', '0');
  49  define ('BOOK_LINK_IMAGE', '1');
  50  define ('BOOK_LINK_TEXT', '2');
  51  
  52  /**
  53   * Preload book chapters and fix toc structure if necessary.
  54   *
  55   * Returns array of chapters with standard 'pagenum', 'id, pagenum, subchapter, title, content, contentformat, hidden'
  56   * and extra 'parent, number, subchapters, prev, next'.
  57   * Please note the content/text of chapters is not included.
  58   *
  59   * @param  stdClass $book
  60   * @return array of id=>chapter
  61   */
  62  function book_preload_chapters($book) {
  63      global $DB;
  64      $chapters = $DB->get_records('book_chapters', array('bookid' => $book->id), 'pagenum', 'id, pagenum,
  65              subchapter, title, content, contentformat, hidden');
  66      if (!$chapters) {
  67          return array();
  68      }
  69  
  70      $prev = null;
  71      $prevsub = null;
  72  
  73      $first = true;
  74      $hidesub = true;
  75      $parent = null;
  76      $pagenum = 0; // chapter sort
  77      $i = 0;       // main chapter num
  78      $j = 0;       // subchapter num
  79      foreach ($chapters as $id => $ch) {
  80          $oldch = clone($ch);
  81          $pagenum++;
  82          $ch->pagenum = $pagenum;
  83          if ($first) {
  84              // book can not start with a subchapter
  85              $ch->subchapter = 0;
  86              $first = false;
  87          }
  88          if (!$ch->subchapter) {
  89              if ($ch->hidden) {
  90                  if ($book->numbering == BOOK_NUM_NUMBERS) {
  91                      $ch->number = 'x';
  92                  } else {
  93                      $ch->number = null;
  94                  }
  95              } else {
  96                  $i++;
  97                  $ch->number = $i;
  98              }
  99              $j = 0;
 100              $prevsub = null;
 101              $hidesub = $ch->hidden;
 102              $parent = $ch->id;
 103              $ch->parent = null;
 104              $ch->subchapters = array();
 105          } else {
 106              $ch->parent = $parent;
 107              $ch->subchapters = null;
 108              $chapters[$parent]->subchapters[$ch->id] = $ch->id;
 109              if ($hidesub) {
 110                  // all subchapters in hidden chapter must be hidden too
 111                  $ch->hidden = 1;
 112              }
 113              if ($ch->hidden) {
 114                  if ($book->numbering == BOOK_NUM_NUMBERS) {
 115                      $ch->number = 'x';
 116                  } else {
 117                      $ch->number = null;
 118                  }
 119              } else {
 120                  $j++;
 121                  $ch->number = $j;
 122              }
 123          }
 124  
 125          if ($oldch->subchapter != $ch->subchapter or $oldch->pagenum != $ch->pagenum or $oldch->hidden != $ch->hidden) {
 126              // update only if something changed
 127              $DB->update_record('book_chapters', $ch);
 128          }
 129          $chapters[$id] = $ch;
 130      }
 131  
 132      return $chapters;
 133  }
 134  
 135  /**
 136   * Returns the title for a given chapter
 137   *
 138   * @param int $chid
 139   * @param array $chapters
 140   * @param stdClass $book
 141   * @param context_module $context
 142   * @return string
 143   */
 144  function book_get_chapter_title($chid, $chapters, $book, $context) {
 145      $ch = $chapters[$chid];
 146      $title = trim(format_string($ch->title, true, array('context'=>$context)));
 147      $numbers = array();
 148      if ($book->numbering == BOOK_NUM_NUMBERS) {
 149          if ($ch->parent and $chapters[$ch->parent]->number) {
 150              $numbers[] = $chapters[$ch->parent]->number;
 151          }
 152          if ($ch->number) {
 153              $numbers[] = $ch->number;
 154          }
 155      }
 156  
 157      if ($numbers) {
 158          $title = implode('.', $numbers) . '. ' . $title;
 159      }
 160  
 161      return $title;
 162  }
 163  
 164  /**
 165   * Add the book TOC sticky block to the default region.
 166   *
 167   * @param   array       $chapters   The Chapters in the book
 168   * @param   stdClass    $chapter    The current chapter
 169   * @param   stdClass    $book       The book
 170   * @param   stdClass    $cm         The course module
 171   * @param   bool|null   $edit       Whether the user is editing
 172   */
 173  function book_add_fake_block($chapters, $chapter, $book, $cm, $edit = null) {
 174      global $PAGE, $USER;
 175  
 176      if ($edit === null) {
 177          if (has_capability('mod/book:edit', context_module::instance($cm->id))) {
 178              if (isset($USER->editing)) {
 179                  $edit = $USER->editing;
 180              } else {
 181                  $edit = 0;
 182              }
 183          } else {
 184              $edit = 0;
 185          }
 186      }
 187  
 188      $toc = book_get_toc($chapters, $chapter, $book, $cm, $edit);
 189  
 190      $bc = new block_contents();
 191      $bc->title = get_string('toc', 'mod_book');
 192      $bc->attributes['class'] = 'block block_book_toc';
 193      $bc->content = $toc;
 194  
 195      $defaultregion = $PAGE->blocks->get_default_region();
 196      $PAGE->blocks->add_fake_block($bc, $defaultregion);
 197  }
 198  
 199  /**
 200   * Generate toc structure
 201   *
 202   * @param array $chapters
 203   * @param stdClass $chapter
 204   * @param stdClass $book
 205   * @param stdClass $cm
 206   * @param bool $edit
 207   * @return string
 208   */
 209  function book_get_toc($chapters, $chapter, $book, $cm, $edit) {
 210      global $USER, $OUTPUT;
 211  
 212      $toc = '';
 213      $nch = 0;   // Chapter number
 214      $ns = 0;    // Subchapter number
 215      $first = 1;
 216  
 217      $context = context_module::instance($cm->id);
 218      $viewhidden = has_capability('mod/book:viewhiddenchapters', $context);
 219  
 220      switch ($book->numbering) {
 221          case BOOK_NUM_NONE:
 222              $toc .= html_writer::start_tag('div', array('class' => 'book_toc book_toc_none clearfix'));
 223              break;
 224          case BOOK_NUM_NUMBERS:
 225              $toc .= html_writer::start_tag('div', array('class' => 'book_toc book_toc_numbered clearfix'));
 226              break;
 227          case BOOK_NUM_BULLETS:
 228              $toc .= html_writer::start_tag('div', array('class' => 'book_toc book_toc_bullets clearfix'));
 229              break;
 230          case BOOK_NUM_INDENTED:
 231              $toc .= html_writer::start_tag('div', array('class' => 'book_toc book_toc_indented clearfix'));
 232              break;
 233      }
 234  
 235      if ($edit) { // Editing on (Teacher's TOC).
 236          $toc .= html_writer::start_tag('ul');
 237          $i = 0;
 238          foreach ($chapters as $ch) {
 239              $i++;
 240              $title = trim(format_string($ch->title, true, array('context' => $context)));
 241              $titleunescaped = trim(format_string($ch->title, true, array('context' => $context, 'escape' => false)));
 242              $titleout = $title;
 243  
 244              if (!$ch->subchapter) {
 245  
 246                  if ($first) {
 247                      $toc .= html_writer::start_tag('li');
 248                  } else {
 249                      $toc .= html_writer::end_tag('ul');
 250                      $toc .= html_writer::end_tag('li');
 251                      $toc .= html_writer::start_tag('li');
 252                  }
 253  
 254                  if (!$ch->hidden) {
 255                      $nch++;
 256                      $ns = 0;
 257                      if ($book->numbering == BOOK_NUM_NUMBERS) {
 258                          $title = "$nch. $title";
 259                          $titleout = $title;
 260                      }
 261                  } else {
 262                      if ($book->numbering == BOOK_NUM_NUMBERS) {
 263                          $title = "x. $title";
 264                      }
 265                      $titleout = html_writer::tag('span', $title, array('class' => 'dimmed_text'));
 266                  }
 267              } else {
 268  
 269                  if ($first) {
 270                      $toc .= html_writer::start_tag('li');
 271                      $toc .= html_writer::start_tag('ul');
 272                      $toc .= html_writer::start_tag('li');
 273                  } else {
 274                      $toc .= html_writer::start_tag('li');
 275                  }
 276  
 277                  if (!$ch->hidden) {
 278                      $ns++;
 279                      if ($book->numbering == BOOK_NUM_NUMBERS) {
 280                          $title = "$nch.$ns. $title";
 281                          $titleout = $title;
 282                      }
 283                  } else {
 284                      if ($book->numbering == BOOK_NUM_NUMBERS) {
 285                          if (empty($chapters[$ch->parent]->hidden)) {
 286                              $title = "$nch.x. $title";
 287                          } else {
 288                              $title = "x.x. $title";
 289                          }
 290                      }
 291                      $titleout = html_writer::tag('span', $title, array('class' => 'dimmed_text'));
 292                  }
 293              }
 294              $toc .= html_writer::start_tag('div', array('class' => 'd-flex'));
 295              if ($ch->id == $chapter->id) {
 296                  $toc .= html_writer::tag('strong', $titleout, array('class' => 'text-truncate'));
 297              } else {
 298                  $toc .= html_writer::link(new moodle_url('view.php', array('id' => $cm->id, 'chapterid' => $ch->id)), $titleout,
 299                      array('title' => $titleunescaped, 'class' => 'text-truncate'));
 300              }
 301  
 302              $toc .= html_writer::start_tag('div', array('class' => 'action-list d-flex ml-auto'));
 303              if ($i != 1) {
 304                  $toc .= html_writer::link(new moodle_url('move.php', array('id' => $cm->id, 'chapterid' => $ch->id, 'up' => '1', 'sesskey' => $USER->sesskey)),
 305                          $OUTPUT->pix_icon('t/up', get_string('movechapterup', 'mod_book', $title)),
 306                          array('title' => get_string('movechapterup', 'mod_book', $titleunescaped)));
 307              }
 308              if ($i != count($chapters)) {
 309                  $toc .= html_writer::link(new moodle_url('move.php', array('id' => $cm->id, 'chapterid' => $ch->id, 'up' => '0', 'sesskey' => $USER->sesskey)),
 310                          $OUTPUT->pix_icon('t/down', get_string('movechapterdown', 'mod_book', $title)),
 311                          array('title' => get_string('movechapterdown', 'mod_book', $titleunescaped)));
 312              }
 313              $toc .= html_writer::link(new moodle_url('edit.php', array('cmid' => $cm->id, 'id' => $ch->id)),
 314                      $OUTPUT->pix_icon('t/edit', get_string('editchapter', 'mod_book', $title)),
 315                      array('title' => get_string('editchapter', 'mod_book', $titleunescaped)));
 316  
 317              $deleteaction = new confirm_action(get_string('deletechapter', 'mod_book', $titleunescaped));
 318              $toc .= $OUTPUT->action_icon(
 319                      new moodle_url('delete.php', [
 320                              'id'        => $cm->id,
 321                              'chapterid' => $ch->id,
 322                              'sesskey'   => sesskey(),
 323                              'confirm'   => 1,
 324                          ]),
 325                      new pix_icon('t/delete', get_string('deletechapter', 'mod_book', $title)),
 326                      $deleteaction,
 327                      ['title' => get_string('deletechapter', 'mod_book', $titleunescaped)]
 328                  );
 329  
 330              if ($ch->hidden) {
 331                  $toc .= html_writer::link(new moodle_url('show.php', array('id' => $cm->id, 'chapterid' => $ch->id, 'sesskey' => $USER->sesskey)),
 332                          $OUTPUT->pix_icon('t/show', get_string('showchapter', 'mod_book', $title)),
 333                          array('title' => get_string('showchapter', 'mod_book', $titleunescaped)));
 334              } else {
 335                  $toc .= html_writer::link(new moodle_url('show.php', array('id' => $cm->id, 'chapterid' => $ch->id, 'sesskey' => $USER->sesskey)),
 336                          $OUTPUT->pix_icon('t/hide', get_string('hidechapter', 'mod_book', $title)),
 337                          array('title' => get_string('hidechapter', 'mod_book', $titleunescaped)));
 338              }
 339  
 340              $buttontitle = get_string('addafterchapter', 'mod_book', ['title' => $ch->title]);
 341              $toc .= html_writer::link(new moodle_url('edit.php', array('cmid' => $cm->id, 'pagenum' => $ch->pagenum, 'subchapter' => $ch->subchapter)),
 342                                              $OUTPUT->pix_icon('add', $buttontitle, 'mod_book'), array('title' => $buttontitle));
 343              $toc .= html_writer::end_tag('div');
 344              $toc .= html_writer::end_tag('div');
 345  
 346              if (!$ch->subchapter) {
 347                  $toc .= html_writer::start_tag('ul');
 348              } else {
 349                  $toc .= html_writer::end_tag('li');
 350              }
 351              $first = 0;
 352          }
 353  
 354          $toc .= html_writer::end_tag('ul');
 355          $toc .= html_writer::end_tag('li');
 356          $toc .= html_writer::end_tag('ul');
 357  
 358      } else { // Editing off. Normal students, teachers view.
 359          $toc .= html_writer::start_tag('ul');
 360          foreach ($chapters as $ch) {
 361              $title = trim(format_string($ch->title, true, array('context'=>$context)));
 362              $titleunescaped = trim(format_string($ch->title, true, array('context' => $context, 'escape' => false)));
 363              if (!$ch->hidden || ($ch->hidden && $viewhidden)) {
 364                  if (!$ch->subchapter) {
 365                      $nch++;
 366                      $ns = 0;
 367  
 368                      if ($first) {
 369                          $toc .= html_writer::start_tag('li');
 370                      } else {
 371                          $toc .= html_writer::end_tag('ul');
 372                          $toc .= html_writer::end_tag('li');
 373                          $toc .= html_writer::start_tag('li');
 374                      }
 375  
 376                      if ($book->numbering == BOOK_NUM_NUMBERS) {
 377                            $title = "$nch. $title";
 378                      }
 379                  } else {
 380                      $ns++;
 381  
 382                      if ($first) {
 383                          $toc .= html_writer::start_tag('li');
 384                          $toc .= html_writer::start_tag('ul');
 385                          $toc .= html_writer::start_tag('li');
 386                      } else {
 387                          $toc .= html_writer::start_tag('li');
 388                      }
 389  
 390                      if ($book->numbering == BOOK_NUM_NUMBERS) {
 391                            $title = "$nch.$ns. $title";
 392                      }
 393                  }
 394  
 395                  $cssclass = ($ch->hidden && $viewhidden) ? 'dimmed_text' : '';
 396  
 397                  if ($ch->id == $chapter->id) {
 398                      $toc .= html_writer::tag('strong', $title, array('class' => $cssclass));
 399                  } else {
 400                      $toc .= html_writer::link(new moodle_url('view.php',
 401                                                array('id' => $cm->id, 'chapterid' => $ch->id)),
 402                                                $title, array('title' => s($titleunescaped), 'class' => $cssclass));
 403                  }
 404  
 405                  if (!$ch->subchapter) {
 406                      $toc .= html_writer::start_tag('ul');
 407                  } else {
 408                      $toc .= html_writer::end_tag('li');
 409                  }
 410  
 411                  $first = 0;
 412              }
 413          }
 414  
 415          $toc .= html_writer::end_tag('ul');
 416          $toc .= html_writer::end_tag('li');
 417          $toc .= html_writer::end_tag('ul');
 418  
 419      }
 420  
 421      $toc .= html_writer::end_tag('div');
 422  
 423      $toc = str_replace('<ul></ul>', '', $toc); // Cleanup of invalid structures.
 424  
 425      return $toc;
 426  }
 427  
 428  /**
 429   * Returns book chapters tagged with a specified tag.
 430   *
 431   * This is a callback used by the tag area mod_book/book_chapters to search for book chapters
 432   * tagged with a specific tag.
 433   *
 434   * @param core_tag_tag $tag
 435   * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
 436   *             are displayed on the page and the per-page limit may be bigger
 437   * @param int $fromctx context id where the link was displayed, may be used by callbacks
 438   *            to display items in the same context first
 439   * @param int $ctx context id where to search for records
 440   * @param bool $rec search in subcontexts as well
 441   * @param int $page 0-based number of page being displayed
 442   * @return \core_tag\output\tagindex
 443   */
 444  function mod_book_get_tagged_chapters($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = true, $page = 0) {
 445      global $OUTPUT;
 446      $perpage = $exclusivemode ? 20 : 5;
 447  
 448      // Build the SQL query.
 449      $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
 450      $query = "SELECT bc.id, bc.title, bc.bookid, bc.hidden,
 451                      cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect
 452                  FROM {book_chapters} bc
 453                  JOIN {book} b ON b.id = bc.bookid
 454                  JOIN {modules} m ON m.name='book'
 455                  JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = b.id
 456                  JOIN {tag_instance} tt ON bc.id = tt.itemid
 457                  JOIN {course} c ON cm.course = c.id
 458                  JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
 459                 WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
 460                   AND cm.deletioninprogress = 0
 461                   AND bc.id %ITEMFILTER% AND c.id %COURSEFILTER%";
 462  
 463      $params = array('itemtype' => 'book_chapters', 'tagid' => $tag->id, 'component' => 'mod_book',
 464                      'coursemodulecontextlevel' => CONTEXT_MODULE);
 465  
 466      if ($ctx) {
 467          $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
 468          $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
 469          $params['contextid'] = $context->id;
 470          $params['path'] = $context->path.'/%';
 471      }
 472  
 473      $query .= " ORDER BY ";
 474      if ($fromctx) {
 475          // In order-clause specify that modules from inside "fromctx" context should be returned first.
 476          $fromcontext = context::instance_by_id($fromctx);
 477          $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
 478          $params['fromcontextid'] = $fromcontext->id;
 479          $params['frompath'] = $fromcontext->path.'/%';
 480      }
 481      $query .= ' c.sortorder, cm.id, bc.id';
 482  
 483      $totalpages = $page + 1;
 484  
 485      // Use core_tag_index_builder to build and filter the list of items.
 486      $builder = new core_tag_index_builder('mod_book', 'book_chapters', $query, $params, $page * $perpage, $perpage + 1);
 487      while ($item = $builder->has_item_that_needs_access_check()) {
 488          context_helper::preload_from_record($item);
 489          $courseid = $item->courseid;
 490          if (!$builder->can_access_course($courseid)) {
 491              $builder->set_accessible($item, false);
 492              continue;
 493          }
 494          $modinfo = get_fast_modinfo($builder->get_course($courseid));
 495          // Set accessibility of this item and all other items in the same course.
 496          $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) {
 497              if ($taggeditem->courseid == $courseid) {
 498                  $accessible = false;
 499                  if (($cm = $modinfo->get_cm($taggeditem->cmid)) && $cm->uservisible) {
 500                      if (empty($taggeditem->hidden)) {
 501                          $accessible = true;
 502                      } else {
 503                          $accessible = has_capability('mod/book:viewhiddenchapters', context_module::instance($cm->id));
 504                      }
 505                  }
 506                  $builder->set_accessible($taggeditem, $accessible);
 507              }
 508          });
 509      }
 510  
 511      $items = $builder->get_items();
 512      if (count($items) > $perpage) {
 513          $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
 514          array_pop($items);
 515      }
 516  
 517      // Build the display contents.
 518      if ($items) {
 519          $tagfeed = new core_tag\output\tagfeed();
 520          foreach ($items as $item) {
 521              context_helper::preload_from_record($item);
 522              $modinfo = get_fast_modinfo($item->courseid);
 523              $cm = $modinfo->get_cm($item->cmid);
 524              $pageurl = new moodle_url('/mod/book/view.php', array('chapterid' => $item->id, 'b' => $item->bookid));
 525              $pagename = format_string($item->title, true, array('context' => context_module::instance($item->cmid)));
 526              $pagename = html_writer::link($pageurl, $pagename);
 527              $courseurl = course_get_url($item->courseid, $cm->sectionnum);
 528              $cmname = html_writer::link($cm->url, $cm->get_formatted_name());
 529              $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
 530              $coursename = html_writer::link($courseurl, $coursename);
 531              $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
 532              $tagfeed->add($icon, $pagename, $cmname.'<br>'.$coursename);
 533          }
 534  
 535          $content = $OUTPUT->render_from_template('core_tag/tagfeed',
 536              $tagfeed->export_for_template($OUTPUT));
 537  
 538          return new core_tag\output\tagindex($tag, 'mod_book', 'book_chapters', $content,
 539              $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
 540      }
 541  }
 542  
 543  /**
 544   * File browsing support class
 545   *
 546   * @copyright  2010-2011 Petr Skoda {@link http://skodak.org}
 547   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 548   */
 549  class book_file_info extends file_info {
 550      /** @var stdClass Course object */
 551      protected $course;
 552      /** @var stdClass Course module object */
 553      protected $cm;
 554      /** @var array Available file areas */
 555      protected $areas;
 556      /** @var string File area to browse */
 557      protected $filearea;
 558  
 559      /**
 560       * Constructor
 561       *
 562       * @param file_browser $browser file_browser instance
 563       * @param stdClass $course course object
 564       * @param stdClass $cm course module object
 565       * @param stdClass $context module context
 566       * @param array $areas available file areas
 567       * @param string $filearea file area to browse
 568       */
 569      public function __construct($browser, $course, $cm, $context, $areas, $filearea) {
 570          parent::__construct($browser, $context);
 571          $this->course   = $course;
 572          $this->cm       = $cm;
 573          $this->areas    = $areas;
 574          $this->filearea = $filearea;
 575      }
 576  
 577      /**
 578       * Returns list of standard virtual file/directory identification.
 579       * The difference from stored_file parameters is that null values
 580       * are allowed in all fields
 581       * @return array with keys contextid, filearea, itemid, filepath and filename
 582       */
 583      public function get_params() {
 584          return array('contextid'=>$this->context->id,
 585                       'component'=>'mod_book',
 586                       'filearea' =>$this->filearea,
 587                       'itemid'   =>null,
 588                       'filepath' =>null,
 589                       'filename' =>null);
 590      }
 591  
 592      /**
 593       * Returns localised visible name.
 594       * @return string
 595       */
 596      public function get_visible_name() {
 597          return $this->areas[$this->filearea];
 598      }
 599  
 600      /**
 601       * Can I add new files or directories?
 602       * @return bool
 603       */
 604      public function is_writable() {
 605          return false;
 606      }
 607  
 608      /**
 609       * Is directory?
 610       * @return bool
 611       */
 612      public function is_directory() {
 613          return true;
 614      }
 615  
 616      /**
 617       * Returns list of children.
 618       * @return array of file_info instances
 619       */
 620      public function get_children() {
 621          return $this->get_filtered_children('*', false, true);
 622      }
 623  
 624      /**
 625       * Help function to return files matching extensions or their count
 626       *
 627       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 628       * @param bool|int $countonly if false returns the children, if an int returns just the
 629       *    count of children but stops counting when $countonly number of children is reached
 630       * @param bool $returnemptyfolders if true returns items that don't have matching files inside
 631       * @return array|int array of file_info instances or the count
 632       */
 633      private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
 634          global $DB;
 635          $params = array('contextid' => $this->context->id,
 636              'component' => 'mod_book',
 637              'filearea' => $this->filearea,
 638              'bookid' => $this->cm->instance);
 639          $sql = 'SELECT DISTINCT bc.id, bc.pagenum
 640                      FROM {files} f, {book_chapters} bc
 641                      WHERE f.contextid = :contextid
 642                      AND f.component = :component
 643                      AND f.filearea = :filearea
 644                      AND bc.bookid = :bookid
 645                      AND bc.id = f.itemid';
 646          if (!$returnemptyfolders) {
 647              $sql .= ' AND filename <> :emptyfilename';
 648              $params['emptyfilename'] = '.';
 649          }
 650          list($sql2, $params2) = $this->build_search_files_sql($extensions, 'f');
 651          $sql .= ' '.$sql2;
 652          $params = array_merge($params, $params2);
 653          if ($countonly === false) {
 654              $sql .= ' ORDER BY bc.pagenum';
 655          }
 656  
 657          $rs = $DB->get_recordset_sql($sql, $params);
 658          $children = array();
 659          foreach ($rs as $record) {
 660              if ($child = $this->browser->get_file_info($this->context, 'mod_book', $this->filearea, $record->id)) {
 661                  if ($returnemptyfolders || $child->count_non_empty_children($extensions)) {
 662                      $children[] = $child;
 663                  }
 664              }
 665              if ($countonly !== false && count($children) >= $countonly) {
 666                  break;
 667              }
 668          }
 669          $rs->close();
 670          if ($countonly !== false) {
 671              return count($children);
 672          }
 673          return $children;
 674      }
 675  
 676      /**
 677       * Returns list of children which are either files matching the specified extensions
 678       * or folders that contain at least one such file.
 679       *
 680       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 681       * @return array of file_info instances
 682       */
 683      public function get_non_empty_children($extensions = '*') {
 684          return $this->get_filtered_children($extensions, false);
 685      }
 686  
 687      /**
 688       * Returns the number of children which are either files matching the specified extensions
 689       * or folders containing at least one such file.
 690       *
 691       * @param string|array $extensions, for example '*' or array('.gif','.jpg')
 692       * @param int $limit stop counting after at least $limit non-empty children are found
 693       * @return int
 694       */
 695      public function count_non_empty_children($extensions = '*', $limit = 1) {
 696          return $this->get_filtered_children($extensions, $limit);
 697      }
 698  
 699      /**
 700       * Returns parent file_info instance
 701       * @return file_info or null for root
 702       */
 703      public function get_parent() {
 704          return $this->browser->get_file_info($this->context);
 705      }
 706  }