1<?php
2// Copyright (c) 2004 ars Cognita Inc., all rights reserved
3/* ******************************************************************************
4    Released under both BSD license and Lesser GPL library license.
5 	Whenever there is any discrepancy between the two licenses,
6 	the BSD license will take precedence.
7*******************************************************************************/
8/**
9 * xmlschema is a class that allows the user to quickly and easily
10 * build a database on any ADOdb-supported platform using a simple
11 * XML schema.
12 *
13 * Last Editor: $Author: jlim $
14 * @author Richard Tango-Lowy & Dan Cech
15 * @version $Revision: 1.12 $
16 *
17 * @package axmls
18 * @tutorial getting_started.pkg
19 */
20
21function _file_get_contents($file)
22{
23 	if (function_exists('file_get_contents')) return file_get_contents($file);
24
25	$f = fopen($file,'r');
26	if (!$f) return '';
27	$t = '';
28
29	while ($s = fread($f,100000)) $t .= $s;
30	fclose($f);
31	return $t;
32}
33
34
35/**
36* Debug on or off
37*/
38if( !defined( 'XMLS_DEBUG' ) ) {
39	define( 'XMLS_DEBUG', FALSE );
40}
41
42/**
43* Default prefix key
44*/
45if( !defined( 'XMLS_PREFIX' ) ) {
46	define( 'XMLS_PREFIX', '%%P' );
47}
48
49/**
50* Maximum length allowed for object prefix
51*/
52if( !defined( 'XMLS_PREFIX_MAXLEN' ) ) {
53	define( 'XMLS_PREFIX_MAXLEN', 10 );
54}
55
56/**
57* Execute SQL inline as it is generated
58*/
59if( !defined( 'XMLS_EXECUTE_INLINE' ) ) {
60	define( 'XMLS_EXECUTE_INLINE', FALSE );
61}
62
63/**
64* Continue SQL Execution if an error occurs?
65*/
66if( !defined( 'XMLS_CONTINUE_ON_ERROR' ) ) {
67	define( 'XMLS_CONTINUE_ON_ERROR', FALSE );
68}
69
70/**
71* Current Schema Version
72*/
73if( !defined( 'XMLS_SCHEMA_VERSION' ) ) {
74	define( 'XMLS_SCHEMA_VERSION', '0.2' );
75}
76
77/**
78* Default Schema Version.  Used for Schemas without an explicit version set.
79*/
80if( !defined( 'XMLS_DEFAULT_SCHEMA_VERSION' ) ) {
81	define( 'XMLS_DEFAULT_SCHEMA_VERSION', '0.1' );
82}
83
84/**
85* Default Schema Version.  Used for Schemas without an explicit version set.
86*/
87if( !defined( 'XMLS_DEFAULT_UPGRADE_METHOD' ) ) {
88	define( 'XMLS_DEFAULT_UPGRADE_METHOD', 'ALTER' );
89}
90
91/**
92* Include the main ADODB library
93*/
94if( !defined( '_ADODB_LAYER' ) ) {
95	require( 'adodb.inc.php' );
96	require( 'adodb-datadict.inc.php' );
97}
98
99/**
100* Abstract DB Object. This class provides basic methods for database objects, such
101* as tables and indexes.
102*
103* @package axmls
104* @access private
105*/
106class dbObject {
107
108	/**
109	* var object Parent
110	*/
111	var $parent;
112
113	/**
114	* var string current element
115	*/
116	var $currentElement;
117
118	/**
119	* NOP
120	*/
121	function __construct( &$parent, $attributes = NULL ) {
122		$this->parent = $parent;
123	}
124
125	/**
126	* XML Callback to process start elements
127	*
128	* @access private
129	*/
130	function _tag_open( &$parser, $tag, $attributes ) {
131
132	}
133
134	/**
135	* XML Callback to process CDATA elements
136	*
137	* @access private
138	*/
139	function _tag_cdata( &$parser, $cdata ) {
140
141	}
142
143	/**
144	* XML Callback to process end elements
145	*
146	* @access private
147	*/
148	function _tag_close( &$parser, $tag ) {
149
150	}
151
152	function create(&$xmls) {
153		return array();
154	}
155
156	/**
157	* Destroys the object
158	*/
159	function destroy() {
160	}
161
162	/**
163	* Checks whether the specified RDBMS is supported by the current
164	* database object or its ranking ancestor.
165	*
166	* @param string $platform RDBMS platform name (from ADODB platform list).
167	* @return boolean TRUE if RDBMS is supported; otherwise returns FALSE.
168	*/
169	function supportedPlatform( $platform = NULL ) {
170		return is_object( $this->parent ) ? $this->parent->supportedPlatform( $platform ) : TRUE;
171	}
172
173	/**
174	* Returns the prefix set by the ranking ancestor of the database object.
175	*
176	* @param string $name Prefix string.
177	* @return string Prefix.
178	*/
179	function prefix( $name = '' ) {
180		return is_object( $this->parent ) ? $this->parent->prefix( $name ) : $name;
181	}
182
183	/**
184	* Extracts a field ID from the specified field.
185	*
186	* @param string $field Field.
187	* @return string Field ID.
188	*/
189	function FieldID( $field ) {
190		return strtoupper( preg_replace( '/^`(.+)`$/', '$1', $field ) );
191	}
192}
193
194/**
195* Creates a table object in ADOdb's datadict format
196*
197* This class stores information about a database table. As charactaristics
198* of the table are loaded from the external source, methods and properties
199* of this class are used to build up the table description in ADOdb's
200* datadict format.
201*
202* @package axmls
203* @access private
204*/
205class dbTable extends dbObject {
206
207	/**
208	* @var string Table name
209	*/
210	var $name;
211
212	/**
213	* @var array Field specifier: Meta-information about each field
214	*/
215	var $fields = array();
216
217	/**
218	* @var array List of table indexes.
219	*/
220	var $indexes = array();
221
222	/**
223	* @var array Table options: Table-level options
224	*/
225	var $opts = array();
226
227	/**
228	* @var string Field index: Keeps track of which field is currently being processed
229	*/
230	var $current_field;
231
232	/**
233	* @var boolean Mark table for destruction
234	* @access private
235	*/
236	var $drop_table;
237
238	/**
239	* @var boolean Mark field for destruction (not yet implemented)
240	* @access private
241	*/
242	var $drop_field = array();
243
244	/**
245	* Iniitializes a new table object.
246	*
247	* @param string $prefix DB Object prefix
248	* @param array $attributes Array of table attributes.
249	*/
250	function __construct( &$parent, $attributes = NULL ) {
251		$this->parent = $parent;
252		$this->name = $this->prefix($attributes['NAME']);
253	}
254
255	/**
256	* XML Callback to process start elements. Elements currently
257	* processed are: INDEX, DROP, FIELD, KEY, NOTNULL, AUTOINCREMENT & DEFAULT.
258	*
259	* @access private
260	*/
261	function _tag_open( &$parser, $tag, $attributes ) {
262		$this->currentElement = strtoupper( $tag );
263
264		switch( $this->currentElement ) {
265			case 'INDEX':
266				if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
267					$index = $this->addIndex( $attributes );
268					xml_set_object( $parser,  $index );
269				}
270				break;
271			case 'DATA':
272				if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
273					$data = $this->addData( $attributes );
274					xml_set_object( $parser, $data );
275				}
276				break;
277			case 'DROP':
278				$this->drop();
279				break;
280			case 'FIELD':
281				// Add a field
282				$fieldName = $attributes['NAME'];
283				$fieldType = $attributes['TYPE'];
284				$fieldSize = isset( $attributes['SIZE'] ) ? $attributes['SIZE'] : NULL;
285				$fieldOpts = isset( $attributes['OPTS'] ) ? $attributes['OPTS'] : NULL;
286
287				$this->addField( $fieldName, $fieldType, $fieldSize, $fieldOpts );
288				break;
289			case 'KEY':
290			case 'NOTNULL':
291			case 'AUTOINCREMENT':
292				// Add a field option
293				$this->addFieldOpt( $this->current_field, $this->currentElement );
294				break;
295			case 'DEFAULT':
296				// Add a field option to the table object
297
298				// Work around ADOdb datadict issue that misinterprets empty strings.
299				if( $attributes['VALUE'] == '' ) {
300					$attributes['VALUE'] = " '' ";
301				}
302
303				$this->addFieldOpt( $this->current_field, $this->currentElement, $attributes['VALUE'] );
304				break;
305			case 'DEFDATE':
306			case 'DEFTIMESTAMP':
307				// Add a field option to the table object
308				$this->addFieldOpt( $this->current_field, $this->currentElement );
309				break;
310			default:
311				// print_r( array( $tag, $attributes ) );
312		}
313	}
314
315	/**
316	* XML Callback to process CDATA elements
317	*
318	* @access private
319	*/
320	function _tag_cdata( &$parser, $cdata ) {
321		switch( $this->currentElement ) {
322			// Table constraint
323			case 'CONSTRAINT':
324				if( isset( $this->current_field ) ) {
325					$this->addFieldOpt( $this->current_field, $this->currentElement, $cdata );
326				} else {
327					$this->addTableOpt( $cdata );
328				}
329				break;
330			// Table option
331			case 'OPT':
332				$this->addTableOpt( $cdata );
333				break;
334			default:
335
336		}
337	}
338
339	/**
340	* XML Callback to process end elements
341	*
342	* @access private
343	*/
344	function _tag_close( &$parser, $tag ) {
345		$this->currentElement = '';
346
347		switch( strtoupper( $tag ) ) {
348			case 'TABLE':
349				$this->parent->addSQL( $this->create( $this->parent ) );
350				xml_set_object( $parser, $this->parent );
351				$this->destroy();
352				break;
353			case 'FIELD':
354				unset($this->current_field);
355				break;
356
357		}
358	}
359
360	/**
361	* Adds an index to a table object
362	*
363	* @param array $attributes Index attributes
364	* @return object dbIndex object
365	*/
366	function addIndex( $attributes ) {
367		$name = strtoupper( $attributes['NAME'] );
368		$this->indexes[$name] = new dbIndex( $this, $attributes );
369		return $this->indexes[$name];
370	}
371
372	/**
373	* Adds data to a table object
374	*
375	* @param array $attributes Data attributes
376	* @return object dbData object
377	*/
378	function addData( $attributes ) {
379		if( !isset( $this->data ) ) {
380			$this->data = new dbData( $this, $attributes );
381		}
382		return $this->data;
383	}
384
385	/**
386	* Adds a field to a table object
387	*
388	* $name is the name of the table to which the field should be added.
389	* $type is an ADODB datadict field type. The following field types
390	* are supported as of ADODB 3.40:
391	* 	- C:  varchar
392	*	- X:  CLOB (character large object) or largest varchar size
393	*	   if CLOB is not supported
394	*	- C2: Multibyte varchar
395	*	- X2: Multibyte CLOB
396	*	- B:  BLOB (binary large object)
397	*	- D:  Date (some databases do not support this, and we return a datetime type)
398	*	- T:  Datetime or Timestamp
399	*	- L:  Integer field suitable for storing booleans (0 or 1)
400	*	- I:  Integer (mapped to I4)
401	*	- I1: 1-byte integer
402	*	- I2: 2-byte integer
403	*	- I4: 4-byte integer
404	*	- I8: 8-byte integer
405	*	- F:  Floating point number
406	*	- N:  Numeric or decimal number
407	*
408	* @param string $name Name of the table to which the field will be added.
409	* @param string $type	ADODB datadict field type.
410	* @param string $size	Field size
411	* @param array $opts	Field options array
412	* @return array Field specifier array
413	*/
414	function addField( $name, $type, $size = NULL, $opts = NULL ) {
415		$field_id = $this->FieldID( $name );
416
417		// Set the field index so we know where we are
418		$this->current_field = $field_id;
419
420		// Set the field name (required)
421		$this->fields[$field_id]['NAME'] = $name;
422
423		// Set the field type (required)
424		$this->fields[$field_id]['TYPE'] = $type;
425
426		// Set the field size (optional)
427		if( isset( $size ) ) {
428			$this->fields[$field_id]['SIZE'] = $size;
429		}
430
431		// Set the field options
432		if( isset( $opts ) ) {
433			$this->fields[$field_id]['OPTS'][] = $opts;
434		}
435	}
436
437	/**
438	* Adds a field option to the current field specifier
439	*
440	* This method adds a field option allowed by the ADOdb datadict
441	* and appends it to the given field.
442	*
443	* @param string $field	Field name
444	* @param string $opt ADOdb field option
445	* @param mixed $value Field option value
446	* @return array Field specifier array
447	*/
448	function addFieldOpt( $field, $opt, $value = NULL ) {
449		if( !isset( $value ) ) {
450			$this->fields[$this->FieldID( $field )]['OPTS'][] = $opt;
451		// Add the option and value
452		} else {
453			$this->fields[$this->FieldID( $field )]['OPTS'][] = array( $opt => $value );
454		}
455	}
456
457	/**
458	* Adds an option to the table
459	*
460	* This method takes a comma-separated list of table-level options
461	* and appends them to the table object.
462	*
463	* @param string $opt Table option
464	* @return array Options
465	*/
466	function addTableOpt( $opt ) {
467		if(isset($this->currentPlatform)) {
468			$this->opts[$this->parent->db->databaseType] = $opt;
469		}
470		return $this->opts;
471	}
472
473
474	/**
475	* Generates the SQL that will create the table in the database
476	*
477	* @param object $xmls adoSchema object
478	* @return array Array containing table creation SQL
479	*/
480	function create( &$xmls ) {
481		$sql = array();
482
483		// drop any existing indexes
484		if( is_array( $legacy_indexes = $xmls->dict->MetaIndexes( $this->name ) ) ) {
485			foreach( $legacy_indexes as $index => $index_details ) {
486				$sql[] = $xmls->dict->DropIndexSQL( $index, $this->name );
487			}
488		}
489
490		// remove fields to be dropped from table object
491		foreach( $this->drop_field as $field ) {
492			unset( $this->fields[$field] );
493		}
494
495		// if table exists
496		if( is_array( $legacy_fields = $xmls->dict->MetaColumns( $this->name ) ) ) {
497			// drop table
498			if( $this->drop_table ) {
499				$sql[] = $xmls->dict->DropTableSQL( $this->name );
500
501				return $sql;
502			}
503
504			// drop any existing fields not in schema
505			foreach( $legacy_fields as $field_id => $field ) {
506				if( !isset( $this->fields[$field_id] ) ) {
507					$sql[] = $xmls->dict->DropColumnSQL( $this->name, '`'.$field->name.'`' );
508				}
509			}
510		// if table doesn't exist
511		} else {
512			if( $this->drop_table ) {
513				return $sql;
514			}
515
516			$legacy_fields = array();
517		}
518
519		// Loop through the field specifier array, building the associative array for the field options
520		$fldarray = array();
521
522		foreach( $this->fields as $field_id => $finfo ) {
523			// Set an empty size if it isn't supplied
524			if( !isset( $finfo['SIZE'] ) ) {
525				$finfo['SIZE'] = '';
526			}
527
528			// Initialize the field array with the type and size
529			$fldarray[$field_id] = array(
530				'NAME' => $finfo['NAME'],
531				'TYPE' => $finfo['TYPE'],
532				'SIZE' => $finfo['SIZE']
533			);
534
535			// Loop through the options array and add the field options.
536			if( isset( $finfo['OPTS'] ) ) {
537				foreach( $finfo['OPTS'] as $opt ) {
538					// Option has an argument.
539					if( is_array( $opt ) ) {
540						$key = key( $opt );
541						$value = $opt[key( $opt )];
542						@$fldarray[$field_id][$key] .= $value;
543					// Option doesn't have arguments
544					} else {
545						$fldarray[$field_id][$opt] = $opt;
546					}
547				}
548			}
549		}
550
551		if( empty( $legacy_fields ) ) {
552			// Create the new table
553			$sql[] = $xmls->dict->CreateTableSQL( $this->name, $fldarray, $this->opts );
554			logMsg( end( $sql ), 'Generated CreateTableSQL' );
555		} else {
556			// Upgrade an existing table
557			logMsg( "Upgrading {$this->name} using '{$xmls->upgrade}'" );
558			switch( $xmls->upgrade ) {
559				// Use ChangeTableSQL
560				case 'ALTER':
561					logMsg( 'Generated ChangeTableSQL (ALTERing table)' );
562					$sql[] = $xmls->dict->ChangeTableSQL( $this->name, $fldarray, $this->opts );
563					break;
564				case 'REPLACE':
565					logMsg( 'Doing upgrade REPLACE (testing)' );
566					$sql[] = $xmls->dict->DropTableSQL( $this->name );
567					$sql[] = $xmls->dict->CreateTableSQL( $this->name, $fldarray, $this->opts );
568					break;
569				// ignore table
570				default:
571					return array();
572			}
573		}
574
575		foreach( $this->indexes as $index ) {
576			$sql[] = $index->create( $xmls );
577		}
578
579		if( isset( $this->data ) ) {
580			$sql[] = $this->data->create( $xmls );
581		}
582
583		return $sql;
584	}
585
586	/**
587	* Marks a field or table for destruction
588	*/
589	function drop() {
590		if( isset( $this->current_field ) ) {
591			// Drop the current field
592			logMsg( "Dropping field '{$this->current_field}' from table '{$this->name}'" );
593			// $this->drop_field[$this->current_field] = $xmls->dict->DropColumnSQL( $this->name, $this->current_field );
594			$this->drop_field[$this->current_field] = $this->current_field;
595		} else {
596			// Drop the current table
597			logMsg( "Dropping table '{$this->name}'" );
598			// $this->drop_table = $xmls->dict->DropTableSQL( $this->name );
599			$this->drop_table = TRUE;
600		}
601	}
602}
603
604/**
605* Creates an index object in ADOdb's datadict format
606*
607* This class stores information about a database index. As charactaristics
608* of the index are loaded from the external source, methods and properties
609* of this class are used to build up the index description in ADOdb's
610* datadict format.
611*
612* @package axmls
613* @access private
614*/
615class dbIndex extends dbObject {
616
617	/**
618	* @var string	Index name
619	*/
620	var $name;
621
622	/**
623	* @var array	Index options: Index-level options
624	*/
625	var $opts = array();
626
627	/**
628	* @var array	Indexed fields: Table columns included in this index
629	*/
630	var $columns = array();
631
632	/**
633	* @var boolean Mark index for destruction
634	* @access private
635	*/
636	var $drop = FALSE;
637
638	/**
639	* Initializes the new dbIndex object.
640	*
641	* @param object $parent Parent object
642	* @param array $attributes Attributes
643	*
644	* @internal
645	*/
646	function __construct( &$parent, $attributes = NULL ) {
647		$this->parent = $parent;
648
649		$this->name = $this->prefix ($attributes['NAME']);
650	}
651
652	/**
653	* XML Callback to process start elements
654	*
655	* Processes XML opening tags.
656	* Elements currently processed are: DROP, CLUSTERED, BITMAP, UNIQUE, FULLTEXT & HASH.
657	*
658	* @access private
659	*/
660	function _tag_open( &$parser, $tag, $attributes ) {
661		$this->currentElement = strtoupper( $tag );
662
663		switch( $this->currentElement ) {
664			case 'DROP':
665				$this->drop();
666				break;
667			case 'CLUSTERED':
668			case 'BITMAP':
669			case 'UNIQUE':
670			case 'FULLTEXT':
671			case 'HASH':
672				// Add index Option
673				$this->addIndexOpt( $this->currentElement );
674				break;
675			default:
676				// print_r( array( $tag, $attributes ) );
677		}
678	}
679
680	/**
681	* XML Callback to process CDATA elements
682	*
683	* Processes XML cdata.
684	*
685	* @access private
686	*/
687	function _tag_cdata( &$parser, $cdata ) {
688		switch( $this->currentElement ) {
689			// Index field name
690			case 'COL':
691				$this->addField( $cdata );
692				break;
693			default:
694
695		}
696	}
697
698	/**
699	* XML Callback to process end elements
700	*
701	* @access private
702	*/
703	function _tag_close( &$parser, $tag ) {
704		$this->currentElement = '';
705
706		switch( strtoupper( $tag ) ) {
707			case 'INDEX':
708				xml_set_object( $parser, $this->parent );
709				break;
710		}
711	}
712
713	/**
714	* Adds a field to the index
715	*
716	* @param string $name Field name
717	* @return string Field list
718	*/
719	function addField( $name ) {
720		$this->columns[$this->FieldID( $name )] = $name;
721
722		// Return the field list
723		return $this->columns;
724	}
725
726	/**
727	* Adds options to the index
728	*
729	* @param string $opt Comma-separated list of index options.
730	* @return string Option list
731	*/
732	function addIndexOpt( $opt ) {
733		$this->opts[] = $opt;
734
735		// Return the options list
736		return $this->opts;
737	}
738
739	/**
740	* Generates the SQL that will create the index in the database
741	*
742	* @param object $xmls adoSchema object
743	* @return array Array containing index creation SQL
744	*/
745	function create( &$xmls ) {
746		if( $this->drop ) {
747			return NULL;
748		}
749
750		// eliminate any columns that aren't in the table
751		foreach( $this->columns as $id => $col ) {
752			if( !isset( $this->parent->fields[$id] ) ) {
753				unset( $this->columns[$id] );
754			}
755		}
756
757		return $xmls->dict->CreateIndexSQL( $this->name, $this->parent->name, $this->columns, $this->opts );
758	}
759
760	/**
761	* Marks an index for destruction
762	*/
763	function drop() {
764		$this->drop = TRUE;
765	}
766}
767
768/**
769* Creates a data object in ADOdb's datadict format
770*
771* This class stores information about table data.
772*
773* @package axmls
774* @access private
775*/
776class dbData extends dbObject {
777
778	var $data = array();
779
780	var $row;
781
782	/**
783	* Initializes the new dbIndex object.
784	*
785	* @param object $parent Parent object
786	* @param array $attributes Attributes
787	*
788	* @internal
789	*/
790	function __construct( &$parent, $attributes = NULL ) {
791		$this->parent = $parent;
792	}
793
794	/**
795	* XML Callback to process start elements
796	*
797	* Processes XML opening tags.
798	* Elements currently processed are: DROP, CLUSTERED, BITMAP, UNIQUE, FULLTEXT & HASH.
799	*
800	* @access private
801	*/
802	function _tag_open( &$parser, $tag, $attributes ) {
803		$this->currentElement = strtoupper( $tag );
804
805		switch( $this->currentElement ) {
806			case 'ROW':
807				$this->row = count( $this->data );
808				$this->data[$this->row] = array();
809				break;
810			case 'F':
811				$this->addField($attributes);
812			default:
813				// print_r( array( $tag, $attributes ) );
814		}
815	}
816
817	/**
818	* XML Callback to process CDATA elements
819	*
820	* Processes XML cdata.
821	*
822	* @access private
823	*/
824	function _tag_cdata( &$parser, $cdata ) {
825		switch( $this->currentElement ) {
826			// Index field name
827			case 'F':
828				$this->addData( $cdata );
829				break;
830			default:
831
832		}
833	}
834
835	/**
836	* XML Callback to process end elements
837	*
838	* @access private
839	*/
840	function _tag_close( &$parser, $tag ) {
841		$this->currentElement = '';
842
843		switch( strtoupper( $tag ) ) {
844			case 'DATA':
845				xml_set_object( $parser, $this->parent );
846				break;
847		}
848	}
849
850	/**
851	* Adds a field to the index
852	*
853	* @param string $name Field name
854	* @return string Field list
855	*/
856	function addField( $attributes ) {
857		if( isset( $attributes['NAME'] ) ) {
858			$name = $attributes['NAME'];
859		} else {
860			$name = count($this->data[$this->row]);
861		}
862
863		// Set the field index so we know where we are
864		$this->current_field = $this->FieldID( $name );
865	}
866
867	/**
868	* Adds options to the index
869	*
870	* @param string $opt Comma-separated list of index options.
871	* @return string Option list
872	*/
873	function addData( $cdata ) {
874		if( !isset( $this->data[$this->row] ) ) {
875			$this->data[$this->row] = array();
876		}
877
878		if( !isset( $this->data[$this->row][$this->current_field] ) ) {
879			$this->data[$this->row][$this->current_field] = '';
880		}
881
882		$this->data[$this->row][$this->current_field] .= $cdata;
883	}
884
885	/**
886	* Generates the SQL that will create the index in the database
887	*
888	* @param object $xmls adoSchema object
889	* @return array Array containing index creation SQL
890	*/
891	function create( &$xmls ) {
892		$table = $xmls->dict->TableName($this->parent->name);
893		$table_field_count = count($this->parent->fields);
894		$sql = array();
895
896		// eliminate any columns that aren't in the table
897		foreach( $this->data as $row ) {
898			$table_fields = $this->parent->fields;
899			$fields = array();
900
901			foreach( $row as $field_id => $field_data ) {
902				if( !array_key_exists( $field_id, $table_fields ) ) {
903					if( is_numeric( $field_id ) ) {
904						$field_id = reset( array_keys( $table_fields ) );
905					} else {
906						continue;
907					}
908				}
909
910				$name = $table_fields[$field_id]['NAME'];
911
912				switch( $table_fields[$field_id]['TYPE'] ) {
913					case 'C':
914					case 'C2':
915					case 'X':
916					case 'X2':
917						$fields[$name] = $xmls->db->qstr( $field_data );
918						break;
919					case 'I':
920					case 'I1':
921					case 'I2':
922					case 'I4':
923					case 'I8':
924						$fields[$name] = intval($field_data);
925						break;
926					default:
927						$fields[$name] = $field_data;
928				}
929
930				unset($table_fields[$field_id]);
931			}
932
933			// check that at least 1 column is specified
934			if( empty( $fields ) ) {
935				continue;
936			}
937
938			// check that no required columns are missing
939			if( count( $fields ) < $table_field_count ) {
940				foreach( $table_fields as $field ) {
941					if (isset( $field['OPTS'] ))
942						if( ( in_array( 'NOTNULL', $field['OPTS'] ) || in_array( 'KEY', $field['OPTS'] ) ) && !in_array( 'AUTOINCREMENT', $field['OPTS'] ) ) {
943							continue(2);
944						}
945				}
946			}
947
948			$sql[] = 'INSERT INTO '. $table .' ('. implode( ',', array_keys( $fields ) ) .') VALUES ('. implode( ',', $fields ) .')';
949		}
950
951		return $sql;
952	}
953}
954
955/**
956* Creates the SQL to execute a list of provided SQL queries
957*
958* @package axmls
959* @access private
960*/
961class dbQuerySet extends dbObject {
962
963	/**
964	* @var array	List of SQL queries
965	*/
966	var $queries = array();
967
968	/**
969	* @var string	String used to build of a query line by line
970	*/
971	var $query;
972
973	/**
974	* @var string	Query prefix key
975	*/
976	var $prefixKey = '';
977
978	/**
979	* @var boolean	Auto prefix enable (TRUE)
980	*/
981	var $prefixMethod = 'AUTO';
982
983	/**
984	* Initializes the query set.
985	*
986	* @param object $parent Parent object
987	* @param array $attributes Attributes
988	*/
989	function __construct( &$parent, $attributes = NULL ) {
990		$this->parent = $parent;
991
992		// Overrides the manual prefix key
993		if( isset( $attributes['KEY'] ) ) {
994			$this->prefixKey = $attributes['KEY'];
995		}
996
997		$prefixMethod = isset( $attributes['PREFIXMETHOD'] ) ? strtoupper( trim( $attributes['PREFIXMETHOD'] ) ) : '';
998
999		// Enables or disables automatic prefix prepending
1000		switch( $prefixMethod ) {
1001			case 'AUTO':
1002				$this->prefixMethod = 'AUTO';
1003				break;
1004			case 'MANUAL':
1005				$this->prefixMethod = 'MANUAL';
1006				break;
1007			case 'NONE':
1008				$this->prefixMethod = 'NONE';
1009				break;
1010		}
1011	}
1012
1013	/**
1014	* XML Callback to process start elements. Elements currently
1015	* processed are: QUERY.
1016	*
1017	* @access private
1018	*/
1019	function _tag_open( &$parser, $tag, $attributes ) {
1020		$this->currentElement = strtoupper( $tag );
1021
1022		switch( $this->currentElement ) {
1023			case 'QUERY':
1024				// Create a new query in a SQL queryset.
1025				// Ignore this query set if a platform is specified and it's different than the
1026				// current connection platform.
1027				if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
1028					$this->newQuery();
1029				} else {
1030					$this->discardQuery();
1031				}
1032				break;
1033			default:
1034				// print_r( array( $tag, $attributes ) );
1035		}
1036	}
1037
1038	/**
1039	* XML Callback to process CDATA elements
1040	*/
1041	function _tag_cdata( &$parser, $cdata ) {
1042		switch( $this->currentElement ) {
1043			// Line of queryset SQL data
1044			case 'QUERY':
1045				$this->buildQuery( $cdata );
1046				break;
1047			default:
1048
1049		}
1050	}
1051
1052	/**
1053	* XML Callback to process end elements
1054	*
1055	* @access private
1056	*/
1057	function _tag_close( &$parser, $tag ) {
1058		$this->currentElement = '';
1059
1060		switch( strtoupper( $tag ) ) {
1061			case 'QUERY':
1062				// Add the finished query to the open query set.
1063				$this->addQuery();
1064				break;
1065			case 'SQL':
1066				$this->parent->addSQL( $this->create( $this->parent ) );
1067				xml_set_object( $parser, $this->parent );
1068				$this->destroy();
1069				break;
1070			default:
1071
1072		}
1073	}
1074
1075	/**
1076	* Re-initializes the query.
1077	*
1078	* @return boolean TRUE
1079	*/
1080	function newQuery() {
1081		$this->query = '';
1082
1083		return TRUE;
1084	}
1085
1086	/**
1087	* Discards the existing query.
1088	*
1089	* @return boolean TRUE
1090	*/
1091	function discardQuery() {
1092		unset( $this->query );
1093
1094		return TRUE;
1095	}
1096
1097	/**
1098	* Appends a line to a query that is being built line by line
1099	*
1100	* @param string $data Line of SQL data or NULL to initialize a new query
1101	* @return string SQL query string.
1102	*/
1103	function buildQuery( $sql = NULL ) {
1104		if( !isset( $this->query ) OR empty( $sql ) ) {
1105			return FALSE;
1106		}
1107
1108		$this->query .= $sql;
1109
1110		return $this->query;
1111	}
1112
1113	/**
1114	* Adds a completed query to the query list
1115	*
1116	* @return string	SQL of added query
1117	*/
1118	function addQuery() {
1119		if( !isset( $this->query ) ) {
1120			return FALSE;
1121		}
1122
1123		$this->queries[] = $return = trim($this->query);
1124
1125		unset( $this->query );
1126
1127		return $return;
1128	}
1129
1130	/**
1131	* Creates and returns the current query set
1132	*
1133	* @param object $xmls adoSchema object
1134	* @return array Query set
1135	*/
1136	function create( &$xmls ) {
1137		foreach( $this->queries as $id => $query ) {
1138			switch( $this->prefixMethod ) {
1139				case 'AUTO':
1140					// Enable auto prefix replacement
1141
1142					// Process object prefix.
1143					// Evaluate SQL statements to prepend prefix to objects
1144					$query = $this->prefixQuery( '/^\s*((?is)INSERT\s+(INTO\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix );
1145					$query = $this->prefixQuery( '/^\s*((?is)UPDATE\s+(FROM\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix );
1146					$query = $this->prefixQuery( '/^\s*((?is)DELETE\s+(FROM\s+)?)((\w+\s*,?\s*)+)(\s.*$)/', $query, $xmls->objectPrefix );
1147
1148					// SELECT statements aren't working yet
1149					#$data = preg_replace( '/(?ias)(^\s*SELECT\s+.*\s+FROM)\s+(\W\s*,?\s*)+((?i)\s+WHERE.*$)/', "\1 $prefix\2 \3", $data );
1150
1151				case 'MANUAL':
1152					// If prefixKey is set and has a value then we use it to override the default constant XMLS_PREFIX.
1153					// If prefixKey is not set, we use the default constant XMLS_PREFIX
1154					if( isset( $this->prefixKey ) AND( $this->prefixKey !== '' ) ) {
1155						// Enable prefix override
1156						$query = str_replace( $this->prefixKey, $xmls->objectPrefix, $query );
1157					} else {
1158						// Use default replacement
1159						$query = str_replace( XMLS_PREFIX , $xmls->objectPrefix, $query );
1160					}
1161			}
1162
1163			$this->queries[$id] = trim( $query );
1164		}
1165
1166		// Return the query set array
1167		return $this->queries;
1168	}
1169
1170	/**
1171	* Rebuilds the query with the prefix attached to any objects
1172	*
1173	* @param string $regex Regex used to add prefix
1174	* @param string $query SQL query string
1175	* @param string $prefix Prefix to be appended to tables, indices, etc.
1176	* @return string Prefixed SQL query string.
1177	*/
1178	function prefixQuery( $regex, $query, $prefix = NULL ) {
1179		if( !isset( $prefix ) ) {
1180			return $query;
1181		}
1182
1183		if( preg_match( $regex, $query, $match ) ) {
1184			$preamble = $match[1];
1185			$postamble = $match[5];
1186			$objectList = explode( ',', $match[3] );
1187			// $prefix = $prefix . '_';
1188
1189			$prefixedList = '';
1190
1191			foreach( $objectList as $object ) {
1192				if( $prefixedList !== '' ) {
1193					$prefixedList .= ', ';
1194				}
1195
1196				$prefixedList .= $prefix . trim( $object );
1197			}
1198
1199			$query = $preamble . ' ' . $prefixedList . ' ' . $postamble;
1200		}
1201
1202		return $query;
1203	}
1204}
1205
1206/**
1207* Loads and parses an XML file, creating an array of "ready-to-run" SQL statements
1208*
1209* This class is used to load and parse the XML file, to create an array of SQL statements
1210* that can be used to build a database, and to build the database using the SQL array.
1211*
1212* @tutorial getting_started.pkg
1213*
1214* @author Richard Tango-Lowy & Dan Cech
1215* @version $Revision: 1.12 $
1216*
1217* @package axmls
1218*/
1219class adoSchema {
1220
1221	/**
1222	* @var array	Array containing SQL queries to generate all objects
1223	* @access private
1224	*/
1225	var $sqlArray;
1226
1227	/**
1228	* @var object	ADOdb connection object
1229	* @access private
1230	*/
1231	var $db;
1232
1233	/**
1234	* @var object	ADOdb Data Dictionary
1235	* @access private
1236	*/
1237	var $dict;
1238
1239	/**
1240	* @var string Current XML element
1241	* @access private
1242	*/
1243	var $currentElement = '';
1244
1245	/**
1246	* @var string If set (to 'ALTER' or 'REPLACE'), upgrade an existing database
1247	* @access private
1248	*/
1249	var $upgrade = '';
1250
1251	/**
1252	* @var string Optional object prefix
1253	* @access private
1254	*/
1255	var $objectPrefix = '';
1256
1257	/**
1258	* @var long	Original Magic Quotes Runtime value
1259	* @access private
1260	*/
1261	var $mgq;
1262
1263	/**
1264	* @var long	System debug
1265	* @access private
1266	*/
1267	var $debug;
1268
1269	/**
1270	* @var string Regular expression to find schema version
1271	* @access private
1272	*/
1273	var $versionRegex = '/<schema.*?( version="([^"]*)")?.*?>/';
1274
1275	/**
1276	* @var string Current schema version
1277	* @access private
1278	*/
1279	var $schemaVersion;
1280
1281	/**
1282	* @var int	Success of last Schema execution
1283	*/
1284	var $success;
1285
1286	/**
1287	* @var bool	Execute SQL inline as it is generated
1288	*/
1289	var $executeInline;
1290
1291	/**
1292	* @var bool	Continue SQL execution if errors occur
1293	*/
1294	var $continueOnError;
1295
1296	/**
1297	* Creates an adoSchema object
1298	*
1299	* Creating an adoSchema object is the first step in processing an XML schema.
1300	* The only parameter is an ADOdb database connection object, which must already
1301	* have been created.
1302	*
1303	* @param object $db ADOdb database connection object.
1304	*/
1305	function __construct( $db ) {
1306		// Initialize the environment
1307		$this->mgq = get_magic_quotes_runtime();
1308		if ($this->mgq !== false) {
1309			ini_set('magic_quotes_runtime', 0);
1310		}
1311
1312		$this->db = $db;
1313		$this->debug = $this->db->debug;
1314		$this->dict = NewDataDictionary( $this->db );
1315		$this->sqlArray = array();
1316		$this->schemaVersion = XMLS_SCHEMA_VERSION;
1317		$this->executeInline( XMLS_EXECUTE_INLINE );
1318		$this->continueOnError( XMLS_CONTINUE_ON_ERROR );
1319		$this->setUpgradeMethod();
1320	}
1321
1322	/**
1323	* Sets the method to be used for upgrading an existing database
1324	*
1325	* Use this method to specify how existing database objects should be upgraded.
1326	* The method option can be set to ALTER, REPLACE, BEST, or NONE. ALTER attempts to
1327	* alter each database object directly, REPLACE attempts to rebuild each object
1328	* from scratch, BEST attempts to determine the best upgrade method for each
1329	* object, and NONE disables upgrading.
1330	*
1331	* This method is not yet used by AXMLS, but exists for backward compatibility.
1332	* The ALTER method is automatically assumed when the adoSchema object is
1333	* instantiated; other upgrade methods are not currently supported.
1334	*
1335	* @param string $method Upgrade method (ALTER|REPLACE|BEST|NONE)
1336	* @returns string Upgrade method used
1337	*/
1338	function SetUpgradeMethod( $method = '' ) {
1339		if( !is_string( $method ) ) {
1340			return FALSE;
1341		}
1342
1343		$method = strtoupper( $method );
1344
1345		// Handle the upgrade methods
1346		switch( $method ) {
1347			case 'ALTER':
1348				$this->upgrade = $method;
1349				break;
1350			case 'REPLACE':
1351				$this->upgrade = $method;
1352				break;
1353			case 'BEST':
1354				$this->upgrade = 'ALTER';
1355				break;
1356			case 'NONE':
1357				$this->upgrade = 'NONE';
1358				break;
1359			default:
1360				// Use default if no legitimate method is passed.
1361				$this->upgrade = XMLS_DEFAULT_UPGRADE_METHOD;
1362		}
1363
1364		return $this->upgrade;
1365	}
1366
1367	/**
1368	* Enables/disables inline SQL execution.
1369	*
1370	* Call this method to enable or disable inline execution of the schema. If the mode is set to TRUE (inline execution),
1371	* AXMLS applies the SQL to the database immediately as each schema entity is parsed. If the mode
1372	* is set to FALSE (post execution), AXMLS parses the entire schema and you will need to call adoSchema::ExecuteSchema()
1373	* to apply the schema to the database.
1374	*
1375	* @param bool $mode execute
1376	* @return bool current execution mode
1377	*
1378	* @see ParseSchema(), ExecuteSchema()
1379	*/
1380	function ExecuteInline( $mode = NULL ) {
1381		if( is_bool( $mode ) ) {
1382			$this->executeInline = $mode;
1383		}
1384
1385		return $this->executeInline;
1386	}
1387
1388	/**
1389	* Enables/disables SQL continue on error.
1390	*
1391	* Call this method to enable or disable continuation of SQL execution if an error occurs.
1392	* If the mode is set to TRUE (continue), AXMLS will continue to apply SQL to the database, even if an error occurs.
1393	* If the mode is set to FALSE (halt), AXMLS will halt execution of generated sql if an error occurs, though parsing
1394	* of the schema will continue.
1395	*
1396	* @param bool $mode execute
1397	* @return bool current continueOnError mode
1398	*
1399	* @see addSQL(), ExecuteSchema()
1400	*/
1401	function ContinueOnError( $mode = NULL ) {
1402		if( is_bool( $mode ) ) {
1403			$this->continueOnError = $mode;
1404		}
1405
1406		return $this->continueOnError;
1407	}
1408
1409	/**
1410	* Loads an XML schema from a file and converts it to SQL.
1411	*
1412	* Call this method to load the specified schema (see the DTD for the proper format) from
1413	* the filesystem and generate the SQL necessary to create the database described.
1414	* @see ParseSchemaString()
1415	*
1416	* @param string $file Name of XML schema file.
1417	* @param bool $returnSchema Return schema rather than parsing.
1418	* @return array Array of SQL queries, ready to execute
1419	*/
1420	function ParseSchema( $filename, $returnSchema = FALSE ) {
1421		return $this->ParseSchemaString( $this->ConvertSchemaFile( $filename ), $returnSchema );
1422	}
1423
1424	/**
1425	* Loads an XML schema from a file and converts it to SQL.
1426	*
1427	* Call this method to load the specified schema from a file (see the DTD for the proper format)
1428	* and generate the SQL necessary to create the database described by the schema.
1429	*
1430	* @param string $file Name of XML schema file.
1431	* @param bool $returnSchema Return schema rather than parsing.
1432	* @return array Array of SQL queries, ready to execute.
1433	*
1434	* @deprecated Replaced by adoSchema::ParseSchema() and adoSchema::ParseSchemaString()
1435	* @see ParseSchema(), ParseSchemaString()
1436	*/
1437	function ParseSchemaFile( $filename, $returnSchema = FALSE ) {
1438		// Open the file
1439		if( !($fp = fopen( $filename, 'r' )) ) {
1440			// die( 'Unable to open file' );
1441			return FALSE;
1442		}
1443
1444		// do version detection here
1445		if( $this->SchemaFileVersion( $filename ) != $this->schemaVersion ) {
1446			return FALSE;
1447		}
1448
1449		if ( $returnSchema )
1450		{
1451			$xmlstring = '';
1452			while( $data = fread( $fp, 100000 ) ) {
1453				$xmlstring .= $data;
1454			}
1455			return $xmlstring;
1456		}
1457
1458		$this->success = 2;
1459
1460		$xmlParser = $this->create_parser();
1461
1462		// Process the file
1463		while( $data = fread( $fp, 4096 ) ) {
1464			if( !xml_parse( $xmlParser, $data, feof( $fp ) ) ) {
1465				die( sprintf(
1466					"XML error: %s at line %d",
1467					xml_error_string( xml_get_error_code( $xmlParser) ),
1468					xml_get_current_line_number( $xmlParser)
1469				) );
1470			}
1471		}
1472
1473		xml_parser_free( $xmlParser );
1474
1475		return $this->sqlArray;
1476	}
1477
1478	/**
1479	* Converts an XML schema string to SQL.
1480	*
1481	* Call this method to parse a string containing an XML schema (see the DTD for the proper format)
1482	* and generate the SQL necessary to create the database described by the schema.
1483	* @see ParseSchema()
1484	*
1485	* @param string $xmlstring XML schema string.
1486	* @param bool $returnSchema Return schema rather than parsing.
1487	* @return array Array of SQL queries, ready to execute.
1488	*/
1489	function ParseSchemaString( $xmlstring, $returnSchema = FALSE ) {
1490		if( !is_string( $xmlstring ) OR empty( $xmlstring ) ) {
1491			return FALSE;
1492		}
1493
1494		// do version detection here
1495		if( $this->SchemaStringVersion( $xmlstring ) != $this->schemaVersion ) {
1496			return FALSE;
1497		}
1498
1499		if ( $returnSchema )
1500		{
1501			return $xmlstring;
1502		}
1503
1504		$this->success = 2;
1505
1506		$xmlParser = $this->create_parser();
1507
1508		if( !xml_parse( $xmlParser, $xmlstring, TRUE ) ) {
1509			die( sprintf(
1510				"XML error: %s at line %d",
1511				xml_error_string( xml_get_error_code( $xmlParser) ),
1512				xml_get_current_line_number( $xmlParser)
1513			) );
1514		}
1515
1516		xml_parser_free( $xmlParser );
1517
1518		return $this->sqlArray;
1519	}
1520
1521	/**
1522	* Loads an XML schema from a file and converts it to uninstallation SQL.
1523	*
1524	* Call this method to load the specified schema (see the DTD for the proper format) from
1525	* the filesystem and generate the SQL necessary to remove the database described.
1526	* @see RemoveSchemaString()
1527	*
1528	* @param string $file Name of XML schema file.
1529	* @param bool $returnSchema Return schema rather than parsing.
1530	* @return array Array of SQL queries, ready to execute
1531	*/
1532	function RemoveSchema( $filename, $returnSchema = FALSE ) {
1533		return $this->RemoveSchemaString( $this->ConvertSchemaFile( $filename ), $returnSchema );
1534	}
1535
1536	/**
1537	* Converts an XML schema string to uninstallation SQL.
1538	*
1539	* Call this method to parse a string containing an XML schema (see the DTD for the proper format)
1540	* and generate the SQL necessary to uninstall the database described by the schema.
1541	* @see RemoveSchema()
1542	*
1543	* @param string $schema XML schema string.
1544	* @param bool $returnSchema Return schema rather than parsing.
1545	* @return array Array of SQL queries, ready to execute.
1546	*/
1547	function RemoveSchemaString( $schema, $returnSchema = FALSE ) {
1548
1549		// grab current version
1550		if( !( $version = $this->SchemaStringVersion( $schema ) ) ) {
1551			return FALSE;
1552		}
1553
1554		return $this->ParseSchemaString( $this->TransformSchema( $schema, 'remove-' . $version), $returnSchema );
1555	}
1556
1557	/**
1558	* Applies the current XML schema to the database (post execution).
1559	*
1560	* Call this method to apply the current schema (generally created by calling
1561	* ParseSchema() or ParseSchemaString() ) to the database (creating the tables, indexes,
1562	* and executing other SQL specified in the schema) after parsing.
1563	* @see ParseSchema(), ParseSchemaString(), ExecuteInline()
1564	*
1565	* @param array $sqlArray Array of SQL statements that will be applied rather than
1566	*		the current schema.
1567	* @param boolean $continueOnErr Continue to apply the schema even if an error occurs.
1568	* @returns integer 0 if failure, 1 if errors, 2 if successful.
1569	*/
1570	function ExecuteSchema( $sqlArray = NULL, $continueOnErr =  NULL ) {
1571		if( !is_bool( $continueOnErr ) ) {
1572			$continueOnErr = $this->ContinueOnError();
1573		}
1574
1575		if( !isset( $sqlArray ) ) {
1576			$sqlArray = $this->sqlArray;
1577		}
1578
1579		if( !is_array( $sqlArray ) ) {
1580			$this->success = 0;
1581		} else {
1582			$this->success = $this->dict->ExecuteSQLArray( $sqlArray, $continueOnErr );
1583		}
1584
1585		return $this->success;
1586	}
1587
1588	/**
1589	* Returns the current SQL array.
1590	*
1591	* Call this method to fetch the array of SQL queries resulting from
1592	* ParseSchema() or ParseSchemaString().
1593	*
1594	* @param string $format Format: HTML, TEXT, or NONE (PHP array)
1595	* @return array Array of SQL statements or FALSE if an error occurs
1596	*/
1597	function PrintSQL( $format = 'NONE' ) {
1598		$sqlArray = null;
1599		return $this->getSQL( $format, $sqlArray );
1600	}
1601
1602	/**
1603	* Saves the current SQL array to the local filesystem as a list of SQL queries.
1604	*
1605	* Call this method to save the array of SQL queries (generally resulting from a
1606	* parsed XML schema) to the filesystem.
1607	*
1608	* @param string $filename Path and name where the file should be saved.
1609	* @return boolean TRUE if save is successful, else FALSE.
1610	*/
1611	function SaveSQL( $filename = './schema.sql' ) {
1612
1613		if( !isset( $sqlArray ) ) {
1614			$sqlArray = $this->sqlArray;
1615		}
1616		if( !isset( $sqlArray ) ) {
1617			return FALSE;
1618		}
1619
1620		$fp = fopen( $filename, "w" );
1621
1622		foreach( $sqlArray as $key => $query ) {
1623			fwrite( $fp, $query . ";\n" );
1624		}
1625		fclose( $fp );
1626	}
1627
1628	/**
1629	* Create an xml parser
1630	*
1631	* @return object PHP XML parser object
1632	*
1633	* @access private
1634	*/
1635	function create_parser() {
1636		// Create the parser
1637		$xmlParser = xml_parser_create();
1638		xml_set_object( $xmlParser, $this );
1639
1640		// Initialize the XML callback functions
1641		xml_set_element_handler( $xmlParser, '_tag_open', '_tag_close' );
1642		xml_set_character_data_handler( $xmlParser, '_tag_cdata' );
1643
1644		return $xmlParser;
1645	}
1646
1647	/**
1648	* XML Callback to process start elements
1649	*
1650	* @access private
1651	*/
1652	function _tag_open( &$parser, $tag, $attributes ) {
1653		switch( strtoupper( $tag ) ) {
1654			case 'TABLE':
1655				$this->obj = new dbTable( $this, $attributes );
1656				xml_set_object( $parser, $this->obj );
1657				break;
1658			case 'SQL':
1659				if( !isset( $attributes['PLATFORM'] ) OR $this->supportedPlatform( $attributes['PLATFORM'] ) ) {
1660					$this->obj = new dbQuerySet( $this, $attributes );
1661					xml_set_object( $parser, $this->obj );
1662				}
1663				break;
1664			default:
1665				// print_r( array( $tag, $attributes ) );
1666		}
1667
1668	}
1669
1670	/**
1671	* XML Callback to process CDATA elements
1672	*
1673	* @access private
1674	*/
1675	function _tag_cdata( &$parser, $cdata ) {
1676	}
1677
1678	/**
1679	* XML Callback to process end elements
1680	*
1681	* @access private
1682	* @internal
1683	*/
1684	function _tag_close( &$parser, $tag ) {
1685
1686	}
1687
1688	/**
1689	* Converts an XML schema string to the specified DTD version.
1690	*
1691	* Call this method to convert a string containing an XML schema to a different AXMLS
1692	* DTD version. For instance, to convert a schema created for an pre-1.0 version for
1693	* AXMLS (DTD version 0.1) to a newer version of the DTD (e.g. 0.2). If no DTD version
1694	* parameter is specified, the schema will be converted to the current DTD version.
1695	* If the newFile parameter is provided, the converted schema will be written to the specified
1696	* file.
1697	* @see ConvertSchemaFile()
1698	*
1699	* @param string $schema String containing XML schema that will be converted.
1700	* @param string $newVersion DTD version to convert to.
1701	* @param string $newFile File name of (converted) output file.
1702	* @return string Converted XML schema or FALSE if an error occurs.
1703	*/
1704	function ConvertSchemaString( $schema, $newVersion = NULL, $newFile = NULL ) {
1705
1706		// grab current version
1707		if( !( $version = $this->SchemaStringVersion( $schema ) ) ) {
1708			return FALSE;
1709		}
1710
1711		if( !isset ($newVersion) ) {
1712			$newVersion = $this->schemaVersion;
1713		}
1714
1715		if( $version == $newVersion ) {
1716			$result = $schema;
1717		} else {
1718			$result = $this->TransformSchema( $schema, 'convert-' . $version . '-' . $newVersion);
1719		}
1720
1721		if( is_string( $result ) AND is_string( $newFile ) AND ( $fp = fopen( $newFile, 'w' ) ) ) {
1722			fwrite( $fp, $result );
1723			fclose( $fp );
1724		}
1725
1726		return $result;
1727	}
1728
1729	// compat for pre-4.3 - jlim
1730	function _file_get_contents($path)
1731	{
1732		if (function_exists('file_get_contents')) return file_get_contents($path);
1733		return join('',file($path));
1734	}
1735
1736	/**
1737	* Converts an XML schema file to the specified DTD version.
1738	*
1739	* Call this method to convert the specified XML schema file to a different AXMLS
1740	* DTD version. For instance, to convert a schema created for an pre-1.0 version for
1741	* AXMLS (DTD version 0.1) to a newer version of the DTD (e.g. 0.2). If no DTD version
1742	* parameter is specified, the schema will be converted to the current DTD version.
1743	* If the newFile parameter is provided, the converted schema will be written to the specified
1744	* file.
1745	* @see ConvertSchemaString()
1746	*
1747	* @param string $filename Name of XML schema file that will be converted.
1748	* @param string $newVersion DTD version to convert to.
1749	* @param string $newFile File name of (converted) output file.
1750	* @return string Converted XML schema or FALSE if an error occurs.
1751	*/
1752	function ConvertSchemaFile( $filename, $newVersion = NULL, $newFile = NULL ) {
1753
1754		// grab current version
1755		if( !( $version = $this->SchemaFileVersion( $filename ) ) ) {
1756			return FALSE;
1757		}
1758
1759		if( !isset ($newVersion) ) {
1760			$newVersion = $this->schemaVersion;
1761		}
1762
1763		if( $version == $newVersion ) {
1764			$result = _file_get_contents( $filename );
1765
1766			// remove unicode BOM if present
1767			if( substr( $result, 0, 3 ) == sprintf( '%c%c%c', 239, 187, 191 ) ) {
1768				$result = substr( $result, 3 );
1769			}
1770		} else {
1771			$result = $this->TransformSchema( $filename, 'convert-' . $version . '-' . $newVersion, 'file' );
1772		}
1773
1774		if( is_string( $result ) AND is_string( $newFile ) AND ( $fp = fopen( $newFile, 'w' ) ) ) {
1775			fwrite( $fp, $result );
1776			fclose( $fp );
1777		}
1778
1779		return $result;
1780	}
1781
1782	function TransformSchema( $schema, $xsl, $schematype='string' )
1783	{
1784		// Fail if XSLT extension is not available
1785		if( ! function_exists( 'xslt_create' ) ) {
1786			return FALSE;
1787		}
1788
1789		$xsl_file = dirname( __FILE__ ) . '/xsl/' . $xsl . '.xsl';
1790
1791		// look for xsl
1792		if( !is_readable( $xsl_file ) ) {
1793			return FALSE;
1794		}
1795
1796		switch( $schematype )
1797		{
1798			case 'file':
1799				if( !is_readable( $schema ) ) {
1800					return FALSE;
1801				}
1802
1803				$schema = _file_get_contents( $schema );
1804				break;
1805			case 'string':
1806			default:
1807				if( !is_string( $schema ) ) {
1808					return FALSE;
1809				}
1810		}
1811
1812		$arguments = array (
1813			'/_xml' => $schema,
1814			'/_xsl' => _file_get_contents( $xsl_file )
1815		);
1816
1817		// create an XSLT processor
1818		$xh = xslt_create ();
1819
1820		// set error handler
1821		xslt_set_error_handler ($xh, array (&$this, 'xslt_error_handler'));
1822
1823		// process the schema
1824		$result = xslt_process ($xh, 'arg:/_xml', 'arg:/_xsl', NULL, $arguments);
1825
1826		xslt_free ($xh);
1827
1828		return $result;
1829	}
1830
1831	/**
1832	* Processes XSLT transformation errors
1833	*
1834	* @param object $parser XML parser object
1835	* @param integer $errno Error number
1836	* @param integer $level Error level
1837	* @param array $fields Error information fields
1838	*
1839	* @access private
1840	*/
1841	function xslt_error_handler( $parser, $errno, $level, $fields ) {
1842		if( is_array( $fields ) ) {
1843			$msg = array(
1844				'Message Type' => ucfirst( $fields['msgtype'] ),
1845				'Message Code' => $fields['code'],
1846				'Message' => $fields['msg'],
1847				'Error Number' => $errno,
1848				'Level' => $level
1849			);
1850
1851			switch( $fields['URI'] ) {
1852				case 'arg:/_xml':
1853					$msg['Input'] = 'XML';
1854					break;
1855				case 'arg:/_xsl':
1856					$msg['Input'] = 'XSL';
1857					break;
1858				default:
1859					$msg['Input'] = $fields['URI'];
1860			}
1861
1862			$msg['Line'] = $fields['line'];
1863		} else {
1864			$msg = array(
1865				'Message Type' => 'Error',
1866				'Error Number' => $errno,
1867				'Level' => $level,
1868				'Fields' => var_export( $fields, TRUE )
1869			);
1870		}
1871
1872		$error_details = $msg['Message Type'] . ' in XSLT Transformation' . "\n"
1873					   . '<table>' . "\n";
1874
1875		foreach( $msg as $label => $details ) {
1876			$error_details .= '<tr><td><b>' . $label . ': </b></td><td>' . htmlentities( $details ) . '</td></tr>' . "\n";
1877		}
1878
1879		$error_details .= '</table>';
1880
1881		trigger_error( $error_details, E_USER_ERROR );
1882	}
1883
1884	/**
1885	* Returns the AXMLS Schema Version of the requested XML schema file.
1886	*
1887	* Call this method to obtain the AXMLS DTD version of the requested XML schema file.
1888	* @see SchemaStringVersion()
1889	*
1890	* @param string $filename AXMLS schema file
1891	* @return string Schema version number or FALSE on error
1892	*/
1893	function SchemaFileVersion( $filename ) {
1894		// Open the file
1895		if( !($fp = fopen( $filename, 'r' )) ) {
1896			// die( 'Unable to open file' );
1897			return FALSE;
1898		}
1899
1900		// Process the file
1901		while( $data = fread( $fp, 4096 ) ) {
1902			if( preg_match( $this->versionRegex, $data, $matches ) ) {
1903				return !empty( $matches[2] ) ? $matches[2] : XMLS_DEFAULT_SCHEMA_VERSION;
1904			}
1905		}
1906
1907		return FALSE;
1908	}
1909
1910	/**
1911	* Returns the AXMLS Schema Version of the provided XML schema string.
1912	*
1913	* Call this method to obtain the AXMLS DTD version of the provided XML schema string.
1914	* @see SchemaFileVersion()
1915	*
1916	* @param string $xmlstring XML schema string
1917	* @return string Schema version number or FALSE on error
1918	*/
1919	function SchemaStringVersion( $xmlstring ) {
1920		if( !is_string( $xmlstring ) OR empty( $xmlstring ) ) {
1921			return FALSE;
1922		}
1923
1924		if( preg_match( $this->versionRegex, $xmlstring, $matches ) ) {
1925			return !empty( $matches[2] ) ? $matches[2] : XMLS_DEFAULT_SCHEMA_VERSION;
1926		}
1927
1928		return FALSE;
1929	}
1930
1931	/**
1932	* Extracts an XML schema from an existing database.
1933	*
1934	* Call this method to create an XML schema string from an existing database.
1935	* If the data parameter is set to TRUE, AXMLS will include the data from the database
1936	* in the schema.
1937	*
1938	* @param boolean $data Include data in schema dump
1939	* @return string Generated XML schema
1940	*/
1941	function ExtractSchema( $data = FALSE ) {
1942		$old_mode = $this->db->SetFetchMode( ADODB_FETCH_NUM );
1943
1944		$schema = '<?xml version="1.0"?>' . "\n"
1945				. '<schema version="' . $this->schemaVersion . '">' . "\n";
1946
1947		if( is_array( $tables = $this->db->MetaTables( 'TABLES' ) ) ) {
1948			foreach( $tables as $table ) {
1949				$schema .= '	<table name="' . $table . '">' . "\n";
1950
1951				// grab details from database
1952				$rs = $this->db->Execute( 'SELECT * FROM ' . $table . ' WHERE 1=1' );
1953				$fields = $this->db->MetaColumns( $table );
1954				$indexes = $this->db->MetaIndexes( $table );
1955
1956				if( is_array( $fields ) ) {
1957					foreach( $fields as $details ) {
1958						$extra = '';
1959						$content = array();
1960
1961						if( $details->max_length > 0 ) {
1962							$extra .= ' size="' . $details->max_length . '"';
1963						}
1964
1965						if( $details->primary_key ) {
1966							$content[] = '<KEY/>';
1967						} elseif( $details->not_null ) {
1968							$content[] = '<NOTNULL/>';
1969						}
1970
1971						if( $details->has_default ) {
1972							$content[] = '<DEFAULT value="' . $details->default_value . '"/>';
1973						}
1974
1975						if( $details->auto_increment ) {
1976							$content[] = '<AUTOINCREMENT/>';
1977						}
1978
1979						// this stops the creation of 'R' columns,
1980						// AUTOINCREMENT is used to create auto columns
1981						$details->primary_key = 0;
1982						$type = $rs->MetaType( $details );
1983
1984						$schema .= '		<field name="' . $details->name . '" type="' . $type . '"' . $extra . '>';
1985
1986						if( !empty( $content ) ) {
1987							$schema .= "\n			" . implode( "\n			", $content ) . "\n		";
1988						}
1989
1990						$schema .= '</field>' . "\n";
1991					}
1992				}
1993
1994				if( is_array( $indexes ) ) {
1995					foreach( $indexes as $index => $details ) {
1996						$schema .= '		<index name="' . $index . '">' . "\n";
1997
1998						if( $details['unique'] ) {
1999							$schema .= '			<UNIQUE/>' . "\n";
2000						}
2001
2002						foreach( $details['columns'] as $column ) {
2003							$schema .= '			<col>' . $column . '</col>' . "\n";
2004						}
2005
2006						$schema .= '		</index>' . "\n";
2007					}
2008				}
2009
2010				if( $data ) {
2011					$rs = $this->db->Execute( 'SELECT * FROM ' . $table );
2012
2013					if( is_object( $rs ) ) {
2014						$schema .= '		<data>' . "\n";
2015
2016						while( $row = $rs->FetchRow() ) {
2017							foreach( $row as $key => $val ) {
2018								$row[$key] = htmlentities($val);
2019							}
2020
2021							$schema .= '			<row><f>' . implode( '</f><f>', $row ) . '</f></row>' . "\n";
2022						}
2023
2024						$schema .= '		</data>' . "\n";
2025					}
2026				}
2027
2028				$schema .= '	</table>' . "\n";
2029			}
2030		}
2031
2032		$this->db->SetFetchMode( $old_mode );
2033
2034		$schema .= '</schema>';
2035		return $schema;
2036	}
2037
2038	/**
2039	* Sets a prefix for database objects
2040	*
2041	* Call this method to set a standard prefix that will be prepended to all database tables
2042	* and indices when the schema is parsed. Calling setPrefix with no arguments clears the prefix.
2043	*
2044	* @param string $prefix Prefix that will be prepended.
2045	* @param boolean $underscore If TRUE, automatically append an underscore character to the prefix.
2046	* @return boolean TRUE if successful, else FALSE
2047	*/
2048	function SetPrefix( $prefix = '', $underscore = TRUE ) {
2049		switch( TRUE ) {
2050			// clear prefix
2051			case empty( $prefix ):
2052				logMsg( 'Cleared prefix' );
2053				$this->objectPrefix = '';
2054				return TRUE;
2055			// prefix too long
2056			case strlen( $prefix ) > XMLS_PREFIX_MAXLEN:
2057			// prefix contains invalid characters
2058			case !preg_match( '/^[a-z][a-z0-9_]+$/i', $prefix ):
2059				logMsg( 'Invalid prefix: ' . $prefix );
2060				return FALSE;
2061		}
2062
2063		if( $underscore AND substr( $prefix, -1 ) != '_' ) {
2064			$prefix .= '_';
2065		}
2066
2067		// prefix valid
2068		logMsg( 'Set prefix: ' . $prefix );
2069		$this->objectPrefix = $prefix;
2070		return TRUE;
2071	}
2072
2073	/**
2074	* Returns an object name with the current prefix prepended.
2075	*
2076	* @param string	$name Name
2077	* @return string	Prefixed name
2078	*
2079	* @access private
2080	*/
2081	function prefix( $name = '' ) {
2082		// if prefix is set
2083		if( !empty( $this->objectPrefix ) ) {
2084			// Prepend the object prefix to the table name
2085			// prepend after quote if used
2086			return preg_replace( '/^(`?)(.+)$/', '$1' . $this->objectPrefix . '$2', $name );
2087		}
2088
2089		// No prefix set. Use name provided.
2090		return $name;
2091	}
2092
2093	/**
2094	* Checks if element references a specific platform
2095	*
2096	* @param string $platform Requested platform
2097	* @returns boolean TRUE if platform check succeeds
2098	*
2099	* @access private
2100	*/
2101	function supportedPlatform( $platform = NULL ) {
2102		$regex = '/^(\w*\|)*' . $this->db->databaseType . '(\|\w*)*$/';
2103
2104		if( !isset( $platform ) OR preg_match( $regex, $platform ) ) {
2105			logMsg( "Platform $platform is supported" );
2106			return TRUE;
2107		} else {
2108			logMsg( "Platform $platform is NOT supported" );
2109			return FALSE;
2110		}
2111	}
2112
2113	/**
2114	* Clears the array of generated SQL.
2115	*
2116	* @access private
2117	*/
2118	function clearSQL() {
2119		$this->sqlArray = array();
2120	}
2121
2122	/**
2123	* Adds SQL into the SQL array.
2124	*
2125	* @param mixed $sql SQL to Add
2126	* @return boolean TRUE if successful, else FALSE.
2127	*
2128	* @access private
2129	*/
2130	function addSQL( $sql = NULL ) {
2131		if( is_array( $sql ) ) {
2132			foreach( $sql as $line ) {
2133				$this->addSQL( $line );
2134			}
2135
2136			return TRUE;
2137		}
2138
2139		if( is_string( $sql ) ) {
2140			$this->sqlArray[] = $sql;
2141
2142			// if executeInline is enabled, and either no errors have occurred or continueOnError is enabled, execute SQL.
2143			if( $this->ExecuteInline() && ( $this->success == 2 || $this->ContinueOnError() ) ) {
2144				$saved = $this->db->debug;
2145				$this->db->debug = $this->debug;
2146				$ok = $this->db->Execute( $sql );
2147				$this->db->debug = $saved;
2148
2149				if( !$ok ) {
2150					if( $this->debug ) {
2151						ADOConnection::outp( $this->db->ErrorMsg() );
2152					}
2153
2154					$this->success = 1;
2155				}
2156			}
2157
2158			return TRUE;
2159		}
2160
2161		return FALSE;
2162	}
2163
2164	/**
2165	* Gets the SQL array in the specified format.
2166	*
2167	* @param string $format Format
2168	* @return mixed SQL
2169	*
2170	* @access private
2171	*/
2172	function getSQL( $format = NULL, $sqlArray = NULL ) {
2173		if( !is_array( $sqlArray ) ) {
2174			$sqlArray = $this->sqlArray;
2175		}
2176
2177		if( !is_array( $sqlArray ) ) {
2178			return FALSE;
2179		}
2180
2181		switch( strtolower( $format ) ) {
2182			case 'string':
2183			case 'text':
2184				return !empty( $sqlArray ) ? implode( ";\n\n", $sqlArray ) . ';' : '';
2185			case'html':
2186				return !empty( $sqlArray ) ? nl2br( htmlentities( implode( ";\n\n", $sqlArray ) . ';' ) ) : '';
2187		}
2188
2189		return $this->sqlArray;
2190	}
2191
2192	/**
2193	* Destroys an adoSchema object.
2194	*
2195	* Call this method to clean up after an adoSchema object that is no longer in use.
2196	* @deprecated adoSchema now cleans up automatically.
2197	*/
2198	function Destroy() {
2199		if ($this->mgq !== false) {
2200			ini_set('magic_quotes_runtime', $this->mgq );
2201		}
2202	}
2203}
2204
2205/**
2206* Message logging function
2207*
2208* @access private
2209*/
2210function logMsg( $msg, $title = NULL, $force = FALSE ) {
2211	if( XMLS_DEBUG or $force ) {
2212		echo '<pre>';
2213
2214		if( isset( $title ) ) {
2215			echo '<h3>' . htmlentities( $title ) . '</h3>';
2216		}
2217
2218		if( is_object( $this ) ) {
2219			echo '[' . get_class( $this ) . '] ';
2220		}
2221
2222		print_r( $msg );
2223
2224		echo '</pre>';
2225	}
2226}
2227