- Fixed #16160: Call to undefined function ezi18n()
[tinyz:tinyz.git] / kernel / classes / datatypes / ezxmltext / ezxmlinputparser.php
1 <?php
2 //
3 // Definition of eZXMLInputParser class
4 //
5 // Created on: <27-Mar-2006 15:28:39 ks>
6 //
7 // ## BEGIN COPYRIGHT, LICENSE AND WARRANTY NOTICE ##
8 // SOFTWARE NAME: eZ Publish
9 // SOFTWARE RELEASE: 4.1.x
10 // COPYRIGHT NOTICE: Copyright (C) 1999-2010 eZ Systems AS
11 // SOFTWARE LICENSE: GNU General Public License v2.0
12 // NOTICE: >
13 //   This program is free software; you can redistribute it and/or
14 //   modify it under the terms of version 2.0  of the GNU General
15 //   Public License as published by the Free Software Foundation.
16 //
17 //   This program is distributed in the hope that it will be useful,
18 //   but WITHOUT ANY WARRANTY; without even the implied warranty of
19 //   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 //   GNU General Public License for more details.
21 //
22 //   You should have received a copy of version 2.0 of the GNU General
23 //   Public License along with this program; if not, write to the Free
24 //   Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 //   MA 02110-1301, USA.
26 //
27 //
28 // ## END COPYRIGHT, LICENSE AND WARRANTY NOTICE ##
29 //
30
31 /*
32     Base class for the input parser.
33     The goal of the parser is XML/HTML analyzing, fixing and transforming.
34     The input is processed in 2 passes:
35     - 1st pass: Parsing input, check for syntax errors, build DOM tree.
36     - 2nd pass: Walking through DOM tree, checking validity by XML schema,
37                 calling tag handlers to transform the tree.
38
39     Both passes are controlled by the arrays described bellow and user handler functions.
40
41 */
42
43 // if ( !class_exists( 'eZXMLSchema' ) ) // AS 21-09-2007: commented out because of include_once being commented out
44 class eZXMLInputParser
45 {
46     /// \deprecated (back-compatibility)
47     const SHOW_NO_ERRORS = 0;
48     const SHOW_SCHEMA_ERRORS = 1;
49     const SHOW_ALL_ERRORS = 2;
50
51     /// Use these constants for error types
52     const ERROR_NONE = 0;
53     const ERROR_SYNTAX = 4;
54     const ERROR_SCHEMA = 8;
55     const ERROR_DATA = 16;
56     const ERROR_ALL = 28; // 4+8+16
57
58     /* $InputTags array contains properties of elements that come from the input.
59
60     Each array element describes a tag that comes from the input. Arrays index is
61     a tag's name. Each element is an array that may contain the following members:
62
63     'name'        - a string representing a new name of the tag,
64     'nameHandler' - a name of the function that returns new tag name. Function format:
65                     function tagNameHandler( $tagName, &$attributes )
66
67     If no of those elements are defined the original tag's name is used.
68
69     'noChildren'  - boolean value that determines if this tag could have child tags,
70                     default value is false.
71
72     Example:
73
74     public $InputTags = array(
75
76         'original-name' => array( 'name' => 'new-name' ),
77
78         'original-name2' => array( 'nameHandler' => 'tagNameHandler',
79                                    'noChildren' => true ),
80
81          ...
82
83          );
84     */
85
86     public $InputTags = array();
87
88     /*
89     $OutputTags array contains properties of elements that are produced in the output.
90     Each array element describes a tag presented in the output. Arrays index is
91     a tag's name. Each element is an array that may contain the following members:
92
93     'parsingHandler' - "Parsing handler" called at parse pass 1 before processing tag's children.
94     'initHandler'    - "Init handler" called at pass 2 before proccessing tag's children.
95     'structHandler'  - "Structure handler" called at pass 2 after proccessing tag's children,
96                        but before schema validity check. It can be used to implement structure
97                        transformations.
98     'publishHandler' - "Publish handler" called at pass 2 after schema validity check, so it is called
99                        in case the element has it's guaranteed place in the DOM tree.
100
101     'attributes'     - an array that describes attributes transformations. Array's index is the
102                        original name of an attribute, and the value is the new name.
103
104     'requiredInputAttributes' - attributes that are required in the input tag. If they are not presented
105                                 it raises invalid input flag.
106
107     Example:
108
109     public $OutputTags = array(
110
111         'custom'    => array( 'parsingHandler' => 'parsingHandlerCustom',
112                               'initHandler' => 'initHandlerCustom',
113                               'structHandler' => 'structHandlerCustom',
114                               'publishHandler' => 'publishHandlerCustom',
115                               'attributes' => array( 'title' => 'name' ) ),
116
117         ...
118     );
119
120     */
121
122     public $OutputTags = array();
123
124     public $Namespaces = array( 'image' => 'http://ez.no/namespaces/ezpublish3/image/',
125                              'xhtml' => 'http://ez.no/namespaces/ezpublish3/xhtml/',
126                              'custom' => 'http://ez.no/namespaces/ezpublish3/custom/',
127                              'tmp' => 'http://ez.no/namespaces/ezpublish3/temporary/' );
128
129     /*!
130
131     The constructor.
132
133     \param $validate
134     \param $validateErrorLevel Determines types of errors that break input processing
135                                It's possible to combine any error types, by creating a bitmask of EZ_XMLINPUTPARSER_ERROR_* constants.
136                                \c true value means that all errors defined by $detectErrorLevel parameter will break further processing
137     \param $detectErrorLevel Determines types of errors that will be detected and added to error log ($Messages).
138     */
139
140     function eZXMLInputParser( $validateErrorLevel = self::ERROR_NONE, $detectErrorLevel = self::ERROR_NONE, $parseLineBreaks = false,
141                                $removeDefaultAttrs = false )
142     {
143         // Back-compatibility fixes:
144         if ( $detectErrorLevel === self::SHOW_SCHEMA_ERRORS )
145         {
146             $detectErrorLevel = self::ERROR_SCHEMA;
147         }
148         elseif ( $detectErrorLevel === self::SHOW_ALL_ERRORS )
149         {
150             $detectErrorLevel = self::ERROR_ALL;
151         }
152
153         if ( $validateErrorLevel === false )
154         {
155             $validateErrorLevel = self::ERROR_NONE;
156         }
157         elseif ( $validateErrorLevel === true )
158         {
159             $validateErrorLevel = $detectErrorLevel;
160         }
161
162         $this->ValidateErrorLevel = $validateErrorLevel;
163         $this->DetectErrorLevel = $detectErrorLevel;
164
165         $this->RemoveDefaultAttrs = $removeDefaultAttrs;
166         $this->ParseLineBreaks = $parseLineBreaks;
167
168         $this->XMLSchema = eZXMLSchema::instance();
169
170         $this->eZPublishVersion = eZPublishSDK::majorVersion() + eZPublishSDK::minorVersion() * 0.1;
171
172         $ini = eZINI::instance( 'ezxml.ini' );
173         if ( $ini->hasVariable( 'InputSettings', 'TrimSpaces' ) )
174         {
175             $trimSpaces = $ini->variable( 'InputSettings', 'TrimSpaces' );
176             $this->TrimSpaces = $trimSpaces == 'true' ? true : false;
177         }
178
179         if ( $ini->hasVariable( 'InputSettings', 'AllowMultipleSpaces' ) )
180         {
181             $allowMultipleSpaces = $ini->variable( 'InputSettings', 'AllowMultipleSpaces' );
182             $this->AllowMultipleSpaces = $allowMultipleSpaces == 'true' ? true : false;
183         }
184
185         if ( $ini->hasVariable( 'InputSettings', 'AllowNumericEntities' ) )
186         {
187             $allowNumericEntities = $ini->variable( 'InputSettings', 'AllowNumericEntities' );
188             $this->AllowNumericEntities = $allowNumericEntities == 'true' ? true : false;
189         }
190
191         $contentIni = eZINI::instance( 'content.ini' );
192         $useStrictHeaderRule = $contentIni->variable( 'header', 'UseStrictHeaderRule' );
193         $this->StrictHeaders = $useStrictHeaderRule == 'true' ? true : false;
194     }
195
196     /// \public
197     function setDOMDocumentClass( $DOMDocumentClass )
198     {
199         $this->DOMDocumentClass = $DOMDocumentClass;
200     }
201
202     /// \public
203     function setParseLineBreaks( $value )
204     {
205         $this->ParseLineBreaks = $value;
206     }
207
208     /// \public
209     function setRemoveDefaultAttrs( $value )
210     {
211         $this->RemoveDefaultAttrs = $value;
212     }
213
214     /// \public
215     function createRootNode()
216     {
217         if ( !$this->Document )
218         {
219             $this->Document = new $this->DOMDocumentClass( '1.0', 'utf-8' );
220         }
221
222         // Creating root section with namespaces definitions
223         $mainSection = $this->Document->createElement( 'section' );
224         $this->Document->appendChild( $mainSection );
225         foreach( array( 'image', 'xhtml', 'custom' ) as $prefix )
226         {
227             $mainSection->setAttributeNS( 'http://www.w3.org/2000/xmlns/', 'xmlns:' . $prefix, $this->Namespaces[$prefix] );
228         }
229         return $this->Document;
230     }
231
232     /*!
233         \public
234         Call this function to process your input
235     */
236     function process( $text, $createRootNode = true )
237     {
238         $text = str_replace( "\r", '', $text);
239         $text = str_replace( "\t", ' ', $text);
240         if ( !$this->ParseLineBreaks )
241         {
242             $text = str_replace( "\n", '', $text);
243         }
244
245         $this->Document = new $this->DOMDocumentClass( '1.0', 'utf-8' );
246
247         if ( $createRootNode )
248         {
249             $this->createRootNode();
250         }
251
252         // Perform pass 1
253         // Parsing the source string
254         $this->performPass1( $text );
255
256         //$this->Document->formatOutput = true;
257         eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'XML after pass 1' );
258
259         if ( $this->QuitProcess )
260         {
261             return false;
262         }
263
264         // Perform pass 2
265         $this->performPass2();
266
267         //$this->Document->formatOutput = true;
268         eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'XML after pass 2' );
269
270         if ( $this->QuitProcess )
271         {
272             return false;
273         }
274
275         return $this->Document;
276     }
277
278     /*
279        \public
280        Pass 1: Parsing the source HTML string.
281     */
282
283     function performPass1( &$data )
284     {
285         $ret = true;
286         $pos = 0;
287
288         if ( $this->Document->documentElement )
289         {
290             do
291             {
292                 $this->parseTag( $data, $pos, $this->Document->documentElement );
293                 if ( $this->QuitProcess )
294                 {
295                     $ret = false;
296                     break;
297                 }
298
299             }
300             while( $pos < strlen( $data ) );
301         }
302         else
303         {
304             $tmp = null;
305             $this->parseTag( $data, $pos, $tmp );
306             if ( $this->QuitProcess )
307             {
308                 $ret = false;
309             }
310         }
311         return $ret;
312     }
313
314     // The main recursive function for pass 1
315
316     function parseTag( &$data, &$pos, &$parent )
317     {
318         // Find tag, determine it's type, name and attributes.
319         $initialPos = $pos;
320
321         if ( $pos >= strlen( $data ) )
322         {
323             return true;
324         }
325         $tagBeginPos = strpos( $data, '<', $pos );
326
327         if ( $this->ParseLineBreaks )
328         {
329             // Regard line break as a start tag position
330             $lineBreakPos = strpos( $data, "\n", $pos );
331             if ( $lineBreakPos !== false )
332             {
333                 $tagBeginPos = $tagBeginPos === false ? $lineBreakPos : min( $tagBeginPos, $lineBreakPos );
334             }
335         }
336
337         $tagName = '';
338         $attributes = null;
339         // If it doesn't begin with '<' then its a text node.
340         if ( $tagBeginPos != $pos || $tagBeginPos === false )
341         {
342             $pos = $initialPos;
343             $tagName = $newTagName = '#text';
344             $noChildren = true;
345
346             if ( !$tagBeginPos )
347             {
348                 $tagBeginPos = strlen( $data );
349             }
350
351             $textContent = substr( $data, $pos, $tagBeginPos - $pos );
352
353             $textContent = $this->washText( $textContent );
354
355             $pos = $tagBeginPos;
356             if ( $textContent === '' )
357             {
358                 return false;
359             }
360         }
361         // Process closing tag.
362         elseif ( $data[$tagBeginPos] == '<' && $tagBeginPos + 1 < strlen( $data ) &&
363                  $data[$tagBeginPos + 1] == '/' )
364         {
365             $tagEndPos = strpos( $data, '>', $tagBeginPos + 1 );
366             if ( $tagEndPos === false )
367             {
368                 $pos = $tagBeginPos + 1;
369
370                 $this->handleError( self::ERROR_SYNTAX, ezpI18n::translate( 'kernel/classes/datatypes/ezxmltext', 'Wrong closing tag' ) );
371                 return false;
372             }
373
374             $pos = $tagEndPos + 1;
375             $closedTagName = strtolower( trim( substr( $data, $tagBeginPos + 2, $tagEndPos - $tagBeginPos - 2 ) ) );
376
377             // Find matching tag in ParentStack array
378             $firstLoop = true;
379             for( $i = count( $this->ParentStack ) - 1; $i >= 0; $i-- )
380             {
381                 $parentNames = $this->ParentStack[$i];
382                 if ( $parentNames[0] == $closedTagName )
383                 {
384                     array_pop( $this->ParentStack );
385                     if ( !$firstLoop )
386                     {
387                         $pos = $tagBeginPos;
388                         return true;
389                     }
390                     // If newTagName was '' we don't break children loop
391                     elseif ( $parentNames[1] !== '' )
392                     {
393                         return true;
394                     }
395                     else
396                     {
397                         return false;
398                     }
399                 }
400                 $firstLoop = false;
401             }
402
403             $this->handleError( self::ERROR_SYNTAX, ezpI18n::translate( 'kernel/classes/datatypes/ezxmltext', 'Wrong closing tag : &lt;/%1&gt;.', false, array( $closedTagName ) ) );
404
405             return false;
406         }
407         // Insert <br/> instead of linebreaks
408         elseif ( $this->ParseLineBreaks && $data[$tagBeginPos] == "\n" )
409         {
410             $newTagName = 'br';
411             $noChildren = true;
412             $pos = $tagBeginPos + 1;
413         }
414         //  Regular tag: get tag's name and attributes.
415         else
416         {
417             $tagEndPos = strpos( $data, '>', $tagBeginPos );
418             if ( $tagEndPos === false )
419             {
420                 $pos = $tagBeginPos + 1;
421
422                 $this->handleError( self::ERROR_SYNTAX, ezpI18n::translate( 'kernel/classes/datatypes/ezxmltext', 'Wrong opening tag' ) );
423                 return false;
424             }
425
426             $pos = $tagEndPos + 1;
427             $tagString = substr( $data, $tagBeginPos + 1, $tagEndPos - $tagBeginPos - 1 );
428             // Check for final backslash
429             $noChildren = substr( $tagString, -1, 1 ) == '/' ? true : false;
430             // Remove final backslash and spaces
431             $tagString = preg_replace( "/\s*\/$/", "", $tagString );
432
433             $firstSpacePos = strpos( $tagString, ' ' );
434             if ( $firstSpacePos === false )
435             {
436                 $tagName = strtolower( trim( $tagString ) );
437                 $attributeString = '';
438             }
439             else
440             {
441                 $tagName = strtolower( substr( $tagString, 0, $firstSpacePos ) );
442                 $attributeString = substr( $tagString, $firstSpacePos + 1 );
443                 $attributeString = trim( $attributeString );
444                 // Parse attribute string
445                 if ( $attributeString )
446                 {
447                     $attributes = $this->parseAttributes( $attributeString );
448                 }
449             }
450
451             // Determine tag's name
452             if ( isset( $this->InputTags[$tagName] ) )
453             {
454                 $thisInputTag = $this->InputTags[$tagName];
455
456                 if ( isset( $thisInputTag['name'] ) )
457                 {
458                     $newTagName = $thisInputTag['name'];
459                 }
460                 else
461                 {
462                     $newTagName = $this->callInputHandler( 'nameHandler', $tagName, $attributes );
463                 }
464             }
465             else
466             {
467                 if ( $this->XMLSchema->exists( $tagName ) )
468                 {
469                     $newTagName = $tagName;
470                 }
471                 else
472                 {
473                     $this->handleError( self::ERROR_SYNTAX, ezpI18n::translate( 'kernel/classes/datatypes/ezxmltext', 'Unknown tag: &lt;%1&gt;.', false, array( $tagName ) ) );
474                     return false;
475                 }
476             }
477
478             // Check 'noChildren' property
479             if ( isset( $thisInputTag['noChildren'] ) )
480             {
481                 $noChildren = true;
482             }
483
484             $thisOutputTag = isset( $this->OutputTags[$newTagName] ) ? $this->OutputTags[$newTagName] : null;
485
486             // Implementation of 'autoCloseOn' rule ( Handling of unclosed tags, ex.: <p>, <li> )
487             if ( isset( $thisOutputTag['autoCloseOn'] ) &&
488                  $parent &&
489                  $parent->parentNode instanceof DOMElement &&
490                  in_array( $parent->nodeName, $thisOutputTag['autoCloseOn'] ) )
491             {
492                 // Wrong nesting: auto-close parent and try to re-parse this tag at higher level
493                 array_pop( $this->ParentStack );
494                 $pos = $tagBeginPos;
495                 return true;
496             }
497
498             // Append to parent stack
499             if ( !$noChildren && $newTagName !== false )
500             {
501                 $this->ParentStack[] = array( $tagName, $newTagName, $attributeString );
502             }
503
504             if ( !$newTagName )
505             {
506                 // If $newTagName is an empty string then it's not a error
507                 if ( $newTagName === false )
508                     $this->handleError( self::ERROR_SYNTAX, ezpI18n::translate( 'kernel/classes/datatypes/ezxmltext', "Can't convert tag's name: &lt;%1&gt;.", false, array( $tagName ) ) );
509
510                 return false;
511             }
512
513             // wordmatch.ini support
514             if ( $attributeString )
515             {
516                 $attributes = $this->wordMatchSupport( $newTagName, $attributes, $attributeString );
517             }
518         }
519
520         // Create text or normal node.
521         if ( $newTagName == '#text' )
522         {
523             $element = $this->Document->createTextNode( $textContent );
524         }
525         else
526         {
527             $element = $this->Document->createElement( $newTagName );
528         }
529
530         if ( $attributes )
531         {
532             $this->setAttributes( $element, $attributes );
533         }
534
535         // Append element as a child or set it as root if there is no parent.
536         if ( $parent )
537         {
538             $parent->appendChild( $element );
539         }
540         else
541         {
542             $this->Document->appendChild( $element );
543         }
544
545         $params = array();
546         $params[] =& $data;
547         $params[] =& $pos;
548         $params[] =& $tagBeginPos;
549         $result = $this->callOutputHandler( 'parsingHandler', $element, $params );
550
551         if ( $result === false )
552         {
553             // This tag is already parsed in handler
554             if ( !$noChildren )
555             {
556                 array_pop( $this->ParentStack );
557             }
558             return false;
559         }
560
561         if ( $this->QuitProcess )
562         {
563             return false;
564         }
565
566         // Process children
567         if ( !$noChildren )
568         {
569             do
570             {
571                 $parseResult = $this->parseTag( $data, $pos, $element );
572                 if ( $this->QuitProcess )
573                 {
574                     return false;
575                 }
576             }
577             while( $parseResult !== true );
578         }
579
580         return false;
581     }
582
583     /*
584         Helper functions for pass 1
585     */
586
587     function parseAttributes( $attributeString )
588     {
589         // Convert single quotes to double quotes
590         $attributeString = preg_replace( "/ +([a-zA-Z0-9:-_#\-]+) *\='(.*?)'/e", "' \\1'.'=\"'.'\\2'.'\"'", ' ' . $attributeString );
591
592         // Convert no quotes to double quotes and remove extra spaces
593         $attributeString = preg_replace( "/ +([a-zA-Z0-9:-_#\-]+) *\= *([^\s'\"]+)/e", "' \\1'.'=\"'.'\\2'.'\" '", $attributeString );
594
595         // Split by quotes followed by spaces
596         $attributeArray = preg_split( "#(?<=\") +#", $attributeString );
597
598         $attributes = array();
599         foreach( $attributeArray as $attrStr )
600         {
601             if ( !$attrStr || strlen( $attrStr ) < 4 )
602             {
603                 continue;
604             }
605
606             list( $attrName, $attrValue ) = preg_split( "/ *= *\"/", $attrStr );
607
608             $attrName = strtolower( trim( $attrName ) );
609             if ( !$attrName )
610             {
611                 continue;
612             }
613
614             $attrValue = substr( $attrValue, 0, -1 );
615             if ( $attrValue === '' || $attrValue === false )
616             {
617                 continue;
618             }
619
620             $attributes[$attrName] = $attrValue;
621         }
622
623         return $attributes;
624     }
625
626     function setAttributes( $element, $attributes )
627     {
628         $thisOutputTag = $this->OutputTags[$element->nodeName];
629
630         foreach( $attributes as $key => $value )
631         {
632             // Convert attribute names
633             if ( isset( $thisOutputTag['attributes'] ) &&
634                  isset( $thisOutputTag['attributes'][$key] ) )
635             {
636                 $qualifiedName = $thisOutputTag['attributes'][$key];
637             }
638             else
639             {
640                 $qualifiedName = $key;
641             }
642
643             // Filter classes
644             if ( $qualifiedName == 'class' )
645             {
646                 $classesList = $this->XMLSchema->getClassesList( $element->nodeName );
647                 if ( !in_array( $value, $classesList ) )
648                 {
649                     $this->handleError( self::ERROR_DATA,
650                                         ezpI18n::translate( 'kernel/classes/datatypes/ezxmltext', "Class '%1' is not allowed for element &lt;%2&gt; (check content.ini).",
651                                         false, array( $value, $element->nodeName ) ) );
652                     continue;
653                 }
654             }
655
656             // Create attribute nodes
657             if ( $qualifiedName )
658             {
659                 if ( strpos( $qualifiedName, ':' ) )
660                 {
661                     list( $prefix, $name ) = explode( ':', $qualifiedName );
662                     if ( isset( $this->Namespaces[$prefix] ) )
663                     {
664                         $URI = $this->Namespaces[$prefix];
665                         $element->setAttributeNS( $URI, $qualifiedName, $value );
666                     }
667                     else
668                     {
669                         eZDebug::writeWarning( "No namespace defined for prefix '$prefix'.", 'eZXML input parser' );
670                     }
671                 }
672                 else
673                 {
674                     $element->setAttribute( $qualifiedName, $value );
675                 }
676             }
677         }
678
679         // Check for required attrs are present
680         if ( isset( $this->OutputTags[$element->nodeName]['requiredInputAttributes'] ) )
681         {
682             foreach( $this->OutputTags[$element->nodeName]['requiredInputAttributes'] as $reqAttrName )
683             {
684                 $presented = false;
685                 foreach( $attributes as $key => $value )
686                 {
687                     if ( $key == $reqAttrName )
688                     {
689                         $presented = true;
690                         break;
691                     }
692                 }
693                 if ( !$presented )
694                 {
695                     $this->handleError( self::ERROR_SCHEMA,
696                                         ezpI18n::translate( 'kernel/classes/datatypes/ezxmltext', "Required attribute '%1' is not presented in tag &lt;%2&gt;.",
697                                         false, array( $reqAttrName, $element->nodeName ) ) );
698                 }
699             }
700         }
701     }
702
703     function washText( $textContent )
704     {
705         $textContent = $this->entitiesDecode( $textContent );
706
707         if ( !$this->AllowNumericEntities )
708         {
709             $textContent = $this->convertNumericEntities( $textContent );
710         }
711
712         if ( !$this->AllowMultipleSpaces )
713         {
714             $textContent = preg_replace( "/ {2,}/", " ", $textContent );
715         }
716
717         return $textContent;
718     }
719
720     function entitiesDecode( $text )
721     {
722         $text = str_replace( '&#039;', "'", $text );
723
724         $text = str_replace( '&gt;', '>', $text );
725         $text = str_replace( '&lt;', '<', $text );
726         $text = str_replace( '&apos;', "'", $text );
727         $text = str_replace( '&quot;', '"', $text );
728         $text = str_replace( '&amp;', '&', $text );
729         $text = str_replace( '&nbsp;', ' ', $text );
730         return $text;
731     }
732
733     function convertNumericEntities( $text )
734     {
735         if ( strlen( $text ) < 4 )
736         {
737             return $text;
738         }
739         // Convert other HTML entities to the current charset characters.
740         $codec = eZTextCodec::instance( 'unicode', false );
741         $pos = 0;
742         $domString = "";
743         while ( $pos < strlen( $text ) - 1 )
744         {
745             $startPos = $pos;
746             while( !( $text[$pos] == '&' && $text[$pos + 1] == '#' ) && $pos < strlen( $text ) - 1 )
747             {
748                 $pos++;
749             }
750
751             $domString .= substr( $text, $startPos, $pos - $startPos );
752
753             if ( $pos < strlen( $text ) - 1 )
754             {
755                 $endPos = strpos( $text, ';', $pos + 2 );
756                 if ( $endPos === false )
757                 {
758                     $convertedText .= '&#';
759                     $pos += 2;
760                     continue;
761                 }
762
763                 $code = substr( $text, $pos + 2, $endPos - ( $pos + 2 ) );
764                 $char = $codec->convertString( array( $code ) );
765
766                 $pos = $endPos + 1;
767                 $domString .= $char;
768             }
769             else
770             {
771                 $domString .= substr( $text, $pos, 2 );
772             }
773         }
774         return $domString;
775     }
776
777     /*!
778      Returns modified attributes parameter
779      */
780     protected function wordMatchSupport( $newTagName, $attributes, $attributeString )
781     {
782         $ini = eZINI::instance( 'wordmatch.ini' );
783         if ( $ini->hasVariable( $newTagName, 'MatchString' ) )
784         {
785             $matchArray = $ini->variable( $newTagName, 'MatchString' );
786             if ( $matchArray )
787             {
788                 foreach ( array_keys( $matchArray ) as $key )
789                 {
790                     $matchString = $matchArray[$key];
791                     if (  preg_match( "/$matchString/i", $attributeString ) )
792                     {
793                         $attributes['class'] = $key;
794                         unset( $attributes['style'] );
795                     }
796                 }
797             }
798         }
799         return $attributes;
800     }
801
802
803     /*!
804         \public
805         Pass 2: Process the tree, run handlers, rebuild and validate.
806     */
807
808     function performPass2()
809     {
810         $tmp = null;
811
812         $this->processSubtree( $this->Document->documentElement, $tmp );
813     }
814
815     // main recursive function for pass 2
816
817     function processSubtree( $element, &$lastHandlerResult )
818     {
819         $ret = null;
820         $tmp = null;
821
822         // Call "Init handler"
823         $this->callOutputHandler( 'initHandler', $element, $tmp );
824
825         // Process children
826         if ( $element->hasChildNodes() )
827         {
828             // Make another copy of children to save primary structure
829             $childNodes = $element->childNodes;
830             $childrenCount = $childNodes->length;
831
832             // we can not loop directly over the childNodes property, because this will change while we are working on it's parent's children
833             $children = array();
834             foreach ( $childNodes as $childNode )
835             {
836                 $children[] = $childNode;
837             }
838
839             $lastResult = null;
840             $newElements = array();
841             foreach ( $children as $child )
842             {
843                 eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', 'processing children, current child: ' . $child->nodeName );
844                 $childReturn = $this->processSubtree( $child, $lastResult );
845
846                 unset( $lastResult );
847                 if ( isset( $childReturn['result'] ) )
848                 {
849                     eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', 'return result is set for child ' . $child->nodeName );
850                     $lastResult = $childReturn['result'];
851                 }
852
853                 if ( isset( $childReturn['new_elements'] ) )
854                 {
855                     $newElements = array_merge( $newElements, $childReturn['new_elements'] );
856                 }
857
858                 if ( $this->QuitProcess )
859                 {
860                     return $ret;
861                 }
862             }
863
864             eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'XML before processNewElements for element ' . $element->nodeName );
865             // process elements created in children handlers
866             $this->processNewElements( $newElements );
867             eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'XML after processNewElements for element ' . $element->nodeName );
868         }
869
870         // Call "Structure handler"
871         eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'XML before callOutputHandler structHandler for element ' . $element->nodeName );
872         $ret = $this->callOutputHandler( 'structHandler', $element, $lastHandlerResult );
873         eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'XML after callOutputHandler structHandler for element ' . $element->nodeName );
874         eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $ret, 'return value of callOutputHandler structHandler for element ' . $element->nodeName );
875
876         // Process by schema (check if element is allowed to exist)
877         if ( !$this->processBySchemaPresence( $element ) )
878         {
879             eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'XML after processBySchemaPresence for element ' . $element->nodeName );
880             return $ret;
881         }
882
883         eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'XML after processBySchemaPresence for element ' . $element->nodeName );
884
885         // Process by schema (check place in the tree)
886         if ( !$this->processBySchemaTree( $element ) )
887         {
888             eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'XML after processBySchemaTree for element ' . $element->nodeName );
889             return $ret;
890         }
891
892         eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'XML after processBySchemaTree for element ' . $element->nodeName );
893
894
895         $tmp = null;
896         // Call "Publish handler"
897         $this->callOutputHandler( 'publishHandler', $element, $tmp );
898
899         // Process attributes according to the schema
900         if ( $element->hasAttributes() )
901         {
902             if ( !$this->XMLSchema->hasAttributes( $element ) )
903             {
904                 eZXMLInputParser::removeAllAttributes( $element );
905             }
906             else
907             {
908                 $this->processAttributesBySchema( $element );
909             }
910         }
911         return $ret;
912     }
913     /*
914         Helper functions for pass 2
915     */
916
917     /*!
918        Removes all attribute nodes from element node $element
919     */
920     function removeAllAttributes( DOMElement $element )
921     {
922         $attribs = $element->attributes;
923         for ( $i = $attribs->length - 1; $i >= 0; $i-- )
924         {
925             $element->removeAttributeNode( $attribs->item( $i ) );
926         }
927     }
928
929     // Check if the element is allowed to exist in this document and remove it if not.
930     function processBySchemaPresence( $element )
931     {
932         $parent = $element->parentNode;
933         if ( $parent instanceof DOMElement )
934         {
935             // If this is a foreign element, remove it
936             if ( !$this->XMLSchema->exists( $element ) )
937             {
938                 if ( $element->nodeName == 'custom' )
939                 {
940                     $this->handleError( self::ERROR_SCHEMA,
941                                         ezpI18n::translate( 'kernel/classes/datatypes/ezxmltext', "Custom tag '%1' is not allowed.",
942                                         false, array( $element->getAttribute( 'name' ) ) ) );
943                 }
944                 $element = $parent->removeChild( $element );
945                 return false;
946             }
947
948             // Delete if children required and no children
949             // If this is an auto-added element, then do not throw error
950
951             if ( $element->nodeType == XML_ELEMENT_NODE && ( $this->XMLSchema->childrenRequired( $element ) || $element->getAttribute( 'children_required' ) )
952                  && !$element->hasChildNodes() )
953             {
954                 $element = $parent->removeChild( $element );
955                 if ( !$element->getAttributeNS( 'http://ez.no/namespaces/ezpublish3/temporary/', 'new-element' ) )
956                 {
957                     $this->handleError( self::ERROR_SCHEMA, ezpI18n::translate( 'kernel/classes/datatypes/ezxmltext', "&lt;%1&gt; tag can't be empty.",
958                                         false, array( $element->nodeName ) ) );
959                     return false;
960                 }
961             }
962         }
963         // TODO: break processing of any node that doesn't have parent
964         //       and is not a root node.
965         elseif ( $element->nodeName != 'section' )
966         {
967             return false;
968         }
969         return true;
970     }
971
972     // Check that element has a correct position in the tree and fix it if not.
973     function processBySchemaTree( $element )
974     {
975         $parent = $element->parentNode;
976
977         if ( $parent instanceof DOMElement )
978         {
979             $schemaCheckResult = $this->XMLSchema->check( $parent, $element );
980             if ( !$schemaCheckResult )
981             {
982                 if ( $schemaCheckResult === false )
983                 {
984                     // Remove indenting spaces
985                     if ( $element->nodeType == XML_TEXT_NODE && !trim( $element->textContent ) )
986                     {
987                         $element = $parent->removeChild( $element );
988                         return false;
989                     }
990
991                     $elementName = $element->nodeType == XML_ELEMENT_NODE ? '&lt;' . $element->nodeName . '&gt;' : $element->nodeName;
992                     $this->handleError( self::ERROR_SCHEMA, ezpI18n::translate( 'kernel/classes/datatypes/ezxmltext', "%1 is not allowed to be a child of &lt;%2&gt;.",
993                                         false, array( $elementName, $parent->nodeName ) ) );
994                 }
995                 $this->fixSubtree( $element, $element );
996                 return false;
997             }
998         }
999         // TODO: break processing of any node that doesn't have parent
1000         //       and is not a root node.
1001         elseif ( $element->nodeName != 'section' )
1002         {
1003             return false;
1004         }
1005         return true;
1006     }
1007
1008     // Remove only nodes that don't match schema (recursively)
1009     function fixSubtree( $element, $mainChild )
1010     {
1011         $parent = $element->parentNode;
1012         $mainParent = $mainChild->parentNode;
1013         while ( $element->hasChildNodes() )
1014         {
1015             $child = $element->firstChild;
1016
1017             $child = $element->removeChild( $child );
1018             $child = $mainParent->insertBefore( $child, $mainChild );
1019
1020             if ( !$this->XMLSchema->check( $mainParent, $child ) )
1021             {
1022                 $this->fixSubtree( $child, $mainChild );
1023             }
1024         }
1025         $parent->removeChild( $element );
1026     }
1027
1028     function processAttributesBySchema( $element )
1029     {
1030         // Remove attributes that don't match schema
1031         $schemaAttributes = $this->XMLSchema->attributes( $element );
1032         $schemaCustomAttributes = $this->XMLSchema->customAttributes( $element );
1033
1034         $attributes = $element->attributes;
1035
1036         for ( $i = $attributes->length - 1; $i >=0; $i-- )
1037         {
1038             $attr = $attributes->item( $i );
1039             if ( $attr->prefix == 'tmp' )
1040             {
1041                 $element->removeAttributeNode( $attr );
1042                 continue;
1043             }
1044
1045             $allowed = false;
1046             $removeAttr = false;
1047
1048             $fullName = $attr->prefix ? $attr->prefix . ':' . $attr->localName : $attr->nodeName;
1049
1050             // check for allowed custom attributes (3.9)
1051             if ( $attr->prefix == 'custom' && in_array( $attr->localName, $schemaCustomAttributes ) )
1052             {
1053                 $allowed = true;
1054             }
1055             else
1056             {
1057                 if ( in_array( $fullName, $schemaAttributes ) )
1058                 {
1059                    $allowed = true;
1060                 }
1061                 elseif ( in_array( $fullName, $schemaCustomAttributes ) )
1062                 {
1063                     // add 'custom' prefix if it is not given
1064                     $allowed = true;
1065                     $removeAttr = true;
1066                     $element->setAttributeNS( $this->Namespaces['custom'], 'custom:' . $fullName, $attr->value );
1067                 }
1068             }
1069
1070             if ( !$allowed )
1071             {
1072                 $removeAttr = true;
1073                 $this->handleError( self::ERROR_SCHEMA,
1074                                     ezpI18n::translate( 'kernel/classes/datatypes/ezxmltext', "Attribute '%1' is not allowed in &lt;%2&gt; element.",
1075                                     false, array( $fullName, $element->nodeName ) ) );
1076             }
1077             elseif ( $this->RemoveDefaultAttrs )
1078             {
1079                 // Remove attributes having default values
1080                 $default = $this->XMLSchema->attrDefaultValue( $element->nodeName, $fullName );
1081                 if ( $attr->value == $default )
1082                 {
1083                     $removeAttr = true;
1084                 }
1085             }
1086
1087             if ( $removeAttr )
1088             {
1089                 $element->removeAttributeNode( $attr );
1090             }
1091         }
1092     }
1093
1094     function callInputHandler( $handlerName, $tagName, &$attributes )
1095     {
1096         $result = null;
1097         $thisInputTag = $this->InputTags[$tagName];
1098         if ( isset( $thisInputTag[$handlerName] ) )
1099         {
1100             if ( is_callable( array( $this, $thisInputTag[$handlerName] ) ) )
1101             {
1102                 $result = call_user_func_array( array( $this, $thisInputTag[$handlerName] ),
1103                                                 array( $tagName, &$attributes ) );
1104             }
1105             else
1106             {
1107                 eZDebug::writeWarning( "'$handlerName' input handler for tag <$tagName> doesn't exist: '" . $thisInputTag[$handlerName] . "'.", 'eZXML input parser' );
1108             }
1109         }
1110         return $result;
1111     }
1112
1113     function callOutputHandler( $handlerName, $element, &$params )
1114     {
1115         $result = null;
1116         $thisOutputTag = $this->OutputTags[$element->nodeName];
1117         if ( isset( $thisOutputTag[$handlerName] ) )
1118         {
1119             if ( is_callable( array( $this, $thisOutputTag[$handlerName] ) ) )
1120             {
1121                 $result = call_user_func_array( array( $this, $thisOutputTag[$handlerName] ),
1122                                                 array( $element, &$params ) );
1123             }
1124             else
1125             {
1126                 eZDebug::writeWarning( "'$handlerName' output handler for tag <$element->nodeName> doesn't exist: '" . $thisOutputTag[$handlerName] . "'.", 'eZXML input parser' );
1127             }
1128         }
1129
1130         return $result;
1131     }
1132
1133     // Creates new element and adds it to array for further post-processing.
1134     // Use this function if you need to process newly created element (check it by schema
1135     // and call 'structure' and 'publish' handlers)
1136     function createAndPublishElement( $elementName, &$ret )
1137     {
1138         $element = $this->Document->createElement( $elementName );
1139         $element->setAttributeNS( 'http://ez.no/namespaces/ezpublish3/temporary/', 'tmp:new-element', 'true' );
1140
1141         if ( !isset( $ret['new_elements'] ) )
1142         {
1143             $ret['new_elements'] = array();
1144         }
1145
1146         $ret['new_elements'][] = $element;
1147         return $element;
1148     }
1149
1150     function processNewElements( $createdElements )
1151     {
1152         // Call handlers for newly created elements
1153         foreach ( $createdElements as $element )
1154         {
1155             eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', 'processing new element ' . $element->nodeName );
1156             $tmp = null;
1157
1158             if ( !$this->processBySchemaPresence( $element ) )
1159             {
1160                 eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'xml string after processBySchemaPresence for new element ' . $element->nodeName );
1161                 continue;
1162             }
1163             eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'xml string after processBySchemaPresence for new element ' . $element->nodeName );
1164
1165
1166             // Call "Structure handler"
1167             $this->callOutputHandler( 'structHandler', $element, $tmp );
1168
1169             if ( !$this->processBySchemaTree( $element ) )
1170             {
1171                 eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'xml string after processBySchemaTree for new element ' . $element->nodeName );
1172                 continue;
1173             }
1174             eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'xml string after processBySchemaTree for new element ' . $element->nodeName );
1175
1176
1177             $tmp2 = null;
1178             // Call "Publish handler"
1179             $this->callOutputHandler( 'publishHandler', $element, $tmp2 );
1180             eZDebugSetting::writeDebug( 'kernel-datatype-ezxmltext', $this->Document->saveXML(), 'xml string after callOutputHandler publishHandler for new element ' . $element->nodeName );
1181
1182             // Process attributes according to the schema
1183             if( $element->hasAttributes() )
1184             {
1185                 if ( !$this->XMLSchema->hasAttributes( $element ) )
1186                 {
1187                     eZXMLInputParser::removeAllAttributes( $element );
1188                 }
1189                 else
1190                 {
1191                     $this->processAttributesBySchema( $element );
1192                 }
1193             }
1194         }
1195     }
1196
1197     /// \public
1198     function getMessages()
1199     {
1200         return $this->Messages;
1201     }
1202
1203     /// \public
1204     function isValid()
1205     {
1206         return $this->IsInputValid;
1207     }
1208
1209     function handleError( $type, $message )
1210     {
1211         if ( $type & $this->DetectErrorLevel )
1212         {
1213             $this->IsInputValid = false;
1214             if ( $message )
1215             {
1216                 $this->Messages[] = $message;
1217             }
1218         }
1219
1220         if ( $type & $this->ValidateErrorLevel )
1221         {
1222             $this->IsInputValid = false;
1223             $this->QuitProcess = true;
1224         }
1225     }
1226
1227     public $DOMDocumentClass = 'DOMDOcument';
1228
1229     public $XMLSchema;
1230     public $Document = null;
1231     public $Messages = array();
1232     public $eZPublishVersion;
1233
1234     public $ParentStack = array();
1235
1236     public $ValidateErrorLevel;
1237     public $DetectErrorLevel;
1238
1239     public $IsInputValid = true;
1240     public $QuitProcess = false;
1241
1242     // options that depend on settings
1243     public $TrimSpaces = true;
1244     public $AllowMultipleSpaces = false;
1245     public $AllowNumericEntities = false;
1246     public $StrictHeaders = false;
1247
1248     // options that depend on parameters passed
1249     public $ParseLineBreaks = false;
1250     public $RemoveDefaultAttrs = false;
1251 }
1252 ?>