* Invision Power Services * IP.Board v3.1.4 * XML Handler: Can use PHP's internal XML handler, or a lite hand-rolled parser, to create and read XML files * Last Updated: $Date: 2010-01-15 10:18:25 -0500 (Fri, 15 Jan 2010) $ * * * @author $Author: bfarber $ * @copyright (c) 2001 - 2009 Invision Power Services, Inc. * @license http://www.invisionpower.com/community/board/license.html * @package IP.Board * @subpackage Kernel * @link http://www.invisionpower.com * @since 25th February 2004 * @version $Revision: 370 $ * * * Example Usage: * * * * * This is a descrption * Baked Beans * 103 * * * * * // Set the root tag * $xml->xml_set_root( 'productlist', array( 'name' => 'myname', 'version' => '1.0' ) ); * * // Add a group * $xml->xml_add_group( 'productgroup', array( 'name' => 'thisgroup' ) ); * * // Build entry content * $content[] = $xml->xml_build_simple_tag( 'description', "This is a descrption" ); * $content[] = $xml->xml_build_simple_tag( 'title' , "Baked Beans" ); * $content[] = $xml->xml_build_simple_tag( 'room' , '103' , array( 'store' => 1 ) ); * * // Build entry * $entry[] = $xml->xml_build_entry( 'product', $content, array( 'id' => '1.0' ) ); * * // Add to group 'productlist' * $xml->xml_add_entry_to_group( 'productgroup', $entry ); * * // Format.. * $xml->xml_format_document(); * * // Get XML document * $filecontents = $xml->xml_document; * * // Parse XML document * $xml->xml_parse_document( $filecontents ); * * // Show XML array * print_r( $xml->xml_array ); * * // print "Baked Beans"; * print $xml->xml_array['productlist']['productgroup']['product'][0]['title']['VALUE']; * * // print "1"; * print $xml->xml_array['productlist']['productgroup']['product'][0]['room']['ATTRIBUTES']['store']; * * */ class class_xml { /** * XML header * * @access public * @var string */ public $header = ""; /** * Root tag name * * @access public * @var string */ public $root_tag = ''; /** * Root attributes * * @access public * @var string */ public $root_attributes = ""; /** * Array of entries * * @access public * @var array */ public $entries = array(); /** * String of compiled XML document * * @access public * @var string */ public $xml_document = ""; /** * Work variable * * @access public * @var integer */ public $depth = 0; /** * Tmp doc, used during creation * * @access public * @var string */ public $tmp_doc = ""; /** * Tag groups * * @access public * @var string */ public $groups = ""; /** * Index numerically flag * * @access public * @var integer */ public $index_numeric = 0; /** * Collapse duplicate tags flag * * @access public * @var integer */ public $collapse_dups = 1; /** * Main XML array of parsed components * * @access public * @var array */ public $xml_array = array(); /** * Collapse newlines in CDATA tags * * @access public * @var integer */ public $collapse_newlines = 1; /** * Use lite parser flag * * @access public * @var integer */ public $lite_parser = 0; /** * DOC type * * @access public * @var string */ public $doc_type = 'UTF-8'; /** * Use docytype in document * * @access public * @var integer */ public $use_doctype = 1; /** * Parse an XML document into an array of field and values * * @access public * @param string Raw XML Data * @return void */ public function xml_parse_document( $xml ) { $i = -1; //----------------------------------------- // Use "lite" parser //----------------------------------------- if ( $this->lite_parser ) { $lite = new xml_lite_parse(); $lite->xml_parse_it( $xml ); $this->xml_array = $this->_xmlGetChildren( $lite->stack, $i ); //----------------------------------------- // Free willy..er..memory //----------------------------------------- $lite->garbage_collect(); } //----------------------------------------- // Use PHP EXPAT Parser? //----------------------------------------- else { if ( $this->use_doctype && in_array( strtolower($this->doc_type), array( 'us-ascii', 'utf-8', 'iso-8859-1' ) ) ) { $parser = xml_parser_create( $this->doc_type ); } else { $parser = xml_parser_create(); } xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE , 0); xml_parse_into_struct($parser, $xml, $vals); xml_parser_free($parser); $this->xml_array = $this->_xmlGetChildren($vals, $i); } //----------------------------------------- // Garbage collect //----------------------------------------- unset( $vals ); unset( $xml ); } /** * Parse an array into an XML document * * @access public * @param array Entry array * @return void */ public function xml_format_document( $entry=array() ) { $this->header = 'doc_type.'"?'.'>'; $this->xml_document = $this->header ? $this->header."\n" : ''; $this->xml_document .= "<".$this->root_tag.$this->root_attributes.">\n"; $this->xml_document .= $this->tmp_doc; $this->xml_document .= "\nroot_tag.">"; $this->tmp_doc = ""; } /** * Set the root tag * * @access public * @param string Root tag name * @param array Root tag attributes * @return void */ public function xml_set_root($tag, $attributes=array() ) { $this->root_tag = $tag; $this->root_attributes = $this->_xmlBuildAttributeString( $attributes ); } /** * Add entry to XML group * * @access public * @param string Tag name * @param array Entry values * @return void */ public function xml_add_entry_to_group($tag, $entry=array() ) { $this->tmp_doc .= "\t".$this->groups[ $tag ]; if ( is_array( $entry ) and count( $entry ) ) { foreach( $entry as $e ) { $this->tmp_doc .= "\n\t\t".$e."\n"; } } $this->tmp_doc .= "\t\n"; } /** * Build an XML entry * * @access public * @param string Tag name * @param array Content array * @param array Attributes * @return string */ public function xml_build_entry( $tag, $content=array(), $attributes=array() ) { $entry = "<" . $tag . $this->_xmlBuildAttributeString($attributes) . ">\n"; if ( is_array( $content ) and count( $content ) ) { foreach( $content as $c ) { $entry .= "\t\t\t".$c."\n"; } } $entry .= "\t\t"; return $entry; } /** * Add a group to an XML document * * @access public * @param string Tag name * @param array Attributes * @return string */ public function xml_add_group( $tag, $attributes=array() ) { $this->groups[ $tag ] = "<" . $tag . $this->_xmlBuildAttributeString($attributes) . ">"; } /** * Build and XML simple tag * * @access public * @param string Tag name * @param string Tag value * @param array Attributes * @return string */ public function xml_build_simple_tag( $tag, $description="", $attributes=array() ) { return "<" . $tag . $this->_xmlBuildAttributeString($attributes) . ">" . $this->_xmlEncodeString($description) . ""; } /** * Build tree node * Adapted from http://eric.pollmann.net/work/public_domain/ * * @access private * @param array Values * @param array Values * @param string Counter * @param string Tag type * @return array */ private function _xmlBuildTag( $thisvals, $vals, &$i, $type ) { $tag = array(); if ( isset($thisvals['attributes']) ) { $tag['ATTRIBUTES'] = $this->_xmlDecodeAttribute($thisvals['attributes']); } if ( $type === 'complete' ) { if( isset( $thisvals['value'] ) ) { $tag['VALUE'] = $this->xmlUnconvertSafecdata( $thisvals['value'] ); } else { $tag['VALUE'] = ''; } } else { $tag = array_merge( $tag, $this->_xmlGetChildren($vals, $i) ); } return $tag; } /** * Build a nested array of children * Adapted from http://eric.pollmann.net/work/public_domain/ * * @access private * @param array Values * @param string Counter * @return array */ private function _xmlGetChildren($vals, &$i) { $children = array(); //----------------------------------- // CDATA before children //----------------------------------- if ( $i > -1 && isset( $vals[$i]['value'] ) ) { $children['VALUE'] = $this->xmlUnconvertSafecdata( $vals[$i]['value'] ); } //----------------------------------- // Loopy loo //----------------------------------- while( ++$i < count( $vals ) ) { $type = $vals[$i]['type']; //----------------------------------- // CDATA after children //----------------------------------- if ($type === 'cdata') { $children['VALUE'] .= $this->xmlUnconvertSafecdata( $vals[$i]['value'] ); } //----------------------------------- // COMPLETE: At end of current branch // OPEN: Node has children, recurse //----------------------------------- else if ( $type === 'complete' OR $type === 'open' ) { $tag = $this->_xmlBuildTag( $vals[$i], $vals, $i, $type ); if ( $this->index_numeric ) { $tag['TAG'] = $vals[$i]['tag']; $children[] = $tag; } else { $children[$vals[$i]['tag']][] = $tag; } } //----------------------------------- // End of node? //----------------------------------- else if ($type === 'close') { break; } } if ( $this->collapse_dups ) { foreach( $children as $key => $value ) { if ( is_array($value) && (count($value) == 1) ) { $children[$key] = $value[0]; } } } return $children; } /** * Builds attribute string * * @access private * @param array Values * @return string */ private function _xmlBuildAttributeString( $array = array() ) { if ( is_array( $array ) and count( $array ) ) { $string = array(); foreach( $array as $k => $v ) { $v = trim( $this->_xmlEncodeAttribute($v) ); $string[] = $k.'="'.$v.'"'; } return ' ' . implode( " ", $string ); } } /** * Encode XML attribute (Make safe for transport) * * @access private * @param string Raw data * @return string Converted Data */ private function _xmlEncodeAttribute( $t ) { $t = preg_replace("/&(?!#[0-9]+;)/s", '&', $t ); $t = str_replace( "<", "<" , $t ); $t = str_replace( ">", ">" , $t ); $t = str_replace( '"', """, $t ); $t = str_replace( "'", ''', $t ); return $t; } /** * Decode XML attribute (Make safe for transport) * * @access private * @param string Raw data * @return string Converted Data */ private function _xmlDecodeAttribute( $t ) { $t = str_replace( "&" , "&", $t ); $t = str_replace( "<" , "<", $t ); $t = str_replace( ">" , ">", $t ); $t = str_replace( """, '"', $t ); $t = str_replace( "'", "'", $t ); return $t; } /** * Encode XML attribute (Make safe for transport) * * @access private * @param string Raw data * @return string Converted Data */ private function _xmlEncodeString( $v ) { if ( preg_match( "/['\"\[\]<>&]/", $v ) ) { $v = "_xmlConvertSafecdata($v) . "]]>"; } if ( $this->collapse_newlines ) { $v = str_replace( "\r\n", "\n", $v ); } return $v; } /** * Encode CDATA XML attribute (Make safe for transport) * * @access private * @param string Raw data * @return string Converted Data */ private function _xmlConvertSafecdata( $v ) { # Legacy //$v = str_replace( "" , "|ᄁ]>" , $v ); # New $v = str_replace( "" , "|#^#]>" , $v ); return $v; } /** * Decode CDATA XML attribute (Make safe for transport) * * @access public * @param string Raw data * @return string Converted Data */ public function xmlUnconvertSafecdata( $v ) { # Legacy $v = str_replace( "" , "]]>" , $v ); # New $v = str_replace( "" , "]]>" , $v ); return $v; } } //====================================================================================== // CLASS: XML_LITE_PARSE (Takes parsed XML and puts into EXPAT compatible array //====================================================================================== /** * XML-LITE Extraction Sub class * * Methods and functions for handling XML documents * * @package IP.Board * @subpackage Kernel * @author $Author: bfarber $ * @since 25th February 2004 * @version $Revision: 370 $ */ class xml_lite_parse { /** * XML object * * @access public * @var object */ public $xml_class; /** * Parser object * * @access public * @var object */ public $parser; /** * Preserve cdata flag * * @access public * @var integer */ public $preserve_cdata = 1; /** * Internal: stack management * * @access private * @var array */ private $stack = array(); /** * Depth level * * @access public * @var integer */ public $level = 1; /** * Current tagname * * @access public * @var string */ public $tagname = ""; /** * Current array id * * @access public * @var integer */ public $array_id = 0; /** * Last managed id * * @access public * @var integer */ public $last_id = 0; /** * Tags opened * * @access public * @var array */ public $tagopen = array(); /** * XML Document * * @access public * @var string */ public $xmldoc = ""; /** * Parse XML document * * @access public * @param string Raw xml document * @return void */ public function xml_parse_it( $xmldoc ) { $parser = new xml_extract(); $this->xmldoc = $xmldoc; unset( $xmldoc ); //----------------------------------------- // Set up element handlers //----------------------------------------- $parser->_myXmlSetElementHandler( array(&$this, '_myStartElement'), array(&$this, '_myEndElement') ); $parser->_myXmlSetCharacterDataHandler(array(&$this, '_myDataElement')); if ( $this->preserve_cdata ) { $parser->_myXmlSetCdataSectionHandler( array(&$this, '_myCdataElement') ); } $parser->xml_parse_document( $this->xmldoc ); } /** * Start element callback * * @access private * @param object Parser ref * @param string Tag name * @param array Attributes * @return void */ private function _myStartElement( &$parser_obj, $name, $attr ) { //------------------------------- // Add to stack //------------------------------- $this->stack[ $this->array_id ] = array( 'tag' => $name, 'type' => 'open', 'level' => $this->level, 'value' => '', ); //------------------------------- // Attributes? //------------------------------- if ( is_array( $attr ) and count( $attr ) ) { $this->stack[ $this->array_id ]['attributes'] = $attr; } //------------------------------- // Flying higher than an eagle? //------------------------------- if ( $this->tagname != $name ) { if ( $this->tagopen[ $name ] ) { $this->level = $this->tagopen[ $name ]; } else { $this->level++; } } //------------------------------- // Set current tag name //------------------------------- $this->tagname = $name; //------------------------------- // Inc. array ID //------------------------------- $this->array_id++; //------------------------------- // Set tag == depth //------------------------------- $this->tagopen[ $name ] = $this->level; } /** * End element callback * * @access private * @param object Parser ref * @param string Tag name * @return void */ private function _myEndElement( &$parser_obj, $name ) { $this->stack[ $this->array_id ] = array( 'tag' => $name, 'type' => 'close', 'level' => $this->tagopen[ $name ] - 1, ); //------------------------------- // Update already done data? //------------------------------- if ( ( $this->stack[ $this->array_id - 2 ]['tag'] == $this->tagname ) AND ( $this->stack[ $this->array_id - 1 ]['_data'] == 1 ) ) { //------------------------------- // Update previous tag //------------------------------- $this->stack[ $this->array_id - 2 ]['value'] = class_xml::xmlUnconvertSafecdata( $this->stack[ $this->array_id - 1 ]['value'] ); $this->stack[ $this->array_id - 2 ]['type'] = 'complete'; unset( $this->stack[ $this->array_id - 1 ] ); unset( $this->stack[ $this->array_id ] ); $this->array_id -= 2; $this->last_id = $this->array_id - 1; } $this->tagname = ""; $this->array_id++; $this->level--; } /** * Parse data * * @access private * @param object Parser ref * @param string Tag name * @return void */ private function _myDataElement( &$parser_obj, $data ) { if ( $this->tagname ) { $this->stack[ $this->array_id ] = array( 'tag' => $this->tagname, 'type' => 'open', 'level' => $this->level, 'value' => class_xml::xmlUnconvertSafecdata($data), '_data' => 1 ); } //------------------------------- // Inc. array ID //------------------------------- $this->array_id++; } /** * Parse CDATA * * @access private * @param object Parser ref * @param string Tag name * @return void */ private function _myCdataElement( &$parser_obj, $data ) { $this->_myDataElement( $parser_obj, $data ); } /** * Garbage collection * * @access public * @return void */ public function garbage_collect() { $this->stack = array(); $this->tagname = array(); $this->array_id = 0; $this->level = 0; $this->xmldoc = ""; } } //====================================================================================== // CLASS: XML_EXTRACT (Parses XML file) //====================================================================================== /** * XML-LITE Extraction Sub class * * Methods and functions for handling XML documents * * @package IP.Board * @subpackage Kernel * @author $Author: bfarber $ * @since 25th February 2004 * @version $Revision: 370 $ */ class xml_extract { /** * Array of XML data * * @access public * @var array */ public $xml_array = array(); /** * Characters so far parsed * * @access public * @var integer */ public $chr_sofar = 0; /** * Handler object * * @access public * @var object */ public $handler_cdata_handler; /** * Handler object * * @access public * @var object */ public $handler_character_data; /** * Handler object * * @access public * @var object */ public $handler_end_element; /** * Handler object * * @access public * @var object */ public $handler_start_element; /** * Notation mapping * * @access public * @var array */ public $xml_constants = array( 'CDATA_TAG' => '![CDATA[', 'CDATA_LEN' => 8, 'NOTATION' => '!NOTATION', 'DOCTYPE' => '!DOCTYPE' ); /** * Document parsing * * @access public * @param string XML Document * @return void */ public function xml_parse_document( $xml ) { //----------------------------------------- // Grab all relevant XML data // Strip off header, DOC TYPE, etc //----------------------------------------- $xml = preg_replace( "#^(?:.*?)?(<.*>)(?:.*?)?$#s", "\\1", $xml ); $xml_strlen = strlen( $xml ); //----------------------------------------- // Pick through, char by char //----------------------------------------- for( $i = 0 ; $i < $xml_strlen; $i++ ) { $chr = $xml{$i}; switch( $chr ) { case '<': if ( substr( $this->chr_sofar, 0, $this->xml_constants['CDATA_LEN'] ) == $this->xml_constants['CDATA_TAG'] ) { //----------------------------------------- // Processing CDATA //----------------------------------------- $this->chr_sofar .= $chr; } else { $this->_parseBetweenTags( $this->chr_sofar ); $this->chr_sofar = ''; } break; case '>': if ( ( substr( $this->chr_sofar, 0, $this->xml_constants['CDATA_LEN'] ) == $this->xml_constants['CDATA_TAG'] ) AND ! ( ( $this->_getNthCharFromEnd( $this->chr_sofar, 0 ) == ']' ) AND ( $this->_getNthCharFromEnd( $this->chr_sofar, 1 ) == ']' ) ) ) { $this->chr_sofar .= $chr; } else { if( $xml{ strlen($this->chr_sofar +1) } == ']' ) { $this->chr_sofar .= $chr; } else { $this->_parseTag( $this->chr_sofar ); $this->chr_sofar = ''; } } break; default: $this->chr_sofar .= $chr; } } unset($xml); } /** * Parse a tag * * @access private * @param string Text * @return void */ private function _parseTag( $text ) { $attr = array(); $text = trim($text); $fchr = $text{0}; switch ($fchr) { //----------------------------------------- // First char is closing tag? //----------------------------------------- case '/': $tag_name = substr($text, 1); $this->_execEndElement($tag_name); break; //----------------------------------------- // First char is instruction/doctype? //----------------------------------------- case '!': $uc_tag_text = strtoupper($text); if ( strpos($uc_tag_text, $this->xml_constants['CDATA_TAG']) !== false ) { //----------------------------------------- // CDATA text //----------------------------------------- $total = strlen($text); $openbrace_cnt = 0; $tn_text = ''; for ( $i = 0; $i < $total; $i++ ) { $cc = $text{$i}; //----------------------------------------- // End of CDATA? //----------------------------------------- if ( ($cc == ']') && ( $text{($i + 1)} == ']' ) ) { if( ! $text{($i + 2)} == ']' ) { break; } else { $tn_text .= $cc; } } else if ($openbrace_cnt > 1) { $tn_text .= $cc; } else if ($cc == '[') { //----------------------------------------- // Won't get here until first OB reached //----------------------------------------- $openbrace_cnt ++; } } if ( $this->handler_cdata_handler == null ) { $this->_execCharacterData($tn_text); } else { $this->_execCdataElement($tn_text); } } else if ( strpos( $uc_tag_text, $this->xml_constants['NOTATION'] ) !== false ) { //----------------------------------------- // !NOTATION? Ignore! //----------------------------------------- return; } else if ( substr($text, 0, 2) == '!-' ) { //----------------------------------------- // !Comment? Ignore! //----------------------------------------- return; } break; //----------------------------------------- // Case is ?INSTRUCTION? //----------------------------------------- case '?': //----------------------------------------- // Instruction? Ignore! //----------------------------------------- return; //----------------------------------------- // Normal tag - woohoo! //----------------------------------------- default: if ( (strpos($text, '"') !== false) || (strpos($text, "'") !== false) ) { $total = strlen($text); $tag_name = ''; for ($i = 0; $i < $total; $i++) { $cc = $text{$i}; if ($cc == ' ') { $attr = $this->_parseAttr(substr($text, $i)); break; } else { $tag_name.= $cc; } } if ( strrpos($text, '/') == ( strlen($text) - 1 ) ) { $this->_execStartElement($tag_name, $attr); $this->_execEndElement($tag_name); } else { $this->_execStartElement($tag_name, $attr); } } else { if ( strpos($text, '/') !== false ) { $text = trim( substr( $text, 0, ( strrchr($text, '/' ) - 1 ) ) ); $this->_execStartElement($text, $attr); $this->_execEndElement($text); } else { $this->_execStartElement($text, $attr); } } } } /** * Parse an attribute * * @access private * @param string Text * @return array Attributes */ private function _parseAttr( $text ) { $text = trim($text); $attr_array = array(); $query_entity = false; $total = strlen($text); $dump_key = ''; $dump_value = ''; $cur_state = 0; // 0 = none, 1 = key, 2 = value $quote_type = ''; for ($i = 0; $i < $total; $i++) { $cc = $text{$i}; if ( $cur_state == 0 ) { if ( trim($cc != '') ) { $cur_state = 1; } } switch ($cc) { //----------------------------------------- // Tab and we're in a value? //----------------------------------------- case "\t": if ( $cur_state == 2 ) { $dump_value .= $cc; } else { $cc = ''; } break; //----------------------------------------- // Newlines.. //----------------------------------------- case "\n": case "\r": $cc = ''; break; //----------------------------------------- // Param value //----------------------------------------- case '=': if ( $cur_state == 2 ) { $dump_value .= $cc; } else { $cur_state = 2; $quote_type = ''; $query_entity = false; } break; //----------------------------------------- // Quoted //----------------------------------------- case '"': if ($cur_state == 2) { if ($quote_type == '') { $quote_type = '"'; } else { if ($quote_type == $cc) { $attr_array[ trim($dump_key) ] = trim($dump_value); $dump_key = $dump_value = $quote_type = ''; $cur_state = 0; } else { $dump_value .= $cc; } } } break; //----------------------------------------- // Quoted //----------------------------------------- case "'": if ($cur_state == 2) { if ($quote_type == '') { $quote_type = "'"; } else { if ($quote_type == $cc) { $attr_array[ trim($dump_key) ] = trim($dump_value); $dump_key = $dump_value = $quote_type = ''; $cur_state = 0; } else { $dump_value .= $cc; } } } break; //----------------------------------------- // Entity? //----------------------------------------- case '&': $query_entity = true; $dump_value .= $cc; break; default: if ($cur_state == 1) { $dump_key .= $cc; } else { $dump_value .= $cc; } } } return $attr_array; } /** * Parse between element tags * * @access private * @param string Text * @return void */ private function _parseBetweenTags( $t ) { if ( trim($t ) != '') { $this->_execCharacterData($t); } } /** * Exec character callback * * @access private * @param string Data * @return void */ private function _execCharacterData( $data ) { call_user_func( $this->handler_character_data, $this, $data ); } /** * Exec start callback * * @access private * @param string Data * @return void */ private function _execStartElement( $tagname, $attr ) { call_user_func( $this->handler_start_element, $this, $tagname, $attr ); } /** * Exec end callback * * @access private * @param string Data * @return void */ private function _execEndElement( $tagname ) { call_user_func( $this->handler_end_element, $this, $tagname ); } /** * Exec cdata callback * * @access private * @param string Data * @return void */ private function _execCdataElement( $data ) { call_user_func( $this->handler_cdata_handler, $this, $data ); } /** * Set the element handler * * @access private * @param string start handler * @param string end handler * @return void */ private function _myXmlSetElementHandler($startHandler, $endHandler) { $this->handler_start_element = $startHandler; $this->handler_end_element = $endHandler; } /** * Set the data handler * * @access private * @param string start handler * @param string end handler * @return void */ private function _myXmlSetCharacterDataHandler($handler) { $this->handler_character_data = &$handler; } /** * Set the cdata handler * * @access private * @param string start handler * @param string end handler * @return void */ private function _myXmlSetCdataSectionHandler($handler) { $this->handler_cdata_handler = &$handler; } /** * Get nth character of text * * @access private * @param string Text * @param integer Position to get * @return string Character */ private function _getNthCharFromEnd( $t, $i ) { return $t{ ( strlen( $t ) - 1 - $i ) }; } } ?>