Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

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