Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

   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          if (empty($this->data->singletemplate)) {
 337              // Use default template if the template is not created.
 338              $this->data->singletemplate = data_generate_default_template($this->data, 'singletemplate', 0, false, false);
 339          }
 340  
 341          // actual replacement of the tags
 342          return array(str_ireplace($patterns, $replacement, $this->data->singletemplate), $files);
 343      }
 344  
 345      /**
 346       * Given the fields being exported, and the single record,
 347       * work out which export format(s) we can use
 348       *
 349       * @param array $fields array of field objects
 350       * @param object $record The data record object
 351       *
 352       * @uses PORTFOLIO_FORMAT_PLAINHTML
 353       * @uses PORTFOLIO_FORMAT_RICHHTML
 354       *
 355       * @return array of PORTFOLIO_XX constants
 356       */
 357      public static function formats($fields, $record) {
 358          $formats = array(PORTFOLIO_FORMAT_PLAINHTML);
 359          $includedfiles = array();
 360          foreach ($fields as $singlefield) {
 361              if (is_callable(array($singlefield, 'get_file'))) {
 362                  if ($file = $singlefield->get_file($record->id)) {
 363                      $includedfiles[] = $file;
 364                  }
 365              }
 366          }
 367          if (count($includedfiles) == 1 && count($fields) == 1) {
 368              $formats = array(portfolio_format_from_mimetype($includedfiles[0]->get_mimetype()));
 369          } else if (count($includedfiles) > 0) {
 370              $formats = array(PORTFOLIO_FORMAT_RICHHTML);
 371          }
 372          return array($formats, $includedfiles);
 373      }
 374  
 375      public static function has_files($data) {
 376          global $DB;
 377          $fieldrecords = $DB->get_records('data_fields', array('dataid' => $data->id), 'id');
 378          // populate objets for this databases fields
 379          foreach ($fieldrecords as $fieldrecord) {
 380              $field = data_get_field($fieldrecord, $data);
 381              if (is_callable(array($field, 'get_file'))) {
 382                  return true;
 383              }
 384          }
 385          return false;
 386      }
 387  
 388      /**
 389       * base supported formats before we know anything about the export
 390       */
 391      public static function base_supported_formats() {
 392          return array(PORTFOLIO_FORMAT_RICHHTML, PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_LEAP2A);
 393      }
 394  
 395      public function has_export_config() {
 396          // if we're exporting more than just a single entry,
 397          // and we have the capability to export all entries,
 398          // then ask whether we want just our own, or all of them
 399          return (empty($this->recordid) // multi-entry export
 400              && $this->minecount > 0    // some of them are mine
 401              && $this->minecount != count($this->records) // not all of them are mine
 402              && has_capability('mod/data:exportallentries', context_module::instance($this->cm->id))); // they actually have a choice in the matter
 403      }
 404  
 405      public function export_config_form(&$mform, $instance) {
 406          if (!$this->has_export_config()) {
 407              return;
 408          }
 409          $mform->addElement('selectyesno', 'mineonly', get_string('exportownentries', 'data', (object)array('mine' => $this->minecount, 'all' => count($this->records))));
 410          $mform->setDefault('mineonly', 1);
 411      }
 412  
 413      public function get_allowed_export_config() {
 414          return array('mineonly');
 415      }
 416  }
 417  
 418  
 419  /**
 420   * Class representing the virtual node with all itemids in the file browser
 421   *
 422   * @category  files
 423   * @copyright 2012 David Mudrak <david@moodle.com>
 424   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 425   */
 426  class data_file_info_container extends file_info {
 427      /** @var file_browser */
 428      protected $browser;
 429      /** @var stdClass */
 430      protected $course;
 431      /** @var stdClass */
 432      protected $cm;
 433      /** @var string */
 434      protected $component;
 435      /** @var stdClass */
 436      protected $context;
 437      /** @var array */
 438      protected $areas;
 439      /** @var string */
 440      protected $filearea;
 441  
 442      /**
 443       * Constructor (in case you did not realize it ;-)
 444       *
 445       * @param file_browser $browser
 446       * @param stdClass $course
 447       * @param stdClass $cm
 448       * @param stdClass $context
 449       * @param array $areas
 450       * @param string $filearea
 451       */
 452      public function __construct($browser, $course, $cm, $context, $areas, $filearea) {
 453          parent::__construct($browser, $context);
 454          $this->browser = $browser;
 455          $this->course = $course;
 456          $this->cm = $cm;
 457          $this->component = 'mod_data';
 458          $this->context = $context;
 459          $this->areas = $areas;
 460          $this->filearea = $filearea;
 461      }
 462  
 463      /**
 464       * @return array with keys contextid, filearea, itemid, filepath and filename
 465       */
 466      public function get_params() {
 467          return array(
 468              'contextid' => $this->context->id,
 469              'component' => $this->component,
 470              'filearea' => $this->filearea,
 471              'itemid' => null,
 472              'filepath' => null,
 473              'filename' => null,
 474          );
 475      }
 476  
 477      /**
 478       * Can new files or directories be added via the file browser
 479       *
 480       * @return bool
 481       */
 482      public function is_writable() {
 483          return false;
 484      }
 485  
 486      /**
 487       * Should this node be considered as a folder in the file browser
 488       *
 489       * @return bool
 490       */
 491      public function is_directory() {
 492          return true;
 493      }
 494  
 495      /**
 496       * Returns localised visible name of this node
 497       *
 498       * @return string
 499       */
 500      public function get_visible_name() {
 501          return $this->areas[$this->filearea];
 502      }
 503  
 504      /**
 505       * Returns list of children nodes
 506       *
 507       * @return array of file_info instances
 508       */
 509      public function get_children() {
 510          return $this->get_filtered_children('*', false, true);
 511      }
 512  
 513      /**
 514       * Help function to return files matching extensions or their count
 515       *
 516       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 517       * @param bool|int $countonly if false returns the children, if an int returns just the
 518       *    count of children but stops counting when $countonly number of children is reached
 519       * @param bool $returnemptyfolders if true returns items that don't have matching files inside
 520       * @return array|int array of file_info instances or the count
 521       */
 522      private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
 523          global $DB;
 524          $params = array('contextid' => $this->context->id,
 525              'component' => $this->component,
 526              'filearea' => $this->filearea);
 527          $sql = 'SELECT DISTINCT itemid
 528                      FROM {files}
 529                      WHERE contextid = :contextid
 530                      AND component = :component
 531                      AND filearea = :filearea';
 532          if (!$returnemptyfolders) {
 533              $sql .= ' AND filename <> :emptyfilename';
 534              $params['emptyfilename'] = '.';
 535          }
 536          list($sql2, $params2) = $this->build_search_files_sql($extensions);
 537          $sql .= ' '.$sql2;
 538          $params = array_merge($params, $params2);
 539          if ($countonly === false) {
 540              $sql .= ' ORDER BY itemid DESC';
 541          }
 542  
 543          $rs = $DB->get_recordset_sql($sql, $params);
 544          $children = array();
 545          foreach ($rs as $record) {
 546              if ($child = $this->browser->get_file_info($this->context, 'mod_data', $this->filearea, $record->itemid)) {
 547                  $children[] = $child;
 548              }
 549              if ($countonly !== false && count($children) >= $countonly) {
 550                  break;
 551              }
 552          }
 553          $rs->close();
 554          if ($countonly !== false) {
 555              return count($children);
 556          }
 557          return $children;
 558      }
 559  
 560      /**
 561       * Returns list of children which are either files matching the specified extensions
 562       * or folders that contain at least one such file.
 563       *
 564       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 565       * @return array of file_info instances
 566       */
 567      public function get_non_empty_children($extensions = '*') {
 568          return $this->get_filtered_children($extensions, false);
 569      }
 570  
 571      /**
 572       * Returns the number of children which are either files matching the specified extensions
 573       * or folders containing at least one such file.
 574       *
 575       * @param string|array $extensions, for example '*' or array('.gif','.jpg')
 576       * @param int $limit stop counting after at least $limit non-empty children are found
 577       * @return int
 578       */
 579      public function count_non_empty_children($extensions = '*', $limit = 1) {
 580          return $this->get_filtered_children($extensions, $limit);
 581      }
 582  
 583      /**
 584       * Returns parent file_info instance
 585       *
 586       * @return file_info or null for root
 587       */
 588      public function get_parent() {
 589          return $this->browser->get_file_info($this->context);
 590      }
 591  }
 592  
 593  /**
 594   * This creates new calendar events given as timeavailablefrom and timeclose by $data.
 595   *
 596   * @param stdClass $data
 597   * @return void
 598   */
 599  function data_set_events($data) {
 600      global $DB, $CFG;
 601  
 602      require_once($CFG->dirroot.'/calendar/lib.php');
 603  
 604      // Get CMID if not sent as part of $data.
 605      if (!isset($data->coursemodule)) {
 606          $cm = get_coursemodule_from_instance('data', $data->id, $data->course);
 607          $data->coursemodule = $cm->id;
 608      }
 609      // Data start calendar events.
 610      $event = new stdClass();
 611      $event->eventtype = DATA_EVENT_TYPE_OPEN;
 612      // The DATA_EVENT_TYPE_OPEN event should only be an action event if no close time was specified.
 613      $event->type = empty($data->timeavailableto) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD;
 614      if ($event->id = $DB->get_field('event', 'id',
 615              array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) {
 616          if ($data->timeavailablefrom > 0) {
 617              // Calendar event exists so update it.
 618              $event->name         = get_string('calendarstart', 'data', $data->name);
 619              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 620              $event->format       = FORMAT_HTML;
 621              $event->timestart    = $data->timeavailablefrom;
 622              $event->timesort     = $data->timeavailablefrom;
 623              $event->visible      = instance_is_visible('data', $data);
 624              $event->timeduration = 0;
 625              $calendarevent = calendar_event::load($event->id);
 626              $calendarevent->update($event, false);
 627          } else {
 628              // Calendar event is on longer needed.
 629              $calendarevent = calendar_event::load($event->id);
 630              $calendarevent->delete();
 631          }
 632      } else {
 633          // Event doesn't exist so create one.
 634          if (isset($data->timeavailablefrom) && $data->timeavailablefrom > 0) {
 635              $event->name         = get_string('calendarstart', 'data', $data->name);
 636              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 637              $event->format       = FORMAT_HTML;
 638              $event->courseid     = $data->course;
 639              $event->groupid      = 0;
 640              $event->userid       = 0;
 641              $event->modulename   = 'data';
 642              $event->instance     = $data->id;
 643              $event->timestart    = $data->timeavailablefrom;
 644              $event->timesort     = $data->timeavailablefrom;
 645              $event->visible      = instance_is_visible('data', $data);
 646              $event->timeduration = 0;
 647              calendar_event::create($event, false);
 648          }
 649      }
 650  
 651      // Data end calendar events.
 652      $event = new stdClass();
 653      $event->type = CALENDAR_EVENT_TYPE_ACTION;
 654      $event->eventtype = DATA_EVENT_TYPE_CLOSE;
 655      if ($event->id = $DB->get_field('event', 'id',
 656              array('modulename' => 'data', 'instance' => $data->id, 'eventtype' => $event->eventtype))) {
 657          if ($data->timeavailableto > 0) {
 658              // Calendar event exists so update it.
 659              $event->name         = get_string('calendarend', 'data', $data->name);
 660              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 661              $event->format       = FORMAT_HTML;
 662              $event->timestart    = $data->timeavailableto;
 663              $event->timesort     = $data->timeavailableto;
 664              $event->visible      = instance_is_visible('data', $data);
 665              $event->timeduration = 0;
 666              $calendarevent = calendar_event::load($event->id);
 667              $calendarevent->update($event, false);
 668          } else {
 669              // Calendar event is on longer needed.
 670              $calendarevent = calendar_event::load($event->id);
 671              $calendarevent->delete();
 672          }
 673      } else {
 674          // Event doesn't exist so create one.
 675          if (isset($data->timeavailableto) && $data->timeavailableto > 0) {
 676              $event->name         = get_string('calendarend', 'data', $data->name);
 677              $event->description  = format_module_intro('data', $data, $data->coursemodule, false);
 678              $event->format       = FORMAT_HTML;
 679              $event->courseid     = $data->course;
 680              $event->groupid      = 0;
 681              $event->userid       = 0;
 682              $event->modulename   = 'data';
 683              $event->instance     = $data->id;
 684              $event->timestart    = $data->timeavailableto;
 685              $event->timesort     = $data->timeavailableto;
 686              $event->visible      = instance_is_visible('data', $data);
 687              $event->timeduration = 0;
 688              calendar_event::create($event, false);
 689          }
 690      }
 691  }
 692  
 693  /**
 694   * Check if a database is available for the current user.
 695   *
 696   * @param  stdClass  $data            database record
 697   * @param  boolean $canmanageentries  optional, if the user can manage entries
 698   * @param  stdClass  $context         Module context, required if $canmanageentries is not set
 699   * @return array                      status (available or not and possible warnings)
 700   * @since  Moodle 3.3
 701   */
 702  function data_get_time_availability_status($data, $canmanageentries = null, $context = null) {
 703      $open = true;
 704      $closed = false;
 705      $warnings = array();
 706  
 707      if ($canmanageentries === null) {
 708          $canmanageentries = has_capability('mod/data:manageentries', $context);
 709      }
 710  
 711      if (!$canmanageentries) {
 712          $timenow = time();
 713  
 714          if (!empty($data->timeavailablefrom) and $data->timeavailablefrom > $timenow) {
 715              $open = false;
 716          }
 717          if (!empty($data->timeavailableto) and $timenow > $data->timeavailableto) {
 718              $closed = true;
 719          }
 720  
 721          if (!$open or $closed) {
 722              if (!$open) {
 723                  $warnings['notopenyet'] = userdate($data->timeavailablefrom);
 724              }
 725              if ($closed) {
 726                  $warnings['expired'] = userdate($data->timeavailableto);
 727              }
 728              return array(false, $warnings);
 729          }
 730      }
 731  
 732      // Database is available.
 733      return array(true, $warnings);
 734  }
 735  
 736  /**
 737   * Requires a database to be available for the current user.
 738   *
 739   * @param  stdClass  $data            database record
 740   * @param  boolean $canmanageentries  optional, if the user can manage entries
 741   * @param  stdClass  $context          Module context, required if $canmanageentries is not set
 742   * @throws moodle_exception
 743   * @since  Moodle 3.3
 744   */
 745  function data_require_time_available($data, $canmanageentries = null, $context = null) {
 746  
 747      list($available, $warnings) = data_get_time_availability_status($data, $canmanageentries, $context);
 748  
 749      if (!$available) {
 750          $reason = current(array_keys($warnings));
 751          throw new moodle_exception($reason, 'data', '', $warnings[$reason]);
 752      }
 753  }
 754  
 755  /**
 756   * Return the number of entries left to add to complete the activity.
 757   *
 758   * @param  stdClass $data           database object
 759   * @param  int $numentries          the number of entries the current user has created
 760   * @param  bool $canmanageentries   whether the user can manage entries (teachers, managers)
 761   * @return int the number of entries left, 0 if no entries left or if is not required
 762   * @since  Moodle 3.3
 763   */
 764  function data_get_entries_left_to_add($data, $numentries, $canmanageentries) {
 765      if ($data->requiredentries > 0 && $numentries < $data->requiredentries && !$canmanageentries) {
 766          return $data->requiredentries - $numentries;
 767      }
 768      return 0;
 769  }
 770  
 771  /**
 772   * Return the number of entires left to add to view other users entries..
 773   *
 774   * @param  stdClass $data           database object
 775   * @param  int $numentries          the number of entries the current user has created
 776   * @param  bool $canmanageentries   whether the user can manage entries (teachers, managers)
 777   * @return int the number of entries left, 0 if no entries left or if is not required
 778   * @since  Moodle 3.3
 779   */
 780  function data_get_entries_left_to_view($data, $numentries, $canmanageentries) {
 781      if ($data->requiredentriestoview > 0 && $numentries < $data->requiredentriestoview && !$canmanageentries) {
 782          return $data->requiredentriestoview - $numentries;
 783      }
 784      return 0;
 785  }
 786  
 787  /**
 788   * Returns data records tagged with a specified tag.
 789   *
 790   * This is a callback used by the tag area mod_data/data_records to search for data records
 791   * tagged with a specific tag.
 792   *
 793   * @param core_tag_tag $tag
 794   * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
 795   *             are displayed on the page and the per-page limit may be bigger
 796   * @param int $fromctx context id where the link was displayed, may be used by callbacks
 797   *            to display items in the same context first
 798   * @param int $ctx context id where to search for records
 799   * @param bool $rec search in subcontexts as well
 800   * @param int $page 0-based number of page being displayed
 801   * @return \core_tag\output\tagindex
 802   */
 803  function mod_data_get_tagged_records($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = true, $page = 0) {
 804      global $DB, $OUTPUT, $USER;
 805      $perpage = $exclusivemode ? 20 : 5;
 806  
 807      // Build the SQL query.
 808      $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
 809      $query = "SELECT dr.id, dr.dataid, dr.approved, d.timeviewfrom, d.timeviewto, dr.groupid, d.approval, dr.userid,
 810                       d.requiredentriestoview, cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect
 811                  FROM {data_records} dr
 812                  JOIN {data} d
 813                    ON d.id = dr.dataid
 814                  JOIN {modules} m
 815                    ON m.name = 'data'
 816                  JOIN {course_modules} cm
 817                    ON cm.module = m.id AND cm.instance = d.id
 818                  JOIN {tag_instance} tt
 819                    ON dr.id = tt.itemid
 820                  JOIN {course} c
 821                    ON cm.course = c.id
 822                  JOIN {context} ctx
 823                    ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
 824                 WHERE tt.itemtype = :itemtype
 825                   AND tt.tagid = :tagid
 826                   AND tt.component = :component
 827                   AND cm.deletioninprogress = 0
 828                   AND dr.id %ITEMFILTER%
 829                   AND c.id %COURSEFILTER%";
 830  
 831      $params = array(
 832          'itemtype' => 'data_records',
 833          'tagid' => $tag->id,
 834          'component' => 'mod_data',
 835          'coursemodulecontextlevel' => CONTEXT_MODULE
 836      );
 837  
 838      if ($ctx) {
 839          $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
 840          $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
 841          $params['contextid'] = $context->id;
 842          $params['path'] = $context->path . '/%';
 843      }
 844  
 845      $query .= " ORDER BY ";
 846      if ($fromctx) {
 847          // In order-clause specify that modules from inside "fromctx" context should be returned first.
 848          $fromcontext = context::instance_by_id($fromctx);
 849          $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
 850          $params['fromcontextid'] = $fromcontext->id;
 851          $params['frompath'] = $fromcontext->path . '/%';
 852      }
 853      $query .= ' c.sortorder, cm.id, dr.id';
 854  
 855      $totalpages = $page + 1;
 856  
 857      // Use core_tag_index_builder to build and filter the list of items.
 858      $builder = new core_tag_index_builder('mod_data', 'data_records', $query, $params, $page * $perpage, $perpage + 1);
 859      $now = time();
 860      $entrycount = [];
 861      $activitygroupmode = [];
 862      $usergroups = [];
 863      $titlefields = [];
 864      while ($item = $builder->has_item_that_needs_access_check()) {
 865          context_helper::preload_from_record($item);
 866          $modinfo = get_fast_modinfo($item->courseid);
 867          $cm = $modinfo->get_cm($item->cmid);
 868          $context = \context_module::instance($cm->id);
 869          $courseid = $item->courseid;
 870  
 871          if (!$builder->can_access_course($courseid)) {
 872              $builder->set_accessible($item, false);
 873              continue;
 874          }
 875  
 876          if (!$cm->uservisible) {
 877              $builder->set_accessible($item, false);
 878              continue;
 879          }
 880  
 881          if (!has_capability('mod/data:viewentry', $context)) {
 882              $builder->set_accessible($item, false);
 883              continue;
 884          }
 885  
 886          if ($USER->id != $item->userid && (($item->timeviewfrom && $now < $item->timeviewfrom)
 887                  || ($item->timeviewto && $now > $item->timeviewto))) {
 888              $builder->set_accessible($item, false);
 889              continue;
 890          }
 891  
 892          if ($USER->id != $item->userid && $item->approval && !$item->approved) {
 893              $builder->set_accessible($item, false);
 894              continue;
 895          }
 896  
 897          if ($item->requiredentriestoview) {
 898              if (!isset($entrycount[$item->dataid])) {
 899                  $entrycount[$item->dataid] = $DB->count_records('data_records', array('dataid' => $item->dataid));
 900              }
 901              $sufficiententries = $item->requiredentriestoview > $entrycount[$item->dataid];
 902              $builder->set_accessible($item, $sufficiententries);
 903          }
 904  
 905          if (!isset($activitygroupmode[$cm->id])) {
 906              $activitygroupmode[$cm->id] = groups_get_activity_groupmode($cm);
 907          }
 908  
 909          if (!isset($usergroups[$item->groupid])) {
 910              $usergroups[$item->groupid] = groups_is_member($item->groupid, $USER->id);
 911          }
 912  
 913          if ($activitygroupmode[$cm->id] == SEPARATEGROUPS && !$usergroups[$item->groupid]) {
 914              $builder->set_accessible($item, false);
 915              continue;
 916          }
 917  
 918          $builder->set_accessible($item, true);
 919      }
 920  
 921      $items = $builder->get_items();
 922      if (count($items) > $perpage) {
 923          $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
 924          array_pop($items);
 925      }
 926  
 927      // Build the display contents.
 928      if ($items) {
 929          $tagfeed = new core_tag\output\tagfeed();
 930          foreach ($items as $item) {
 931              context_helper::preload_from_record($item);
 932              $modinfo = get_fast_modinfo($item->courseid);
 933              $cm = $modinfo->get_cm($item->cmid);
 934              $pageurl = new moodle_url('/mod/data/view.php', array(
 935                      'rid' => $item->id,
 936                      'd' => $item->dataid
 937              ));
 938  
 939              if (!isset($titlefields[$item->dataid])) {
 940                  $titlefields[$item->dataid] = data_get_tag_title_field($item->dataid);
 941              }
 942  
 943              $pagename = data_get_tag_title_for_entry($titlefields[$item->dataid], $item);
 944              $pagename = html_writer::link($pageurl, $pagename);
 945              $courseurl = course_get_url($item->courseid, $cm->sectionnum);
 946              $cmname = html_writer::link($cm->url, $cm->get_formatted_name());
 947              $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
 948              $coursename = html_writer::link($courseurl, $coursename);
 949              $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
 950              $tagfeed->add($icon, $pagename, $cmname . '<br>' . $coursename);
 951          }
 952          $content = $OUTPUT->render_from_template('core_tag/tagfeed', $tagfeed->export_for_template($OUTPUT));
 953  
 954          return new core_tag\output\tagindex($tag, 'mod_data', 'data_records', $content, $exclusivemode,
 955              $fromctx, $ctx, $rec, $page, $totalpages);
 956      }
 957  }
 958  
 959  /**
 960   * Get the title of a field to show when displaying tag results.
 961   *
 962   * @param int $dataid The id of the data field
 963   * @return stdClass The field data from the 'data_fields' table as well as it's priority
 964   */
 965  function data_get_tag_title_field($dataid) {
 966      global $DB, $CFG;
 967  
 968      $validfieldtypes = array('text', 'textarea', 'menu', 'radiobutton', 'checkbox', 'multimenu', 'url');
 969      $fields = $DB->get_records('data_fields', ['dataid' => $dataid]);
 970      $template = $DB->get_field('data', 'addtemplate', ['id' => $dataid]);
 971      if (empty($template)) {
 972          $data = $DB->get_record('data', ['id' => $dataid]);
 973          $template = data_generate_default_template($data, 'addtemplate', 0, false, false);
 974      }
 975  
 976      $filteredfields = [];
 977  
 978      foreach ($fields as $field) {
 979          if (!in_array($field->type, $validfieldtypes)) {
 980              continue;
 981          }
 982          $field->addtemplateposition = strpos($template, '[['.$field->name.']]');
 983          if ($field->addtemplateposition === false) {
 984              continue;
 985          }
 986          $filepath = $CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php';
 987          if (!file_exists($filepath)) {
 988              continue;
 989          }
 990          require_once($filepath);
 991          $classname = 'data_field_' . $field->type;
 992          $field->priority = $classname::get_priority();
 993          $filteredfields[] = $field;
 994      }
 995  
 996      $sort = function($record1, $record2) {
 997          // If a content's fieldtype is compulsory in the database than it would have priority than any other non-compulsory content.
 998          if (($record1->required && $record2->required) || (!$record1->required && !$record2->required)) {
 999              if ($record1->priority === $record2->priority) {
1000                  return $record1->id < $record2->id ? 1 : -1;
1001              }
1002  
1003              return $record1->priority < $record2->priority ? -1 : 1;
1004          } else if ($record1->required && !$record2->required) {
1005              return 1;
1006          } else {
1007              return -1;
1008          }
1009      };
1010  
1011      usort($filteredfields, $sort);
1012  
1013      return array_shift($filteredfields);
1014  }
1015  
1016  /**
1017   * Get the title of an entry to show when displaying tag results.
1018   *
1019   * @param stdClass $field The field from the 'data_fields' table
1020   * @param stdClass $entry The entry from the 'data_records' table
1021   * @return string|null It will return the title of the entry or null if the field type is not available.
1022   */
1023  function data_get_tag_title_for_entry($field, $entry) {
1024      global $CFG, $DB;
1025  
1026      if (!isset($field->type)) {
1027          return null;
1028      }
1029      $filepath = $CFG->dirroot . '/mod/data/field/' . $field->type . '/field.class.php';
1030      if (!file_exists($filepath)) {
1031          return null;
1032      }
1033      require_once($filepath);
1034  
1035      $classname = 'data_field_' . $field->type;
1036      $sql = "SELECT dc.*
1037                FROM {data_content} dc
1038          INNER JOIN {data_fields} df
1039                  ON dc.fieldid = df.id
1040               WHERE df.id = :fieldid
1041                 AND dc.recordid = :recordid";
1042      $fieldcontents = $DB->get_record_sql($sql, array('recordid' => $entry->id, 'fieldid' => $field->id));
1043  
1044      return $classname::get_content_value($fieldcontents);
1045  }
1046  
1047  /**
1048   * Search entries in a database.
1049   *
1050   * @param  stdClass  $data         database object
1051   * @param  stdClass  $cm           course module object
1052   * @param  stdClass  $context      context object
1053   * @param  stdClass  $mode         in which mode we are viewing the database (list, single)
1054   * @param  int  $currentgroup      the current group being used
1055   * @param  str  $search            search for this text in the entry data
1056   * @param  str  $sort              the field to sort by
1057   * @param  str  $order             the order to use when sorting
1058   * @param  int $page               for pagination, the current page
1059   * @param  int $perpage            entries per page
1060   * @param  bool  $advanced         whether we are using or not advanced search
1061   * @param  array  $searcharray     when using advanced search, the advanced data to use
1062   * @param  stdClass  $record       if we jsut want this record after doing all the access checks
1063   * @return array the entries found among other data related to the search
1064   * @since  Moodle 3.3
1065   */
1066  function data_search_entries($data, $cm, $context, $mode, $currentgroup, $search = '', $sort = null, $order = null, $page = 0,
1067          $perpage = 0, $advanced = null, $searcharray = null, $record = null) {
1068      global $DB, $USER;
1069  
1070      if ($sort === null) {
1071          $sort = $data->defaultsort;
1072      }
1073      if ($order === null) {
1074          $order = ($data->defaultsortdir == 0) ? 'ASC' : 'DESC';
1075      }
1076      if ($searcharray === null) {
1077          $searcharray = array();
1078      }
1079  
1080      if (core_text::strlen($search) < 2) {
1081          $search = '';
1082      }
1083  
1084      $approvecap = has_capability('mod/data:approve', $context);
1085      $canmanageentries = has_capability('mod/data:manageentries', $context);
1086  
1087      // If a student is not part of a group and seperate groups is enabled, we don't
1088      // want them seeing all records.
1089      $groupmode = groups_get_activity_groupmode($cm);
1090      if ($currentgroup == 0 && $groupmode == 1 && !$canmanageentries) {
1091          $canviewallrecords = false;
1092      } else {
1093          $canviewallrecords = true;
1094      }
1095  
1096      $numentries = data_numentries($data);
1097      $requiredentriesallowed = true;
1098      if (data_get_entries_left_to_view($data, $numentries, $canmanageentries)) {
1099          $requiredentriesallowed = false;
1100      }
1101  
1102      // Initialise the first group of params for advanced searches.
1103      $initialparams   = array();
1104      $params = array(); // Named params array.
1105  
1106      // Setup group and approve restrictions.
1107      if (!$approvecap && $data->approval) {
1108          if (isloggedin()) {
1109              $approveselect = ' AND (r.approved=1 OR r.userid=:myid1) ';
1110              $params['myid1'] = $USER->id;
1111              $initialparams['myid1'] = $params['myid1'];
1112          } else {
1113              $approveselect = ' AND r.approved=1 ';
1114          }
1115      } else {
1116          $approveselect = ' ';
1117      }
1118  
1119      if ($currentgroup) {
1120          $groupselect = " AND (r.groupid = :currentgroup OR r.groupid = 0)";
1121          $params['currentgroup'] = $currentgroup;
1122          $initialparams['currentgroup'] = $params['currentgroup'];
1123      } else {
1124          if ($canviewallrecords) {
1125              $groupselect = ' ';
1126          } else {
1127              // If separate groups are enabled and the user isn't in a group or
1128              // a teacher, manager, admin etc, then just show them entries for 'All participants'.
1129              $groupselect = " AND r.groupid = 0";
1130          }
1131      }
1132  
1133      // Init some variables to be used by advanced search.
1134      $advsearchselect = '';
1135      $advwhere        = '';
1136      $advtables       = '';
1137      $advparams       = array();
1138      // This is used for the initial reduction of advanced search results with required entries.
1139      $entrysql        = '';
1140      $userfieldsapi = \core_user\fields::for_userpic()->excluding('id');
1141      $namefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
1142  
1143      // Find the field we are sorting on.
1144      if ($sort <= 0 || !($sortfield = data_get_field_from_id($sort, $data))) {
1145  
1146          switch ($sort) {
1147              case DATA_LASTNAME:
1148                  $ordering = "u.lastname $order, u.firstname $order";
1149                  break;
1150              case DATA_FIRSTNAME:
1151                  $ordering = "u.firstname $order, u.lastname $order";
1152                  break;
1153              case DATA_APPROVED:
1154                  $ordering = "r.approved $order, r.timecreated $order";
1155                  break;
1156              case DATA_TIMEMODIFIED:
1157                  $ordering = "r.timemodified $order";
1158                  break;
1159              case DATA_TIMEADDED:
1160              default:
1161                  $sort     = 0;
1162                  $ordering = "r.timecreated $order";
1163          }
1164  
1165          $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields;
1166          $count = ' COUNT(DISTINCT c.recordid) ';
1167          $tables = '{data_content} c,{data_records} r, {user} u ';
1168          $where = 'WHERE c.recordid = r.id
1169                       AND r.dataid = :dataid
1170                       AND r.userid = u.id ';
1171          $params['dataid'] = $data->id;
1172          $sortorder = " ORDER BY $ordering, r.id $order";
1173          $searchselect = '';
1174  
1175          // If requiredentries is not reached, only show current user's entries.
1176          if (!$requiredentriesallowed) {
1177              $where .= ' AND u.id = :myid2 ';
1178              $entrysql = ' AND r.userid = :myid3 ';
1179              $params['myid2'] = $USER->id;
1180              $initialparams['myid3'] = $params['myid2'];
1181          }
1182  
1183          if ($search) {
1184              $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)."
1185                                OR ".$DB->sql_like('u.firstname', ':search2', false)."
1186                                OR ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
1187              $params['search1'] = "%$search%";
1188              $params['search2'] = "%$search%";
1189              $params['search3'] = "%$search%";
1190          } else {
1191              $searchselect = ' ';
1192          }
1193  
1194      } else {
1195  
1196          $sortcontent = $DB->sql_compare_text('s.' . $sortfield->get_sort_field());
1197          $sortcontentfull = $sortfield->get_sort_sql($sortcontent);
1198  
1199          $what = ' DISTINCT r.id, r.approved, r.timecreated, r.timemodified, r.userid, r.groupid, r.dataid, ' . $namefields . ',
1200                  ' . $sortcontentfull . ' AS sortorder ';
1201          $count = ' COUNT(DISTINCT c.recordid) ';
1202          $tables = '{data_content} c, {data_records} r, {user} u ';
1203          $where = 'WHERE c.recordid = r.id
1204                       AND r.dataid = :dataid
1205                       AND r.userid = u.id ';
1206          if (!$advanced) {
1207              $where .= 'AND s.fieldid = :sort AND s.recordid = r.id';
1208              $tables .= ',{data_content} s ';
1209          }
1210          $params['dataid'] = $data->id;
1211          $params['sort'] = $sort;
1212          $sortorder = ' ORDER BY sortorder '.$order.' , r.id ASC ';
1213          $searchselect = '';
1214  
1215          // If requiredentries is not reached, only show current user's entries.
1216          if (!$requiredentriesallowed) {
1217              $where .= ' AND u.id = :myid2';
1218              $entrysql = ' AND r.userid = :myid3';
1219              $params['myid2'] = $USER->id;
1220              $initialparams['myid3'] = $params['myid2'];
1221          }
1222  
1223          if ($search) {
1224              $searchselect = " AND (".$DB->sql_like('c.content', ':search1', false)." OR
1225                  ".$DB->sql_like('u.firstname', ':search2', false)." OR
1226                  ".$DB->sql_like('u.lastname', ':search3', false)." ) ";
1227              $params['search1'] = "%$search%";
1228              $params['search2'] = "%$search%";
1229              $params['search3'] = "%$search%";
1230          } else {
1231              $searchselect = ' ';
1232          }
1233      }
1234  
1235      // To actually fetch the records.
1236  
1237      $fromsql    = "FROM $tables $advtables $where $advwhere $groupselect $approveselect $searchselect $advsearchselect";
1238      $allparams  = array_merge($params, $advparams);
1239  
1240      // Provide initial sql statements and parameters to reduce the number of total records.
1241      $initialselect = $groupselect . $approveselect . $entrysql;
1242  
1243      $recordids = data_get_all_recordids($data->id, $initialselect, $initialparams);
1244      $newrecordids = data_get_advance_search_ids($recordids, $searcharray, $data->id);
1245      $selectdata = $where . $groupselect . $approveselect;
1246  
1247      if (!empty($advanced)) {
1248          $advancedsearchsql = data_get_advanced_search_sql($sort, $data, $newrecordids, $selectdata, $sortorder);
1249          $sqlselect = $advancedsearchsql['sql'];
1250          $allparams = array_merge($allparams, $advancedsearchsql['params']);
1251          $totalcount = count($newrecordids);
1252      } else {
1253          $sqlselect  = "SELECT $what $fromsql $sortorder";
1254          $sqlcountselect  = "SELECT $count $fromsql";
1255          $totalcount = $DB->count_records_sql($sqlcountselect, $allparams);
1256      }
1257  
1258      // Work out the paging numbers and counts.
1259      if (empty($searchselect) && empty($advsearchselect)) {
1260          $maxcount = $totalcount;
1261      } else {
1262          $maxcount = count($recordids);
1263      }
1264  
1265      if ($record) {     // We need to just show one, so where is it in context?
1266          $nowperpage = 1;
1267          $mode = 'single';
1268          $page = 0;
1269          // TODO MDL-33797 - Reduce this or consider redesigning the paging system.
1270          if ($allrecordids = $DB->get_fieldset_sql($sqlselect, $allparams)) {
1271              $page = (int)array_search($record->id, $allrecordids);
1272              unset($allrecordids);
1273          }
1274      } else if ($mode == 'single') {  // We rely on ambient $page settings
1275          $nowperpage = 1;
1276  
1277      } else {
1278          $nowperpage = $perpage;
1279      }
1280  
1281      // Get the actual records.
1282      if (!$records = $DB->get_records_sql($sqlselect, $allparams, $page * $nowperpage, $nowperpage)) {
1283          // Nothing to show!
1284          if ($record) {         // Something was requested so try to show that at least (bug 5132)
1285              if (data_can_view_record($data, $record, $currentgroup, $canmanageentries)) {
1286                  // OK, we can show this one
1287                  $records = array($record->id => $record);
1288                  $totalcount = 1;
1289              }
1290          }
1291  
1292      }
1293  
1294      return [$records, $maxcount, $totalcount, $page, $nowperpage, $sort, $mode];
1295  }
1296  
1297  /**
1298   * Check if the current user can view the given record.
1299   *
1300   * @param  stdClass $data           database record
1301   * @param  stdClass $record         the record (entry) to check
1302   * @param  int $currentgroup        current group
1303   * @param  bool $canmanageentries   if the user can manage entries
1304   * @return bool true if the user can view the entry
1305   * @since  Moodle 3.3
1306   */
1307  function data_can_view_record($data, $record, $currentgroup, $canmanageentries) {
1308      global $USER;
1309  
1310      if ($canmanageentries || empty($data->approval) ||
1311               $record->approved || (isloggedin() && $record->userid == $USER->id)) {
1312  
1313          if (!$currentgroup || $record->groupid == $currentgroup || $record->groupid == 0) {
1314              return true;
1315          }
1316      }
1317      return false;
1318  }
1319  
1320  /**
1321   * Return all the field instances for a given database.
1322   *
1323   * @param  stdClass $data database object
1324   * @return array field instances
1325   * @since  Moodle 3.3
1326   */
1327  function data_get_field_instances($data) {
1328      global $DB;
1329  
1330      $instances = [];
1331      if ($fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id')) {
1332          foreach ($fields as $field) {
1333              $instances[] = data_get_field($field, $data);
1334          }
1335      }
1336      return $instances;
1337  }
1338  
1339  /**
1340   * Build the search array.
1341   *
1342   * @param  stdClass $data      the database object
1343   * @param  bool $paging        if paging is being used
1344   * @param  array $searcharray  the current search array (saved by session)
1345   * @param  array $defaults     default values for the searchable fields
1346   * @param  str $fn             the first name to search (optional)
1347   * @param  str $ln             the last name to search (optional)
1348   * @return array               the search array and plain search build based on the different elements
1349   * @since  Moodle 3.3
1350   */
1351  function data_build_search_array($data, $paging, $searcharray, $defaults = null, $fn = '', $ln = '') {
1352      global $DB;
1353  
1354      $search = '';
1355      $vals = array();
1356      $fields = $DB->get_records('data_fields', array('dataid' => $data->id));
1357  
1358      if (!empty($fields)) {
1359          foreach ($fields as $field) {
1360              $searchfield = data_get_field_from_id($field->id, $data);
1361              // Get field data to build search sql with.  If paging is false, get from user.
1362              // If paging is true, get data from $searcharray which is obtained from the $SESSION (see line 116).
1363              if (!$paging && $searchfield->type != 'unknown') {
1364                  $val = $searchfield->parse_search_field($defaults);
1365              } else {
1366                  // Set value from session if there is a value @ the required index.
1367                  if (isset($searcharray[$field->id])) {
1368                      $val = $searcharray[$field->id]->data;
1369                  } else { // If there is not an entry @ the required index, set value to blank.
1370                      $val = '';
1371                  }
1372              }
1373              if (!empty($val)) {
1374                  $searcharray[$field->id] = new stdClass();
1375                  list($searcharray[$field->id]->sql, $searcharray[$field->id]->params) = $searchfield->generate_sql('c'.$field->id, $val);
1376                  $searcharray[$field->id]->data = $val;
1377                  $vals[] = $val;
1378              } else {
1379                  // Clear it out.
1380                  unset($searcharray[$field->id]);
1381              }
1382          }
1383      }
1384  
1385      $rawtagnames = optional_param_array('tags', false, PARAM_TAGLIST);
1386  
1387      if ($rawtagnames) {
1388          $searcharray[DATA_TAGS] = new stdClass();
1389          $searcharray[DATA_TAGS]->params = [];
1390          $searcharray[DATA_TAGS]->rawtagnames = $rawtagnames;
1391          $searcharray[DATA_TAGS]->sql = '';
1392      } else {
1393          unset($searcharray[DATA_TAGS]);
1394      }
1395  
1396      if (!$paging) {
1397          // Name searching.
1398          $fn = optional_param('u_fn', $fn, PARAM_NOTAGS);
1399          $ln = optional_param('u_ln', $ln, PARAM_NOTAGS);
1400      } else {
1401          $fn = isset($searcharray[DATA_FIRSTNAME]) ? $searcharray[DATA_FIRSTNAME]->data : '';
1402          $ln = isset($searcharray[DATA_LASTNAME]) ? $searcharray[DATA_LASTNAME]->data : '';
1403      }
1404      if (!empty($fn)) {
1405          $searcharray[DATA_FIRSTNAME] = new stdClass();
1406          $searcharray[DATA_FIRSTNAME]->sql    = '';
1407          $searcharray[DATA_FIRSTNAME]->params = array();
1408          $searcharray[DATA_FIRSTNAME]->field  = 'u.firstname';
1409          $searcharray[DATA_FIRSTNAME]->data   = $fn;
1410          $vals[] = $fn;
1411      } else {
1412          unset($searcharray[DATA_FIRSTNAME]);
1413      }
1414      if (!empty($ln)) {
1415          $searcharray[DATA_LASTNAME] = new stdClass();
1416          $searcharray[DATA_LASTNAME]->sql     = '';
1417          $searcharray[DATA_LASTNAME]->params = array();
1418          $searcharray[DATA_LASTNAME]->field   = 'u.lastname';
1419          $searcharray[DATA_LASTNAME]->data    = $ln;
1420          $vals[] = $ln;
1421      } else {
1422          unset($searcharray[DATA_LASTNAME]);
1423      }
1424  
1425      // In case we want to switch to simple search later - there might be multiple values there ;-).
1426      if ($vals) {
1427          $val = reset($vals);
1428          if (is_string($val)) {
1429              $search = $val;
1430          }
1431      }
1432      return [$searcharray, $search];
1433  }
1434  
1435  /**
1436   * Approves or unapproves an entry.
1437   *
1438   * @param  int $entryid the entry to approve or unapprove.
1439   * @param  bool $approve Whether to approve or unapprove (true for approve false otherwise).
1440   * @since  Moodle 3.3
1441   */
1442  function data_approve_entry($entryid, $approve) {
1443      global $DB;
1444  
1445      $newrecord = new stdClass();
1446      $newrecord->id = $entryid;
1447      $newrecord->approved = $approve ? 1 : 0;
1448      $DB->update_record('data_records', $newrecord);
1449  }
1450  
1451  /**
1452   * Populate the field contents of a new record with the submitted data.
1453   * An event has been previously triggered upon the creation of the new record in data_add_record().
1454   *
1455   * @param  stdClass $data           database object
1456   * @param  stdClass $context        context object
1457   * @param  int $recordid            the new record id
1458   * @param  array $fields            list of fields of the database
1459   * @param  stdClass $datarecord     the submitted data
1460   * @param  stdClass $processeddata  pre-processed submitted fields
1461   * @since  Moodle 3.3
1462   */
1463  function data_add_fields_contents_to_new_record($data, $context, $recordid, $fields, $datarecord, $processeddata) {
1464      global $DB;
1465  
1466      // Insert a whole lot of empty records to make sure we have them.
1467      $records = array();
1468      foreach ($fields as $field) {
1469          $content = new stdClass();
1470          $content->recordid = $recordid;
1471          $content->fieldid = $field->id;
1472          $records[] = $content;
1473      }
1474  
1475      // Bulk insert the records now. Some records may have no data but all must exist.
1476      $DB->insert_records('data_content', $records);
1477  
1478      // Add all provided content.
1479      foreach ($processeddata->fields as $fieldname => $field) {
1480          $field->update_content($recordid, $datarecord->$fieldname, $fieldname);
1481      }
1482  }
1483  
1484  /**
1485   * Updates the fields contents of an existing record.
1486   *
1487   * @param  stdClass $data           database object
1488   * @param  stdClass $record         record to update object
1489   * @param  stdClass $context        context object
1490   * @param  stdClass $datarecord     the submitted data
1491   * @param  stdClass $processeddata  pre-processed submitted fields
1492   * @since  Moodle 3.3
1493   */
1494  function data_update_record_fields_contents($data, $record, $context, $datarecord, $processeddata) {
1495      global $DB;
1496  
1497      // Reset the approved flag after edit if the user does not have permission to approve their own entries.
1498      if (!has_capability('mod/data:approve', $context)) {
1499          $record->approved = 0;
1500      }
1501  
1502      // Update the parent record.
1503      $record->timemodified = time();
1504      $DB->update_record('data_records', $record);
1505  
1506      // Update all content.
1507      foreach ($processeddata->fields as $fieldname => $field) {
1508          $field->update_content($record->id, $datarecord->$fieldname, $fieldname);
1509      }
1510  
1511      // Trigger an event for updating this record.
1512      $event = \mod_data\event\record_updated::create(array(
1513          'objectid' => $record->id,
1514          'context' => $context,
1515          'courseid' => $data->course,
1516          'other' => array(
1517              'dataid' => $data->id
1518          )
1519      ));
1520      $event->add_record_snapshot('data', $data);
1521      $event->trigger();
1522  }