Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 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    moodlecore
  20   * @subpackage backup-helper
  21   * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  /**
  26   * Base abstract class for all the helper classes providing various operations
  27   *
  28   * TODO: Finish phpdocs
  29   */
  30  abstract class backup_helper {
  31  
  32      /**
  33       * Given one backupid, create all the needed dirs to have one backup temp dir available
  34       */
  35      static public function check_and_create_backup_dir($backupid) {
  36          $backupiddir = make_backup_temp_directory($backupid, false);
  37          if (empty($backupiddir)) {
  38              throw new backup_helper_exception('cannot_create_backup_temp_dir');
  39          }
  40      }
  41  
  42      /**
  43       * Given one backupid, ensure its temp dir is completely empty
  44       *
  45       * If supplied, progress object should be ready to receive indeterminate
  46       * progress reports.
  47       *
  48       * @param string $backupid Backup id
  49       * @param \core\progress\base $progress Optional progress reporting object
  50       */
  51      static public function clear_backup_dir($backupid, \core\progress\base $progress = null) {
  52          $backupiddir = make_backup_temp_directory($backupid, false);
  53          if (!self::delete_dir_contents($backupiddir, '', $progress)) {
  54              throw new backup_helper_exception('cannot_empty_backup_temp_dir');
  55          }
  56          return true;
  57      }
  58  
  59      /**
  60       * Given one backupid, delete completely its temp dir
  61       *
  62       * If supplied, progress object should be ready to receive indeterminate
  63       * progress reports.
  64       *
  65       * @param string $backupid Backup id
  66       * @param \core\progress\base $progress Optional progress reporting object
  67       */
  68       static public function delete_backup_dir($backupid, \core\progress\base $progress = null) {
  69           $backupiddir = make_backup_temp_directory($backupid, false);
  70           self::clear_backup_dir($backupid, $progress);
  71           return rmdir($backupiddir);
  72       }
  73  
  74       /**
  75       * Given one fullpath to directory, delete its contents recursively
  76       * Copied originally from somewhere in the net.
  77       * TODO: Modernise this
  78       *
  79       * If supplied, progress object should be ready to receive indeterminate
  80       * progress reports.
  81       *
  82       * @param string $dir Directory to delete
  83       * @param string $excludedir Exclude this directory
  84       * @param \core\progress\base $progress Optional progress reporting object
  85       */
  86      static public function delete_dir_contents($dir, $excludeddir='', \core\progress\base $progress = null) {
  87          global $CFG;
  88  
  89          if ($progress) {
  90              $progress->progress();
  91          }
  92  
  93          if (!is_dir($dir)) {
  94              // if we've been given a directory that doesn't exist yet, return true.
  95              // this happens when we're trying to clear out a course that has only just
  96              // been created.
  97              return true;
  98          }
  99          $slash = "/";
 100  
 101          // Create arrays to store files and directories
 102          $dir_files      = array();
 103          $dir_subdirs    = array();
 104  
 105          // Make sure we can delete it
 106          chmod($dir, $CFG->directorypermissions);
 107  
 108          if ((($handle = opendir($dir))) == false) {
 109              // The directory could not be opened
 110              return false;
 111          }
 112  
 113          // Loop through all directory entries, and construct two temporary arrays containing files and sub directories
 114          while (false !== ($entry = readdir($handle))) {
 115              if (is_dir($dir. $slash .$entry) && $entry != ".." && $entry != "." && $entry != $excludeddir) {
 116                  $dir_subdirs[] = $dir. $slash .$entry;
 117  
 118              } else if ($entry != ".." && $entry != "." && $entry != $excludeddir) {
 119                  $dir_files[] = $dir. $slash .$entry;
 120              }
 121          }
 122  
 123          // Delete all files in the curent directory return false and halt if a file cannot be removed
 124          for ($i=0; $i<count($dir_files); $i++) {
 125              chmod($dir_files[$i], $CFG->directorypermissions);
 126              if (((unlink($dir_files[$i]))) == false) {
 127                  return false;
 128              }
 129          }
 130  
 131          // Empty sub directories and then remove the directory
 132          for ($i=0; $i<count($dir_subdirs); $i++) {
 133              chmod($dir_subdirs[$i], $CFG->directorypermissions);
 134              if (self::delete_dir_contents($dir_subdirs[$i], '', $progress) == false) {
 135                  return false;
 136              } else {
 137                  if (remove_dir($dir_subdirs[$i]) == false) {
 138                      return false;
 139                  }
 140              }
 141          }
 142  
 143          // Close directory
 144          closedir($handle);
 145  
 146          // Success, every thing is gone return true
 147          return true;
 148      }
 149  
 150      /**
 151       * Delete all the temp dirs older than the time specified.
 152       *
 153       * If supplied, progress object should be ready to receive indeterminate
 154       * progress reports.
 155       *
 156       * @param int $deletebefore Delete files and directories older than this time
 157       * @param \core\progress\base $progress Optional progress reporting object
 158       */
 159      static public function delete_old_backup_dirs($deletebefore, \core\progress\base $progress = null) {
 160          $status = true;
 161          // Get files and directories in the backup temp dir.
 162          $backuptempdir = make_backup_temp_directory('');
 163          $items = new DirectoryIterator($backuptempdir);
 164          foreach ($items as $item) {
 165              if ($item->isDot()) {
 166                  continue;
 167              }
 168              if ($item->getMTime() < $deletebefore) {
 169                  if ($item->isDir()) {
 170                      // The item is a directory for some backup.
 171                      if (!self::delete_backup_dir($item->getFilename(), $progress)) {
 172                          // Something went wrong. Finish the list of items and then throw an exception.
 173                          $status = false;
 174                      }
 175                  } else if ($item->isFile()) {
 176                      unlink($item->getPathname());
 177                  }
 178              }
 179          }
 180          if (!$status) {
 181              throw new backup_helper_exception('problem_deleting_old_backup_temp_dirs');
 182          }
 183      }
 184  
 185      /**
 186       * This function will be invoked by any log() method in backup/restore, acting
 187       * as a simple forwarder to the standard loggers but also, if the $display
 188       * parameter is true, supporting translation via get_string() and sending to
 189       * standard output.
 190       */
 191      static public function log($message, $level, $a, $depth, $display, $logger) {
 192          // Send to standard loggers
 193          $logmessage = $message;
 194          $options = empty($depth) ? array() : array('depth' => $depth);
 195          if (!empty($a)) {
 196              $logmessage = $logmessage . ' ' . implode(', ', (array)$a);
 197          }
 198          $logger->process($logmessage, $level, $options);
 199  
 200          // If $display specified, send translated string to output_controller
 201          if ($display) {
 202              output_controller::get_instance()->output($message, 'backup', $a, $depth);
 203          }
 204      }
 205  
 206      /**
 207       * Given one backupid and the (FS) final generated file, perform its final storage
 208       * into Moodle file storage. For stored files it returns the complete file_info object
 209       *
 210       * Note: the $filepath is deleted if the backup file is created successfully
 211       *
 212       * If you specify the progress monitor, this will start a new progress section
 213       * to track progress in processing (in case this task takes a long time).
 214       *
 215       * @param int $backupid
 216       * @param string $filepath zip file containing the backup
 217       * @param \core\progress\base $progress Optional progress monitor
 218       * @return stored_file if created, null otherwise
 219       *
 220       * @throws moodle_exception in case of any problems
 221       */
 222      static public function store_backup_file($backupid, $filepath, \core\progress\base $progress = null) {
 223          global $CFG;
 224  
 225          // First of all, get some information from the backup_controller to help us decide
 226          list($dinfo, $cinfo, $sinfo) = backup_controller_dbops::get_moodle_backup_information(
 227                  $backupid, $progress);
 228  
 229          // Extract useful information to decide
 230          $hasusers  = (bool)$sinfo['users']->value;     // Backup has users
 231          $isannon   = (bool)$sinfo['anonymize']->value; // Backup is anonymised
 232          $filename  = $sinfo['filename']->value;        // Backup filename
 233          $backupmode= $dinfo[0]->mode;                  // Backup mode backup::MODE_GENERAL/IMPORT/HUB
 234          $backuptype= $dinfo[0]->type;                  // Backup type backup::TYPE_1ACTIVITY/SECTION/COURSE
 235          $userid    = $dinfo[0]->userid;                // User->id executing the backup
 236          $id        = $dinfo[0]->id;                    // Id of activity/section/course (depends of type)
 237          $courseid  = $dinfo[0]->courseid;              // Id of the course
 238          $format    = $dinfo[0]->format;                // Type of backup file
 239  
 240          // Quick hack. If for any reason, filename is blank, fix it here.
 241          // TODO: This hack will be out once MDL-22142 - P26 gets fixed
 242          if (empty($filename)) {
 243              $filename = backup_plan_dbops::get_default_backup_filename('moodle2', $backuptype, $id, $hasusers, $isannon);
 244          }
 245  
 246          // Backups of type IMPORT aren't stored ever
 247          if ($backupmode == backup::MODE_IMPORT) {
 248              return null;
 249          }
 250  
 251          if (!is_readable($filepath)) {
 252              // we have a problem if zip file does not exist
 253              throw new coding_exception('backup_helper::store_backup_file() expects valid $filepath parameter');
 254  
 255          }
 256  
 257          // Calculate file storage options of id being backup
 258          $ctxid     = 0;
 259          $filearea  = '';
 260          $component = '';
 261          $itemid    = 0;
 262          switch ($backuptype) {
 263              case backup::TYPE_1ACTIVITY:
 264                  $ctxid     = context_module::instance($id)->id;
 265                  $component = 'backup';
 266                  $filearea  = 'activity';
 267                  $itemid    = 0;
 268                  break;
 269              case backup::TYPE_1SECTION:
 270                  $ctxid     = context_course::instance($courseid)->id;
 271                  $component = 'backup';
 272                  $filearea  = 'section';
 273                  $itemid    = $id;
 274                  break;
 275              case backup::TYPE_1COURSE:
 276                  $ctxid     = context_course::instance($courseid)->id;
 277                  $component = 'backup';
 278                  $filearea  = 'course';
 279                  $itemid    = 0;
 280                  break;
 281          }
 282  
 283          if ($backupmode == backup::MODE_AUTOMATED) {
 284              // Automated backups have there own special area!
 285              $filearea  = 'automated';
 286  
 287              // If we're keeping the backup only in a chosen path, just move it there now
 288              // this saves copying from filepool to here later and filling trashdir.
 289              $config = get_config('backup');
 290              $dir = $config->backup_auto_destination;
 291              if ($config->backup_auto_storage == 1 and $dir and is_dir($dir) and is_writable($dir)) {
 292                  $filedest = $dir.'/'
 293                          .backup_plan_dbops::get_default_backup_filename(
 294                                  $format,
 295                                  $backuptype,
 296                                  $courseid,
 297                                  $hasusers,
 298                                  $isannon,
 299                                  !$config->backup_shortname,
 300                                  (bool)$config->backup_auto_files);
 301                  // first try to move the file, if it is not possible copy and delete instead
 302                  if (@rename($filepath, $filedest)) {
 303                      return null;
 304                  }
 305                  umask($CFG->umaskpermissions);
 306                  if (copy($filepath, $filedest)) {
 307                      @chmod($filedest, $CFG->filepermissions); // may fail because the permissions may not make sense outside of dataroot
 308                      unlink($filepath);
 309                      return null;
 310                  } else {
 311                      $bc = backup_controller::load_controller($backupid);
 312                      $bc->log('Attempt to copy backup file to the specified directory using filesystem failed - ',
 313                              backup::LOG_WARNING, $dir);
 314                      $bc->destroy();
 315                  }
 316                  // bad luck, try to deal with the file the old way - keep backup in file area if we can not copy to ext system
 317              }
 318          }
 319  
 320          // Backups of type HUB (by definition never have user info)
 321          // are sent to user's "user_tohub" file area. The upload process
 322          // will be responsible for cleaning that filearea once finished
 323          if ($backupmode == backup::MODE_HUB) {
 324              $ctxid     = context_user::instance($userid)->id;
 325              $component = 'user';
 326              $filearea  = 'tohub';
 327              $itemid    = 0;
 328          }
 329  
 330          // Backups without user info or with the anonymise functionality
 331          // enabled are sent to user's "user_backup"
 332          // file area. Maintenance of such area is responsibility of
 333          // the user via corresponding file manager frontend
 334          if (($backupmode == backup::MODE_GENERAL  || $backupmode == backup::MODE_ASYNC) && (!$hasusers || $isannon)) {
 335              $ctxid     = context_user::instance($userid)->id;
 336              $component = 'user';
 337              $filearea  = 'backup';
 338              $itemid    = 0;
 339          }
 340  
 341          // Let's send the file to file storage, everything already defined
 342          $fs = get_file_storage();
 343          $fr = array(
 344              'contextid'   => $ctxid,
 345              'component'   => $component,
 346              'filearea'    => $filearea,
 347              'itemid'      => $itemid,
 348              'filepath'    => '/',
 349              'filename'    => $filename,
 350              'userid'      => $userid,
 351              'timecreated' => time(),
 352              'timemodified'=> time());
 353          // If file already exists, delete if before
 354          // creating it again. This is BC behaviour - copy()
 355          // overwrites by default
 356          if ($fs->file_exists($fr['contextid'], $fr['component'], $fr['filearea'], $fr['itemid'], $fr['filepath'], $fr['filename'])) {
 357              $pathnamehash = $fs->get_pathname_hash($fr['contextid'], $fr['component'], $fr['filearea'], $fr['itemid'], $fr['filepath'], $fr['filename']);
 358              $sf = $fs->get_file_by_hash($pathnamehash);
 359              $sf->delete();
 360          }
 361          $file = $fs->create_file_from_pathname($fr, $filepath);
 362          unlink($filepath);
 363          return $file;
 364      }
 365  
 366      /**
 367       * This function simply marks one param to be considered as straight sql
 368       * param, so it won't be searched in the structure tree nor converted at
 369       * all. Useful for better integration of definition of sources in structure
 370       * and DB stuff
 371       */
 372      public static function is_sqlparam($value) {
 373          return array('sqlparam' => $value);
 374      }
 375  
 376      /**
 377       * This function returns one array of itemnames that are being handled by
 378       * inforef.xml files. Used both by backup and restore
 379       */
 380      public static function get_inforef_itemnames() {
 381          return array('user', 'grouping', 'group', 'role', 'file', 'scale', 'outcome', 'grade_item', 'question_category');
 382      }
 383  }
 384  
 385  /*
 386   * Exception class used by all the @helper stuff
 387   */
 388  class backup_helper_exception extends backup_exception {
 389  
 390      public function __construct($errorcode, $a=NULL, $debuginfo=null) {
 391          parent::__construct($errorcode, $a, $debuginfo);
 392      }
 393  }