Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * @package   mod_data
  20   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  21   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22   */
  23  
  24  defined('MOODLE_INTERNAL') || die();
  25  
  26  require_once($CFG->dirroot . '/mod/data/lib.php');
  27  require_once($CFG->libdir . '/portfolio/caller.php');
  28  require_once($CFG->libdir . '/filelib.php');
  29  
  30  /**
  31   * The class to handle entry exports of a database module
  32   */
  33  class data_portfolio_caller extends portfolio_module_caller_base {
  34  
  35      /** @var int the single record to export */
  36      protected $recordid;
  37  
  38      /** @var object the record from the data table */
  39      private $data;
  40  
  41      /**#@+ @var array the fields used and their fieldtypes */
  42      private $fields;
  43      private $fieldtypes;
  44  
  45      /** @var object the records to export */
  46      private $records;
  47  
  48      /** @var int how many records are 'mine' */
  49      private $minecount;
  50  
  51      /**
  52       * the required callback arguments for a single-record export
  53       *
  54       * @return array
  55       */
  56      public static function expected_callbackargs() {
  57          return array(
  58              'id'       => true,
  59              'recordid' => false,
  60          );
  61      }
  62  
  63      /**
  64       * @param array $callbackargs the arguments passed through
  65       */
  66      public function __construct($callbackargs) {
  67          parent::__construct($callbackargs);
  68          // set up the list of fields to export
  69          $this->selectedfields = array();
  70          foreach ($callbackargs as $key => $value) {
  71              if (strpos($key, 'field_') === 0) {
  72                  $this->selectedfields[] = substr($key, 6);
  73              }
  74          }
  75      }
  76  
  77      /**
  78       * load up the data needed for the export
  79       *
  80       * @global object $DB
  81       */
  82      public function load_data() {
  83          global $DB, $USER;
  84          if (!$this->cm = get_coursemodule_from_id('data', $this->id)) {
  85              throw new portfolio_caller_exception('invalidid', 'data');
  86          }
  87          if (!$this->data = $DB->get_record('data', array('id' => $this->cm->instance))) {
  88              throw new portfolio_caller_exception('invalidid', 'data');
  89          }
  90          $fieldrecords = $DB->get_records('data_fields', array('dataid' => $this->cm->instance), 'id');
  91          // populate objets for this databases fields
  92          $this->fields = array();
  93          foreach ($fieldrecords as $fieldrecord) {
  94              $tmp = data_get_field($fieldrecord, $this->data);
  95              $this->fields[] = $tmp;
  96              $this->fieldtypes[]  = $tmp->type;
  97          }
  98  
  99          $this->records = array();
 100          if ($this->recordid) {
 101              $tmp = $DB->get_record('data_records', array('id' => $this->recordid));
 102              $tmp->content = $DB->get_records('data_content', array('recordid' => $this->recordid));
 103              $this->records[] = $tmp;
 104          } else {
 105              $where = array('dataid' => $this->data->id);
 106              if (!has_capability('mod/data:exportallentries', context_module::instance($this->cm->id))) {
 107                  $where['userid'] = $USER->id; // get them all in case, we'll unset ones that aren't ours later if necessary
 108              }
 109              $tmp = $DB->get_records('data_records', $where);
 110              foreach ($tmp as $t) {
 111                  $t->content = $DB->get_records('data_content', array('recordid' => $t->id));
 112                  $this->records[] = $t;
 113              }
 114              $this->minecount = $DB->count_records('data_records', array('dataid' => $this->data->id, 'userid' => $USER->id));
 115          }
 116  
 117          if ($this->recordid) {
 118              list($formats, $files) = self::formats($this->fields, $this->records[0]);
 119              $this->set_file_and_format_data($files);
 120          }
 121      }
 122  
 123      /**
 124       * How long we think the export will take
 125       * Single entry is probably not too long.
 126       * But we check for filesizes
 127       * Else base it on the number of records
 128       *
 129       * @return one of PORTFOLIO_TIME_XX constants
 130       */
 131      public function expected_time() {
 132          if ($this->recordid) {
 133              return $this->expected_time_file();
 134          } else {
 135              return portfolio_expected_time_db(count($this->records));
 136          }
 137      }
 138  
 139      /**
 140       * Calculate the shal1 of this export
 141       * Dependent on the export format.
 142       * @return string
 143       */
 144      public function get_sha1() {
 145          // in the case that we're exporting a subclass of 'file' and we have a singlefile,
 146          // then we're not exporting any metadata, just the file by itself by mimetype.
 147          if ($this->exporter->get('format') instanceof portfolio_format_file && $this->singlefile) {
 148              return $this->get_sha1_file();
 149          }
 150          // otherwise we're exporting some sort of multipart content so use the data
 151          $str = '';
 152          foreach ($this->records as $record) {
 153              foreach ($record as $data) {
 154                  if (is_array($data) || is_object($data)) {
 155                      $keys = array_keys($data);
 156                      $testkey = array_pop($keys);
 157                      if (is_array($data[$testkey]) || is_object($data[$testkey])) {
 158                          foreach ($data as $d) {
 159                              $str .= implode(',', (array)$d);
 160                          }
 161                      } else {
 162                          $str .= implode(',', (array)$data);
 163                      }
 164                  } else {
 165                      $str .= $data;
 166                  }
 167              }
 168          }
 169          return sha1($str . ',' . $this->exporter->get('formatclass'));
 170      }
 171  
 172      /**
 173       * Prepare the package for export
 174       *
 175       * @return stored_file object
 176       */
 177      public function prepare_package() {
 178          global $DB;
 179          $leapwriter = null;
 180          $content = '';
 181          $filename = '';
 182          $uid = $this->exporter->get('user')->id;
 183          $users = array(); //cache
 184          $onlymine = $this->get_export_config('mineonly');
 185          if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
 186              $leapwriter = $this->exporter->get('format')->leap2a_writer();
 187              $ids = array();
 188          }
 189  
 190          if ($this->exporter->get('format') instanceof portfolio_format_file && $this->singlefile) {
 191              return $this->get('exporter')->copy_existing_file($this->singlefile);
 192          }
 193          foreach ($this->records  as $key => $record) {
 194              if ($onlymine && $record->userid != $uid) {
 195                  unset($this->records[$key]); // sha1
 196                  continue;
 197              }
 198              list($tmpcontent, $files)  = $this->exportentry($record);
 199              $content .= $tmpcontent;
 200              if ($leapwriter) {
 201                  $entry = new portfolio_format_leap2a_entry('dataentry' . $record->id, $this->data->name, 'resource', $tmpcontent);
 202                  $entry->published = $record->timecreated;
 203                  $entry->updated = $record->timemodified;
 204                  if ($record->userid != $uid) {
 205                      if (!array_key_exists($record->userid, $users)) {
 206                          $users[$record->userid] = $DB->get_record('user', array('id' => $record->userid), 'id,firstname,lastname');
 207                      }
 208                      $entry->author = $users[$record->userid];
 209                  }
 210                  $ids[] = $entry->id;
 211                  $leapwriter->link_files($entry, $files, 'dataentry' . $record->id . 'file');
 212                  $leapwriter->add_entry($entry);
 213              }
 214          }
 215          if ($leapwriter) {
 216              if (count($this->records) > 1) { // make a selection element to tie them all together
 217                  $selection = new portfolio_format_leap2a_entry('datadb' . $this->data->id,
 218                      get_string('entries', 'data') . ': ' . $this->data->name, 'selection');
 219                  $leapwriter->add_entry($selection);
 220                  $leapwriter->make_selection($selection, $ids, 'Grouping');
 221              }
 222              $filename = $this->exporter->get('format')->manifest_name();
 223              $content = $leapwriter->to_xml();
 224          } else {
 225              if (count($this->records) == 1) {
 226                  $filename = clean_filename($this->cm->name . '-entry.html');
 227              } else {
 228                  $filename = clean_filename($this->cm->name . '-full.html');
 229              }
 230          }
 231          return $this->exporter->write_new_file(
 232              $content,
 233              $filename,
 234              ($this->exporter->get('format') instanceof PORTFOLIO_FORMAT_RICH) // if we have associate files, this is a 'manifest'
 235          );
 236      }
 237  
 238      /**
 239       * Verify the user can still export this entry
 240       *
 241       * @return bool
 242       */
 243      public function check_permissions() {
 244          if ($this->recordid) {
 245              if (data_isowner($this->recordid)) {
 246                  return has_capability('mod/data:exportownentry', context_module::instance($this->cm->id));
 247              }
 248              return has_capability('mod/data:exportentry', context_module::instance($this->cm->id));
 249          }
 250          if ($this->has_export_config() && !$this->get_export_config('mineonly')) {
 251              return has_capability('mod/data:exportallentries', context_module::instance($this->cm->id));
 252          }
 253          return has_capability('mod/data:exportownentry', context_module::instance($this->cm->id));
 254      }
 255  
 256      /**
 257       *  @return string
 258       */
 259      public static function display_name() {
 260          return get_string('modulename', 'data');
 261      }
 262  
 263      /**
 264       * @global object
 265       * @return bool|void
 266       */
 267      public function __wakeup() {
 268          global $CFG;
 269          if (empty($CFG)) {
 270              return true; // too early yet
 271          }
 272          foreach ($this->fieldtypes as $key => $field) {
 273              $filepath = $CFG->dirroot . '/mod/data/field/' . $field .'/field.class.php';
 274              if (!file_exists($filepath)) {
 275                  continue;
 276              }
 277              require_once($filepath);
 278              $this->fields[$key] = unserialize(serialize($this->fields[$key]));
 279          }
 280      }
 281  
 282      /**
 283       * Prepare a single entry for export, replacing all the content etc
 284       *
 285       * @param stdclass $record the entry to export
 286       *
 287       * @return array with key 0 = the html content, key 1 = array of attachments
 288       */
 289      private function exportentry($record) {
 290      // Replacing tags
 291          $patterns = array();
 292          $replacement = array();
 293  
 294          $files = array();
 295      // Then we generate strings to replace for normal tags
 296          $format = $this->get('exporter')->get('format');
 297          foreach ($this->fields as $field) {
 298              $patterns[]='[['.$field->field->name.']]';
 299              if (is_callable(array($field, 'get_file'))) {
 300                  if (!$file = $field->get_file($record->id)) {
 301                      $replacement[] = '';
 302                      continue; // probably left empty
 303                  }
 304                  $replacement[] = $format->file_output($file);
 305                  $this->get('exporter')->copy_existing_file($file);
 306                  $files[] = $file;
 307              } else {
 308                  $replacement[] = $field->display_browse_field($record->id, 'singletemplate');
 309              }
 310          }
 311  
 312      // Replacing special tags (##Edit##, ##Delete##, ##More##)
 313          $patterns[]='##edit##';
 314          $patterns[]='##delete##';
 315          $patterns[]='##export##';
 316          $patterns[]='##more##';
 317          $patterns[]='##moreurl##';
 318          $patterns[]='##user##';
 319          $patterns[]='##approve##';
 320          $patterns[]='##disapprove##';
 321          $patterns[]='##comments##';
 322          $patterns[] = '##timeadded##';
 323          $patterns[] = '##timemodified##';
 324          $replacement[] = '';
 325          $replacement[] = '';
 326          $replacement[] = '';
 327          $replacement[] = '';
 328          $replacement[] = '';
 329          $replacement[] = '';
 330          $replacement[] = '';
 331          $replacement[] = '';
 332          $replacement[] = '';
 333          $replacement[] = userdate($record->timecreated);
 334          $replacement[] = userdate($record->timemodified);
 335  
 336          // actual replacement of the tags
 337          return array(str_ireplace($patterns, $replacement, $this->data->singletemplate), $files);
 338      }
 339  
 340      /**
 341       * Given the fields being exported, and the single record,
 342       * work out which export format(s) we can use
 343       *
 344       * @param array $fields array of field objects
 345       * @param object $record The data record object
 346       *
 347       * @uses PORTFOLIO_FORMAT_PLAINHTML
 348       * @uses PORTFOLIO_FORMAT_RICHHTML
 349       *
 350       * @return array of PORTFOLIO_XX constants
 351       */
 352      public static function formats($fields, $record) {
 353          $formats = array(PORTFOLIO_FORMAT_PLAINHTML);
 354          $includedfiles = array();
 355          foreach ($fields as $singlefield) {
 356              if (is_callable(array($singlefield, 'get_file'))) {
 357                  if ($file = $singlefield->get_file($record->id)) {
 358                      $includedfiles[] = $file;
 359                  }
 360              }
 361          }
 362          if (count($includedfiles) == 1 && count($fields) == 1) {
 363              $formats = array(portfolio_format_from_mimetype($includedfiles[0]->get_mimetype()));
 364          } else if (count($includedfiles) > 0) {
 365              $formats = array(PORTFOLIO_FORMAT_RICHHTML);
 366          }
 367          return array($formats, $includedfiles);
 368      }
 369  
 370      public static function has_files($data) {
 371          global $DB;
 372          $fieldrecords = $DB->get_records('data_fields', array('dataid' => $data->id), 'id');
 373          // populate objets for this databases fields
 374          foreach ($fieldrecords as $fieldrecord) {
 375              $field = data_get_field($fieldrecord, $data);
 376              if (is_callable(array($field, 'get_file'))) {
 377                  return true;
 378              }
 379          }
 380          return false;
 381      }
 382  
 383      /**
 384       * base supported formats before we know anything about the export
 385       */
 386      public static function base_supported_formats() {
 387          return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A);
 388      }
 389  
 390      public function has_export_config() {
 391          // if we're exporting more than just a single entry,
 392          // and we have the capability to export all entries,
 393          // then ask whether we want just our own, or all of them
 394          return (empty($this->recordid) // multi-entry export
 395              && $this->minecount > 0    // some of them are mine
 396              && $this->minecount != count($this->records) // not all of them are mine
 397              && has_capability('mod/data:exportallentries', context_module::instance($this->cm->id))); // they actually have a choice in the matter
 398      }
 399  
 400      public function export_config_form(&$mform, $instance) {
 401          if (!$this->has_export_config()) {
 402              return;
 403          }
 404          $mform->addElement('selectyesno', 'mineonly', get_string('exportownentries', 'data', (object)array('mine' => $this->minecount, 'all' => count($this->records))));
 405          $mform->setDefault('mineonly', 1);
 406      }
 407  
 408      public function get_allowed_export_config() {
 409          return array('mineonly');
 410      }
 411  }
 412  
 413  
 414  /**
 415   * Class representing the virtual node with all itemids in the file browser
 416   *
 417   * @category  files
 418   * @copyright 2012 David Mudrak <david@moodle.com>
 419   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 420   */
 421  class data_file_info_container extends file_info {
 422      /** @var file_browser */
 423      protected $browser;
 424      /** @var stdClass */
 425      protected $course;
 426      /** @var stdClass */
 427      protected $cm;
 428      /** @var string */
 429      protected $component;
 430      /** @var stdClass */
 431      protected $context;
 432      /** @var array */
 433      protected $areas;
 434      /** @var string */
 435      protected $filearea;
 436  
 437      /**
 438       * Constructor (in case you did not realize it ;-)
 439       *
 440       * @param file_browser $browser
 441       * @param stdClass $course
 442       * @param stdClass $cm
 443       * @param stdClass $context
 444       * @param array $areas
 445       * @param string $filearea
 446       */
 447      public function __construct($browser, $course, $cm, $context, $areas, $filearea) {
 448          parent::__construct($browser, $context);
 449          $this->browser = $browser;
 450          $this->course = $course;
 451          $this->cm = $cm;
 452          $this->component = 'mod_data';
 453          $this->context = $context;
 454          $this->areas = $areas;
 455          $this->filearea = $filearea;
 456      }
 457  
 458      /**
 459       * @return array with keys contextid, filearea, itemid, filepath and filename
 460       */
 461      public function get_params() {
 462          return array(
 463              'contextid' => $this->context->id,
 464              'component' => $this->component,
 465              'filearea' => $this->filearea,
 466              'itemid' => null,
 467              'filepath' => null,
 468              'filename' => null,
 469          );
 470      }
 471  
 472      /**
 473       * Can new files or directories be added via the file browser
 474       *
 475       * @return bool
 476       */
 477      public function is_writable() {
 478          return false;
 479      }
 480  
 481      /**
 482       * Should this node be considered as a folder in the file browser
 483       *
 484       * @return bool
 485       */
 486      public function is_directory() {
 487          return true;
 488      }
 489  
 490      /**
 491       * Returns localised visible name of this node
 492       *
 493       * @return string
 494       */
 495      public function get_visible_name() {
 496          return $this->areas[$this->filearea];
 497      }
 498  
 499      /**
 500       * Returns list of children nodes
 501       *
 502       * @return array of file_info instances
 503       */
 504      public function get_children() {
 505          return $this->get_filtered_children('*', false, true);
 506      }
 507  
 508      /**
 509       * Help function to return files matching extensions or their count
 510       *
 511       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 512       * @param bool|int $countonly if false returns the children, if an int returns just the
 513       *    count of children but stops counting when $countonly number of children is reached
 514       * @param bool $returnemptyfolders if true returns items that don't have matching files inside
 515       * @return array|int array of file_info instances or the count
 516       */
 517      private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
 518          global $DB;
 519          $params = array('contextid' => $this->context->id,
 520              'component' => $this->component,
 521              'filearea' => $this->filearea);
 522          $sql = 'SELECT DISTINCT itemid
 523                      FROM {files}
 524                      WHERE contextid = :contextid
 525                      AND component = :component
 526                      AND filearea = :filearea';
 527          if (!$returnemptyfolders) {
 528              $sql .= ' AND filename <> :emptyfilename';
 529              $params['emptyfilename'] = '.';
 530          }
 531          list($sql2, $params2) = $this->build_search_files_sql($extensions);
 532          $sql .= ' '.$sql2;
 533          $params = array_merge($params, $params2);
 534          if ($countonly === false) {
 535              $sql .= ' ORDER BY itemid DESC';
 536          }
 537  
 538          $rs = $DB->get_recordset_sql($sql, $params);
 539          $children = array();
 540          foreach ($rs as $record) {
 541              if ($child = $this->browser->get_file_info($this->context, 'mod_data', $this->filearea, $record->itemid)) {
 542                  $children[] = $child;
 543              }
 544              if ($countonly !== false && count($children) >= $countonly) {
 545                  break;
 546              }
 547          }
 548          $rs->close();
 549          if ($countonly !== false) {
 550              return count($children);
 551          }
 552          return $children;
 553      }
 554  
 555      /**
 556       * Returns list of children which are either files matching the specified extensions
 557       * or folders that contain at least one such file.
 558       *
 559       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 560       * @return array of file_info instances
 561       */
 562      public function get_non_empty_children($extensions = '*') {
 563          return $this->get_filtered_children($extensions, false);
 564      }
 565  
 566      /**
 567       * Returns the number of children which are either files matching the specified extensions
 568       * or folders containing at least one such file.
 569       *
 570       * @param string|array $extensions, for example '*' or array('.gif','.jpg')
 571       * @param int $limit stop counting after at least $limit non-empty children are found
 572       * @return int
 573       */
 574      public function count_non_empty_children($extensions = '*', $limit = 1) {
 575          return $this->get_filtered_children($extensions, $limit);
 576      }
 577  
 578      /**
 579       * Returns parent file_info instance
 580       *
 581       * @return file_info or null for root
 582       */
 583      public function get_parent() {
 584          return $this->browser->get_file_info($this->context);
 585      }
 586  }
 587  
 588  /**
 589   * This creates new calendar events given as timeavailablefrom and timeclose by $data.
 590   *
 591   * @param stdClass $data
 592   * @return void
 593   */
 594  function data_set_events($data) {
 595      global $DB, $CFG;
 596  
 597      require_once($CFG->dirroot.'/calendar/lib.php');
 598  
 599      // Get CMID if not sent as part of $data.
 600      if (!isset($data->coursemodule)) {
 601          $cm = get_coursemodule_from_instance('data', $data->id, $data->course);
 602          $data->coursemodule = $cm->id;
 603      }
 604      // Data start calendar events.
 605      $event = new stdClass();
 606      $event->eventtype = DATA_EVENT_TYPE_OPEN;
 607      // The DATA_EVENT_TYPE_OPEN event should only be an action event if no close time was specified.
 608      $event->type = empty($data->timeavailableto) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD;
 609      if ($event->id = $DB->get_field('event', 'id',
 610              array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) {
 611          if ($data->timeavailablefrom > 0) {
 612              // Calendar event exists so update it.
 613              $event->name         = get_string('calendarstart', 'data', $data->name);
 614              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 615              $event->format       = FORMAT_HTML;
 616              $event->timestart    = $data->timeavailablefrom;
 617              $event->timesort     = $data->timeavailablefrom;
 618              $event->visible      = instance_is_visible('data', $data);
 619              $event->timeduration = 0;
 620              $calendarevent = calendar_event::load($event->id);
 621              $calendarevent->update($event, false);
 622          } else {
 623              // Calendar event is on longer needed.
 624              $calendarevent = calendar_event::load($event->id);
 625              $calendarevent->delete();
 626          }
 627      } else {
 628          // Event doesn't exist so create one.
 629          if (isset($data->timeavailablefrom) && $data->timeavailablefrom > 0) {
 630              $event->name         = get_string('calendarstart', 'data', $data->name);
 631              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 632              $event->format       = FORMAT_HTML;
 633              $event->courseid     = $data->course;
 634              $event->groupid      = 0;
 635              $event->userid       = 0;
 636              $event->modulename   = 'data';
 637              $event->instance     = $data->id;
 638              $event->timestart    = $data->timeavailablefrom;
 639              $event->timesort     = $data->timeavailablefrom;
 640              $event->visible      = instance_is_visible('data', $data);
 641              $event->timeduration = 0;
 642              calendar_event::create($event, false);
 643          }
 644      }
 645  
 646      // Data end calendar events.
 647      $event = new stdClass();
 648      $event->type = CALENDAR_EVENT_TYPE_ACTION;
 649      $event->eventtype = DATA_EVENT_TYPE_CLOSE;
 650      if ($event->id = $DB->get_field('event', 'id',
 651              array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) {
 652          if ($data->timeavailableto > 0) {
 653              // Calendar event exists so update it.
 654              $event->name         = get_string('calendarend', 'data', $data->name);
 655              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 656              $event->format       = FORMAT_HTML;
 657              $event->timestart    = $data->timeavailableto;
 658              $event->timesort     = $data->timeavailableto;
 659              $event->visible      = instance_is_visible('data', $data);
 660              $event->timeduration = 0;
 661              $calendarevent = calendar_event::load($event->id);
 662              $calendarevent->update($event, false);
 663          } else {
 664              // Calendar event is on longer needed.
 665              $calendarevent = calendar_event::load($event->id);
 666              $calendarevent->delete();
 667          }
 668      } else {
 669          // Event doesn't exist so create one.
 670          if (isset($data->timeavailableto) && $data->timeavailableto > 0) {
 671              $event->name         = get_string('calendarend', 'data', $data->name);
 672              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 673              $event->format       = FORMAT_HTML;
 674              $event->courseid     = $data->course;
 675              $event->groupid      = 0;
 676              $event->userid       = 0;
 677              $event->modulename   = 'data';
 678              $event->instance     = $data->id;
 679              $event->timestart    = $data->timeavailableto;
 680              $event->timesort     = $data->timeavailableto;
 681              $event->visible      = instance_is_visible('data', $data);
 682              $event->timeduration = 0;
 683              calendar_event::create($event, false);
 684          }
 685      }
 686  }
 687  
 688  /**
 689   * Check if a database is available for the current user.
 690   *
 691   * @param  stdClass  $data            database record
 692   * @param  boolean $canmanageentries  optional, if the user can manage entries
 693   * @param  stdClass  $context         Module context, required if $canmanageentries is not set
 694   * @return array                      status (available or not and possible warnings)
 695   * @since  Moodle 3.3
 696   */
 697  function data_get_time_availability_status($data, $canmanageentries = null, $context = null) {
 698      $open = true;
 699      $closed = false;
 700      $warnings = array();
 701  
 702      if ($canmanageentries === null) {
 703          $canmanageentries = has_capability('mod/data:manageentries', $context);
 704      }
 705  
 706      if (!$canmanageentries) {
 707          $timenow = time();
 708  
 709          if (!empty($data->timeavailablefrom) and $data->timeavailablefrom > $timenow) {
 710              $open = false;
 711          }
 712          if (!empty($data->timeavailableto) and $timenow > $data->timeavailableto) {
 713              $closed = true;
 714          }
 715  
 716          if (!$open or $closed) {
 717              if (!$open) {
 718                  $warnings['notopenyet'] = userdate($data->timeavailablefrom);
 719              }
 720              if ($closed) {
 721                  $warnings['expired'] = userdate($data->timeavailableto);
 722              }
 723              return array(false, $warnings);
 724          }
 725      }
 726  
 727      // Database is available.
 728      return array(true, $warnings);
 729  }
 730  
 731  /**
 732   * Requires a database to be available for the current user.
 733   *
 734   * @param  stdClass  $data            database record
 735   * @param  boolean $canmanageentries  optional, if the user can manage entries
 736   * @param  stdClass  $context          Module context, required if $canmanageentries is not set
 737   * @throws moodle_exception
 738   * @since  Moodle 3.3
 739   */
 740  function data_require_time_available($data, $canmanageentries = null, $context = null) {
 741  
 742      list($available, $warnings) = data_get_time_availability_status($data, $canmanageentries, $context);
 743  
 744      if (!$available) {
 745          $reason = current(array_keys($warnings));
 746          throw new moodle_exception($reason, 'data', '', $warnings[$reason]);
 747      }
 748  }
 749  
 750  /**
 751   * Return the number of entries left to add to complete the activity.
 752   *
 753   * @param  stdClass $data           database object
 754   * @param  int $numentries          the number of entries the current user has created
 755   * @param  bool $canmanageentries   whether the user can manage entries (teachers, managers)
 756   * @return int the number of entries left, 0 if no entries left or if is not required
 757   * @since  Moodle 3.3
 758   */
 759  function data_get_entries_left_to_add($data, $numentries, $canmanageentries) {
 760      if ($data->requiredentries > 0 && $numentries < $data->requiredentries && !$canmanageentries) {
 761          return $data->requiredentries - $numentries;
 762      }
 763      return 0;
 764  }
 765  
 766  /**
 767   * Return the number of entires left to add to view other users entries..
 768   *
 769   * @param  stdClass $data           database object
 770   * @param  int $numentries          the number of entries the current user has created
 771   * @param  bool $canmanageentries   whether the user can manage entries (teachers, managers)
 772   * @return int the number of entries left, 0 if no entries left or if is not required
 773   * @since  Moodle 3.3
 774   */
 775  function data_get_entries_left_to_view($data, $numentries, $canmanageentries) {
 776      if ($data->requiredentriestoview > 0 && $numentries < $data->requiredentriestoview && !$canmanageentries) {
 777          return $data->requiredentriestoview - $numentries;
 778      }
 779      return 0;
 780  }
 781  
 782  /**
 783   * Returns data records tagged with a specified tag.
 784   *
 785   * This is a callback used by the tag area mod_data/data_records to search for data records
 786   * tagged with a specific tag.
 787   *
 788   * @param core_tag_tag $tag
 789   * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
 790   *             are displayed on the page and the per-page limit may be bigger
 791   * @param int $fromctx context id where the link was displayed, may be used by callbacks
 792   *            to display items in the same context first
 793   * @param int $ctx context id where to search for records
 794   * @param bool $rec search in subcontexts as well
 795   * @param int $page 0-based number of page being displayed
 796   * @return \core_tag\output\tagindex
 797   */
 798  function mod_data_get_tagged_records($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = true, $page = 0) {
 799      global $DB, $OUTPUT, $USER;
 800      $perpage = $exclusivemode ? 20 : 5;
 801  
 802      // Build the SQL query.
 803      $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
 804      $query = "SELECT dr.id, dr.dataid, dr.approved, d.timeviewfrom, d.timeviewto, dr.groupid, d.approval, dr.userid,
 805                       d.requiredentriestoview, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect
 806                  FROM {data_records} dr
 807                  JOIN {data} d
 808                    ON d.id = dr.dataid
 809                  JOIN {modules} m
 810                    ON m.name = 'data'
 811                  JOIN {course_modules} cm
 812                    ON cm.module = m.id AND cm.instance = d.id
 813                  JOIN {tag_instance} tt
 814                    ON dr.id = tt.itemid
 815                  JOIN {course} c
 816                    ON cm.course = c.id
 817                  JOIN {context} ctx
 818                    ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
 819                 WHERE tt.itemtype = :itemtype
 820                   AND tt.tagid = :tagid
 821                   AND tt.component = :component
 822                   AND cm.deletioninprogress = 0
 823                   AND dr.id %ITEMFILTER%
 824                   AND c.id %COURSEFILTER%";
 825  
 826      $params = array(
 827          'itemtype' => 'data_records',
 828          'tagid' => $tag->id,
 829          'component' => 'mod_data',
 830          'coursemodulecontextlevel' => CONTEXT_MODULE
 831      );
 832  
 833      if ($ctx) {
 834          $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
 835          $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
 836          $params['contextid'] = $context->id;
 837          $params['path'] = $context->path . '/%';
 838      }
 839  
 840      $query .= " ORDER BY ";
 841      if ($fromctx) {
 842          // In order-clause specify that modules from inside "fromctx" context should be returned first.
 843          $fromcontext = context::instance_by_id($fromctx);
 844          $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
 845          $params['fromcontextid'] = $fromcontext->id;
 846          $params['frompath'] = $fromcontext->path . '/%';
 847      }
 848      $query .= ' c.sortorder, cm.id, dr.id';
 849  
 850      $totalpages = $page + 1;
 851  
 852      // Use core_tag_index_builder to build and filter the list of items.
 853      $builder = new core_tag_index_builder('mod_data', 'data_records', $query, $params, $page * $perpage, $perpage + 1);
 854      $now = time();
 855      $entrycount = [];
 856      $activitygroupmode = [];
 857      $usergroups = [];
 858      $titlefields = [];
 859      while ($item = $builder->has_item_that_needs_access_check()) {
 860          context_helper::preload_from_record($item);
 861          $modinfo = get_fast_modinfo($item->courseid);
 862          $cm = $modinfo->get_cm($item->cmid);
 863          $context = \context_module::instance($cm->id);
 864          $courseid = $item->courseid;
 865  
 866          if (!$builder->can_access_course($courseid)) {
 867              $builder->set_accessible($item, false);
 868              continue;
 869          }
 870  
 871          if (!$cm->uservisible) {
 872              $builder->set_accessible($item, false);
 873              continue;
 874          }
 875  
 876          if (!has_capability('mod/data:viewentry', $context)) {
 877              $builder->set_accessible($item, false);
 878              continue;
 879          }
 880  
 881          if ($USER->id != $item->userid && (($item->timeviewfrom && $now < $item->timeviewfrom)
 882                  || ($item->timeviewto && $now > $item->timeviewto))) {
 883              $builder->set_accessible($item, false);
 884              continue;
 885          }
 886  
 887          if ($USER->id != $item->userid && $item->approval && !$item->approved) {
 888              $builder->set_accessible($item, false);
 889              continue;
 890          }
 891  
 892          if ($item->requiredentriestoview) {
 893              if (!isset($entrycount[$item->dataid])) {
 894                  $entrycount[$item->dataid] = $DB->count_records('data_records', array('dataid' => $item->dataid));
 895              }
 896              $sufficiententries = $item->requiredentriestoview > $entrycount[$item->dataid];
 897              $builder->set_accessible($item, $sufficiententries);
 898          }
 899  
 900          if (!isset($activitygroupmode[$cm->id])) {
 901              $activitygroupmode[$cm->id] = groups_get_activity_groupmode($cm);
 902          }
 903  
 904          if (!isset($usergroups[$item->groupid])) {
 905              $usergroups[$item->groupid] = groups_is_member($item->groupid, $USER->id);
 906          }
 907  
 908          if ($activitygroupmode[$cm->id] == SEPARATEGROUPS && !$usergroups[$item->groupid]) {
 909              $builder->set_accessible($item, false);
 910              continue;
 911          }
 912  
 913          $builder->set_accessible($item, true);
 914      }
 915  
 916      $items = $builder->get_items();
 917      if (count($items) > $perpage) {
 918          $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
 919          array_pop($items);
 920      }
 921  
 922      // Build the display contents.
 923      if ($items) {
 924          $tagfeed = new core_tag\output\tagfeed();
 925          foreach ($items as $item) {
 926              context_helper::preload_from_record($item);
 927              $modinfo = get_fast_modinfo($item->courseid);
 928              $cm = $modinfo->get_cm($item->cmid);
 929              $pageurl = new moodle_url('/mod/data/view.php', array(
 930                      'rid' => $item->id,
 931                      'd' => $item->dataid
 932              ));
 933  
 934              if (!isset($titlefields[$item->dataid])) {
 935                  $titlefields[$item->dataid] = data_get_tag_title_field($item->dataid);
 936              }
 937  
 938              $pagename = data_get_tag_title_for_entry($titlefields[$item->dataid], $item);
 939              $pagename = html_writer::link($pageurl, $pagename);
 940              $courseurl = course_get_url($item->courseid, $cm->sectionnum);
 941              $cmname = html_writer::link($cm->url, $cm->get_formatted_name());
 942              $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
 943              $coursename = html_writer::link($courseurl, $coursename);
 944              $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
 945              $tagfeed->add($icon, $pagename, $cmname . '<br>' . $coursename);
 946          }
 947          $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT));
 948  
 949          return new core_tag\output\tagindex($tag, 'mod_data', 'data_records', $content, $exclusivemode,
 950              $fromctx, $ctx, $rec, $page, $totalpages);
 951      }
 952  }
 953  
 954  /**
 955   * Get the title of a field to show when displaying tag results.
 956   *
 957   * @param int $dataid The id of the data field
 958   * @return stdClass The field data from the 'data_fields' table as well as it's priority
 959   */
 960  function data_get_tag_title_field($dataid) {
 961      global $DB, $CFG;
 962  
 963      $validfieldtypes = array('text', 'textarea', 'menu', 'radiobutton', 'checkbox', 'multimenu', 'url');
 964      $fields = $DB->get_records('data_fields', ['dataid' => $dataid]);
 965      $template = $DB->get_field('data', 'addtemplate', ['id' => $dataid]);
 966  
 967      $filteredfields = [];
 968  
 969      foreach ($fields as $field) {
 970          if (!in_array($field->type, $validfieldtypes)) {
 971              continue;
 972          }
 973          $field->addtemplateposition = strpos($template, '[['.$field->name.']]');
 974          if ($field->addtemplateposition === false) {
 975              continue;
 976          }
 977          $filepath = $CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php';
 978          if (!file_exists($filepath)) {
 979              continue;
 980          }
 981          require_once($filepath);
 982          $classname = 'data_field_' . $field->type;
 983          $field->priority = $classname::get_priority();
 984          $filteredfields[] = $field;
 985      }
 986  
 987      $sort = function($record1, $record2) {
 988          // If a content's fieldtype is compulsory in the database than it would have priority than any other non-compulsory content.
 989          if (($record1->required && $record2->required) || (!$record1->required && !$record2->required)) {
 990              if ($record1->priority === $record2->priority) {
 991                  return $record1->id < $record2->id ? 1 : -1;
 992              }
 993  
 994              return $record1->priority < $record2->priority ? -1 : 1;
 995          } else if ($record1->required && !$record2->required) {
 996              return 1;
 997          } else {
 998              return -1;
 999          }
1000      };
1001  
1002      usort($filteredfields, $sort);
1003  
1004      return array_shift($filteredfields);
1005  }
1006  
1007  /**
1008   * Get the title of an entry to show when displaying tag results.
1009   *
1010   * @param stdClass $field The field from the 'data_fields' table
1011   * @param stdClass $entry The entry from the 'data_records' table
1012   * @return string|null It will return the title of the entry or null if the field type is not available.
1013   */
1014  function data_get_tag_title_for_entry($field, $entry) {
1015      global $CFG, $DB;
1016      if (!isset($field->type)) {
1017          return null;
1018      }
1019      $filepath = $CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php';
1020      if (!file_exists($filepath)) {
1021          return null;
1022      }
1023      require_once($filepath);
1024  
1025      $classname = 'data_field_' . $field->type;
1026      $sql = "SELECT dc.*
1027                FROM {data_content} dc
1028          INNER JOIN {data_fields} df
1029                  ON dc.fieldid = df.id
1030               WHERE df.id = :fieldid
1031                 AND dc.recordid = :recordid";
1032      $fieldcontents = $DB->get_record_sql($sql, array('recordid' => $entry->id, 'fieldid' => $field->id));
1033  
1034      return $classname::get_content_value($fieldcontents);
1035  }
1036  
1037  /**
1038   * Search entries in a database.
1039   *
1040   * @param  stdClass  $data         database object
1041   * @param  stdClass  $cm           course module object
1042   * @param  stdClass  $context      context object
1043   * @param  stdClass  $mode         in which mode we are viewing the database (list, single)
1044   * @param  int  $currentgroup      the current group being used
1045   * @param  str  $search            search for this text in the entry data
1046   * @param  str  $sort              the field to sort by
1047   * @param  str  $order             the order to use when sorting
1048   * @param  int $page               for pagination, the current page
1049   * @param  int $perpage            entries per page
1050   * @param  bool  $advanced         whether we are using or not advanced search
1051   * @param  array  $searcharray     when using advanced search, the advanced data to use
1052   * @param  stdClass  $record       if we jsut want this record after doing all the access checks
1053   * @return array the entries found among other data related to the search
1054   * @since  Moodle 3.3
1055   */
1056  function data_search_entries($data, $cm, $context, $mode, $currentgroup, $search = '', $sort = null, $order = null, $page = 0,
1057          $perpage = 0, $advanced = null, $searcharray = null, $record = null) {
1058      global $DB, $USER;
1059  
1060      if ($sort === null) {
1061          $sort = $data->defaultsort;
1062      }
1063      if ($order === null) {
1064          $order = ($data->defaultsortdir == 0) ? 'ASC' : 'DESC';
1065      }
1066      if ($searcharray === null) {
1067          $searcharray = array();
1068      }
1069  
1070      if (core_text::strlen($search) < 2) {
1071          $search = '';
1072      }
1073  
1074      $approvecap = has_capability('mod/data:approve', $context);
1075      $canmanageentries = has_capability('mod/data:manageentries', $context);
1076  
1077      // If a student is not part of a group and seperate groups is enabled, we don't
1078      // want them seeing all records.
1079      $groupmode = groups_get_activity_groupmode($cm);
1080      if ($currentgroup == 0 && $groupmode == 1 && !$canmanageentries) {
1081          $canviewallrecords = false;
1082      } else {
1083          $canviewallrecords = true;
1084      }
1085  
1086      $numentries = data_numentries($data);
1087      $requiredentriesallowed = true;
1088      if (data_get_entries_left_to_view($data, $numentries, $canmanageentries)) {
1089          $requiredentriesallowed = false;
1090      }
1091  
1092      // Initialise the first group of params for advanced searches.
1093      $initialparams   = array();
1094      $params = array(); // Named params array.
1095  
1096      // Setup group and approve restrictions.
1097      if (!$approvecap && $data->approval) {
1098          if (isloggedin()) {
1099              $approveselect = ' AND (r.approved=1 OR r.userid=:myid1) ';
1100              $params['myid1'] = $USER->id;
1101              $initialparams['myid1'] = $params['myid1'];
1102          } else {
1103              $approveselect = ' AND r.approved=1 ';
1104          }
1105      } else {
1106          $approveselect = ' ';
1107      }
1108  
1109      if ($currentgroup) {
1110          $groupselect = " AND (r.groupid = :currentgroup OR r.groupid = 0)";
1111          $params['currentgroup'] = $currentgroup;
1112          $initialparams['currentgroup'] = $params['currentgroup'];
1113      } else {
1114          if ($canviewallrecords) {
1115              $groupselect = ' ';
1116          } else {
1117              // If separate groups are enabled and the user isn't in a group or
1118              // a teacher, manager, admin etc, then just show them entries for 'All participants'.
1119              $groupselect = " AND r.groupid = 0";
1120          }
1121      }
1122  
1123      // Init some variables to be used by advanced search.
1124      $advsearchselect = '';
1125      $advwhere        = '';
1126      $advtables       = '';
1127      $advparams       = array();
1128      // This is used for the initial reduction of advanced search results with required entries.
1129      $entrysql        = '';
1130      $userfieldsapi = \core_user\fields::for_userpic()->excluding('id');
1131      $namefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
1132  
1133      // Find the field we are sorting on.
1134      if ($sort <= 0 || !($sortfield = data_get_field_from_id($sort, $data))) {
1135  
1136          switch ($sort) {
1137              case DATA_LASTNAME:
1138                  $ordering = "u.lastname $order, u.firstname $order";
1139                  break;
1140              case DATA_FIRSTNAME:
1141                  $ordering = "u.firstname $order, u.lastname $order";
1142                  break;
1143              case DATA_APPROVED:
1144                  $ordering = "r.approved $order, r.timecreated $order";
1145                  break;
1146              case DATA_TIMEMODIFIED:
1147                  $ordering = "r.timemodified $order";
1148                  break;
1149              case DATA_TIMEADDED:
1150              default:
1151                  $sort     = 0;
1152                  $ordering = "r.timecreated $order";
1153          }
1154  
1155          $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields;
1156          $count = ' COUNT(DISTINCT c.recordid) ';
1157          $tables = '{data_content} c,{data_records} r, {user} u ';
1158          $where = 'WHERE c.recordid = r.id
1159                       AND r.dataid = :dataid
1160                       AND r.userid = u.id ';
1161          $params['dataid'] = $data->id;
1162          $sortorder = " ORDER BY $ordering, r.id $order";
1163          $searchselect = '';
1164  
1165          // If requiredentries is not reached, only show current user's entries.
1166          if (!$requiredentriesallowed) {
1167              $where .= ' AND u.id = :myid2 ';
1168              $entrysql = ' AND r.userid = :myid3 ';
1169              $params['myid2'] = $USER->id;
1170              $initialparams['myid3'] = $params['myid2'];
1171          }
1172  
1173          if ($search) {
1174              $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)."
1175                                OR ".$DB->sql_like('u.firstname', ':search2', false)."
1176                                OR ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
1177              $params['search1'] = "%$search%";
1178              $params['search2'] = "%$search%";
1179              $params['search3'] = "%$search%";
1180          } else {
1181              $searchselect = ' ';
1182          }
1183  
1184      } else {
1185  
1186          $sortcontent = $DB->sql_compare_text('s.' . $sortfield->get_sort_field());
1187          $sortcontentfull = $sortfield->get_sort_sql($sortcontent);
1188  
1189          $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields . ',
1190                  ' . $sortcontentfull . ' AS sortorder ';
1191          $count = ' COUNT(DISTINCT c.recordid) ';
1192          $tables = '{data_content} c, {data_records} r, {user} u ';
1193          $where = 'WHERE c.recordid = r.id
1194                       AND r.dataid = :dataid
1195                       AND r.userid = u.id ';
1196          if (!$advanced) {
1197              $where .= 'AND s.fieldid = :sort AND s.recordid = r.id';
1198              $tables .= ',{data_content} s ';
1199          }
1200          $params['dataid'] = $data->id;
1201          $params['sort'] = $sort;
1202          $sortorder = ' ORDER BY sortorder '.$order.' , r.id ASC ';
1203          $searchselect = '';
1204  
1205          // If requiredentries is not reached, only show current user's entries.
1206          if (!$requiredentriesallowed) {
1207              $where .= ' AND u.id = :myid2';
1208              $entrysql = ' AND r.userid = :myid3';
1209              $params['myid2'] = $USER->id;
1210              $initialparams['myid3'] = $params['myid2'];
1211          }
1212  
1213          if ($search) {
1214              $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR
1215                  ".$DB->sql_like('u.firstname', ':search2', false)." OR
1216                  ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
1217              $params['search1'] = "%$search%";
1218              $params['search2'] = "%$search%";
1219              $params['search3'] = "%$search%";
1220          } else {
1221              $searchselect = ' ';
1222          }
1223      }
1224  
1225      // To actually fetch the records.
1226  
1227      $fromsql    = "FROM $tables $advtables $where $advwhere $groupselect $approveselect $searchselect $advsearchselect";
1228      $allparams  = array_merge($params, $advparams);
1229  
1230      // Provide initial sql statements and parameters to reduce the number of total records.
1231      $initialselect = $groupselect . $approveselect . $entrysql;
1232  
1233      $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams);
1234      $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data->id);
1235      $selectdata = $where . $groupselect . $approveselect;
1236  
1237      if (!empty($advanced)) {
1238          $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
1239          $sqlselect = $advancedsearchsql['sql'];
1240          $allparams = array_merge($allparams, $advancedsearchsql['params']);
1241          $totalcount = count($newrecordids);
1242      } else {
1243          $sqlselect  = "SELECT $what $fromsql $sortorder";
1244          $sqlcountselect  = "SELECT $count $fromsql";
1245          $totalcount = $DB->count_records_sql($sqlcountselect, $allparams);
1246      }
1247  
1248      // Work out the paging numbers and counts.
1249      if (empty($searchselect) && empty($advsearchselect)) {
1250          $maxcount = $totalcount;
1251      } else {
1252          $maxcount = count($recordids);
1253      }
1254  
1255      if ($record) {     // We need to just show one, so where is it in context?
1256          $nowperpage = 1;
1257          $mode = 'single';
1258          $page = 0;
1259          // TODO MDL-33797 - Reduce this or consider redesigning the paging system.
1260          if ($allrecordids = $DB->get_fieldset_sql($sqlselect, $allparams)) {
1261              $page = (int)array_search($record->id, $allrecordids);
1262              unset($allrecordids);
1263          }
1264      } else if ($mode == 'single') {  // We rely on ambient $page settings
1265          $nowperpage = 1;
1266  
1267      } else {
1268          $nowperpage = $perpage;
1269      }
1270  
1271      // Get the actual records.
1272      if (!$records = $DB->get_records_sql($sqlselect, $allparams, $page * $nowperpage, $nowperpage)) {
1273          // Nothing to show!
1274          if ($record) {         // Something was requested so try to show that at least (bug 5132)
1275              if (data_can_view_record($data, $record, $currentgroup, $canmanageentries)) {
1276                  // OK, we can show this one
1277                  $records = array($record->id => $record);
1278                  $totalcount = 1;
1279              }
1280          }
1281  
1282      }
1283  
1284      return [$records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode];
1285  }
1286  
1287  /**
1288   * Check if the current user can view the given record.
1289   *
1290   * @param  stdClass $data           database record
1291   * @param  stdClass $record         the record (entry) to check
1292   * @param  int $currentgroup        current group
1293   * @param  bool $canmanageentries   if the user can manage entries
1294   * @return bool true if the user can view the entry
1295   * @since  Moodle 3.3
1296   */
1297  function data_can_view_record($data, $record, $currentgroup, $canmanageentries) {
1298      global $USER;
1299  
1300      if ($canmanageentries || empty($data->approval) ||
1301               $record->approved || (isloggedin() && $record->userid == $USER->id)) {
1302  
1303          if (!$currentgroup || $record->groupid == $currentgroup || $record->groupid == 0) {
1304              return true;
1305          }
1306      }
1307      return false;
1308  }
1309  
1310  /**
1311   * Return all the field instances for a given database.
1312   *
1313   * @param  stdClass $data database object
1314   * @return array field instances
1315   * @since  Moodle 3.3
1316   */
1317  function data_get_field_instances($data) {
1318      global $DB;
1319  
1320      $instances = [];
1321      if ($fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id')) {
1322          foreach ($fields as $field) {
1323              $instances[] = data_get_field($field, $data);
1324          }
1325      }
1326      return $instances;
1327  }
1328  
1329  /**
1330   * Build the search array.
1331   *
1332   * @param  stdClass $data      the database object
1333   * @param  bool $paging        if paging is being used
1334   * @param  array $searcharray  the current search array (saved by session)
1335   * @param  array $defaults     default values for the searchable fields
1336   * @param  str $fn             the first name to search (optional)
1337   * @param  str $ln             the last name to search (optional)
1338   * @return array               the search array and plain search build based on the different elements
1339   * @since  Moodle 3.3
1340   */
1341  function data_build_search_array($data, $paging, $searcharray, $defaults = null, $fn = '', $ln = '') {
1342      global $DB;
1343  
1344      $search = '';
1345      $vals = array();
1346      $fields = $DB->get_records('data_fields', array('dataid' => $data->id));
1347  
1348      if (!empty($fields)) {
1349          foreach ($fields as $field) {
1350              $searchfield = data_get_field_from_id($field->id, $data);
1351              // Get field data to build search sql with.  If paging is false, get from user.
1352              // If paging is true, get data from $searcharray which is obtained from the $SESSION (see line 116).
1353              if (!$paging && $searchfield->type != 'unknown') {
1354                  $val = $searchfield->parse_search_field($defaults);
1355              } else {
1356                  // Set value from session if there is a value @ the required index.
1357                  if (isset($searcharray[$field->id])) {
1358                      $val = $searcharray[$field->id]->data;
1359                  } else { // If there is not an entry @ the required index, set value to blank.
1360                      $val = '';
1361                  }
1362              }
1363              if (!empty($val)) {
1364                  $searcharray[$field->id] = new stdClass();
1365                  list($searcharray[$field->id]->sql, $searcharray[$field->id]->params) = $searchfield->generate_sql('c'.$field->id, $val);
1366                  $searcharray[$field->id]->data = $val;
1367                  $vals[] = $val;
1368              } else {
1369                  // Clear it out.
1370                  unset($searcharray[$field->id]);
1371              }
1372          }
1373      }
1374  
1375      $rawtagnames = optional_param_array('tags', false, PARAM_TAGLIST);
1376  
1377      if ($rawtagnames) {
1378          $searcharray[DATA_TAGS] = new stdClass();
1379          $searcharray[DATA_TAGS]->params = [];
1380          $searcharray[DATA_TAGS]->rawtagnames = $rawtagnames;
1381          $searcharray[DATA_TAGS]->sql = '';
1382      } else {
1383          unset($searcharray[DATA_TAGS]);
1384      }
1385  
1386      if (!$paging) {
1387          // Name searching.
1388          $fn = optional_param('u_fn', $fn, PARAM_NOTAGS);
1389          $ln = optional_param('u_ln', $ln, PARAM_NOTAGS);
1390      } else {
1391          $fn = isset($searcharray[DATA_FIRSTNAME]) ? $searcharray[DATA_FIRSTNAME]->data : '';
1392          $ln = isset($searcharray[DATA_LASTNAME]) ? $searcharray[DATA_LASTNAME]->data : '';
1393      }
1394      if (!empty($fn)) {
1395          $searcharray[DATA_FIRSTNAME] = new stdClass();
1396          $searcharray[DATA_FIRSTNAME]->sql    = '';
1397          $searcharray[DATA_FIRSTNAME]->params = array();
1398          $searcharray[DATA_FIRSTNAME]->field  = 'u.firstname';
1399          $searcharray[DATA_FIRSTNAME]->data   = $fn;
1400          $vals[] = $fn;
1401      } else {
1402          unset($searcharray[DATA_FIRSTNAME]);
1403      }
1404      if (!empty($ln)) {
1405          $searcharray[DATA_LASTNAME] = new stdClass();
1406          $searcharray[DATA_LASTNAME]->sql     = '';
1407          $searcharray[DATA_LASTNAME]->params = array();
1408          $searcharray[DATA_LASTNAME]->field   = 'u.lastname';
1409          $searcharray[DATA_LASTNAME]->data    = $ln;
1410          $vals[] = $ln;
1411      } else {
1412          unset($searcharray[DATA_LASTNAME]);
1413      }
1414  
1415      // In case we want to switch to simple search later - there might be multiple values there ;-).
1416      if ($vals) {
1417          $val = reset($vals);
1418          if (is_string($val)) {
1419              $search = $val;
1420          }
1421      }
1422      return [$searcharray, $search];
1423  }
1424  
1425  /**
1426   * Approves or unapproves an entry.
1427   *
1428   * @param  int $entryid the entry to approve or unapprove.
1429   * @param  bool $approve Whether to approve or unapprove (true for approve false otherwise).
1430   * @since  Moodle 3.3
1431   */
1432  function data_approve_entry($entryid, $approve) {
1433      global $DB;
1434  
1435      $newrecord = new stdClass();
1436      $newrecord->id = $entryid;
1437      $newrecord->approved = $approve ? 1 : 0;
1438      $DB->update_record('data_records', $newrecord);
1439  }
1440  
1441  /**
1442   * Populate the field contents of a new record with the submitted data.
1443   * An event has been previously triggered upon the creation of the new record in data_add_record().
1444   *
1445   * @param  stdClass $data           database object
1446   * @param  stdClass $context        context object
1447   * @param  int $recordid            the new record id
1448   * @param  array $fields            list of fields of the database
1449   * @param  stdClass $datarecord     the submitted data
1450   * @param  stdClass $processeddata  pre-processed submitted fields
1451   * @since  Moodle 3.3
1452   */
1453  function data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata) {
1454      global $DB;
1455  
1456      // Insert a whole lot of empty records to make sure we have them.
1457      $records = array();
1458      foreach ($fields as $field) {
1459          $content = new stdClass();
1460          $content->recordid = $recordid;
1461          $content->fieldid = $field->id;
1462          $records[] = $content;
1463      }
1464  
1465      // Bulk insert the records now. Some records may have no data but all must exist.
1466      $DB->insert_records('data_content', $records);
1467  
1468      // Add all provided content.
1469      foreach ($processeddata->fields as $fieldname => $field) {
1470          $field->update_content($recordid, $datarecord->$fieldname, $fieldname);
1471      }
1472  }
1473  
1474  /**
1475   * Updates the fields contents of an existing record.
1476   *
1477   * @param  stdClass $data           database object
1478   * @param  stdClass $record         record to update object
1479   * @param  stdClass $context        context object
1480   * @param  stdClass $datarecord     the submitted data
1481   * @param  stdClass $processeddata  pre-processed submitted fields
1482   * @since  Moodle 3.3
1483   */
1484  function data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata) {
1485      global $DB;
1486  
1487      // Reset the approved flag after edit if the user does not have permission to approve their own entries.
1488      if (!has_capability('mod/data:approve', $context)) {
1489          $record->approved = 0;
1490      }
1491  
1492      // Update the parent record.
1493      $record->timemodified = time();
1494      $DB->update_record('data_records', $record);
1495  
1496      // Update all content.
1497      foreach ($processeddata->fields as $fieldname => $field) {
1498          $field->update_content($record->id, $datarecord->$fieldname, $fieldname);
1499      }
1500  
1501      // Trigger an event for updating this record.
1502      $event = \mod_data\event\record_updated::create(array(
1503          'objectid' => $record->id,
1504          'context' => $context,
1505          'courseid' => $data->course,
1506          'other' => array(
1507              'dataid' => $data->id
1508          )
1509      ));
1510      $event->add_record_snapshot('data', $data);
1511      $event->trigger();
1512  }