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]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * The main interface for recycle bin methods.
  19   *
  20   * @package    tool_recyclebin
  21   * @copyright  2015 University of Kent
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace tool_recyclebin;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  define('TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA', 'recyclebin_coursecat');
  30  
  31  /**
  32   * Represents a category's recyclebin.
  33   *
  34   * @package    tool_recyclebin
  35   * @copyright  2015 University of Kent
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class category_bin extends base_bin {
  39  
  40      /**
  41       * @var int The category id.
  42       */
  43      protected $_categoryid;
  44  
  45      /**
  46       * Constructor.
  47       *
  48       * @param int $categoryid The category id.
  49       */
  50      public function __construct($categoryid) {
  51          $this->_categoryid = $categoryid;
  52      }
  53  
  54      /**
  55       * Is this recyclebin enabled?
  56       *
  57       * @return bool true if enabled, false if not.
  58       */
  59      public static function is_enabled() {
  60          return get_config('tool_recyclebin', 'categorybinenable');
  61      }
  62  
  63      /**
  64       * Returns an item from the recycle bin.
  65       *
  66       * @param int $itemid Item ID to retrieve.
  67       * @return \stdClass the item.
  68       */
  69      public function get_item($itemid) {
  70          global $DB;
  71  
  72          $item = $DB->get_record('tool_recyclebin_category', array(
  73              'id' => $itemid
  74          ), '*', MUST_EXIST);
  75  
  76          $item->name = get_course_display_name_for_list($item);
  77  
  78          return $item;
  79      }
  80  
  81      /**
  82       * Returns a list of items in the recycle bin for this course.
  83       *
  84       * @return array the list of items.
  85       */
  86      public function get_items() {
  87          global $DB;
  88  
  89          $items = $DB->get_records('tool_recyclebin_category', array(
  90              'categoryid' => $this->_categoryid
  91          ));
  92  
  93          foreach ($items as $item) {
  94              $item->name = get_course_display_name_for_list($item);
  95          }
  96  
  97          return $items;
  98      }
  99  
 100      /**
 101       * Store a course in the recycle bin.
 102       *
 103       * @param \stdClass $course Course
 104       * @throws \moodle_exception
 105       */
 106      public function store_item($course) {
 107          global $CFG, $DB;
 108  
 109          require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
 110  
 111          // As far as recycle bin is using MODE_AUTOMATED, it observes the backup_auto_storage
 112          // settings (storing backups @ real location and potentially not including files).
 113          // For recycle bin we want to ensure that backup files are always stored in Moodle file
 114          // area and always contain the users' files. In order to achieve that, we hack the
 115          // setting here via $CFG->forced_plugin_settings, so it won't interfere other operations.
 116          // See MDL-65218 and MDL-35773 for more information.
 117          // This hack will be removed once recycle bin switches to use its own backup mode, with
 118          // own preferences and 100% separate from MOODLE_AUTOMATED.
 119          // TODO: Remove this as part of MDL-65228.
 120          $CFG->forced_plugin_settings['backup'] = ['backup_auto_storage' => 0, 'backup_auto_files' => 1];
 121  
 122          // Backup the course.
 123          $user = get_admin();
 124          $controller = new \backup_controller(
 125              \backup::TYPE_1COURSE,
 126              $course->id,
 127              \backup::FORMAT_MOODLE,
 128              \backup::INTERACTIVE_NO,
 129              \backup::MODE_AUTOMATED,
 130              $user->id
 131          );
 132          $controller->execute_plan();
 133  
 134          // We don't need the forced setting anymore, hence unsetting it.
 135          // TODO: Remove this as part of MDL-65228.
 136          unset($CFG->forced_plugin_settings['backup']);
 137  
 138          // Grab the result.
 139          $result = $controller->get_results();
 140          if (!isset($result['backup_destination'])) {
 141              throw new \moodle_exception('Failed to backup activity prior to deletion.');
 142          }
 143  
 144          // Have finished with the controller, let's destroy it, freeing mem and resources.
 145          $controller->destroy();
 146  
 147          // Grab the filename.
 148          $file = $result['backup_destination'];
 149          if (!$file->get_contenthash()) {
 150              throw new \moodle_exception('Failed to backup activity prior to deletion (invalid file).');
 151          }
 152  
 153          // Record the activity, get an ID.
 154          $item = new \stdClass();
 155          $item->categoryid = $course->category;
 156          $item->shortname = $course->shortname;
 157          $item->fullname = $course->fullname;
 158          $item->timecreated = time();
 159          $binid = $DB->insert_record('tool_recyclebin_category', $item);
 160  
 161          // Create the location we want to copy this file to.
 162          $filerecord = array(
 163              'contextid' => \context_coursecat::instance($course->category)->id,
 164              'component' => 'tool_recyclebin',
 165              'filearea' => TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA,
 166              'itemid' => $binid,
 167              'timemodified' => time()
 168          );
 169  
 170          // Move the file to our own special little place.
 171          $fs = get_file_storage();
 172          if (!$fs->create_file_from_storedfile($filerecord, $file)) {
 173              // Failed, cleanup first.
 174              $DB->delete_records('tool_recyclebin_category', array(
 175                  'id' => $binid
 176              ));
 177  
 178              throw new \moodle_exception("Failed to copy backup file to recyclebin.");
 179          }
 180  
 181          // Delete the old file.
 182          $file->delete();
 183  
 184          // Fire event.
 185          $event = \tool_recyclebin\event\category_bin_item_created::create(array(
 186              'objectid' => $binid,
 187              'context' => \context_coursecat::instance($course->category)
 188          ));
 189          $event->trigger();
 190      }
 191  
 192      /**
 193       * Restore an item from the recycle bin.
 194       *
 195       * @param \stdClass $item The item database record
 196       * @throws \moodle_exception
 197       */
 198      public function restore_item($item) {
 199          global $CFG, $OUTPUT, $PAGE;
 200  
 201          require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
 202          require_once($CFG->dirroot . '/course/lib.php');
 203  
 204          $user = get_admin();
 205  
 206          // Grab the course category context.
 207          $context = \context_coursecat::instance($this->_categoryid);
 208  
 209          // Get the backup file.
 210          $fs = get_file_storage();
 211          $files = $fs->get_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA, $item->id,
 212              'itemid, filepath, filename', false);
 213  
 214          if (empty($files)) {
 215              throw new \moodle_exception('Invalid recycle bin item!');
 216          }
 217  
 218          if (count($files) > 1) {
 219              throw new \moodle_exception('Too many files found!');
 220          }
 221  
 222          // Get the backup file.
 223          $file = reset($files);
 224  
 225          // Get a backup temp directory name and create it.
 226          $tempdir = \restore_controller::get_tempdir_name($context->id, $user->id);
 227          $fulltempdir = make_backup_temp_directory($tempdir);
 228  
 229          // Extract the backup to tmpdir.
 230          $fb = get_file_packer('application/vnd.moodle.backup');
 231          $fb->extract_to_pathname($file, $fulltempdir);
 232  
 233          // Build a course.
 234          $course = new \stdClass();
 235          $course->category = $this->_categoryid;
 236          $course->shortname = $item->shortname;
 237          $course->fullname = $item->fullname;
 238          $course->summary = '';
 239  
 240          // Create a new course.
 241          $course = create_course($course);
 242          if (!$course) {
 243              throw new \moodle_exception("Could not create course to restore into.");
 244          }
 245  
 246          // As far as recycle bin is using MODE_AUTOMATED, it observes the General restore settings.
 247          // For recycle bin we want to ensure that backup files are always restore the users and groups information.
 248          // In order to achieve that, we hack the setting here via $CFG->forced_plugin_settings,
 249          // so it won't interfere other operations.
 250          // See MDL-65218 and MDL-35773 for more information.
 251          // This hack will be removed once recycle bin switches to use its own backup mode, with
 252          // own preferences and 100% separate from MOODLE_AUTOMATED.
 253          // TODO: Remove this as part of MDL-65228.
 254          $CFG->forced_plugin_settings['restore'] = ['restore_general_users' => 1, 'restore_general_groups' => 1];
 255  
 256          // Define the import.
 257          $controller = new \restore_controller(
 258              $tempdir,
 259              $course->id,
 260              \backup::INTERACTIVE_NO,
 261              \backup::MODE_AUTOMATED,
 262              $user->id,
 263              \backup::TARGET_NEW_COURSE
 264          );
 265  
 266          // Prechecks.
 267          if (!$controller->execute_precheck()) {
 268              $results = $controller->get_precheck_results();
 269  
 270              // Check if errors have been found.
 271              if (!empty($results['errors'])) {
 272                  // Delete the temporary file we created.
 273                  fulldelete($fulltempdir);
 274  
 275                  // Delete the course we created.
 276                  delete_course($course, false);
 277  
 278                  echo $OUTPUT->header();
 279                  $backuprenderer = $PAGE->get_renderer('core', 'backup');
 280                  echo $backuprenderer->precheck_notices($results);
 281                  echo $OUTPUT->continue_button(new \moodle_url('/course/index.php', array('categoryid' => $this->_categoryid)));
 282                  echo $OUTPUT->footer();
 283                  exit();
 284              }
 285          }
 286  
 287          // Run the import.
 288          $controller->execute_plan();
 289  
 290          // We don't need the forced setting anymore, hence unsetting it.
 291          // TODO: Remove this as part of MDL-65228.
 292          unset($CFG->forced_plugin_settings['restore']);
 293  
 294          // Have finished with the controller, let's destroy it, freeing mem and resources.
 295          $controller->destroy();
 296  
 297          // Fire event.
 298          $event = \tool_recyclebin\event\category_bin_item_restored::create(array(
 299              'objectid' => $item->id,
 300              'context' => $context
 301          ));
 302          $event->add_record_snapshot('tool_recyclebin_category', $item);
 303          $event->trigger();
 304  
 305          // Cleanup.
 306          fulldelete($fulltempdir);
 307          $this->delete_item($item);
 308      }
 309  
 310      /**
 311       * Delete an item from the recycle bin.
 312       *
 313       * @param \stdClass $item The item database record
 314       * @throws \coding_exception
 315       */
 316      public function delete_item($item) {
 317          global $DB;
 318  
 319          // Grab the course category context.
 320          $context = \context_coursecat::instance($this->_categoryid, IGNORE_MISSING);
 321          if (!empty($context)) {
 322              // Delete the files.
 323              $fs = get_file_storage();
 324              $fs->delete_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA, $item->id);
 325          } else {
 326              // Course category has been deleted. Find records using $item->id as this is unique for coursecat recylebin.
 327              $files = $DB->get_recordset('files', [
 328                  'component' => 'tool_recyclebin',
 329                  'filearea' => TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA,
 330                  'itemid' => $item->id,
 331              ]);
 332              $fs = get_file_storage();
 333              foreach ($files as $filer) {
 334                  $file = $fs->get_file_instance($filer);
 335                  $file->delete();
 336              }
 337              $files->close();
 338          }
 339  
 340          // Delete the record.
 341          $DB->delete_records('tool_recyclebin_category', array(
 342              'id' => $item->id
 343          ));
 344  
 345          // The coursecat might have been deleted, check we have a context before triggering event.
 346          if (!$context) {
 347              return;
 348          }
 349  
 350          // Fire event.
 351          $event = \tool_recyclebin\event\category_bin_item_deleted::create(array(
 352              'objectid' => $item->id,
 353              'context' => \context_coursecat::instance($item->categoryid)
 354          ));
 355          $event->add_record_snapshot('tool_recyclebin_category', $item);
 356          $event->trigger();
 357      }
 358  
 359      /**
 360       * Can we view items in this recycle bin?
 361       *
 362       * @return bool returns true if they can view, false if not
 363       */
 364      public function can_view() {
 365          $context = \context_coursecat::instance($this->_categoryid);
 366          return has_capability('tool/recyclebin:viewitems', $context);
 367      }
 368  
 369      /**
 370       * Can we restore items in this recycle bin?
 371       *
 372       * @return bool returns true if they can restore, false if not
 373       */
 374      public function can_restore() {
 375          $context = \context_coursecat::instance($this->_categoryid);
 376          return has_capability('tool/recyclebin:restoreitems', $context);
 377      }
 378  
 379      /**
 380       * Can we delete items in this recycle bin?
 381       *
 382       * @return bool returns true if they can delete, false if not
 383       */
 384      public function can_delete() {
 385          $context = \context_coursecat::instance($this->_categoryid);
 386          return has_capability('tool/recyclebin:deleteitems', $context);
 387      }
 388  }