3a3285e8d45b7e3fd0c42c6efd3550aab33e8fe3
[pear] / XML / Parser.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6  * XML_Parser
7  *
8  * XML Parser package
9  *
10  * PHP versions 4 and 5
11  *
12  * LICENSE:
13  *
14  * Copyright (c) 2002-2008 The PHP Group
15  * All rights reserved.
16  *
17  * Redistribution and use in source and binary forms, with or without
18  * modification, are permitted provided that the following conditions
19  * are met:
20  *
21  *    * Redistributions of source code must retain the above copyright
22  *      notice, this list of conditions and the following disclaimer.
23  *    * Redistributions in binary form must reproduce the above copyright
24  *      notice, this list of conditions and the following disclaimer in the
25  *      documentation and/or other materials provided with the distribution.
26  *    * The name of the author may not be used to endorse or promote products
27  *      derived from this software without specific prior written permission.
28  *
29  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
30  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
31  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
32  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
33  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
34  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
35  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
36  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
37  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
38  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
39  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40  *
41  * @category  XML
42  * @package   XML_Parser
43  * @author    Stig Bakken <ssb@fast.no>
44  * @author    Tomas V.V.Cox <cox@idecnet.com>
45  * @author    Stephan Schmidt <schst@php.net>
46  * @copyright 2002-2008 The PHP Group
47  * @license   http://opensource.org/licenses/bsd-license New BSD License
48  * @version   CVS: $Id: Parser.php 302733 2010-08-24 01:09:09Z clockwerx $
49  * @link      http://pear.php.net/package/XML_Parser
50  */
51
52 /**
53  * uses PEAR's error handling
54  */
55 require_once 'PEAR.php';
56
57 /**
58  * resource could not be created
59  */
60 define('XML_PARSER_ERROR_NO_RESOURCE', 200);
61
62 /**
63  * unsupported mode
64  */
65 define('XML_PARSER_ERROR_UNSUPPORTED_MODE', 201);
66
67 /**
68  * invalid encoding was given
69  */
70 define('XML_PARSER_ERROR_INVALID_ENCODING', 202);
71
72 /**
73  * specified file could not be read
74  */
75 define('XML_PARSER_ERROR_FILE_NOT_READABLE', 203);
76
77 /**
78  * invalid input
79  */
80 define('XML_PARSER_ERROR_INVALID_INPUT', 204);
81
82 /**
83  * remote file cannot be retrieved in safe mode
84  */
85 define('XML_PARSER_ERROR_REMOTE', 205);
86
87 /**
88  * XML Parser class.
89  *
90  * This is an XML parser based on PHP's "xml" extension,
91  * based on the bundled expat library.
92  *
93  * Notes:
94  * - It requires PHP 4.0.4pl1 or greater
95  * - From revision 1.17, the function names used by the 'func' mode
96  *   are in the format "xmltag_$elem", for example: use "xmltag_name"
97  *   to handle the <name></name> tags of your xml file.
98  *          - different parsing modes
99  *
100  * @category  XML
101  * @package   XML_Parser
102  * @author    Stig Bakken <ssb@fast.no>
103  * @author    Tomas V.V.Cox <cox@idecnet.com>
104  * @author    Stephan Schmidt <schst@php.net>
105  * @copyright 2002-2008 The PHP Group
106  * @license   http://opensource.org/licenses/bsd-license New BSD License
107  * @version   Release: @package_version@
108  * @link      http://pear.php.net/package/XML_Parser
109  * @todo      create XML_Parser_Namespace to parse documents with namespaces
110  * @todo      create XML_Parser_Pull
111  * @todo      Tests that need to be made:
112  *            - mixing character encodings
113  *            - a test using all expat handlers
114  *            - options (folding, output charset)
115  */
116 class XML_Parser extends PEAR
117 {
118     // {{{ properties
119
120     /**
121      * XML parser handle
122      *
123      * @var  resource
124      * @see  xml_parser_create()
125      */
126     var $parser;
127
128     /**
129      * File handle if parsing from a file
130      *
131      * @var  resource
132      */
133     var $fp;
134
135     /**
136      * Whether to do case folding
137      *
138      * If set to true, all tag and attribute names will
139      * be converted to UPPER CASE.
140      *
141      * @var  boolean
142      */
143     var $folding = true;
144
145     /**
146      * Mode of operation, one of "event" or "func"
147      *
148      * @var  string
149      */
150     var $mode;
151
152     /**
153      * Mapping from expat handler function to class method.
154      *
155      * @var  array
156      */
157     var $handler = array(
158         'character_data_handler'            => 'cdataHandler',
159         'default_handler'                   => 'defaultHandler',
160         'processing_instruction_handler'    => 'piHandler',
161         'unparsed_entity_decl_handler'      => 'unparsedHandler',
162         'notation_decl_handler'             => 'notationHandler',
163         'external_entity_ref_handler'       => 'entityrefHandler'
164     );
165
166     /**
167      * source encoding
168      *
169      * @var string
170      */
171     var $srcenc;
172
173     /**
174      * target encoding
175      *
176      * @var string
177      */
178     var $tgtenc;
179
180     /**
181      * handler object
182      *
183      * @var object
184      */
185     var $_handlerObj;
186
187     /**
188      * valid encodings
189      *
190      * @var array
191      */
192     var $_validEncodings = array('ISO-8859-1', 'UTF-8', 'US-ASCII');
193
194     // }}}
195     // {{{ php4 constructor
196
197     /**
198      * Creates an XML parser.
199      *
200      * This is needed for PHP4 compatibility, it will
201      * call the constructor, when a new instance is created.
202      *
203      * @param string $srcenc source charset encoding, use NULL (default) to use
204      *                       whatever the document specifies
205      * @param string $mode   how this parser object should work, "event" for
206      *                       startelement/endelement-type events, "func"
207      *                       to have it call functions named after elements
208      * @param string $tgtenc a valid target encoding
209      */
210     function XML_Parser($srcenc = null, $mode = 'event', $tgtenc = null)
211     {
212         XML_Parser::__construct($srcenc, $mode, $tgtenc);
213     }
214     // }}}
215     // {{{ php5 constructor
216
217     /**
218      * PHP5 constructor
219      *
220      * @param string $srcenc source charset encoding, use NULL (default) to use
221      *                       whatever the document specifies
222      * @param string $mode   how this parser object should work, "event" for
223      *                       startelement/endelement-type events, "func"
224      *                       to have it call functions named after elements
225      * @param string $tgtenc a valid target encoding
226      */
227     function __construct($srcenc = null, $mode = 'event', $tgtenc = null)
228     {
229         parent::__construct('XML_Parser_Error');
230
231         $this->mode   = $mode;
232         $this->srcenc = $srcenc;
233         $this->tgtenc = $tgtenc;
234     }
235     // }}}
236
237     /**
238      * Sets the mode of the parser.
239      *
240      * Possible modes are:
241      * - func
242      * - event
243      *
244      * You can set the mode using the second parameter
245      * in the constructor.
246      *
247      * This method is only needed, when switching to a new
248      * mode at a later point.
249      *
250      * @param string $mode mode, either 'func' or 'event'
251      *
252      * @return boolean|object  true on success, PEAR_Error otherwise
253      * @access public
254      */
255     function setMode($mode)
256     {
257         if ($mode != 'func' && $mode != 'event') {
258             $this->raiseError('Unsupported mode given', 
259                 XML_PARSER_ERROR_UNSUPPORTED_MODE);
260         }
261
262         $this->mode = $mode;
263         return true;
264     }
265
266     /**
267      * Sets the object, that will handle the XML events
268      *
269      * This allows you to create a handler object independent of the
270      * parser object that you are using and easily switch the underlying
271      * parser.
272      *
273      * If no object will be set, XML_Parser assumes that you
274      * extend this class and handle the events in $this.
275      *
276      * @param object &$obj object to handle the events
277      *
278      * @return boolean will always return true
279      * @access public
280      * @since v1.2.0beta3
281      */
282     function setHandlerObj(&$obj)
283     {
284         $this->_handlerObj = &$obj;
285         return true;
286     }
287
288     /**
289      * Init the element handlers
290      *
291      * @return mixed
292      * @access private
293      */
294     function _initHandlers()
295     {
296         if (!is_resource($this->parser)) {
297             return false;
298         }
299
300         if (!is_object($this->_handlerObj)) {
301             $this->_handlerObj = &$this;
302         }
303         switch ($this->mode) {
304
305         case 'func':
306             xml_set_object($this->parser, $this->_handlerObj);
307             xml_set_element_handler($this->parser, 
308                 array(&$this, 'funcStartHandler'), array(&$this, 'funcEndHandler'));
309             break;
310
311         case 'event':
312             xml_set_object($this->parser, $this->_handlerObj);
313             xml_set_element_handler($this->parser, 'startHandler', 'endHandler');
314             break;
315         default:
316             return $this->raiseError('Unsupported mode given', 
317                 XML_PARSER_ERROR_UNSUPPORTED_MODE);
318             break;
319         }
320
321         /**
322          * set additional handlers for character data, entities, etc.
323          */
324         foreach ($this->handler as $xml_func => $method) {
325             if (method_exists($this->_handlerObj, $method)) {
326                 $xml_func = 'xml_set_' . $xml_func;
327                 $xml_func($this->parser, $method);
328             }
329         }
330     }
331
332     // {{{ _create()
333
334     /**
335      * create the XML parser resource
336      *
337      * Has been moved from the constructor to avoid
338      * problems with object references.
339      *
340      * Furthermore it allows us returning an error
341      * if something fails.
342      *
343      * NOTE: uses '@' error suppresion in this method
344      *
345      * @return bool|PEAR_Error true on success, PEAR_Error otherwise
346      * @access private
347      * @see xml_parser_create
348      */
349     function _create()
350     {
351         if ($this->srcenc === null) {
352             $xp = xml_parser_create();
353         } else {
354             $xp = xml_parser_create($this->srcenc);
355         }
356         if (is_resource($xp)) {
357             if ($this->tgtenc !== null) {
358                 if (!xml_parser_set_option($xp, XML_OPTION_TARGET_ENCODING, 
359                     $this->tgtenc)
360                 ) {
361                     return $this->raiseError('invalid target encoding', 
362                         XML_PARSER_ERROR_INVALID_ENCODING);
363                 }
364             }
365             $this->parser = $xp;
366             $result       = $this->_initHandlers($this->mode);
367             if ($this->isError($result)) {
368                 return $result;
369             }
370             xml_parser_set_option($xp, XML_OPTION_CASE_FOLDING, $this->folding);
371             return true;
372         }
373         if (!empty($this->srcenc) && !in_array(strtoupper($this->srcenc), $this->_validEncodings)) {
374             return $this->raiseError('invalid source encoding', 
375                 XML_PARSER_ERROR_INVALID_ENCODING);
376         }
377         return $this->raiseError('Unable to create XML parser resource.', 
378             XML_PARSER_ERROR_NO_RESOURCE);
379     }
380
381     // }}}
382     // {{{ reset()
383
384     /**
385      * Reset the parser.
386      *
387      * This allows you to use one parser instance
388      * to parse multiple XML documents.
389      *
390      * @access   public
391      * @return   boolean|object     true on success, PEAR_Error otherwise
392      */
393     function reset()
394     {
395         $result = $this->_create();
396         if ($this->isError($result)) {
397             return $result;
398         }
399         return true;
400     }
401
402     // }}}
403     // {{{ setInputFile()
404
405     /**
406      * Sets the input xml file to be parsed
407      *
408      * @param string $file Filename (full path)
409      *
410      * @return resource fopen handle of the given file
411      * @access public
412      * @throws XML_Parser_Error
413      * @see setInput(), setInputString(), parse()
414      */
415     function setInputFile($file)
416     {
417         /**
418          * check, if file is a remote file
419          */
420         if (preg_match('/^(http|ftp):\/\//i', substr($file, 0, 10))) {
421             if (!ini_get('allow_url_fopen')) {
422                 return $this->
423                 raiseError('Remote files cannot be parsed, as safe mode is enabled.',
424                 XML_PARSER_ERROR_REMOTE);
425             }
426         }
427
428         $fp = @fopen($file, 'rb');
429         if (is_resource($fp)) {
430             $this->fp = $fp;
431             return $fp;
432         }
433         return $this->raiseError('File could not be opened.', 
434             XML_PARSER_ERROR_FILE_NOT_READABLE);
435     }
436
437     // }}}
438     // {{{ setInputString()
439
440     /**
441      * XML_Parser::setInputString()
442      *
443      * Sets the xml input from a string
444      *
445      * @param string $data a string containing the XML document
446      *
447      * @return null
448      */
449     function setInputString($data)
450     {
451         $this->fp = $data;
452         return null;
453     }
454
455     // }}}
456     // {{{ setInput()
457
458     /**
459      * Sets the file handle to use with parse().
460      *
461      * You should use setInputFile() or setInputString() if you
462      * pass a string
463      *
464      * @param mixed $fp Can be either a resource returned from fopen(),
465      *                  a URL, a local filename or a string.
466      *
467      * @return mixed
468      * @access public
469      * @see parse()
470      * @uses setInputString(), setInputFile()
471      */
472     function setInput($fp)
473     {
474         if (is_resource($fp)) {
475             $this->fp = $fp;
476             return true;
477         } elseif (preg_match('/^[a-z]+:\/\//i', substr($fp, 0, 10))) {
478             // see if it's an absolute URL (has a scheme at the beginning)
479             return $this->setInputFile($fp);
480         } elseif (file_exists($fp)) {
481             // see if it's a local file
482             return $this->setInputFile($fp);
483         } else {
484             // it must be a string
485             $this->fp = $fp;
486             return true;
487         }
488
489         return $this->raiseError('Illegal input format', 
490             XML_PARSER_ERROR_INVALID_INPUT);
491     }
492
493     // }}}
494     // {{{ parse()
495
496     /**
497      * Central parsing function.
498      *
499      * @return bool|PEAR_Error returns true on success, or a PEAR_Error otherwise
500      * @access public
501      */
502     function parse()
503     {
504         /**
505          * reset the parser
506          */
507         $result = $this->reset();
508         if ($this->isError($result)) {
509             return $result;
510         }
511         // if $this->fp was fopened previously
512         if (is_resource($this->fp)) {
513
514             while ($data = fread($this->fp, 4096)) {
515                 if (!$this->_parseString($data, feof($this->fp))) {
516                     $error = &$this->raiseError();
517                     $this->free();
518                     return $error;
519                 }
520             }
521         } else {
522             // otherwise, $this->fp must be a string
523             if (!$this->_parseString($this->fp, true)) {
524                 $error = &$this->raiseError();
525                 $this->free();
526                 return $error;
527             }
528         }
529         $this->free();
530
531         return true;
532     }
533
534     /**
535      * XML_Parser::_parseString()
536      *
537      * @param string $data data
538      * @param bool   $eof  end-of-file flag
539      *
540      * @return bool
541      * @access private
542      * @see parseString()
543      **/
544     function _parseString($data, $eof = false)
545     {
546         return xml_parse($this->parser, $data, $eof);
547     }
548
549     // }}}
550     // {{{ parseString()
551
552     /**
553      * XML_Parser::parseString()
554      *
555      * Parses a string.
556      *
557      * @param string  $data XML data
558      * @param boolean $eof  If set and TRUE, data is the last piece 
559      *                      of data sent in this parser
560      *
561      * @return bool|PEAR_Error true on success or a PEAR Error
562      * @throws XML_Parser_Error
563      * @see _parseString()
564      */
565     function parseString($data, $eof = false)
566     {
567         if (!isset($this->parser) || !is_resource($this->parser)) {
568             $this->reset();
569         }
570
571         if (!$this->_parseString($data, $eof)) {
572             $error = &$this->raiseError();
573             $this->free();
574             return $error;
575         }
576
577         if ($eof === true) {
578             $this->free();
579         }
580         return true;
581     }
582
583     /**
584      * XML_Parser::free()
585      *
586      * Free the internal resources associated with the parser
587      *
588      * @return null
589      **/
590     function free()
591     {
592         if (isset($this->parser) && is_resource($this->parser)) {
593             xml_parser_free($this->parser);
594             unset( $this->parser );
595         }
596         if (isset($this->fp) && is_resource($this->fp)) {
597             fclose($this->fp);
598         }
599         unset($this->fp);
600         return null;
601     }
602
603     /**
604      * XML_Parser::raiseError()
605      * 
606      * Throws a XML_Parser_Error
607      *
608      * @param string  $msg   the error message
609      * @param integer $ecode the error message code
610      *
611      * @return XML_Parser_Error reference to the error object
612      **/
613     function &raiseError($message= NULL, $code = NULL, $mode = NULL, $options = NULL, $userinfo = NULL, $error_class = NULL, $skipmsg = false)
614     {
615         $code = is_null($code) ? 0 : $code;
616         $msg = !is_null($message) ? $message : $this->parser;
617         $err = new XML_Parser_Error($msg, $ecode);
618         return parent::raiseError($err);
619     }
620
621     // }}}
622     // {{{ funcStartHandler()
623
624     /**
625      * derives and calls the Start Handler function
626      *
627      * @param mixed $xp      ??
628      * @param mixed $elem    ??
629      * @param mixed $attribs ??
630      *
631      * @return void
632      */
633     function funcStartHandler($xp, $elem, $attribs)
634     {
635         $func = 'xmltag_' . $elem;
636         $func = str_replace(array('.', '-', ':'), '_', $func);
637         if (method_exists($this->_handlerObj, $func)) {
638             call_user_func(array(&$this->_handlerObj, $func), $xp, $elem, $attribs);
639         } elseif (method_exists($this->_handlerObj, 'xmltag')) {
640             call_user_func(array(&$this->_handlerObj, 'xmltag'), 
641                 $xp, $elem, $attribs);
642         }
643     }
644
645     // }}}
646     // {{{ funcEndHandler()
647
648     /**
649      * derives and calls the End Handler function
650      *
651      * @param mixed $xp   ??
652      * @param mixed $elem ??
653      *
654      * @return void
655      */
656     function funcEndHandler($xp, $elem)
657     {
658         $func = 'xmltag_' . $elem . '_';
659         $func = str_replace(array('.', '-', ':'), '_', $func);
660         if (method_exists($this->_handlerObj, $func)) {
661             call_user_func(array(&$this->_handlerObj, $func), $xp, $elem);
662         } elseif (method_exists($this->_handlerObj, 'xmltag_')) {
663             call_user_func(array(&$this->_handlerObj, 'xmltag_'), $xp, $elem);
664         }
665     }
666
667     // }}}
668     // {{{ startHandler()
669
670     /**
671      * abstract method signature for Start Handler
672      *
673      * @param mixed $xp       ??
674      * @param mixed $elem     ??
675      * @param mixed &$attribs ??
676      *
677      * @return null
678      * @abstract
679      */
680     function startHandler($xp, $elem, &$attribs)
681     {
682         return null;
683     }
684
685     // }}}
686     // {{{ endHandler()
687
688     /**
689      * abstract method signature for End Handler
690      *
691      * @param mixed $xp   ??
692      * @param mixed $elem ??
693      *
694      * @return null
695      * @abstract
696      */
697     function endHandler($xp, $elem)
698     {
699         return null;
700     }
701
702
703     // }}}me
704 }
705
706 /**
707  * error class, replaces PEAR_Error
708  *
709  * An instance of this class will be returned
710  * if an error occurs inside XML_Parser.
711  *
712  * There are three advantages over using the standard PEAR_Error:
713  * - All messages will be prefixed
714  * - check for XML_Parser error, using is_a( $error, 'XML_Parser_Error' )
715  * - messages can be generated from the xml_parser resource
716  *
717  * @category  XML
718  * @package   XML_Parser
719  * @author    Stig Bakken <ssb@fast.no>
720  * @author    Tomas V.V.Cox <cox@idecnet.com>
721  * @author    Stephan Schmidt <schst@php.net>
722  * @copyright 2002-2008 The PHP Group
723  * @license   http://opensource.org/licenses/bsd-license New BSD License
724  * @version   Release: @package_version@
725  * @link      http://pear.php.net/package/XML_Parser
726  * @see       PEAR_Error
727  */
728 class XML_Parser_Error extends PEAR_Error
729 {
730     // {{{ properties
731
732     /**
733     * prefix for all messages
734     *
735     * @var      string
736     */
737     var $error_message_prefix = 'XML_Parser: ';
738
739     // }}}
740     // {{{ constructor()
741     /**
742     * construct a new error instance
743     *
744     * You may either pass a message or an xml_parser resource as first
745     * parameter. If a resource has been passed, the last error that
746     * happened will be retrieved and returned.
747     *
748     * @param string|resource $msgorparser message or parser resource
749     * @param integer         $code        error code
750     * @param integer         $mode        error handling
751     * @param integer         $level       error level
752     *
753     * @access   public
754     * @todo PEAR CS - can't meet 85char line limit without arg refactoring
755     */
756     function __construct($msgorparser = 'unknown error', $code = NULL, $mode = PEAR_ERROR_RETURN, $level = E_USER_NOTICE)
757     {
758         $code = is_null($code) ? 0 : $code;
759         if (is_resource($msgorparser)) {
760             $code        = xml_get_error_code($msgorparser);
761             $msgorparser = sprintf('%s at XML input line %d:%d',
762                 xml_error_string($code),
763                 xml_get_current_line_number($msgorparser),
764                 xml_get_current_column_number($msgorparser));
765         }
766         parent::__construct($msgorparser, $code, $mode, $level);
767     }
768     // }}}
769 }
770 ?>