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