]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/Xml.php
MediaWiki 1.16.0
[autoinstalls/mediawiki.git] / includes / Xml.php
1 <?php
2
3 /**
4  * Module of static functions for generating XML
5  */
6
7 class Xml {
8         /**
9          * Format an XML element with given attributes and, optionally, text content.
10          * Element and attribute names are assumed to be ready for literal inclusion.
11          * Strings are assumed to not contain XML-illegal characters; special
12          * characters (<, >, &) are escaped but illegals are not touched.
13          *
14          * @param $element String: element name
15          * @param $attribs Array: Name=>value pairs. Values will be escaped.
16          * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default)
17          * @param $allowShortTag Bool: whether '' in $contents will result in a contentless closed tag
18          * @return string
19          */
20         public static function element( $element, $attribs = null, $contents = '', $allowShortTag = true ) {
21                 $out = '<' . $element;
22                 if( !is_null( $attribs ) ) {
23                         $out .=  self::expandAttributes( $attribs );
24                 }
25                 if( is_null( $contents ) ) {
26                         $out .= '>';
27                 } else {
28                         if( $allowShortTag && $contents === '' ) {
29                                 $out .= ' />';
30                         } else {
31                                 $out .= '>' . htmlspecialchars( $contents ) . "</$element>";
32                         }
33                 }
34                 return $out;
35         }
36
37         /**
38          * Given an array of ('attributename' => 'value'), it generates the code
39          * to set the XML attributes : attributename="value".
40          * The values are passed to Sanitizer::encodeAttribute.
41          * Return null if no attributes given.
42          * @param $attribs Array of attributes for an XML element
43          */
44         public static function expandAttributes( $attribs ) {
45                 $out = '';
46                 if( is_null( $attribs ) ) {
47                         return null;
48                 } elseif( is_array( $attribs ) ) {
49                         foreach( $attribs as $name => $val )
50                                 $out .= " {$name}=\"" . Sanitizer::encodeAttribute( $val ) . '"';
51                         return $out;
52                 } else {
53                         throw new MWException( 'Expected attribute array, got something else in ' . __METHOD__ );
54                 }
55         }
56
57         /**
58          * Format an XML element as with self::element(), but run text through the
59          * $wgContLang->normalize() validator first to ensure that no invalid UTF-8
60          * is passed.
61          *
62          * @param $element String:
63          * @param $attribs Array: Name=>value pairs. Values will be escaped.
64          * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default)
65          * @return string
66          */
67         public static function elementClean( $element, $attribs = array(), $contents = '') {
68                 global $wgContLang;
69                 if( $attribs ) {
70                         $attribs = array_map( array( 'UtfNormal', 'cleanUp' ), $attribs );
71                 }
72                 if( $contents ) {
73                         wfProfileIn( __METHOD__ . '-norm' );
74                         $contents = $wgContLang->normalize( $contents );
75                         wfProfileOut( __METHOD__ . '-norm' );
76                 }
77                 return self::element( $element, $attribs, $contents );
78         }
79
80         /**
81          * This opens an XML element
82          *
83          * @param $element name of the element
84          * @param $attribs array of attributes, see Xml::expandAttributes()
85          * @return string
86          */
87         public static function openElement( $element, $attribs = null ) {
88                 return '<' . $element . self::expandAttributes( $attribs ) . '>';
89         }
90
91         /**
92          * Shortcut to close an XML element
93          * @param $element element name
94          * @return string
95          */
96         public static function closeElement( $element ) { return "</$element>"; }
97
98         /**
99          * Same as Xml::element(), but does not escape contents. Handy when the
100          * content you have is already valid xml.
101          *
102          * @param $element element name
103          * @param $attribs array of attributes
104          * @param $contents content of the element
105          * @return string
106          */
107         public static function tags( $element, $attribs = null, $contents ) {
108                 return self::openElement( $element, $attribs ) . $contents . "</$element>";
109         }
110
111         /**
112          * Build a drop-down box for selecting a namespace
113          *
114          * @param $selected Mixed: Namespace which should be pre-selected
115          * @param $all Mixed: Value of an item denoting all namespaces, or null to omit
116          * @param $element_name String: value of the "name" attribute of the select tag
117          * @param $label String: optional label to add to the field
118          * @return string
119          */
120         public static function namespaceSelector( $selected = '', $all = null, $element_name = 'namespace', $label = null ) {
121                 global $wgContLang;
122                 $namespaces = $wgContLang->getFormattedNamespaces();
123                 $options = array();
124
125                 // Godawful hack... we'll be frequently passed selected namespaces
126                 // as strings since PHP is such a shithole.
127                 // But we also don't want blanks and nulls and "all"s matching 0,
128                 // so let's convert *just* string ints to clean ints.
129                 if( preg_match( '/^\d+$/', $selected ) ) {
130                         $selected = intval( $selected );
131                 }
132
133                 if( !is_null( $all ) )
134                         $namespaces = array( $all => wfMsg( 'namespacesall' ) ) + $namespaces;
135                 foreach( $namespaces as $index => $name ) {
136                         if( $index < NS_MAIN )
137                                 continue;
138                         if( $index === 0 )
139                                 $name = wfMsg( 'blanknamespace' );
140                         $options[] = self::option( $name, $index, $index === $selected );
141                 }
142
143                 $ret = Xml::openElement( 'select', array( 'id' => 'namespace', 'name' => $element_name,
144                         'class' => 'namespaceselector' ) )
145                         . "\n"
146                         . implode( "\n", $options )
147                         . "\n"
148                         . Xml::closeElement( 'select' );
149                 if ( !is_null( $label ) ) {
150                         $ret = Xml::label( $label, $element_name ) . '&nbsp;' . $ret;
151                 }
152                 return $ret;
153         }
154
155         /**
156          * Create a date selector
157          *
158          * @param $selected Mixed: the month which should be selected, default ''
159          * @param $allmonths String: value of a special item denoting all month. Null to not include (default)
160          * @param $id String: Element identifier
161          * @return String: Html string containing the month selector
162          */
163         public static function monthSelector( $selected = '', $allmonths = null, $id = 'month' ) {
164                 global $wgLang;
165                 $options = array();
166                 if( is_null( $selected ) )
167                         $selected = '';
168                 if( !is_null( $allmonths ) )
169                         $options[] = self::option( wfMsg( 'monthsall' ), $allmonths, $selected === $allmonths );
170                 for( $i = 1; $i < 13; $i++ )
171                         $options[] = self::option( $wgLang->getMonthName( $i ), $i, $selected === $i );
172                 return self::openElement( 'select', array( 'id' => $id, 'name' => 'month', 'class' => 'mw-month-selector' ) )
173                         . implode( "\n", $options )
174                         . self::closeElement( 'select' );
175         }
176         
177         /**
178          * @param $year Integer
179          * @param $month Integer
180          * @return string Formatted HTML
181          */
182         public static function dateMenu( $year, $month ) {
183                 # Offset overrides year/month selection
184                 if( $month && $month !== -1 ) {
185                         $encMonth = intval( $month );
186                 } else {
187                         $encMonth = '';
188                 }
189                 if( $year ) {
190                         $encYear = intval( $year );
191                 } else if( $encMonth ) {
192                         $thisMonth = intval( gmdate( 'n' ) );
193                         $thisYear = intval( gmdate( 'Y' ) );
194                         if( intval($encMonth) > $thisMonth ) {
195                                 $thisYear--;
196                         }
197                         $encYear = $thisYear;
198                 } else {
199                         $encYear = '';
200                 }
201                 return Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
202                         Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) . ' '.
203                         Xml::label( wfMsg( 'month' ), 'month' ) . ' '.
204                         Xml::monthSelector( $encMonth, -1 );
205         }
206
207         /**
208          *
209          * @param $selected The language code of the selected language
210          * @param $customisedOnly If true only languages which have some content are listed
211          * @return array of label and select
212          */
213         public static function languageSelector( $selected, $customisedOnly = true ) {
214                 global $wgContLanguageCode;
215                 /**
216                  * Make sure the site language is in the list; a custom language code
217                  * might not have a defined name...
218                  */
219                 $languages = Language::getLanguageNames( $customisedOnly );
220                 if( !array_key_exists( $wgContLanguageCode, $languages ) ) {
221                         $languages[$wgContLanguageCode] = $wgContLanguageCode;
222                 }
223                 ksort( $languages );
224
225                 /**
226                  * If a bogus value is set, default to the content language.
227                  * Otherwise, no default is selected and the user ends up
228                  * with an Afrikaans interface since it's first in the list.
229                  */
230                 $selected = isset( $languages[$selected] ) ? $selected : $wgContLanguageCode;
231                 $options = "\n";
232                 foreach( $languages as $code => $name ) {
233                         $options .= Xml::option( "$code - $name", $code, ($code == $selected) ) . "\n";
234                 }
235
236                 return array(
237                         Xml::label( wfMsg('yourlanguage'), 'wpUserLanguage' ),
238                         Xml::tags( 'select',
239                                 array( 'id' => 'wpUserLanguage', 'name' => 'wpUserLanguage' ),
240                                 $options
241                         )
242                 );
243
244         }
245
246         /**
247          * Shortcut to make a span element
248          * @param $text content of the element, will be escaped
249          * @param $class class name of the span element
250          * @param $attribs other attributes
251          * @return string 
252          */
253         public static function span( $text, $class, $attribs=array() ) {
254                 return self::element( 'span', array( 'class' => $class ) + $attribs, $text );
255         }
256
257         /**
258          * Shortcut to make a specific element with a class attribute
259          * @param $text content of the element, will be escaped
260          * @param $class class name of the span element
261          * @param $tag element name
262          * @param $attribs other attributes
263          * @return string 
264          */
265         public static function wrapClass( $text, $class, $tag='span', $attribs=array() ) {
266                 return self::tags( $tag, array( 'class' => $class ) + $attribs, $text );
267         }
268
269         /**
270          * Convenience function to build an HTML text input field
271          * @param $name value of the name attribute
272          * @param $size value of the size attribute
273          * @param $value value of the value attribute
274          * @param $attribs other attributes
275          * @return string HTML
276          */
277         public static function input( $name, $size=false, $value=false, $attribs=array() ) {
278                 return self::element( 'input', array(
279                         'name' => $name,
280                         'size' => $size,
281                         'value' => $value ) + $attribs );
282         }
283
284         /**
285          * Convenience function to build an HTML password input field
286          * @param $name value of the name attribute
287          * @param $size value of the size attribute
288          * @param $value value of the value attribute
289          * @param $attribs other attributes
290          * @return string HTML
291          */
292         public static function password( $name, $size=false, $value=false, $attribs=array() ) {
293                 return self::input( $name, $size, $value, array_merge($attribs, array('type' => 'password')));
294         }
295
296         /**
297          * Internal function for use in checkboxes and radio buttons and such.
298          * @return array
299          */
300         public static function attrib( $name, $present = true ) {
301                 return $present ? array( $name => $name ) : array();
302         }
303
304         /**
305          * Convenience function to build an HTML checkbox
306          * @param $name value of the name attribute
307          * @param $checked Whether the checkbox is checked or not
308          * @param $attribs other attributes
309          * @return string HTML
310          */
311         public static function check( $name, $checked=false, $attribs=array() ) {
312                 return self::element( 'input', array_merge(
313                         array(
314                                 'name' => $name,
315                                 'type' => 'checkbox',
316                                 'value' => 1 ),
317                         self::attrib( 'checked', $checked ),
318                         $attribs ) );
319         }
320
321         /**
322          * Convenience function to build an HTML radio button
323          * @param $name value of the name attribute
324          * @param $value value of the value attribute
325          * @param $checked Whether the checkbox is checked or not
326          * @param $attribs other attributes
327          * @return string HTML
328          */
329         public static function radio( $name, $value, $checked=false, $attribs=array() ) {
330                 return self::element( 'input', array(
331                         'name' => $name,
332                         'type' => 'radio',
333                         'value' => $value ) + self::attrib( 'checked', $checked ) + $attribs );
334         }
335
336         /**
337          * Convenience function to build an HTML form label
338          * @param $label text of the label
339          * @param $id 
340          * @return string HTML
341          */
342         public static function label( $label, $id ) {
343                 return self::element( 'label', array( 'for' => $id ), $label );
344         }
345
346         /**
347          * Convenience function to build an HTML text input field with a label
348          * @param $label text of the label
349          * @param $name value of the name attribute
350          * @param $id id of the input
351          * @param $size value of the size attribute
352          * @param $value value of the value attribute
353          * @param $attribs other attributes
354          * @return string HTML
355          */
356         public static function inputLabel( $label, $name, $id, $size=false, $value=false, $attribs=array() ) {
357                 list( $label, $input ) = self::inputLabelSep( $label, $name, $id, $size, $value, $attribs );
358                 return $label . '&nbsp;' . $input;
359         }
360
361         /**
362          * Same as Xml::inputLabel() but return input and label in an array
363          */
364         public static function inputLabelSep( $label, $name, $id, $size=false, $value=false, $attribs=array() ) {
365                 return array(
366                         Xml::label( $label, $id ),
367                         self::input( $name, $size, $value, array( 'id' => $id ) + $attribs )
368                 );
369         }
370
371         /**
372          * Convenience function to build an HTML checkbox with a label
373          * @return string HTML
374          */
375         public static function checkLabel( $label, $name, $id, $checked=false, $attribs=array() ) {
376                 return self::check( $name, $checked, array( 'id' => $id ) + $attribs ) .
377                         '&nbsp;' .
378                         self::label( $label, $id );
379         }
380
381         /**
382          * Convenience function to build an HTML radio button with a label
383          * @return string HTML
384          */
385         public static function radioLabel( $label, $name, $value, $id, $checked=false, $attribs=array() ) {
386                 return self::radio( $name, $value, $checked, array( 'id' => $id ) + $attribs ) .
387                         '&nbsp;' .
388                         self::label( $label, $id );
389         }
390
391         /**
392          * Convenience function to build an HTML submit button
393          * @param $value String: label text for the button
394          * @param $attribs Array: optional custom attributes
395          * @return string HTML
396          */
397         public static function submitButton( $value, $attribs=array() ) {
398                 return Html::element( 'input', array( 'type' => 'submit', 'value' => $value ) + $attribs );
399         }
400
401         /**
402          * @deprecated Synonymous to Html::hidden()
403          */
404         public static function hidden( $name, $value, $attribs = array() ) {
405                 return Html::hidden( $name, $value, $attribs );
406         }
407
408         /**
409          * Convenience function to build an HTML drop-down list item.
410          * @param $text String: text for this item
411          * @param $value String: form submission value; if empty, use text
412          * @param $selected boolean: if true, will be the default selected item
413          * @param $attribs array: optional additional HTML attributes
414          * @return string HTML
415          */
416         public static function option( $text, $value=null, $selected=false,
417                         $attribs=array() ) {
418                 if( !is_null( $value ) ) {
419                         $attribs['value'] = $value;
420                 }
421                 if( $selected ) {
422                         $attribs['selected'] = 'selected';
423                 }
424                 return self::element( 'option', $attribs, $text );
425         }
426
427         /**
428          * Build a drop-down box from a textual list.
429          *
430          * @param $name Mixed: Name and id for the drop-down
431          * @param $class Mixed: CSS classes for the drop-down
432          * @param $other Mixed: Text for the "Other reasons" option
433          * @param $list Mixed: Correctly formatted text to be used to generate the options
434          * @param $selected Mixed: Option which should be pre-selected
435          * @param $tabindex Mixed: Value of the tabindex attribute
436          * @return string
437          */
438         public static function listDropDown( $name= '', $list = '', $other = '', $selected = '', $class = '', $tabindex = Null ) {
439                 $options = '';
440                 $optgroup = false;
441
442                 $options = self::option( $other, 'other', $selected === 'other' );
443
444                 foreach ( explode( "\n", $list ) as $option) {
445                                 $value = trim( $option );
446                                 if ( $value == '' ) {
447                                         continue;
448                                 } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) {
449                                         // A new group is starting ...
450                                         $value = trim( substr( $value, 1 ) );
451                                         if( $optgroup ) $options .= self::closeElement('optgroup');
452                                         $options .= self::openElement( 'optgroup', array( 'label' => $value ) );
453                                         $optgroup = true;
454                                 } elseif ( substr( $value, 0, 2) == '**' ) {
455                                         // groupmember
456                                         $value = trim( substr( $value, 2 ) );
457                                         $options .= self::option( $value, $value, $selected === $value );
458                                 } else {
459                                         // groupless reason list
460                                         if( $optgroup ) $options .= self::closeElement('optgroup');
461                                         $options .= self::option( $value, $value, $selected === $value );
462                                         $optgroup = false;
463                                 }
464                         }
465                         if( $optgroup ) $options .= self::closeElement('optgroup');
466
467                 $attribs = array();
468                 if( $name ) {
469                         $attribs['id'] = $name;
470                         $attribs['name'] = $name;
471                 }
472                 if( $class ) {
473                         $attribs['class'] = $class;
474                 }
475                 if( $tabindex ) {
476                         $attribs['tabindex'] = $tabindex;
477                 }
478                 return Xml::openElement( 'select', $attribs )
479                         . "\n"
480                         . $options
481                         . "\n"
482                         . Xml::closeElement( 'select' );
483         }
484
485         /**
486          * Shortcut for creating fieldsets.
487          *
488          * @param $legend Legend of the fieldset. If evaluates to false, legend is not added.
489          * @param $content Pre-escaped content for the fieldset. If false, only open fieldset is returned.
490          * @param $attribs Any attributes to fieldset-element.
491          */
492         public static function fieldset( $legend = false, $content = false, $attribs = array() ) {
493                 $s = Xml::openElement( 'fieldset', $attribs ) . "\n";
494                 if ( $legend ) {
495                         $s .= Xml::element( 'legend', null, $legend ) . "\n";
496                 }
497                 if ( $content !== false ) {
498                         $s .= $content . "\n";
499                         $s .= Xml::closeElement( 'fieldset' ) . "\n";
500                 }
501
502                 return $s;
503         }
504         
505         /**
506          * Shortcut for creating textareas.
507          *
508          * @param $name The 'name' for the textarea
509          * @param $content Content for the textarea
510          * @param $cols The number of columns for the textarea
511          * @param $rows The number of rows for the textarea
512          * @param $attribs Any other attributes for the textarea
513          */
514         public static function textarea( $name, $content, $cols = 40, $rows = 5, $attribs = array() ) {
515                 return self::element( 'textarea',
516                                         array(  'name' => $name,
517                                                 'id' => $name,
518                                                 'cols' => $cols,
519                                                 'rows' => $rows
520                                         ) + $attribs,
521                                         $content, false );
522         }
523
524         /**
525          * Returns an escaped string suitable for inclusion in a string literal
526          * for JavaScript source code.
527          * Illegal control characters are assumed not to be present.
528          *
529          * @param $string String to escape
530          * @return String
531          */
532         public static function escapeJsString( $string ) {
533                 // See ECMA 262 section 7.8.4 for string literal format
534                 $pairs = array(
535                         "\\" => "\\\\",
536                         "\"" => "\\\"",
537                         '\'' => '\\\'',
538                         "\n" => "\\n",
539                         "\r" => "\\r",
540
541                         # To avoid closing the element or CDATA section
542                         "<" => "\\x3c",
543                         ">" => "\\x3e",
544
545                         # To avoid any complaints about bad entity refs
546                         "&" => "\\x26",
547
548                         # Work around https://bugzilla.mozilla.org/show_bug.cgi?id=274152
549                         # Encode certain Unicode formatting chars so affected
550                         # versions of Gecko don't misinterpret our strings;
551                         # this is a common problem with Farsi text.
552                         "\xe2\x80\x8c" => "\\u200c", // ZERO WIDTH NON-JOINER
553                         "\xe2\x80\x8d" => "\\u200d", // ZERO WIDTH JOINER
554                 );
555                 return strtr( $string, $pairs );
556         }
557
558         /**
559          * Encode a variable of unknown type to JavaScript.
560          * Arrays are converted to JS arrays, objects are converted to JS associative
561          * arrays (objects). So cast your PHP associative arrays to objects before
562          * passing them to here.
563          */
564         public static function encodeJsVar( $value ) {
565                 if ( is_bool( $value ) ) {
566                         $s = $value ? 'true' : 'false';
567                 } elseif ( is_null( $value ) ) {
568                         $s = 'null';
569                 } elseif ( is_int( $value ) ) {
570                         $s = $value;
571                 } elseif ( is_array( $value ) && // Make sure it's not associative.
572                                         array_keys($value) === range( 0, count($value) - 1 ) ||
573                                         count($value) == 0
574                                 ) {
575                         $s = '[';
576                         foreach ( $value as $elt ) {
577                                 if ( $s != '[' ) {
578                                         $s .= ', ';
579                                 }
580                                 $s .= self::encodeJsVar( $elt );
581                         }
582                         $s .= ']';
583                 } elseif ( is_object( $value ) || is_array( $value ) ) {
584                         // Objects and associative arrays
585                         $s = '{';
586                         foreach ( (array)$value as $name => $elt ) {
587                                 if ( $s != '{' ) {
588                                         $s .= ', ';
589                                 }
590                                 $s .= '"' . self::escapeJsString( $name ) . '": ' .
591                                         self::encodeJsVar( $elt );
592                         }
593                         $s .= '}';
594                 } else {
595                         $s = '"' . self::escapeJsString( $value ) . '"';
596                 }
597                 return $s;
598         }
599
600
601         /**
602          * Check if a string is well-formed XML.
603          * Must include the surrounding tag.
604          *
605          * @param $text String: string to test.
606          * @return bool
607          *
608          * @todo Error position reporting return
609          */
610         public static function isWellFormed( $text ) {
611                 $parser = xml_parser_create( "UTF-8" );
612
613                 # case folding violates XML standard, turn it off
614                 xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
615
616                 if( !xml_parse( $parser, $text, true ) ) {
617                         //$err = xml_error_string( xml_get_error_code( $parser ) );
618                         //$position = xml_get_current_byte_index( $parser );
619                         //$fragment = $this->extractFragment( $html, $position );
620                         //$this->mXmlError = "$err at byte $position:\n$fragment";
621                         xml_parser_free( $parser );
622                         return false;
623                 }
624                 xml_parser_free( $parser );
625                 return true;
626         }
627
628         /**
629          * Check if a string is a well-formed XML fragment.
630          * Wraps fragment in an \<html\> bit and doctype, so it can be a fragment
631          * and can use HTML named entities.
632          *
633          * @param $text String:
634          * @return bool
635          */
636         public static function isWellFormedXmlFragment( $text ) {
637                 $html =
638                         Sanitizer::hackDocType() .
639                         '<html>' .
640                         $text .
641                         '</html>';
642                 return Xml::isWellFormed( $html );
643         }
644
645         /**
646          * Replace " > and < with their respective HTML entities ( &quot;,
647          * &gt;, &lt;)
648          *
649          * @param $in String: text that might contain HTML tags.
650          * @return string Escaped string
651          */
652         public static function escapeTagsOnly( $in ) {
653                 return str_replace(
654                         array( '"', '>', '<' ),
655                         array( '&quot;', '&gt;', '&lt;' ),
656                         $in );
657         }
658         
659         /**
660         * Generate a form (without the opening form element).
661         * Output optionally includes a submit button.
662         * @param $fields Associative array, key is message corresponding to a description for the field (colon is in the message), value is appropriate input.
663         * @param $submitLabel A message containing a label for the submit button.
664         * @return string HTML form.
665         */
666         public static function buildForm( $fields, $submitLabel = null ) {
667                 $form = '';
668                 $form .= "<table><tbody>";
669         
670                 foreach( $fields as $labelmsg => $input ) {
671                         $id = "mw-$labelmsg";
672                         $form .= Xml::openElement( 'tr', array( 'id' => $id ) );
673                         $form .= Xml::tags( 'td', array('class' => 'mw-label'), wfMsgExt( $labelmsg, array('parseinline') ) );
674                         $form .= Xml::openElement( 'td', array( 'class' => 'mw-input' ) ) . $input . Xml::closeElement( 'td' );
675                         $form .= Xml::closeElement( 'tr' );
676                 }
677
678                 if( $submitLabel ) {
679                         $form .= Xml::openElement( 'tr' );
680                         $form .= Xml::tags( 'td', array(), '' );
681                         $form .= Xml::openElement( 'td', array( 'class' => 'mw-submit' ) ) . Xml::submitButton( wfMsg( $submitLabel ) ) . Xml::closeElement( 'td' );
682                         $form .= Xml::closeElement( 'tr' );
683                 }
684         
685                 $form .= "</tbody></table>";
686
687         
688                 return $form;
689         }
690         
691         /**
692          * Build a table of data
693          * @param $rows An array of arrays of strings, each to be a row in a table
694          * @param $attribs An array of attributes to apply to the table tag [optional]
695          * @param $headers An array of strings to use as table headers [optional]
696          * @return string
697          */
698         public static function buildTable( $rows, $attribs = array(), $headers = null ) {
699                 $s = Xml::openElement( 'table', $attribs );
700                 if ( is_array( $headers ) ) {
701                         foreach( $headers as $id => $header ) {
702                                 $attribs = array();
703                                 if ( is_string( $id ) ) $attribs['id'] = $id;
704                                 $s .= Xml::element( 'th', $attribs, $header );
705                         }
706                 }
707                 foreach( $rows as $id => $row ) {
708                         $attribs = array();
709                         if ( is_string( $id ) ) $attribs['id'] = $id;
710                         $s .= Xml::buildTableRow( $attribs, $row );
711                 }
712                 $s .= Xml::closeElement( 'table' );
713                 return $s;
714         }
715         
716         /**
717          * Build a row for a table
718          * @param $attribs An array of attributes to apply to the tr tag
719          * @param $cells An array of strings to put in <td>
720          * @return string
721          */
722         public static function buildTableRow( $attribs, $cells ) {
723                 $s = Xml::openElement( 'tr', $attribs );
724                 foreach( $cells as $id => $cell ) {
725                         $attribs = array();
726                         if ( is_string( $id ) ) $attribs['id'] = $id;
727                         $s .= Xml::element( 'td', $attribs, $cell );
728                 }
729                 $s .= Xml::closeElement( 'tr' );
730                 return $s;
731         }
732 }
733
734 class XmlSelect {
735         protected $options = array();
736         protected $default = false;
737         protected $attributes = array();
738
739         public function __construct( $name = false, $id = false, $default = false ) {
740                 if ( $name ) $this->setAttribute( 'name', $name );
741                 if ( $id ) $this->setAttribute( 'id', $id );
742                 if ( $default ) $this->default = $default;
743         }
744
745         public function setDefault( $default ) {
746                 $this->default = $default;
747         }
748
749         public function setAttribute( $name, $value ) {
750                 $this->attributes[$name] = $value;
751         }
752
753         public function getAttribute( $name ) {
754                 if ( isset($this->attributes[$name]) ) {
755                         return $this->attributes[$name];
756                 } else {
757                         return null;
758                 }
759         }
760
761         public function addOption( $name, $value = false ) {
762                 // Stab stab stab
763                 $value = ($value !== false) ? $value : $name;
764                 $this->options[] = Xml::option( $name, $value, $value === $this->default );
765         }
766         
767         // This accepts an array of form
768         // label => value
769         // label => ( label => value, label => value )
770         public function addOptions( $options ) {
771                 $this->options[] = trim(self::formatOptions( $options, $this->default ));
772         }
773
774         // This accepts an array of form
775         // label => value
776         // label => ( label => value, label => value )  
777         static function formatOptions( $options, $default = false ) {
778                 $data = '';
779                 foreach( $options as $label => $value ) {
780                         if ( is_array( $value ) ) {
781                                 $contents = self::formatOptions( $value, $default );
782                                 $data .= Xml::tags( 'optgroup', array( 'label' => $label ), $contents ) . "\n";
783                         } else {
784                                 $data .= Xml::option( $label, $value, $value === $default ) . "\n";
785                         }
786                 }
787                 
788                 return $data;
789         }
790
791         public function getHTML() {
792                 return Xml::tags( 'select', $this->attributes, implode( "\n", $this->options ) );
793         }
794
795 }