File indexing completed on 2024-05-12 06:02:04

0001 <?php
0002 
0003 /**
0004  * Definition for tables.  The general idea is to extract out all of the
0005  * essential bits, and then reconstruct it later.
0006  *
0007  * This is a bit confusing, because the DTDs and the W3C
0008  * validators seem to disagree on the appropriate definition. The
0009  * DTD claims:
0010  *
0011  *      (CAPTION?, (COL*|COLGROUP*), THEAD?, TFOOT?, TBODY+)
0012  *
0013  * But actually, the HTML4 spec then has this to say:
0014  *
0015  *      The TBODY start tag is always required except when the table
0016  *      contains only one table body and no table head or foot sections.
0017  *      The TBODY end tag may always be safely omitted.
0018  *
0019  * So the DTD is kind of wrong.  The validator is, unfortunately, kind
0020  * of on crack.
0021  *
0022  * The definition changed again in XHTML1.1; and in my opinion, this
0023  * formulation makes the most sense.
0024  *
0025  *      caption?, ( col* | colgroup* ), (( thead?, tfoot?, tbody+ ) | ( tr+ ))
0026  *
0027  * Essentially, we have two modes: thead/tfoot/tbody mode, and tr mode.
0028  * If we encounter a thead, tfoot or tbody, we are placed in the former
0029  * mode, and we *must* wrap any stray tr segments with a tbody. But if
0030  * we don't run into any of them, just have tr tags is OK.
0031  */
0032 class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef
0033 {
0034     /**
0035      * @type bool
0036      */
0037     public $allow_empty = false;
0038 
0039     /**
0040      * @type string
0041      */
0042     public $type = 'table';
0043 
0044     /**
0045      * @type array
0046      */
0047     public $elements = array(
0048         'tr' => true,
0049         'tbody' => true,
0050         'thead' => true,
0051         'tfoot' => true,
0052         'caption' => true,
0053         'colgroup' => true,
0054         'col' => true
0055     );
0056 
0057     public function __construct()
0058     {
0059     }
0060 
0061     /**
0062      * @param array $children
0063      * @param HTMLPurifier_Config $config
0064      * @param HTMLPurifier_Context $context
0065      * @return array
0066      */
0067     public function validateChildren($children, $config, $context)
0068     {
0069         if (empty($children)) {
0070             return false;
0071         }
0072 
0073         // only one of these elements is allowed in a table
0074         $caption = false;
0075         $thead = false;
0076         $tfoot = false;
0077 
0078         // whitespace
0079         $initial_ws = array();
0080         $after_caption_ws = array();
0081         $after_thead_ws = array();
0082         $after_tfoot_ws = array();
0083 
0084         // as many of these as you want
0085         $cols = array();
0086         $content = array();
0087 
0088         $tbody_mode = false; // if true, then we need to wrap any stray
0089                              // <tr>s with a <tbody>.
0090 
0091         $ws_accum =& $initial_ws;
0092 
0093         foreach ($children as $node) {
0094             if ($node instanceof HTMLPurifier_Node_Comment) {
0095                 $ws_accum[] = $node;
0096                 continue;
0097             }
0098             switch ($node->name) {
0099             case 'tbody':
0100                 $tbody_mode = true;
0101                 // fall through
0102             case 'tr':
0103                 $content[] = $node;
0104                 $ws_accum =& $content;
0105                 break;
0106             case 'caption':
0107                 // there can only be one caption!
0108                 if ($caption !== false)  break;
0109                 $caption = $node;
0110                 $ws_accum =& $after_caption_ws;
0111                 break;
0112             case 'thead':
0113                 $tbody_mode = true;
0114                 // XXX This breaks rendering properties with
0115                 // Firefox, which never floats a <thead> to
0116                 // the top. Ever. (Our scheme will float the
0117                 // first <thead> to the top.)  So maybe
0118                 // <thead>s that are not first should be
0119                 // turned into <tbody>? Very tricky, indeed.
0120                 if ($thead === false) {
0121                     $thead = $node;
0122                     $ws_accum =& $after_thead_ws;
0123                 } else {
0124                     // Oops, there's a second one! What
0125                     // should we do?  Current behavior is to
0126                     // transmutate the first and last entries into
0127                     // tbody tags, and then put into content.
0128                     // Maybe a better idea is to *attach
0129                     // it* to the existing thead or tfoot?
0130                     // We don't do this, because Firefox
0131                     // doesn't float an extra tfoot to the
0132                     // bottom like it does for the first one.
0133                     $node->name = 'tbody';
0134                     $content[] = $node;
0135                     $ws_accum =& $content;
0136                 }
0137                 break;
0138             case 'tfoot':
0139                 // see above for some aveats
0140                 $tbody_mode = true;
0141                 if ($tfoot === false) {
0142                     $tfoot = $node;
0143                     $ws_accum =& $after_tfoot_ws;
0144                 } else {
0145                     $node->name = 'tbody';
0146                     $content[] = $node;
0147                     $ws_accum =& $content;
0148                 }
0149                 break;
0150             case 'colgroup':
0151             case 'col':
0152                 $cols[] = $node;
0153                 $ws_accum =& $cols;
0154                 break;
0155             case '#PCDATA':
0156                 // How is whitespace handled? We treat is as sticky to
0157                 // the *end* of the previous element. So all of the
0158                 // nonsense we have worked on is to keep things
0159                 // together.
0160                 if (!empty($node->is_whitespace)) {
0161                     $ws_accum[] = $node;
0162                 }
0163                 break;
0164             }
0165         }
0166 
0167         if (empty($content)) {
0168             return false;
0169         }
0170 
0171         $ret = $initial_ws;
0172         if ($caption !== false) {
0173             $ret[] = $caption;
0174             $ret = array_merge($ret, $after_caption_ws);
0175         }
0176         if ($cols !== false) {
0177             $ret = array_merge($ret, $cols);
0178         }
0179         if ($thead !== false) {
0180             $ret[] = $thead;
0181             $ret = array_merge($ret, $after_thead_ws);
0182         }
0183         if ($tfoot !== false) {
0184             $ret[] = $tfoot;
0185             $ret = array_merge($ret, $after_tfoot_ws);
0186         }
0187 
0188         if ($tbody_mode) {
0189             // we have to shuffle tr into tbody
0190             $current_tr_tbody = null;
0191 
0192             foreach($content as $node) {
0193                 switch ($node->name) {
0194                 case 'tbody':
0195                     $current_tr_tbody = null;
0196                     $ret[] = $node;
0197                     break;
0198                 case 'tr':
0199                     if ($current_tr_tbody === null) {
0200                         $current_tr_tbody = new HTMLPurifier_Node_Element('tbody');
0201                         $ret[] = $current_tr_tbody;
0202                     }
0203                     $current_tr_tbody->children[] = $node;
0204                     break;
0205                 case '#PCDATA':
0206                     //assert($node->is_whitespace);
0207                     if ($current_tr_tbody === null) {
0208                         $ret[] = $node;
0209                     } else {
0210                         $current_tr_tbody->children[] = $node;
0211                     }
0212                     break;
0213                 }
0214             }
0215         } else {
0216             $ret = array_merge($ret, $content);
0217         }
0218 
0219         return $ret;
0220 
0221     }
0222 }
0223 
0224 // vim: et sw=4 sts=4