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] [Versions 401 and 402] [Versions 401 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   * This class represent one XMLDB structure
  19   *
  20   * @package    core_xmldb
  21   * @copyright  1999 onwards Martin Dougiamas     http://dougiamas.com
  22   *             2001-3001 Eloy Lafuente (stronk7) http://contiento.com
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  
  29  class xmldb_structure extends xmldb_object {
  30  
  31      /** @var string */
  32      protected $path;
  33  
  34      /** @var string */
  35      protected $version;
  36  
  37      /** @var array tables */
  38      protected $tables;
  39  
  40      /**
  41       * Creates one new xmldb_structure
  42       * @param string $name
  43       */
  44      public function __construct($name) {
  45          parent::__construct($name);
  46          $this->path = null;
  47          $this->version = null;
  48          $this->tables = array();
  49      }
  50  
  51      /**
  52       * Returns the path of the structure
  53       * @return string
  54       */
  55      public function getPath() {
  56          return $this->path;
  57      }
  58  
  59      /**
  60       * Returns the version of the structure
  61       * @return string
  62       */
  63      public function getVersion() {
  64          return $this->version;
  65      }
  66  
  67      /**
  68       * Returns one xmldb_table
  69       * @param string $tablename
  70       * @return xmldb_table
  71       */
  72      public function getTable($tablename) {
  73          $i = $this->findTableInArray($tablename);
  74          if ($i !== null) {
  75              return $this->tables[$i];
  76          }
  77          return null;
  78      }
  79  
  80      /**
  81       * Returns the position of one table in the array.
  82       * @param string $tablename
  83       * @return mixed
  84       */
  85      public function findTableInArray($tablename) {
  86          foreach ($this->tables as $i => $table) {
  87              if ($tablename == $table->getName()) {
  88                  return $i;
  89              }
  90          }
  91          return null;
  92      }
  93  
  94      /**
  95       * This function will reorder the array of tables
  96       * @return bool success
  97       */
  98      public function orderTables() {
  99          $result = $this->orderElements($this->tables);
 100          if ($result) {
 101              $this->setTables($result);
 102              return true;
 103          } else {
 104              return false;
 105          }
 106      }
 107  
 108      /**
 109       * Returns the tables of the structure
 110       * @return array
 111       */
 112      public function getTables() {
 113          return $this->tables;
 114      }
 115  
 116      /**
 117       * Set the structure version
 118       * @param string version
 119       */
 120      public function setVersion($version) {
 121          $this->version = $version;
 122      }
 123  
 124      /**
 125       * Add one table to the structure, allowing to specify the desired order
 126       * If it's not specified, then the table is added at the end.
 127       * @param xmldb_table $table
 128       * @param mixed $after
 129       */
 130      public function addTable($table, $after=null) {
 131  
 132          // Calculate the previous and next tables
 133          $prevtable = null;
 134          $nexttable = null;
 135  
 136          if (!$after) {
 137              if ($this->tables) {
 138                  end($this->tables);
 139                  $prevtable = $this->tables[key($this->tables)];
 140              }
 141          } else {
 142              $prevtable = $this->getTable($after);
 143          }
 144          if ($prevtable && $prevtable->getNext()) {
 145              $nexttable = $this->getTable($prevtable->getNext());
 146          }
 147  
 148          // Set current table previous and next attributes
 149          if ($prevtable) {
 150              $table->setPrevious($prevtable->getName());
 151              $prevtable->setNext($table->getName());
 152          }
 153          if ($nexttable) {
 154              $table->setNext($nexttable->getName());
 155              $nexttable->setPrevious($table->getName());
 156          }
 157          // Some more attributes
 158          $table->setLoaded(true);
 159          $table->setChanged(true);
 160          // Add the new table
 161          $this->tables[] = $table;
 162          // Reorder the whole structure
 163          $this->orderTables($this->tables);
 164          // Recalculate the hash
 165          $this->calculateHash(true);
 166          // We have one new table, so the structure has changed
 167          $this->setVersion(userdate(time(), '%Y%m%d', 99, false));
 168          $this->setChanged(true);
 169      }
 170  
 171      /**
 172       * Delete one table from the Structure
 173       * @param string $tablename
 174       */
 175      public function deleteTable($tablename) {
 176  
 177          $table = $this->getTable($tablename);
 178          if ($table) {
 179              $i = $this->findTableInArray($tablename);
 180              // Look for prev and next table
 181              $prevtable = $this->getTable($table->getPrevious());
 182              $nexttable = $this->getTable($table->getNext());
 183              // Change their previous and next attributes
 184              if ($prevtable) {
 185                  $prevtable->setNext($table->getNext());
 186              }
 187              if ($nexttable) {
 188                  $nexttable->setPrevious($table->getPrevious());
 189              }
 190              // Delete the table
 191              unset($this->tables[$i]);
 192              // Reorder the tables
 193              $this->orderTables($this->tables);
 194              // Recalculate the hash
 195              $this->calculateHash(true);
 196              // We have one deleted table, so the structure has changed
 197              $this->setVersion(userdate(time(), '%Y%m%d', 99, false));
 198              $this->setChanged(true);
 199          }
 200      }
 201  
 202      /**
 203       * Set the tables
 204       * @param array $tables
 205       */
 206      public function setTables($tables) {
 207          $this->tables = $tables;
 208      }
 209  
 210      /**
 211       * Load data from XML to the structure
 212       * @param array $xmlarr
 213       * @return bool
 214       */
 215      public function arr2xmldb_structure($xmlarr) {
 216  
 217          global $CFG;
 218  
 219          $result = true;
 220  
 221          // Debug the structure
 222          // traverse_xmlize($xmlarr);                   //Debug
 223          // print_object ($GLOBALS['traverse_array']);  //Debug
 224          // $GLOBALS['traverse_array']="";              //Debug
 225  
 226          // Process structure attributes (path, comment and version)
 227          if (isset($xmlarr['XMLDB']['@']['PATH'])) {
 228              $this->path = trim($xmlarr['XMLDB']['@']['PATH']);
 229          } else {
 230              $this->errormsg = 'Missing PATH attribute';
 231              $this->debug($this->errormsg);
 232              $result = false;
 233          }
 234          // Normalize paths to compare them.
 235          $filepath = realpath($this->name); // File path comes in name.
 236          $filename = basename($filepath);
 237          $normalisedpath = $this->path;
 238          if ($CFG->admin !== 'admin') {
 239              $needle = 'admin/';
 240              if (strpos($this->path, $needle) === 0) {
 241                  $normalisedpath = substr_replace($this->path, "$CFG->admin/", 0, strlen($needle));
 242              }
 243          }
 244          $structurepath = realpath($CFG->dirroot . DIRECTORY_SEPARATOR . $normalisedpath . DIRECTORY_SEPARATOR . $filename);
 245          if ($filepath !== $structurepath) {
 246              $relativepath = dirname(str_replace(realpath($CFG->dirroot) . DIRECTORY_SEPARATOR, '', $filepath));
 247              $this->errormsg = 'PATH attribute does not match file directory: ' . $relativepath;
 248              $this->debug($this->errormsg);
 249              $result = false;
 250          }
 251          if (isset($xmlarr['XMLDB']['@']['VERSION'])) {
 252              $this->version = trim($xmlarr['XMLDB']['@']['VERSION']);
 253          } else {
 254              $this->errormsg = 'Missing VERSION attribute';
 255              $this->debug($this->errormsg);
 256              $result = false;
 257          }
 258          if (isset($xmlarr['XMLDB']['@']['COMMENT'])) {
 259              $this->comment = trim($xmlarr['XMLDB']['@']['COMMENT']);
 260          } else if (!empty($CFG->xmldbdisablecommentchecking)) {
 261              $this->comment = '';
 262          } else {
 263              $this->errormsg = 'Missing COMMENT attribute';
 264              $this->debug($this->errormsg);
 265              $result = false;
 266          }
 267  
 268          // Iterate over tables
 269          if (isset($xmlarr['XMLDB']['#']['TABLES']['0']['#']['TABLE'])) {
 270              foreach ($xmlarr['XMLDB']['#']['TABLES']['0']['#']['TABLE'] as $xmltable) {
 271                  if (!$result) { //Skip on error
 272                      continue;
 273                  }
 274                  $name = trim($xmltable['@']['NAME']);
 275                  $table = new xmldb_table($name);
 276                  $table->arr2xmldb_table($xmltable);
 277                  $this->tables[] = $table;
 278                  if (!$table->isLoaded()) {
 279                      $this->errormsg = 'Problem loading table ' . $name;
 280                      $this->debug($this->errormsg);
 281                      $result = false;
 282                  }
 283              }
 284          } else {
 285              $this->errormsg = 'Missing TABLES section';
 286              $this->debug($this->errormsg);
 287              $result = false;
 288          }
 289  
 290          // Perform some general checks over tables
 291          if ($result && $this->tables) {
 292              // Check tables names are ok (lowercase, a-z _-)
 293              if (!$this->checkNameValues($this->tables)) {
 294                  $this->errormsg = 'Some TABLES name values are incorrect';
 295                  $this->debug($this->errormsg);
 296                  $result = false;
 297              }
 298              // Compute prev/next.
 299              $this->fixPrevNext($this->tables);
 300              // Order tables
 301              if ($result && !$this->orderTables($this->tables)) {
 302                  $this->errormsg = 'Error ordering the tables';
 303                  $this->debug($this->errormsg);
 304                  $result = false;
 305              }
 306          }
 307  
 308          // Set some attributes
 309          if ($result) {
 310              $this->loaded = true;
 311          }
 312          $this->calculateHash();
 313          return $result;
 314      }
 315  
 316      /**
 317       * This function calculate and set the hash of one xmldb_structure
 318       * @param bool $recursive
 319       */
 320       public function calculateHash($recursive = false) {
 321          if (!$this->loaded) {
 322              $this->hash = null;
 323          } else {
 324              $key = $this->name . $this->path . $this->comment;
 325              if ($this->tables) {
 326                  foreach ($this->tables as $tbl) {
 327                      $table = $this->getTable($tbl->getName());
 328                      if ($recursive) {
 329                          $table->calculateHash($recursive);
 330                      }
 331                      $key .= $table->getHash();
 332                  }
 333              }
 334              $this->hash = md5($key);
 335          }
 336      }
 337  
 338      /**
 339       * This function will output the XML text for one structure
 340       * @return string
 341       */
 342      public function xmlOutput() {
 343          $o = '<?xml version="1.0" encoding="UTF-8" ?>' . "\n";
 344          $o.= '<XMLDB PATH="' . $this->path . '"';
 345          $o.= ' VERSION="' . $this->version . '"';
 346          if ($this->comment) {
 347              $o.= ' COMMENT="' . htmlspecialchars($this->comment, ENT_COMPAT) . '"'."\n";
 348          }
 349          $rel = array_fill(0, count(explode('/', $this->path)), '..');
 350          $rel = implode('/', $rel);
 351          $o.= '    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'."\n";
 352          $o.= '    xsi:noNamespaceSchemaLocation="'.$rel.'/lib/xmldb/xmldb.xsd"'."\n";
 353          $o.= '>' . "\n";
 354          // Now the tables
 355          if ($this->tables) {
 356              $o.= '  <TABLES>' . "\n";
 357              foreach ($this->tables as $table) {
 358                  $o.= $table->xmlOutput();
 359              }
 360              $o.= '  </TABLES>' . "\n";
 361          }
 362          $o.= '</XMLDB>' . "\n";
 363  
 364          return $o;
 365      }
 366  
 367      /**
 368       * This function returns the number of uses of one table inside
 369       * a whole XMLDStructure. Useful to detect if the table must be
 370       * locked. Return false if no uses are found.
 371       * @param string $tablename
 372       * @return mixed
 373       */
 374      public function getTableUses($tablename) {
 375  
 376          $uses = array();
 377  
 378          // Check if some foreign key in the whole structure is using it
 379          // (by comparing the reftable with the tablename)
 380          if ($this->tables) {
 381              foreach ($this->tables as $table) {
 382                  $keys = $table->getKeys();
 383                  if ($keys) {
 384                      foreach ($keys as $key) {
 385                          if ($key->getType() == XMLDB_KEY_FOREIGN) {
 386                              if ($tablename == $key->getRefTable()) {
 387                                  $uses[] = 'table ' . $table->getName() . ' key ' . $key->getName();
 388                              }
 389                          }
 390                      }
 391                  }
 392              }
 393          }
 394  
 395          // Return result
 396          if (!empty($uses)) {
 397              return $uses;
 398          } else {
 399              return false;
 400          }
 401      }
 402  
 403      /**
 404       * This function returns the number of uses of one field inside
 405       * a whole xmldb_structure. Useful to detect if the field must be
 406       * locked. Return false if no uses are found.
 407       * @param string $tablename
 408       * @param string $fieldname
 409       * @return mixed
 410       */
 411      public function getFieldUses($tablename, $fieldname) {
 412  
 413          $uses = array();
 414  
 415          // Check if any key in the table is using it
 416          $table = $this->getTable($tablename);
 417          if ($keys = $table->getKeys()) {
 418              foreach ($keys as $key) {
 419                  if (in_array($fieldname, $key->getFields()) ||
 420                      in_array($fieldname, $key->getRefFields())) {
 421                          $uses[] = 'table ' . $table->getName() . ' key ' . $key->getName();
 422                  }
 423              }
 424          }
 425          // Check if any index in the table is using it
 426          $table = $this->getTable($tablename);
 427          if ($indexes = $table->getIndexes()) {
 428              foreach ($indexes as $index) {
 429                  if (in_array($fieldname, $index->getFields())) {
 430                      $uses[] = 'table ' . $table->getName() . ' index ' . $index->getName();
 431                  }
 432              }
 433          }
 434          // Check if some foreign key in the whole structure is using it
 435          // By comparing the reftable and refields with the field)
 436          if ($this->tables) {
 437              foreach ($this->tables as $table) {
 438                  $keys = $table->getKeys();
 439                  if ($keys) {
 440                      foreach ($keys as $key) {
 441                          if ($key->getType() == XMLDB_KEY_FOREIGN) {
 442                              if ($tablename == $key->getRefTable()) {
 443                                  $reffieds = $key->getRefFields();
 444                                  if (in_array($fieldname, $key->getRefFields())) {
 445                                      $uses[] = 'table ' . $table->getName() . ' key ' . $key->getName();
 446                                  }
 447                              }
 448                          }
 449                      }
 450                  }
 451              }
 452          }
 453  
 454          // Return result
 455          if (!empty($uses)) {
 456              return $uses;
 457          } else {
 458              return false;
 459          }
 460      }
 461  
 462      /**
 463       * This function returns the number of uses of one key inside
 464       * a whole xmldb_structure. Useful to detect if the key must be
 465       * locked. Return false if no uses are found.
 466       * @param string $tablename
 467       * @param string $keyname
 468       * @return mixed
 469       */
 470      public function getKeyUses($tablename, $keyname) {
 471  
 472          $uses = array();
 473  
 474          // Check if some foreign key in the whole structure is using it
 475          // (by comparing the reftable and reffields with the fields in the key)
 476          $mytable = $this->getTable($tablename);
 477          $mykey = $mytable->getKey($keyname);
 478          if ($this->tables && $mykey) {
 479              foreach ($this->tables as $table) {
 480                  $allkeys = $table->getKeys();
 481                  if ($allkeys) {
 482                      foreach ($allkeys as $key) {
 483                          if ($key->getType() != XMLDB_KEY_FOREIGN) {
 484                              continue;
 485                          }
 486                          if ($key->getRefTable() == $tablename &&
 487                              implode(',', $key->getRefFields()) == implode(',', $mykey->getFields())) {
 488                                  $uses[] = 'table ' . $table->getName() . ' key ' . $key->getName();
 489                          }
 490                      }
 491                  }
 492              }
 493          }
 494  
 495          // Return result
 496          if (!empty($uses)) {
 497              return $uses;
 498          } else {
 499              return false;
 500          }
 501      }
 502  
 503      /**
 504       * This function returns the number of uses of one index inside
 505       * a whole xmldb_structure. Useful to detect if the index must be
 506       * locked. Return false if no uses are found.
 507       * @param string $tablename
 508       * @param string $indexname
 509       * @return mixed
 510       */
 511      public function getIndexUses($tablename, $indexname) {
 512  
 513          $uses = array();
 514  
 515          // Nothing to check, because indexes haven't uses! Leave it here
 516          // for future checks...
 517  
 518          // Return result
 519          if (!empty($uses)) {
 520              return $uses;
 521          } else {
 522              return false;
 523          }
 524      }
 525  
 526      /**
 527       * This function will return all the errors found in one structure
 528       * looking recursively inside each table. Returns
 529       * an array of errors or false
 530       * @return mixed
 531       */
 532      public function getAllErrors() {
 533  
 534          $errors = array();
 535          // First the structure itself
 536          if ($this->getError()) {
 537              $errors[] = $this->getError();
 538          }
 539          // Delegate to tables
 540          if ($this->tables) {
 541              foreach ($this->tables as $table) {
 542                  if ($tableerrors = $table->getAllErrors()) {
 543  
 544                  }
 545              }
 546              // Add them to the errors array
 547              if ($tableerrors) {
 548                  $errors = array_merge($errors, $tableerrors);
 549              }
 550          }
 551          // Return decision
 552          if (count($errors)) {
 553              return $errors;
 554          } else {
 555              return false;
 556          }
 557      }
 558  }