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 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  namespace core_backup;
  18  
  19  use restore_controller_dbops;
  20  use restore_dbops;
  21  
  22  defined('MOODLE_INTERNAL') || die();
  23  
  24  // Include all the needed stuff
  25  global $CFG;
  26  require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
  27  
  28  /**
  29   * @package    core_backup
  30   * @category   test
  31   * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  32   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class restore_dbops_test extends \advanced_testcase {
  35  
  36      /**
  37       * Verify the xxx_ids_cached (in-memory backup_ids cache) stuff works as expected.
  38       *
  39       * Note that those private implementations are tested here by using the public
  40       * backup_ids API and later performing low-level tests.
  41       */
  42      public function test_backup_ids_cached() {
  43          global $DB;
  44          $dbman = $DB->get_manager(); // We are going to use database_manager services.
  45  
  46          $this->resetAfterTest(true); // Playing with temp tables, better reset once finished.
  47  
  48          // Some variables and objects for testing.
  49          $restoreid = 'testrestoreid';
  50  
  51          $mapping = new \stdClass();
  52          $mapping->itemname = 'user';
  53          $mapping->itemid = 1;
  54          $mapping->newitemid = 2;
  55          $mapping->parentitemid = 3;
  56          $mapping->info = 'info';
  57  
  58          // Create the backup_ids temp tables used by restore.
  59          restore_controller_dbops::create_restore_temp_tables($restoreid);
  60  
  61          // Send one mapping using the public api with defaults.
  62          restore_dbops::set_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
  63          // Get that mapping and verify everything is returned as expected.
  64          $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
  65          $this->assertSame($mapping->itemname, $result->itemname);
  66          $this->assertSame($mapping->itemid, $result->itemid);
  67          $this->assertSame(0, $result->newitemid);
  68          $this->assertSame(null, $result->parentitemid);
  69          $this->assertSame(null, $result->info);
  70  
  71          // Drop the backup_xxx_temp temptables manually, so memory cache won't be invalidated.
  72          $dbman->drop_table(new \xmldb_table('backup_ids_temp'));
  73          $dbman->drop_table(new \xmldb_table('backup_files_temp'));
  74  
  75          // Verify the mapping continues returning the same info,
  76          // now from cache (the table does not exist).
  77          $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
  78          $this->assertSame($mapping->itemname, $result->itemname);
  79          $this->assertSame($mapping->itemid, $result->itemid);
  80          $this->assertSame(0, $result->newitemid);
  81          $this->assertSame(null, $result->parentitemid);
  82          $this->assertSame(null, $result->info);
  83  
  84          // Recreate the temp table, just to drop it using the restore API in
  85          // order to check that, then, the cache becomes invalid for the same request.
  86          restore_controller_dbops::create_restore_temp_tables($restoreid);
  87          restore_controller_dbops::drop_restore_temp_tables($restoreid);
  88  
  89          // No cached info anymore, so the mapping request will arrive to
  90          // DB leading to error (temp table does not exist).
  91          try {
  92              $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
  93              $this->fail('Expecting an exception, none occurred');
  94          } catch (\Exception $e) {
  95              $this->assertTrue($e instanceof \dml_exception);
  96              $this->assertSame('Table "backup_ids_temp" does not exist', $e->getMessage());
  97          }
  98  
  99          // Create the backup_ids temp tables once more.
 100          restore_controller_dbops::create_restore_temp_tables($restoreid);
 101  
 102          // Send one mapping using the public api with complete values.
 103          restore_dbops::set_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid,
 104                  $mapping->newitemid, $mapping->parentitemid, $mapping->info);
 105          // Get that mapping and verify everything is returned as expected.
 106          $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
 107          $this->assertSame($mapping->itemname, $result->itemname);
 108          $this->assertSame($mapping->itemid, $result->itemid);
 109          $this->assertSame($mapping->newitemid, $result->newitemid);
 110          $this->assertSame($mapping->parentitemid, $result->parentitemid);
 111          $this->assertSame($mapping->info, $result->info);
 112  
 113          // Finally, drop the temp tables properly and get the DB error again (memory caches empty).
 114          restore_controller_dbops::drop_restore_temp_tables($restoreid);
 115          try {
 116              $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
 117              $this->fail('Expecting an exception, none occurred');
 118          } catch (\Exception $e) {
 119              $this->assertTrue($e instanceof \dml_exception);
 120              $this->assertSame('Table "backup_ids_temp" does not exist', $e->getMessage());
 121          }
 122      }
 123  
 124      /**
 125       * Data provider for {@link test_precheck_user()}
 126       */
 127      public function precheck_user_provider() {
 128  
 129          $emailmultiplier = [
 130              'shortmail' => 'normalusername@example.com',
 131              'longmail' => str_repeat('a', 100)  // It's not validated, hence any string is ok.
 132          ];
 133  
 134          $providercases = [];
 135  
 136          foreach ($emailmultiplier as $emailk => $email) {
 137              // Get the related cases.
 138              $cases = $this->precheck_user_cases($email);
 139              // Rename them (keys).
 140              foreach ($cases as $key => $case) {
 141                  $providercases[$key . ' - ' . $emailk] = $case;
 142              }
 143          }
 144  
 145          return $providercases;
 146      }
 147  
 148      /**
 149       * Get all the cases implemented in {@link restore_dbops::precheck_users()}
 150       *
 151       * @param string $email
 152       */
 153      private function precheck_user_cases($email) {
 154          global $CFG;
 155  
 156          $baseuserarr = [
 157              'username' => 'normalusername',
 158              'email'    => $email,
 159              'mnethostid' => $CFG->mnet_localhost_id,
 160              'firstaccess' => 123456789,
 161              'deleted'    => 0,
 162              'forceemailcleanup' => false, // Hack to force the DB record to have empty mail.
 163              'forceduplicateadminallowed' => false]; // Hack to enable import_general_duplicate_admin_allowed.
 164  
 165          return [
 166              // Cases with samesite = true.
 167              'samesite match existing (1A)' => [
 168                  'dbuser' => $baseuserarr,
 169                  'backupuser' => $baseuserarr,
 170                  'samesite' => true,
 171                  'outcome' => 'match'
 172              ],
 173              'samesite match existing anon (1B)' => [
 174                  'dbuser' => array_merge($baseuserarr, [
 175                      'username' => 'anon01']),
 176                  'backupuser' => array_merge($baseuserarr, [
 177                      'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01',
 178                      'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']),
 179                  'samesite' => true,
 180                  'outcome' => 'match'
 181              ],
 182              'samesite match existing deleted in db, alive in backup, by db username (1C)' => [
 183                  'dbuser' => array_merge($baseuserarr, [
 184                      'deleted' => 1]),
 185                  'backupuser' => array_merge($baseuserarr, [
 186                      'username' => 'this_wont_match']),
 187                  'samesite' => true,
 188                  'outcome' => 'match'
 189              ],
 190              'samesite match existing deleted in db, alive in backup, by db email (1C)' => [
 191                  'dbuser' => array_merge($baseuserarr, [
 192                      'deleted' => 1]),
 193                  'backupuser' => array_merge($baseuserarr, [
 194                      'email' => 'this_wont_match']),
 195                  'samesite' => true,
 196                  'outcome' => 'match'
 197              ],
 198              'samesite match existing alive in db, deleted in backup (1D)' => [
 199                  'dbuser' => $baseuserarr,
 200                  'backupuser' => array_merge($baseuserarr, [
 201                      'deleted' => 1]),
 202                  'samesite' => true,
 203                  'outcome' => 'match'
 204              ],
 205              'samesite conflict (1E)' => [
 206                  'dbuser' => $baseuserarr,
 207                  'backupuser' => array_merge($baseuserarr, ['id' => -1]),
 208                  'samesite' => true,
 209                  'outcome' => false
 210              ],
 211              'samesite create user (1F)' => [
 212                  'dbuser' => $baseuserarr,
 213                  'backupuser' => array_merge($baseuserarr, [
 214                      'username' => 'newusername']),
 215                  'samesite' => false,
 216                  'outcome' => true
 217              ],
 218  
 219              // Cases with samesite = false.
 220              'no samesite match existing, by db email (2A1)' => [
 221                  'dbuser' => $baseuserarr,
 222                  'backupuser' => array_merge($baseuserarr, [
 223                      'firstaccess' => 0]),
 224                  'samesite' => false,
 225                  'outcome' => 'match'
 226              ],
 227              'no samesite match existing, by db firstaccess (2A1)' => [
 228                  'dbuser' => $baseuserarr,
 229                  'backupuser' => array_merge($baseuserarr, [
 230                      'email' => 'this_wont_match@example.con']),
 231                  'samesite' => false,
 232                  'outcome' => 'match'
 233              ],
 234              'no samesite match existing anon (2A1 too)' => [
 235                  'dbuser' => array_merge($baseuserarr, [
 236                      'username' => 'anon01']),
 237                  'backupuser' => array_merge($baseuserarr, [
 238                      'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01',
 239                      'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']),
 240                  'samesite' => false,
 241                  'outcome' => 'match'
 242              ],
 243              'no samesite match dupe admin (2A2)' => [
 244                  'dbuser' => array_merge($baseuserarr, [
 245                      'username' => 'admin_old_site_id',
 246                      'forceduplicateadminallowed' => true]),
 247                  'backupuser' => array_merge($baseuserarr, [
 248                      'username' => 'admin']),
 249                  'samesite' => false,
 250                  'outcome' => 'match'
 251              ],
 252              'no samesite match existing deleted in db, alive in backup, by db username (2B1)' => [
 253                  'dbuser' => array_merge($baseuserarr, [
 254                      'deleted' => 1]),
 255                  'backupuser' => array_merge($baseuserarr, [
 256                      'firstaccess' => 0]),
 257                  'samesite' => false,
 258                  'outcome' => 'match'
 259              ],
 260              'no samesite match existing deleted in db, alive in backup, by db firstaccess (2B1)' => [
 261                  'dbuser' => array_merge($baseuserarr, [
 262                      'deleted' => 1]),
 263                  'backupuser' => array_merge($baseuserarr, [
 264                      'mail' => 'this_wont_match']),
 265                  'samesite' => false,
 266                  'outcome' => 'match'
 267              ],
 268              'no samesite match existing deleted in db, alive in backup (2B2)' => [
 269                  'dbuser' => array_merge($baseuserarr, [
 270                      'deleted' => 1,
 271                      'forceemailcleanup' => true]),
 272                  'backupuser' => $baseuserarr,
 273                  'samesite' => false,
 274                  'outcome' => 'match'
 275              ],
 276              'no samesite match existing alive in db, deleted in backup (2C)' => [
 277                  'dbuser' => $baseuserarr,
 278                  'backupuser' => array_merge($baseuserarr, [
 279                      'deleted' => 1]),
 280                  'samesite' => false,
 281                  'outcome' => 'match'
 282              ],
 283              'no samesite conflict (2D)' => [
 284                  'dbuser' => $baseuserarr,
 285                  'backupuser' => array_merge($baseuserarr, [
 286                      'email' => 'anotheruser@example.com', 'firstaccess' => 0]),
 287                  'samesite' => false,
 288                  'outcome' => false
 289              ],
 290              'no samesite create user (2E)' => [
 291                  'dbuser' => $baseuserarr,
 292                  'backupuser' => array_merge($baseuserarr, [
 293                      'username' => 'newusername']),
 294                  'samesite' => false,
 295                  'outcome' => true
 296              ],
 297  
 298          ];
 299      }
 300  
 301      /**
 302       * Test restore precheck_user method
 303       *
 304       * @dataProvider precheck_user_provider
 305       * @covers \restore_dbops::precheck_user()
 306       *
 307       * @param array $dbuser
 308       * @param array $backupuser
 309       * @param bool $samesite
 310       * @param mixed $outcome
 311       **/
 312      public function test_precheck_user($dbuser, $backupuser, $samesite, $outcome) {
 313          global $DB;
 314  
 315          $this->resetAfterTest();
 316  
 317          $dbuser = (object)$dbuser;
 318          $backupuser = (object)$backupuser;
 319  
 320          $siteid = null;
 321  
 322          // If the backup user must be deleted, simulate it (by temp inserting to DB, deleting and fetching it back).
 323          if ($backupuser->deleted) {
 324              $backupuser->id = $DB->insert_record('user', array_merge((array)$backupuser, ['deleted' => 0]));
 325              delete_user($backupuser);
 326              $backupuser = $DB->get_record('user', ['id' => $backupuser->id]);
 327              $DB->delete_records('user', ['id' => $backupuser->id]);
 328              unset($backupuser->id);
 329          }
 330  
 331          // Create the db user, normally.
 332          $dbuser->id = $DB->insert_record('user', array_merge((array)$dbuser, ['deleted' => 0]));
 333          $backupuser->id = $backupuser->id ?? $dbuser->id;
 334  
 335          // We may want to enable the import_general_duplicate_admin_allowed setting and look for old admin records.
 336          if ($dbuser->forceduplicateadminallowed) {
 337              set_config('import_general_duplicate_admin_allowed', true, 'backup');
 338              $siteid = 'old_site_id';
 339          }
 340  
 341          // If the DB user must be deleted, do it and fetch it back.
 342          if ($dbuser->deleted) {
 343              delete_user($dbuser);
 344              // We may want to clean the mail field (old behavior, not containing the current md5(username).
 345              if ($dbuser->forceemailcleanup) {
 346                  $DB->set_field('user', 'email', '', ['id' => $dbuser->id]);
 347              }
 348          }
 349  
 350          // Get the dbuser  record, because we may have changed it above.
 351          $dbuser = $DB->get_record('user', ['id' => $dbuser->id]);
 352  
 353          $method = (new \ReflectionClass('restore_dbops'))->getMethod('precheck_user');
 354          $method->setAccessible(true);
 355          $result = $method->invoke(null, $backupuser, $samesite, $siteid);
 356  
 357          if (is_bool($result)) {
 358              $this->assertSame($outcome, $result);
 359          } else {
 360              $outcome = $dbuser; // Outcome is not bool, matching found, so it must be the dbuser,
 361              // Just check ids, it means the expected match has been found in database.
 362              $this->assertSame($outcome->id, $result->id);
 363          }
 364      }
 365  }