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]

   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   * Defines various element classes used in specific areas
  20   *
  21   * @package     core_backup
  22   * @subpackage  moodle2
  23   * @category    backup
  24   * @copyright   2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  25   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26   */
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  /**
  31   * Implementation of backup_final_element that provides one interceptor for anonymization of data
  32   *
  33   * This class overwrites the standard set_value() method, in order to get (by name)
  34   * functions from backup_anonymizer_helper executed, producing anonymization of information
  35   * to happen in a clean way
  36   *
  37   * TODO: Finish phpdocs
  38   */
  39  class anonymizer_final_element extends backup_final_element {
  40  
  41      public function set_value($value) {
  42          // Get parent name
  43          $pname = $this->get_parent()->get_name();
  44          // Get my name
  45          $myname = $this->get_name();
  46          // Define class and function name
  47          $classname = 'backup_anonymizer_helper';
  48          $methodname= 'process_' . $pname . '_' . $myname;
  49          // Invoke the interception method
  50          $result = call_user_func(array($classname, $methodname), $value);
  51          // Finally set it
  52          parent::set_value($result);
  53      }
  54  }
  55  
  56  /**
  57   * Implementation of backup_final_element that provides special handling of mnethosturl
  58   *
  59   * This class overwrites the standard set_value() method, in order to decide,
  60   * based on various config options, what to do with the field.
  61   *
  62   * TODO: Finish phpdocs
  63   */
  64  class mnethosturl_final_element extends backup_final_element {
  65  
  66      public function set_value($value) {
  67          global $CFG;
  68  
  69          $localhostwwwroot = backup_plan_dbops::get_mnet_localhost_wwwroot();
  70  
  71          // If user wwwroot matches mnet local host one or if
  72          // there isn't associated wwwroot, skip sending it to file
  73          if ($localhostwwwroot == $value || empty($value)) {
  74              // Do nothing
  75          } else {
  76              parent::set_value($value);
  77          }
  78      }
  79  }
  80  
  81  /**
  82   * Implementation of {@link backup_final_element} that provides base64 encoding.
  83   *
  84   * This final element transparently encodes with base64_encode() contents that
  85   * normally are not safe for being stored in utf-8 xml files (binaries, serialized
  86   * data...).
  87   */
  88  class base64_encode_final_element extends backup_final_element {
  89  
  90      /**
  91       * Set the value for the final element, encoding it as utf-8/xml safe base64.
  92       *
  93       * @param string $value Original value coming from backup step source, usually db.
  94       */
  95      public function set_value($value) {
  96          // Avoid null being passed to base64_encode.
  97          $value = $value ?? '';
  98          parent::set_value(base64_encode($value));
  99      }
 100  }
 101  
 102  /**
 103   * Implementation of {@link backup_final_element} that provides symmetric-key AES-256 encryption of contents.
 104   *
 105   * This final element transparently encrypts, for secure storage and transport, any content
 106   * that shouldn't be shown normally in plain text. Usually, passwords or keys that cannot use
 107   * hashing algorithms, although potentially can encrypt any content. All information is encoded
 108   * using base64.
 109   *
 110   * Features:
 111   *   - requires openssl extension to work. Without it contents are completely omitted.
 112   *   - automatically creates an appropriate default key for the site and stores it into backup_encryptkey config (bas64 encoded).
 113   *   - uses a different appropriate init vector for every operation, which is transmited with the encrypted contents.
 114   *   - all generated data is base64 encoded for safe transmission.
 115   *   - automatically adds "encrypted" attribute for easier detection.
 116   *   - implements HMAC for providing integrity.
 117   *
 118   * @copyright 2017 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
 119   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 120   */
 121  class encrypted_final_element extends backup_final_element {
 122  
 123      /** @var string cypher appropiate raw key for backups in the site. Defaults to backup_encryptkey config. */
 124      protected $key = null;
 125  
 126      /**
 127       * Constructor - instantiates a encrypted_final_element, specifying its basic info.
 128       *
 129       * Overridden to automatically add the 'encrypted' attribute if missing.
 130       *
 131       * @param string $name name of the element
 132       * @param array  $attributes attributes this element will handle (optional, defaults to null)
 133       */
 134      public function __construct($name, $attributes = null) {
 135          parent::__construct($name, $attributes);
 136          if (! $this->get_attribute('encrypted')) {
 137              $this->add_attributes('encrypted');
 138          }
 139      }
 140  
 141      /**
 142       * Set the encryption key manually, overriding default backup_encryptkey config.
 143       *
 144       * @param string $key key to be used for encrypting. Required to be 256-bit key.
 145       *               Use a safe generation technique. See self::generate_encryption_random_key() below.
 146       */
 147      protected function set_key($key) {
 148          $bytes = strlen($key); // Get key length in bytes.
 149  
 150          // Only accept keys with the expected (backup::CIPHERKEYLEN) key length. There are a number of hashing,
 151          // random generators to achieve this esasily, like the one shown below to create the default
 152          // site encryption key and ivs.
 153          if ($bytes !== backup::CIPHERKEYLEN) {
 154              $info = (object)array('expected' => backup::CIPHERKEYLEN, 'found' => $bytes);
 155              throw new base_element_struct_exception('encrypted_final_element incorrect key length', $info);
 156          }
 157          // Everything went ok, store the key.
 158          $this->key = $key;
 159      }
 160  
 161      /**
 162       * Set the value of the field.
 163       *
 164       * This method sets the value of the element, encrypted using the specified key for it,
 165       * defaulting to (and generating) backup_encryptkey config. HMAC is used for integrity.
 166       *
 167       * @param string $value plain-text content the will be stored encrypted and encoded.
 168       */
 169      public function set_value($value) {
 170  
 171          // No openssl available, skip this field completely.
 172          if (!function_exists('openssl_encrypt')) {
 173              return;
 174          }
 175  
 176          // No hmac available, skip this field completely.
 177          if (!function_exists('hash_hmac')) {
 178              return;
 179          }
 180  
 181          // Cypher not available, skip this field completely.
 182          if (!in_array(backup::CIPHER, openssl_get_cipher_methods())) {
 183              return;
 184          }
 185  
 186          // Ensure we have a good key, manual or default.
 187          if (empty($this->key)) {
 188              // The key has not been set manually, look for it at config (base64 encoded there).
 189              $enckey = get_config('backup', 'backup_encryptkey');
 190              if ($enckey === false) {
 191                  // Has not been set, calculate and save an appropiate random key automatically.
 192                  $enckey = base64_encode(self::generate_encryption_random_key(backup::CIPHERKEYLEN));
 193                  set_config('backup_encryptkey', $enckey, 'backup');
 194              }
 195              $this->set_key(base64_decode($enckey));
 196          }
 197  
 198          // Now we need an iv for this operation.
 199          $iv = self::generate_encryption_random_key(openssl_cipher_iv_length(backup::CIPHER));
 200  
 201          // Everything is ready, let's encrypt and prepend the 1-shot iv.
 202          $value = $iv . openssl_encrypt($value ?? '', backup::CIPHER, $this->key, OPENSSL_RAW_DATA, $iv);
 203  
 204          // Calculate the hmac of the value (iv + encrypted) and prepend it.
 205          $hmac = hash_hmac('sha256', $value, $this->key, true);
 206          $value = $hmac . $value;
 207  
 208          // Ready, set the encoded value.
 209          parent::set_value(base64_encode($value));
 210  
 211          // Finally, if the field has an "encrypted" attribute, set it to true.
 212          if ($att = $this->get_attribute('encrypted')) {
 213              $att->set_value('true');
 214          }
 215      }
 216  
 217      /**
 218       * Generate an appropiate random key to be used for encrypting backup information.
 219       *
 220       * Normally used as site default encryption key (backup_encryptkey config) and also
 221       * for calculating the init vectors.
 222       *
 223       * Note that until PHP 5.6.12 openssl_random_pseudo_bytes() did NOT
 224       * use a "cryptographically strong algorithm" {@link https://bugs.php.net/bug.php?id=70014}
 225       * But it's beyond my crypto-knowledge when it's worth finding a *real* better alternative.
 226       *
 227       * @param int $bytes Number of bytes to determine the key length expected.
 228       */
 229      protected static function generate_encryption_random_key($bytes) {
 230          return openssl_random_pseudo_bytes($bytes);
 231      }
 232  }
 233  
 234  /**
 235   * Implementation of backup_nested_element that provides special handling of files
 236   *
 237   * This class overwrites the standard fill_values() method, so it gets intercepted
 238   * for each file record being set to xml, in order to copy, at the same file, the
 239   * physical file from moodle file storage to backup file storage
 240   *
 241   * TODO: Finish phpdocs
 242   */
 243  class file_nested_element extends backup_nested_element {
 244  
 245      protected $backupid;
 246  
 247      public function process($processor) {
 248          // Get current backupid from processor, we'll need later
 249          if (is_null($this->backupid)) {
 250              $this->backupid = $processor->get_var(backup::VAR_BACKUPID);
 251          }
 252          return parent::process($processor);
 253      }
 254  
 255      public function fill_values($values) {
 256          // Fill values
 257          parent::fill_values($values);
 258          // Do our own tasks (copy file from moodle to backup)
 259          try {
 260              backup_file_manager::copy_file_moodle2backup($this->backupid, $values);
 261          } catch (file_exception $e) {
 262              $this->add_result(array('missing_files_in_pool' => true));
 263  
 264              // Build helpful log message with all information necessary to identify
 265              // file location.
 266              $context = context::instance_by_id($values->contextid, IGNORE_MISSING);
 267              $contextname = '';
 268              if ($context) {
 269                  $contextname = ' \'' . $context->get_context_name() . '\'';
 270              }
 271              $message = 'Missing file in pool: ' . $values->filepath  . $values->filename .
 272                      ' (context ' . $values->contextid . $contextname . ', component ' .
 273                      $values->component . ', filearea ' . $values->filearea . ', itemid ' .
 274                      $values->itemid . ') [' . $e->debuginfo . ']';
 275              $this->add_log($message, backup::LOG_WARNING);
 276          }
 277      }
 278  }
 279  
 280  /**
 281   * Implementation of backup_optigroup_element to be used by plugins stuff.
 282   * Split just for better separation and future specialisation
 283   */
 284  class backup_plugin_element extends backup_optigroup_element { }
 285  
 286  /**
 287   * Implementation of backup_optigroup_element to be used by subplugins stuff.
 288   * Split just for better separation and future specialisation
 289   */
 290  class backup_subplugin_element extends backup_optigroup_element { }