334610f8b0c05a125312c77dde8b1f09503a113d
[moodle.git] / lib / xmldb / xmldb_object.php
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/>.
17 /**
18  * This class represent the XMLDB base class where all the common pieces are defined
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  */
26 defined('MOODLE_INTERNAL') || die();
29 class xmldb_object {
31     /** @var string name of obejct */
32     protected $name;
34     /** @var string comment on object */
35     protected $comment;
37     /** @var xmldb_object */
38     protected $previous;
40     /** @var xmldb_object */
41     protected $next;
43     /** @var string hash of object */
44     protected $hash;
46     /** @var bool is it loaded yet */
47     protected $loaded;
49     /** @var bool was object changed */
50     protected $changed;
52     /** @var string error message */
53     protected $errormsg;
55     /**
56      * Creates one new xmldb_object
57      * @param string $name
58      */
59     public function __construct($name) {
60         $this->name = $name;
61         $this->comment = null;
62         $this->previous = null;
63         $this->next = null;
64         $this->hash = null;
65         $this->loaded = false;
66         $this->changed = false;
67         $this->errormsg = null;
68     }
70     /**
71      * This function returns true/false, if the xmldb_object has been loaded
72      * @return bool
73      */
74     public function isLoaded() {
75         return $this->loaded;
76     }
78     /**
79      * This function returns true/false, if the xmldb_object has changed
80      * @return bool
81      */
82     public function hasChanged() {
83         return $this->changed;
84     }
86     /**
87      * This function returns the comment of one xmldb_object
88      * @return string
89      */
90     public function getComment() {
91         return $this->comment;
92     }
94     /**
95      * This function returns the hash of one xmldb_object
96      * @return string
97      */
98     public function getHash() {
99         return $this->hash;
100     }
102     /**
103      * This function will return the name of the previous xmldb_object
104      * @return xmldb_object
105      */
106     public function getPrevious() {
107         return $this->previous;
108     }
110     /**
111      * This function will return the name of the next xmldb_object
112      * @return xmldb_object
113      */
114     public function getNext() {
115         return $this->next;
116     }
118     /**
119      * This function will return the name of the xmldb_object
120      * @return string
121      */
122     public function getName() {
123         return $this->name;
124     }
126     /**
127      * This function will return the error detected in the object
128      * @return string
129      */
130     public function getError() {
131         return $this->errormsg;
132     }
134     /**
135      * This function will set the comment of the xmldb_object
136      * @param string $comment
137      */
138     public function setComment($comment) {
139         $this->comment = $comment;
140     }
142     /**
143      * This function will set the previous of the xmldb_object
144      * @param xmldb_object $previous
145      */
146     public function setPrevious($previous) {
147         $this->previous = $previous;
148     }
150     /**
151      * This function will set the next of the xmldb_object
152      * @param xmldb_object $next
153      */
154     public function setNext($next) {
155         $this->next = $next;
156     }
158     /**
159      * This function will set the hash of the xmldb_object
160      * @param string $hash
161      */
162     public function setHash($hash) {
163         $this->hash = $hash;
164     }
166     /**
167      * This function will set the loaded field of the xmldb_object
168      * @param bool $loaded
169      */
170     public function setLoaded($loaded = true) {
171         $this->loaded = $loaded;
172     }
174     /**
175      * This function will set the changed field of the xmldb_object
176      * @param bool $changed
177      */
178     public function setChanged($changed = true) {
179         $this->changed = $changed;
180     }
181     /**
182      * This function will set the name field of the xmldb_object
183      * @param string $name
184      */
185     public function setName($name) {
186         $this->name = $name;
187     }
190     /**
191      * This function will check if one key name is ok or no (true/false)
192      * only lowercase a-z, 0-9 and _ are allowed
193      * @return bool
194      */
195     public function checkName () {
196         $result = true;
198         if ($this->name != preg_replace('/[^a-z0-9_ -]/i', '', $this->name)) {
199             $result = false;
200         }
201         return $result;
202     }
204     /**
205      * This function will check that all the elements in one array
206      * have a correct name [a-z0-9_]
207      * @param array $arr
208      * @return bool
209      */
210     public function checkNameValues($arr) {
211         $result = true;
212         // TODO: Perhaps, add support for reserved words
214         // Check the name only contains valid chars
215         if ($arr) {
216             foreach($arr as $element) {
217                 if (!$element->checkName()) {
218                     $result = false;
219                 }
220             }
221         }
222         // Check there aren't duplicate names
223         if ($arr) {
224             $existing_fields = array();
225             foreach($arr as $element) {
226                 if (in_array($element->getName(), $existing_fields)) {
227                     debugging('Object ' . $element->getName() . ' is duplicated!', DEBUG_DEVELOPER);
228                     $result = false;
229                 }
230                 $existing_fields[] = $element->getName();
231             }
232         }
233         return $result;
234     }
236     /**
237      * Reconstruct previous/next attributes.
238      * @param array $arr
239      * @return bool true if $arr modified
240      */
241     public function fixPrevNext(&$arr) {
242         global $CFG;
244         if (empty($CFG->xmldbreconstructprevnext)) {
245             return false;
246         }
247         $tweaked = false;
249         $prev = null;
250         foreach ($arr as $key=>$el) {
251             $prev_value = $arr[$key]->previous;
252             $next_value = $arr[$key]->next;
254             $arr[$key]->next     = null;
255             $arr[$key]->previous = null;
256             if ($prev !== null) {
257                 $arr[$prev]->next    = $arr[$key]->name;
258                 $arr[$key]->previous = $arr[$prev]->name;
259             }
260             $prev = $key;
262             if ($prev_value != $arr[$key]->previous or $next_value != $arr[$key]->next) {
263                 $tweaked = true;
264             }
265         }
267         return $tweaked;
268     }
270     /**
271      * This function will check that all the elements in one array
272      * have a consistent info in their previous/next fields
273      * @param array $arr
274      * @return bool true means ok, false invalid prev/next present
275      */
276     public function checkPreviousNextValues($arr) {
277         global $CFG;
278         if (!empty($CFG->xmldbdisablenextprevchecking)) {
279             return true;
280         }
281         $result = true;
282         // Check that only one element has the previous not set
283         if ($arr) {
284             $counter = 0;
285             foreach($arr as $element) {
286                 if (!$element->getPrevious()) {
287                     $counter++;
288                 }
289             }
290             if ($counter != 1) {
291                 debugging('The number of objects with previous not set is different from 1', DEBUG_DEVELOPER);
292                 $result = false;
293             }
294         }
295         // Check that only one element has the next not set
296         if ($result && $arr) {
297             $counter = 0;
298             foreach($arr as $element) {
299                 if (!$element->getNext()) {
300                     $counter++;
301                 }
302             }
303             if ($counter != 1) {
304                 debugging('The number of objects with next not set is different from 1', DEBUG_DEVELOPER);
305                 $result = false;
306             }
307         }
308         // Check that all the previous elements are existing elements
309         if ($result && $arr) {
310             foreach($arr as $element) {
311                 if ($element->getPrevious()) {
312                     $i = $this->findObjectInArray($element->getPrevious(), $arr);
313                     if ($i === null) {
314                         debugging('Object ' . $element->getName() . ' says PREVIOUS="' . $element->getPrevious() . '" but that other object does not exist.', DEBUG_DEVELOPER);
315                         $result = false;
316                     }
317                 }
318             }
319         }
320         // Check that all the next elements are existing elements
321         if ($result && $arr) {
322             foreach($arr as $element) {
323                 if ($element->getNext()) {
324                     $i = $this->findObjectInArray($element->getNext(), $arr);
325                     if ($i === null) {
326                         debugging('Object ' . $element->getName() . ' says NEXT="' . $element->getNext() . '" but that other object does not exist.', DEBUG_DEVELOPER);
327                         $result = false;
328                     }
329                 }
330             }
331         }
332         // Check that there aren't duplicates in the previous values
333         if ($result && $arr) {
334             $existarr = array();
335             foreach($arr as $element) {
336                 if (in_array($element->getPrevious(), $existarr)) {
337                     $result = false;
338                     debugging('Object ' . $element->getName() . ' says PREVIOUS="' . $element->getPrevious() . '" but another object has already said that!', DEBUG_DEVELOPER);
339                 } else {
340                     $existarr[] = $element->getPrevious();
341                 }
342             }
343         }
344         // Check that there aren't duplicates in the next values
345         if ($result && $arr) {
346             $existarr = array();
347             foreach($arr as $element) {
348                 if (in_array($element->getNext(), $existarr)) {
349                     $result = false;
350                     debugging('Object ' . $element->getName() . ' says NEXT="' . $element->getNext() . '" but another object has already said that!', DEBUG_DEVELOPER);
351                 } else {
352                     $existarr[] = $element->getNext();
353                 }
354             }
355         }
356         // Check that there aren't next values pointing to themselves
357         if ($result && $arr) {
358             foreach($arr as $element) {
359                 if ($element->getNext() == $element->getName()) {
360                     $result = false;
361                     debugging('Object ' . $element->getName() . ' says NEXT="' . $element->getNext() . '" and that is wrongly recursive!', DEBUG_DEVELOPER);
362                 }
363             }
364         }
365         // Check that there aren't prev values pointing to themselves
366         if ($result && $arr) {
367             foreach($arr as $element) {
368                 if ($element->getPrevious() == $element->getName()) {
369                     $result = false;
370                     debugging('Object ' . $element->getName() . ' says PREVIOUS="' . $element->getPrevious() . '" and that is wrongly recursive!', DEBUG_DEVELOPER);
371                 }
372             }
373         }
374         return $result;
375     }
377     /**
378      * This function will order all the elements in one array, following
379      * the previous/next rules
380      * @param array $arr
381      * @return array|bool
382      */
383     public function orderElements($arr) {
384         global $CFG;
385         $result = true;
386         if (!empty($CFG->xmldbdisablenextprevchecking)) {
387             return $arr;
388         }
389         // Create a new array
390         $newarr = array();
391         if (!empty($arr)) {
392             $currentelement = null;
393             // Get the element without previous
394             foreach($arr as $key => $element) {
395                 if (!$element->getPrevious()) {
396                     $currentelement = $arr[$key];
397                     $newarr[0] = $arr[$key];
398                 }
399             }
400             if (!$currentelement) {
401                 $result = false;
402             }
403             // Follow the next rules
404             $counter = 1;
405             while ($result && $currentelement->getNext()) {
406                 $i = $this->findObjectInArray($currentelement->getNext(), $arr);
407                 $currentelement = $arr[$i];
408                 $newarr[$counter] = $arr[$i];
409                 $counter++;
410             }
411             // Compare number of elements between original and new array
412             if ($result && count($arr) != count($newarr)) {
413                 $result = false;
414             }
415             // Check that previous/next is ok (redundant but...)
416             if ($this->checkPreviousNextValues($newarr)) {
417                 $result = $newarr;
418             } else {
419                 $result = false;
420             }
421         } else {
422             $result = array();
423         }
424         return $result;
425     }
427     /**
428      * Returns the position of one object in the array.
429      * @param string $objectname
430      * @param array $arr
431      * @return mixed
432      */
433     public function findObjectInArray($objectname, $arr) {
434         foreach ($arr as $i => $object) {
435             if ($objectname == $object->getName()) {
436                 return $i;
437             }
438         }
439         return null;
440     }
442     /**
443      * This function will display a readable info about the xmldb_object
444      * (should be implemented inside each XMLDBxxx object)
445      * @return string
446      */
447     public function readableInfo() {
448         return get_class($this);
449     }
451     /**
452      * This function will perform the central debug of all the XMLDB classes
453      * being called automatically every time one error is found. Apart from
454      * the main actions performed in it (XMLDB agnostic) it looks for one
455      * function called xmldb_debug() and invokes it, passing both the
456      * message code and the whole object.
457      * So, to perform custom debugging just add such function to your libs.
458      *
459      * Call to the external hook function can be disabled by request by
460      * defining XMLDB_SKIP_DEBUG_HOOK
461      * @param string $message
462      */
463     public function debug($message) {
465         // Check for xmldb_debug($message, $xmldb_object)
466         $funcname = 'xmldb_debug';
467         // If exists and XMLDB_SKIP_DEBUG_HOOK is undefined
468         if (function_exists($funcname) && !defined('XMLDB_SKIP_DEBUG_HOOK')) {
469             $funcname($message, $this);
470         }
471     }
473     /**
474      * Returns one array of elements from one comma separated string,
475      * supporting quoted strings containing commas and concat function calls
476      * @param string $string
477      * @return array
478      */
479     public function comma2array($string) {
481         $foundquotes  = array();
482         $foundconcats = array();
484         // Extract all the concat elements from the string
485         preg_match_all("/(CONCAT\(.*?\))/is", $string, $matches);
486         foreach (array_unique($matches[0]) as $key=>$value) {
487             $foundconcats['<#'.$key.'#>'] = $value;
488         }
489         if (!empty($foundconcats)) {
490             $string = str_replace($foundconcats,array_keys($foundconcats),$string);
491         }
493         // Extract all the quoted elements from the string (skipping
494         // backslashed quotes that are part of the content.
495         preg_match_all("/(''|'.*?[^\\\\]')/is", $string, $matches);
496         foreach (array_unique($matches[0]) as $key=>$value) {
497             $foundquotes['<%'.$key.'%>'] = $value;
498         }
499         if (!empty($foundquotes)) {
500             $string = str_replace($foundquotes,array_keys($foundquotes),$string);
501         }
503         // Explode safely the string
504         $arr = explode (',', $string);
506         // Put the concat and quoted elements back again, trimming every element
507         if ($arr) {
508             foreach ($arr as $key => $element) {
509                 // Clear some spaces
510                 $element = trim($element);
511                 // Replace the quoted elements if exists
512                 if (!empty($foundquotes)) {
513                     $element = str_replace(array_keys($foundquotes), $foundquotes, $element);
514                 }
515                 // Replace the concat elements if exists
516                 if (!empty($foundconcats)) {
517                     $element = str_replace(array_keys($foundconcats), $foundconcats, $element);
518                 }
519                 // Delete any backslash used for quotes. XMLDB stuff will add them before insert
520                 $arr[$key] = str_replace("\\'", "'", $element);
521             }
522         }
524         return $arr;
525     }
527     /**
528      * Validates the definition of objects and returns error message.
529      *
530      * The error message should not be localised because it is intended for developers,
531      * end users and admins should never see these problems!
532      *
533      * @param xmldb_table $xmldb_table optional when object is table
534      * @return string null if ok, error message if problem found
535      */
536     public function validateDefinition(xmldb_table $xmldb_table=null) {
537         return null;
538     }