. /** * This class represent one XMLDB table * * @package core_xmldb * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com * 2001-3001 Eloy Lafuente (stronk7) http://contiento.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); class xmldb_table extends xmldb_object { /** @var xmldb_field[] table columns */ protected $fields; /** @var xmldb_key[] keys */ protected $keys; /** @var xmldb_index[] indexes */ protected $indexes; /** * Note: * - Oracle has 30 chars limit for all names, * 2 chars are reserved for prefix. * * @const maximum length of field names */ const NAME_MAX_LENGTH = 28; /** * Creates one new xmldb_table * @param string $name */ public function __construct($name) { parent::__construct($name); $this->fields = array(); $this->keys = array(); $this->indexes = array(); } /** * Add one field to the table, allowing to specify the desired order * If it's not specified, then the field is added at the end * @param xmldb_field $field * @param xmldb_object $after * @return xmldb_field */ public function addField($field, $after=null) { // Detect duplicates first if ($this->getField($field->getName())) { throw new coding_exception('Duplicate field '.$field->getName().' specified in table '.$this->getName()); } // Calculate the previous and next fields $prevfield = null; $nextfield = null; if (!$after) { $allfields = $this->getFields(); if (!empty($allfields)) { end($allfields); $prevfield = $allfields[key($allfields)]; } } else { $prevfield = $this->getField($after); } if ($prevfield && $prevfield->getNext()) { $nextfield = $this->getField($prevfield->getNext()); } // Set current field previous and next attributes if ($prevfield) { $field->setPrevious($prevfield->getName()); $prevfield->setNext($field->getName()); } if ($nextfield) { $field->setNext($nextfield->getName()); $nextfield->setPrevious($field->getName()); } // Some more attributes $field->setLoaded(true); $field->setChanged(true); // Add the new field $this->fields[] = $field; // Reorder the field $this->orderFields($this->fields); // Recalculate the hash $this->calculateHash(true); // We have one new field, so the table has changed $this->setChanged(true); return $field; } /** * Add one key to the table, allowing to specify the desired order * If it's not specified, then the key is added at the end * @param xmldb_key $key * @param xmldb_object $after */ public function addKey($key, $after=null) { // Detect duplicates first if ($this->getKey($key->getName())) { throw new coding_exception('Duplicate key '.$key->getName().' specified in table '.$this->getName()); } // Make sure there are no indexes with the key column specs because they would collide. $newfields = $key->getFields(); $allindexes = $this->getIndexes(); foreach ($allindexes as $index) { $fields = $index->getFields(); if ($fields === $newfields) { throw new coding_exception('Index '.$index->getName().' collides with key'.$key->getName().' specified in table '.$this->getName()); } } // Calculate the previous and next keys $prevkey = null; $nextkey = null; if (!$after) { $allkeys = $this->getKeys(); if (!empty($allkeys)) { end($allkeys); $prevkey = $allkeys[key($allkeys)]; } } else { $prevkey = $this->getKey($after); } if ($prevkey && $prevkey->getNext()) { $nextkey = $this->getKey($prevkey->getNext()); } // Set current key previous and next attributes if ($prevkey) { $key->setPrevious($prevkey->getName()); $prevkey->setNext($key->getName()); } if ($nextkey) { $key->setNext($nextkey->getName()); $nextkey->setPrevious($key->getName()); } // Some more attributes $key->setLoaded(true); $key->setChanged(true); // Add the new key $this->keys[] = $key; // Reorder the keys $this->orderKeys($this->keys); // Recalculate the hash $this->calculateHash(true); // We have one new field, so the table has changed $this->setChanged(true); } /** * Add one index to the table, allowing to specify the desired order * If it's not specified, then the index is added at the end * @param xmldb_index $index * @param xmldb_object $after */ public function addIndex($index, $after=null) { // Detect duplicates first if ($this->getIndex($index->getName())) { throw new coding_exception('Duplicate index '.$index->getName().' specified in table '.$this->getName()); } // Make sure there are no keys with the index column specs because they would collide. $newfields = $index->getFields(); $allkeys = $this->getKeys(); foreach ($allkeys as $key) { $fields = $key->getFields(); if ($fields === $newfields) { throw new coding_exception('Key '.$key->getName().' collides with index'.$index->getName().' specified in table '.$this->getName()); } } // Calculate the previous and next indexes $previndex = null; $nextindex = null; if (!$after) { $allindexes = $this->getIndexes(); if (!empty($allindexes)) { end($allindexes); $previndex = $allindexes[key($allindexes)]; } } else { $previndex = $this->getIndex($after); } if ($previndex && $previndex->getNext()) { $nextindex = $this->getIndex($previndex->getNext()); } // Set current index previous and next attributes if ($previndex) { $index->setPrevious($previndex->getName()); $previndex->setNext($index->getName()); } if ($nextindex) { $index->setNext($nextindex->getName()); $nextindex->setPrevious($index->getName()); } // Some more attributes $index->setLoaded(true); $index->setChanged(true); // Add the new index $this->indexes[] = $index; // Reorder the indexes $this->orderIndexes($this->indexes); // Recalculate the hash $this->calculateHash(true); // We have one new index, so the table has changed $this->setChanged(true); } /** * This function will return the array of fields in the table * @return xmldb_field[] */ public function getFields() { return $this->fields; } /** * This function will return the array of keys in the table * @return xmldb_key[] */ public function getKeys() { return $this->keys; } /** * This function will return the array of indexes in the table * @return xmldb_index[] */ public function getIndexes() { return $this->indexes; } /** * Returns one xmldb_field * @param string $fieldname * @return xmldb_field|null */ public function getField($fieldname) { $i = $this->findFieldInArray($fieldname); if ($i !== null) { return $this->fields[$i]; } return null; } /** * Returns the position of one field in the array. * @param string $fieldname * @return int|null index of the field, or null if not found. */ public function findFieldInArray($fieldname) { foreach ($this->fields as $i => $field) { if ($fieldname == $field->getName()) { return $i; } } return null; } /** * This function will reorder the array of fields * @return bool whether the reordering succeeded. */ public function orderFields() { $result = $this->orderElements($this->fields); if ($result) { $this->setFields($result); return true; } else { return false; } } /** * Returns one xmldb_key * @param string $keyname * @return xmldb_key|null */ public function getKey($keyname) { $i = $this->findKeyInArray($keyname); if ($i !== null) { return $this->keys[$i]; } return null; } /** * Returns the position of one key in the array. * @param string $keyname * @return int|null index of the key, or null if not found. */ public function findKeyInArray($keyname) { foreach ($this->keys as $i => $key) { if ($keyname == $key->getName()) { return $i; } } return null; } /** * This function will reorder the array of keys * @return bool whether the reordering succeeded. */ public function orderKeys() { $result = $this->orderElements($this->keys); if ($result) { $this->setKeys($result); return true; } else { return false; } } /** * Returns one xmldb_index * @param string $indexname * @return xmldb_index|null */ public function getIndex($indexname) { $i = $this->findIndexInArray($indexname); if ($i !== null) { return $this->indexes[$i]; } return null; } /** * Returns the position of one index in the array. * @param string $indexname * @return int|null index of the index, or null if not found. */ public function findIndexInArray($indexname) { foreach ($this->indexes as $i => $index) { if ($indexname == $index->getName()) { return $i; } } return null; } /** * This function will reorder the array of indexes * @return bool whether the reordering succeeded. */ public function orderIndexes() { $result = $this->orderElements($this->indexes); if ($result) { $this->setIndexes($result); return true; } else { return false; } } /** * This function will set the array of fields in the table * @param xmldb_field[] $fields */ public function setFields($fields) { $this->fields = $fields; } /** * This function will set the array of keys in the table * @param xmldb_key[] $keys */ public function setKeys($keys) { $this->keys = $keys; } /** * This function will set the array of indexes in the table * @param xmldb_index[] $indexes */ public function setIndexes($indexes) { $this->indexes = $indexes; } /** * Delete one field from the table * @param string $fieldname */ public function deleteField($fieldname) { $field = $this->getField($fieldname); if ($field) { $i = $this->findFieldInArray($fieldname); // Look for prev and next field $prevfield = $this->getField($field->getPrevious()); $nextfield = $this->getField($field->getNext()); // Change their previous and next attributes if ($prevfield) { $prevfield->setNext($field->getNext()); } if ($nextfield) { $nextfield->setPrevious($field->getPrevious()); } // Delete the field unset($this->fields[$i]); // Reorder the whole structure $this->orderFields($this->fields); // Recalculate the hash $this->calculateHash(true); // We have one deleted field, so the table has changed $this->setChanged(true); } } /** * Delete one key from the table * @param string $keyname */ public function deleteKey($keyname) { $key = $this->getKey($keyname); if ($key) { $i = $this->findKeyInArray($keyname); // Look for prev and next key $prevkey = $this->getKey($key->getPrevious()); $nextkey = $this->getKey($key->getNext()); // Change their previous and next attributes if ($prevkey) { $prevkey->setNext($key->getNext()); } if ($nextkey) { $nextkey->setPrevious($key->getPrevious()); } // Delete the key unset($this->keys[$i]); // Reorder the Keys $this->orderKeys($this->keys); // Recalculate the hash $this->calculateHash(true); // We have one deleted key, so the table has changed $this->setChanged(true); } } /** * Delete one index from the table * @param string $indexname */ public function deleteIndex($indexname) { $index = $this->getIndex($indexname); if ($index) { $i = $this->findIndexInArray($indexname); // Look for prev and next index $previndex = $this->getIndex($index->getPrevious()); $nextindex = $this->getIndex($index->getNext()); // Change their previous and next attributes if ($previndex) { $previndex->setNext($index->getNext()); } if ($nextindex) { $nextindex->setPrevious($index->getPrevious()); } // Delete the index unset($this->indexes[$i]); // Reorder the indexes $this->orderIndexes($this->indexes); // Recalculate the hash $this->calculateHash(true); // We have one deleted index, so the table has changed $this->setChanged(true); } } /** * Load data from XML to the table * @param array $xmlarr * @return bool success */ public function arr2xmldb_table($xmlarr) { global $CFG; $result = true; // Debug the table // traverse_xmlize($xmlarr); //Debug // print_object ($GLOBALS['traverse_array']); //Debug // $GLOBALS['traverse_array']=""; //Debug // Process table attributes (name, comment, previoustable and nexttable) if (isset($xmlarr['@']['NAME'])) { $this->name = trim($xmlarr['@']['NAME']); } else { $this->errormsg = 'Missing NAME attribute'; $this->debug($this->errormsg); $result = false; } if (isset($xmlarr['@']['COMMENT'])) { $this->comment = trim($xmlarr['@']['COMMENT']); } else if (!empty($CFG->xmldbdisablecommentchecking)) { $this->comment = ''; } else { $this->errormsg = 'Missing COMMENT attribute'; $this->debug($this->errormsg); $result = false; } // Iterate over fields if (isset($xmlarr['#']['FIELDS']['0']['#']['FIELD'])) { foreach ($xmlarr['#']['FIELDS']['0']['#']['FIELD'] as $xmlfield) { if (!$result) { //Skip on error continue; } $name = trim($xmlfield['@']['NAME']); $field = new xmldb_field($name); $field->arr2xmldb_field($xmlfield); $this->fields[] = $field; if (!$field->isLoaded()) { $this->errormsg = 'Problem loading field ' . $name; $this->debug($this->errormsg); $result = false; } } } else { $this->errormsg = 'Missing FIELDS section'; $this->debug($this->errormsg); $result = false; } // Perform some general checks over fields if ($result && $this->fields) { // Check field names are ok (lowercase, a-z _-) if (!$this->checkNameValues($this->fields)) { $this->errormsg = 'Some FIELDS name values are incorrect'; $this->debug($this->errormsg); $result = false; } // Compute prev/next. $this->fixPrevNext($this->fields); // Order fields if ($result && !$this->orderFields($this->fields)) { $this->errormsg = 'Error ordering the fields'; $this->debug($this->errormsg); $result = false; } } // Iterate over keys if (isset($xmlarr['#']['KEYS']['0']['#']['KEY'])) { foreach ($xmlarr['#']['KEYS']['0']['#']['KEY'] as $xmlkey) { if (!$result) { //Skip on error continue; } $name = trim($xmlkey['@']['NAME']); $key = new xmldb_key($name); $key->arr2xmldb_key($xmlkey); $this->keys[] = $key; if (!$key->isLoaded()) { $this->errormsg = 'Problem loading key ' . $name; $this->debug($this->errormsg); $result = false; } } } else { $this->errormsg = 'Missing KEYS section (at least one PK must exist)'; $this->debug($this->errormsg); $result = false; } // Perform some general checks over keys if ($result && $this->keys) { // Check keys names are ok (lowercase, a-z _-) if (!$this->checkNameValues($this->keys)) { $this->errormsg = 'Some KEYS name values are incorrect'; $this->debug($this->errormsg); $result = false; } // Compute prev/next. $this->fixPrevNext($this->keys); // Order keys if ($result && !$this->orderKeys($this->keys)) { $this->errormsg = 'Error ordering the keys'; $this->debug($this->errormsg); $result = false; } // TODO: Only one PK // TODO: Not keys with repeated fields // TODO: Check fields and reffieds exist in table } // Iterate over indexes if (isset($xmlarr['#']['INDEXES']['0']['#']['INDEX'])) { foreach ($xmlarr['#']['INDEXES']['0']['#']['INDEX'] as $xmlindex) { if (!$result) { //Skip on error continue; } $name = trim($xmlindex['@']['NAME']); $index = new xmldb_index($name); $index->arr2xmldb_index($xmlindex); $this->indexes[] = $index; if (!$index->isLoaded()) { $this->errormsg = 'Problem loading index ' . $name; $this->debug($this->errormsg); $result = false; } } } // Perform some general checks over indexes if ($result && $this->indexes) { // Check field names are ok (lowercase, a-z _-) if (!$this->checkNameValues($this->indexes)) { $this->errormsg = 'Some INDEXES name values are incorrect'; $this->debug($this->errormsg); $result = false; } // Compute prev/next. $this->fixPrevNext($this->indexes); // Order indexes if ($result && !$this->orderIndexes($this->indexes)) { $this->errormsg = 'Error ordering the indexes'; $this->debug($this->errormsg); $result = false; } // TODO: Not indexes with repeated fields // TODO: Check fields exist in table } // Set some attributes if ($result) { $this->loaded = true; } $this->calculateHash(); return $result; } /** * This function calculate and set the hash of one xmldb_table * @param bool $recursive */ public function calculateHash($recursive = false) { if (!$this->loaded) { $this->hash = null; } else { $key = $this->name . $this->comment; if ($this->fields) { foreach ($this->fields as $fie) { $field = $this->getField($fie->getName()); if ($recursive) { $field->calculateHash($recursive); } $key .= $field->getHash(); } } if ($this->keys) { foreach ($this->keys as $ke) { $k = $this->getKey($ke->getName()); if ($recursive) { $k->calculateHash($recursive); } $key .= $k->getHash(); } } if ($this->indexes) { foreach ($this->indexes as $in) { $index = $this->getIndex($in->getName()); if ($recursive) { $index->calculateHash($recursive); } $key .= $index->getHash(); } } $this->hash = md5($key); } } /** * Validates the table restrictions (does not validate child elements). * * The error message should not be localised because it is intended for developers, * end users and admins should never see these problems! * * @param xmldb_table $xmldb_table optional when object is table * @return string null if ok, error message if problem found */ public function validateDefinition(xmldb_table $xmldb_table=null) { // table parameter is ignored $name = $this->getName(); if (strlen($name) > self::NAME_MAX_LENGTH) { return 'Invalid table name {'.$name.'}: name is too long. Limit is '.self::NAME_MAX_LENGTH.' chars.'; } if (!preg_match('/^[a-z][a-z0-9_]*$/', $name)) { return 'Invalid table name {'.$name.'}: name includes invalid characters.'; } return null; } /** * This function will output the XML text for one table * @return string */ public function xmlOutput() { $o = ''; $o.= '