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.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace tiny_autosave;
  18  
  19  use stdClass;
  20  
  21  /**
  22   * Autosave Manager.
  23   *
  24   * @package   tiny_autosave
  25   * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
  26   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  class autosave_manager {
  29  
  30      /** @var int The contextid */
  31      protected $contextid;
  32  
  33      /** @var string The page hash reference */
  34      protected $pagehash;
  35  
  36      /** @var string The page instance reference */
  37      protected $pageinstance;
  38  
  39      /** @var string The elementid for this editor */
  40      protected $elementid;
  41  
  42      /** @var stdClass The user record */
  43      protected $user;
  44  
  45      /**
  46       * Constructor for the autosave manager.
  47       *
  48       * @param int $contextid The contextid of the session
  49       * @param string $pagehash The page hash
  50       * @param string $pageinstance The page instance
  51       * @param string $elementid The element id
  52       * @param null|stdClass $user The user object for the owner of the autosave
  53       */
  54      public function __construct(
  55          int $contextid,
  56          string $pagehash,
  57          string $pageinstance,
  58          string $elementid,
  59          ?stdClass $user = null
  60      ) {
  61          global $USER;
  62  
  63          $this->contextid = $contextid;
  64          $this->pagehash = $pagehash;
  65          $this->pageinstance = $pageinstance;
  66          $this->elementid = $elementid;
  67          $this->user = $user ?? $USER;
  68      }
  69  
  70      /**
  71       * Get the autosave record for this session.
  72       *
  73       * @return stdClass|null
  74       */
  75      public function get_autosave_record(): ?stdClass {
  76          global $DB;
  77  
  78          $record = $DB->get_record('tiny_autosave', [
  79              'contextid' => $this->contextid,
  80              'userid' => $this->user->id,
  81              'pagehash' => $this->pagehash,
  82              'elementid' => $this->elementid,
  83          ]);
  84  
  85          if (empty($record)) {
  86              return null;
  87          }
  88  
  89          return $record;
  90      }
  91  
  92      /**
  93       * Create an autosave record for the session.
  94       *
  95       * @param string $drafttext The draft text to save
  96       * @param null|int $draftid The draft file area if one is used
  97       * @return stdClass The autosave record
  98       */
  99      public function create_autosave_record(string $drafttext, ?int $draftid = null): stdClass {
 100          global $DB;
 101          $record = (object) [
 102              'userid' => $this->user->id,
 103              'contextid' => $this->contextid,
 104              'pagehash' => $this->pagehash,
 105              'pageinstance' => $this->pageinstance,
 106              'elementid' => $this->elementid,
 107              'drafttext' => $drafttext,
 108              'timemodified' => time(),
 109          ];
 110  
 111          if ($draftid) {
 112              $record->draftid = $draftid;
 113          }
 114  
 115          $record->id = $DB->insert_record('tiny_autosave', $record);
 116  
 117          return $record;
 118      }
 119  
 120      /**
 121       * Update the text of the autosave session.
 122       *
 123       * @param string $drafttext The text to save
 124       * @return stdClass The updated record
 125       */
 126      public function update_autosave_record(string $drafttext): stdClass {
 127          global $DB;
 128  
 129          $record = $this->get_autosave_record();
 130          if ($record) {
 131              $record->drafttext = $drafttext;
 132              $record->timemodified = time();
 133              $DB->update_record('tiny_autosave', $record);
 134  
 135              return $record;
 136          } else {
 137              return $this->create_autosave_record($drafttext);
 138          }
 139      }
 140  
 141      /**
 142       * Resume an autosave session, updating the draft file area if relevant.
 143       *
 144       * @param null|int $draftid The draft file area to update
 145       * @return stdClass The updated autosave record
 146       */
 147      public function resume_autosave_session(?int $draftid = null): stdClass {
 148          $record = $this->get_autosave_record();
 149          if (!$record) {
 150              return $this->create_autosave_record('', $draftid);
 151          }
 152  
 153          if ($this->is_autosave_stale($record)) {
 154              // If the autosave record it stale, remove it and create a new, blank, record.
 155              $this->remove_autosave_record();
 156  
 157              return $this->create_autosave_record('', $draftid);
 158          }
 159  
 160          if (empty($draftid)) {
 161              // There is no file area to handle, so just return the record without any further changes.
 162              return $record;
 163          }
 164  
 165          // This autosave is not stale, so update the draftid and move any files over to the new draft file area.
 166          return $this->update_draftid_for_record($record, $draftid);
 167      }
 168  
 169      /**
 170       * Check whether the autosave data is stale.
 171       *
 172       * Records are considered stale if either of the following conditions are true:
 173       * - The record is older than the stale period
 174       * - Any of the files in the draft area are newer than the autosave data itself
 175       *
 176       * @param stdClass $record The autosave record
 177       * @return bool Whether the record is stale
 178       */
 179      protected function is_autosave_stale(stdClass $record): bool {
 180          $timemodified = $record->timemodified;
 181          // TODO Create the UI for the stale period.
 182          $staleperiod = get_config('tiny_autosave', 'staleperiod');
 183          if (empty($staleperiod)) {
 184              $staleperiod = (4 * DAYSECS);
 185          }
 186  
 187          $stale = $timemodified < (time() - $staleperiod);
 188  
 189          if (empty($record->draftid)) {
 190              return $stale;
 191          }
 192  
 193          $fs = get_file_storage();
 194          $files = $fs->get_directory_files($record->contextid, 'user', 'draft', $record->draftid, '/', true, true);
 195  
 196          $lastfilemodified = 0;
 197          foreach ($files as $file) {
 198              if ($record->timemodified < $file->get_timemodified()) {
 199                  $stale = true;
 200                  break;
 201              }
 202          }
 203  
 204          return $stale;
 205      }
 206  
 207      /**
 208       * Move the files relating to the autosave session to a new draft file area.
 209       *
 210       * @param stdClass $record The autosave record
 211       * @param int $newdraftid The new draftid to move files to
 212       * @return stdClass The updated autosave record
 213       */
 214      protected function update_draftid_for_record(stdClass $record, int $newdraftid): stdClass {
 215          global $CFG, $DB;
 216  
 217          require_once("{$CFG->libdir}/filelib.php");
 218  
 219          // Copy all draft files from the old draft area.
 220          $usercontext = \context_user::instance($this->user->id);
 221  
 222          // This function copies all the files in one draft area, to another area (in this case it's
 223          // another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
 224          $record->drafttext = file_save_draft_area_files(
 225              $record->draftid,
 226              $usercontext->id,
 227              'user',
 228              'draft',
 229              $newdraftid,
 230              [],
 231              $record->drafttext
 232          );
 233  
 234          // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
 235          $record->drafttext = file_rewrite_pluginfile_urls(
 236              $record->drafttext,
 237              'draftfile.php',
 238              $usercontext->id,
 239              'user',
 240              'draft',
 241              $newdraftid
 242          );
 243  
 244          $record->draftid = $newdraftid;
 245          $record->pageinstance = $this->pageinstance;
 246          $record->timemodified = time();
 247  
 248          $DB->update_record('tiny_autosave', $record);
 249  
 250          return $record;
 251      }
 252  
 253      /**
 254       * Remove the autosave record.
 255       */
 256      public function remove_autosave_record(): void {
 257          global $DB;
 258  
 259          $DB->delete_records('tiny_autosave', [
 260              'contextid' => $this->contextid,
 261              'userid' => $this->user->id,
 262              'pagehash' => $this->pagehash,
 263              'elementid' => $this->elementid,
 264          ]);
 265      }
 266  
 267  }