<?php
/**
 *  Base include file for SimpleTest.
 *  @package    SimpleTest
 *  @subpackage WebTester
 *  @version    $Id: tag.php,v 1.5 2012/04/06 12:15:44 moodlerobot Exp $
 */

/**#@+
 * include SimpleTest files
 */
require_once(dirname(__FILE__) . '/page.php');
require_once(dirname(__FILE__) . '/encoding.php');
/**#@-*/

/**
 *    Creates tags and widgets given HTML tag
 *    attributes.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleTagBuilder {

    /**
     *    Factory for the tag objects. Creates the
     *    appropriate tag object for the incoming tag name
     *    and attributes.
     *    @param string $name        HTML tag name.
     *    @param hash $attributes    Element attributes.
     *    @return SimpleTag          Tag object.
     *    @access public
     */
    function createTag($name, $attributes) {
        static $map = array(
                'a' => 'SimpleAnchorTag',
                'title' => 'SimpleTitleTag',
                'base' => 'SimpleBaseTag',
                'button' => 'SimpleButtonTag',
                'textarea' => 'SimpleTextAreaTag',
                'option' => 'SimpleOptionTag',
                'label' => 'SimpleLabelTag',
                'form' => 'SimpleFormTag',
                'frame' => 'SimpleFrameTag');
        $attributes = $this->keysToLowerCase($attributes);
        if (array_key_exists($name, $map)) {
            $tag_class = $map[$name];
            return new $tag_class($attributes);
        } elseif ($name == 'select') {
            return $this->createSelectionTag($attributes);
        } elseif ($name == 'input') {
            return $this->createInputTag($attributes);
        }
        return new SimpleTag($name, $attributes);
    }

    /**
     *    Factory for selection fields.
     *    @param hash $attributes    Element attributes.
     *    @return SimpleTag          Tag object.
     *    @access protected
     */
    protected function createSelectionTag($attributes) {
        if (isset($attributes['multiple'])) {
            return new MultipleSelectionTag($attributes);
        }
        return new SimpleSelectionTag($attributes);
    }

    /**
     *    Factory for input tags.
     *    @param hash $attributes    Element attributes.
     *    @return SimpleTag          Tag object.
     *    @access protected
     */
    protected function createInputTag($attributes) {
        if (! isset($attributes['type'])) {
            return new SimpleTextTag($attributes);
        }
        $type = strtolower(trim($attributes['type']));
        $map = array(
                'submit' => 'SimpleSubmitTag',
                'image' => 'SimpleImageSubmitTag',
                'checkbox' => 'SimpleCheckboxTag',
                'radio' => 'SimpleRadioButtonTag',
                'text' => 'SimpleTextTag',
                'hidden' => 'SimpleTextTag',
                'password' => 'SimpleTextTag',
                'file' => 'SimpleUploadTag');
        if (array_key_exists($type, $map)) {
            $tag_class = $map[$type];
            return new $tag_class($attributes);
        }
        return false;
    }

    /**
     *    Make the keys lower case for case insensitive look-ups.
     *    @param hash $map   Hash to convert.
     *    @return hash       Unchanged values, but keys lower case.
     *    @access private
     */
    protected function keysToLowerCase($map) {
        $lower = array();
        foreach ($map as $key => $value) {
            $lower[strtolower($key)] = $value;
        }
        return $lower;
    }
}

/**
 *    HTML or XML tag.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleTag {
    private $name;
    private $attributes;
    private $content;

    /**
     *    Starts with a named tag with attributes only.
     *    @param string $name        Tag name.
     *    @param hash $attributes    Attribute names and
     *                               string values. Note that
     *                               the keys must have been
     *                               converted to lower case.
     */
    function __construct($name, $attributes) {
        $this->name = strtolower(trim($name));
        $this->attributes = $attributes;
        $this->content = '';
    }

    /**
     *    Check to see if the tag can have both start and
     *    end tags with content in between.
     *    @return boolean        True if content allowed.
     *    @access public
     */
    function expectEndTag() {
        return true;
    }

    /**
     *    The current tag should not swallow all content for
     *    itself as it's searchable page content. Private
     *    content tags are usually widgets that contain default
     *    values.
     *    @return boolean        False as content is available
     *                           to other tags by default.
     *    @access public
     */
    function isPrivateContent() {
        return false;
    }

    /**
     *    Appends string content to the current content.
     *    @param string $content        Additional text.
     *    @access public
     */
    function addContent($content) {
        $this->content .= (string)$content;
        return $this;
    }

    /**
     *    Adds an enclosed tag to the content.
     *    @param SimpleTag $tag    New tag.
     *    @access public
     */
    function addTag($tag) {
    }

    /**
     *    Adds multiple enclosed tags to the content.
     *    @param array            List of SimpleTag objects to be added.
     */
    function addTags($tags) {
        foreach ($tags as $tag) {
            $this->addTag($tag);
        }
    }

    /**
     *    Accessor for tag name.
     *    @return string       Name of tag.
     *    @access public
     */
    function getTagName() {
        return $this->name;
    }

    /**
     *    List of legal child elements.
     *    @return array        List of element names.
     *    @access public
     */
    function getChildElements() {
        return array();
    }

    /**
     *    Accessor for an attribute.
     *    @param string $label    Attribute name.
     *    @return string          Attribute value.
     *    @access public
     */
    function getAttribute($label) {
        $label = strtolower($label);
        if (! isset($this->attributes[$label])) {
            return false;
        }
        return (string)$this->attributes[$label];
    }

    /**
     *    Sets an attribute.
     *    @param string $label    Attribute name.
     *    @return string $value   New attribute value.
     *    @access protected
     */
    protected function setAttribute($label, $value) {
        $this->attributes[strtolower($label)] = $value;
    }

    /**
     *    Accessor for the whole content so far.
     *    @return string       Content as big raw string.
     *    @access public
     */
    function getContent() {
        return $this->content;
    }

    /**
     *    Accessor for content reduced to visible text. Acts
     *    like a text mode browser, normalising space and
     *    reducing images to their alt text.
     *    @return string       Content as plain text.
     *    @access public
     */
    function getText() {
        return SimplePage::normalise($this->content);
    }

    /**
     *    Test to see if id attribute matches.
     *    @param string $id        ID to test against.
     *    @return boolean          True on match.
     *    @access public
     */
    function isId($id) {
        return ($this->getAttribute('id') == $id);
    }
}

/**
 *    Base url.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleBaseTag extends SimpleTag {

    /**
     *    Starts with a named tag with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('base', $attributes);
    }

    /**
     *    Base tag is not a block tag.
     *    @return boolean       false
     *    @access public
     */
    function expectEndTag() {
        return false;
    }
}

/**
 *    Page title.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleTitleTag extends SimpleTag {

    /**
     *    Starts with a named tag with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('title', $attributes);
    }
}

/**
 *    Link.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleAnchorTag extends SimpleTag {

    /**
     *    Starts with a named tag with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('a', $attributes);
    }

    /**
     *    Accessor for URL as string.
     *    @return string    Coerced as string.
     *    @access public
     */
    function getHref() {
        $url = $this->getAttribute('href');
        if (is_bool($url)) {
            $url = '';
        }
        return $url;
    }
}

/**
 *    Form element.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleWidget extends SimpleTag {
    private $value;
    private $label;
    private $is_set;

    /**
     *    Starts with a named tag with attributes only.
     *    @param string $name        Tag name.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($name, $attributes) {
        parent::__construct($name, $attributes);
        $this->value = false;
        $this->label = false;
        $this->is_set = false;
    }

    /**
     *    Accessor for name submitted as the key in
     *    GET/POST privateiables hash.
     *    @return string        Parsed value.
     *    @access public
     */
    function getName() {
        return $this->getAttribute('name');
    }

    /**
     *    Accessor for default value parsed with the tag.
     *    @return string        Parsed value.
     *    @access public
     */
    function getDefault() {
        return $this->getAttribute('value');
    }

    /**
     *    Accessor for currently set value or default if
     *    none.
     *    @return string      Value set by form or default
     *                        if none.
     *    @access public
     */
    function getValue() {
        if (! $this->is_set) {
            return $this->getDefault();
        }
        return $this->value;
    }

    /**
     *    Sets the current form element value.
     *    @param string $value       New value.
     *    @return boolean            True if allowed.
     *    @access public
     */
    function setValue($value) {
        $this->value = $value;
        $this->is_set = true;
        return true;
    }

    /**
     *    Resets the form element value back to the
     *    default.
     *    @access public
     */
    function resetValue() {
        $this->is_set = false;
    }

    /**
     *    Allows setting of a label externally, say by a
     *    label tag.
     *    @param string $label    Label to attach.
     *    @access public
     */
    function setLabel($label) {
        $this->label = trim($label);
        return $this;
    }

    /**
     *    Reads external or internal label.
     *    @param string $label    Label to test.
     *    @return boolean         True is match.
     *    @access public
     */
    function isLabel($label) {
        return $this->label == trim($label);
    }

    /**
     *    Dispatches the value into the form encoded packet.
     *    @param SimpleEncoding $encoding    Form packet.
     *    @access public
     */
    function write($encoding) {
        if ($this->getName()) {
            $encoding->add($this->getName(), $this->getValue());
        }
    }
}

/**
 *    Text, password and hidden field.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleTextTag extends SimpleWidget {

    /**
     *    Starts with a named tag with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('input', $attributes);
        if ($this->getAttribute('value') === false) {
            $this->setAttribute('value', '');
        }
    }

    /**
     *    Tag contains no content.
     *    @return boolean        False.
     *    @access public
     */
    function expectEndTag() {
        return false;
    }

    /**
     *    Sets the current form element value. Cannot
     *    change the value of a hidden field.
     *    @param string $value       New value.
     *    @return boolean            True if allowed.
     *    @access public
     */
    function setValue($value) {
        if ($this->getAttribute('type') == 'hidden') {
            return false;
        }
        return parent::setValue($value);
    }
}

/**
 *    Submit button as input tag.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleSubmitTag extends SimpleWidget {

    /**
     *    Starts with a named tag with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('input', $attributes);
        if ($this->getAttribute('value') === false) {
            $this->setAttribute('value', 'Submit');
        }
    }

    /**
     *    Tag contains no end element.
     *    @return boolean        False.
     *    @access public
     */
    function expectEndTag() {
        return false;
    }

    /**
     *    Disables the setting of the button value.
     *    @param string $value       Ignored.
     *    @return boolean            True if allowed.
     *    @access public
     */
    function setValue($value) {
        return false;
    }

    /**
     *    Value of browser visible text.
     *    @return string        Visible label.
     *    @access public
     */
    function getLabel() {
        return $this->getValue();
    }

    /**
     *    Test for a label match when searching.
     *    @param string $label     Label to test.
     *    @return boolean          True on match.
     *    @access public
     */
    function isLabel($label) {
        return trim($label) == trim($this->getLabel());
    }
}

/**
 *    Image button as input tag.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleImageSubmitTag extends SimpleWidget {

    /**
     *    Starts with a named tag with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('input', $attributes);
    }

    /**
     *    Tag contains no end element.
     *    @return boolean        False.
     *    @access public
     */
    function expectEndTag() {
        return false;
    }

    /**
     *    Disables the setting of the button value.
     *    @param string $value       Ignored.
     *    @return boolean            True if allowed.
     *    @access public
     */
    function setValue($value) {
        return false;
    }

    /**
     *    Value of browser visible text.
     *    @return string        Visible label.
     *    @access public
     */
    function getLabel() {
        if ($this->getAttribute('title')) {
            return $this->getAttribute('title');
        }
        return $this->getAttribute('alt');
    }

    /**
     *    Test for a label match when searching.
     *    @param string $label     Label to test.
     *    @return boolean          True on match.
     *    @access public
     */
    function isLabel($label) {
        return trim($label) == trim($this->getLabel());
    }

    /**
     *    Dispatches the value into the form encoded packet.
     *    @param SimpleEncoding $encoding    Form packet.
     *    @param integer $x                  X coordinate of click.
     *    @param integer $y                  Y coordinate of click.
     *    @access public
     */
    function write($encoding, $x = 1, $y = 1) {
        if ($this->getName()) {
            $encoding->add($this->getName() . '.x', $x);
            $encoding->add($this->getName() . '.y', $y);
        } else {
            $encoding->add('x', $x);
            $encoding->add('y', $y);
        }
    }
}

/**
 *    Submit button as button tag.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleButtonTag extends SimpleWidget {

    /**
     *    Starts with a named tag with attributes only.
     *    Defaults are very browser dependent.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('button', $attributes);
    }

    /**
     *    Check to see if the tag can have both start and
     *    end tags with content in between.
     *    @return boolean        True if content allowed.
     *    @access public
     */
    function expectEndTag() {
        return true;
    }

    /**
     *    Disables the setting of the button value.
     *    @param string $value       Ignored.
     *    @return boolean            True if allowed.
     *    @access public
     */
    function setValue($value) {
        return false;
    }

    /**
     *    Value of browser visible text.
     *    @return string        Visible label.
     *    @access public
     */
    function getLabel() {
        return $this->getContent();
    }

    /**
     *    Test for a label match when searching.
     *    @param string $label     Label to test.
     *    @return boolean          True on match.
     *    @access public
     */
    function isLabel($label) {
        return trim($label) == trim($this->getLabel());
    }
}

/**
 *    Content tag for text area.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleTextAreaTag extends SimpleWidget {

    /**
     *    Starts with a named tag with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('textarea', $attributes);
    }

    /**
     *    Accessor for starting value.
     *    @return string        Parsed value.
     *    @access public
     */
    function getDefault() {
        return $this->wrap(html_entity_decode($this->getContent(), ENT_QUOTES));
    }

    /**
     *    Applies word wrapping if needed.
     *    @param string $value      New value.
     *    @return boolean            True if allowed.
     *    @access public
     */
    function setValue($value) {
        return parent::setValue($this->wrap($value));
    }

    /**
     *    Test to see if text should be wrapped.
     *    @return boolean        True if wrapping on.
     *    @access private
     */
    function wrapIsEnabled() {
        if ($this->getAttribute('cols')) {
            $wrap = $this->getAttribute('wrap');
            if (($wrap == 'physical') || ($wrap == 'hard')) {
                return true;
            }
        }
        return false;
    }

    /**
     *    Performs the formatting that is peculiar to
     *    this tag. There is strange behaviour in this
     *    one, including stripping a leading new line.
     *    Go figure. I am using Firefox as a guide.
     *    @param string $text    Text to wrap.
     *    @return string         Text wrapped with carriage
     *                           returns and line feeds
     *    @access private
     */
    protected function wrap($text) {
        $text = str_replace("\r\r\n", "\r\n", str_replace("\n", "\r\n", $text));
        $text = str_replace("\r\n\n", "\r\n", str_replace("\r", "\r\n", $text));
        if (strncmp($text, "\r\n", strlen("\r\n")) == 0) {
            $text = substr($text, strlen("\r\n"));
        }
        if ($this->wrapIsEnabled()) {
            return wordwrap(
                    $text,
                    (integer)$this->getAttribute('cols'),
                    "\r\n");
        }
        return $text;
    }

    /**
     *    The content of textarea is not part of the page.
     *    @return boolean        True.
     *    @access public
     */
    function isPrivateContent() {
        return true;
    }
}

/**
 *    File upload widget.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleUploadTag extends SimpleWidget {

    /**
     *    Starts with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('input', $attributes);
    }

    /**
     *    Tag contains no content.
     *    @return boolean        False.
     *    @access public
     */
    function expectEndTag() {
        return false;
    }

    /**
     *    Dispatches the value into the form encoded packet.
     *    @param SimpleEncoding $encoding    Form packet.
     *    @access public
     */
    function write($encoding) {
        if (! file_exists($this->getValue())) {
            return;
        }
        $encoding->attach(
                $this->getName(),
                implode('', file($this->getValue())),
                basename($this->getValue()));
    }
}

/**
 *    Drop down widget.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleSelectionTag extends SimpleWidget {
    private $options;
    private $choice;

    /**
     *    Starts with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('select', $attributes);
        $this->options = array();
        $this->choice = false;
    }

    /**
     *    Adds an option tag to a selection field.
     *    @param SimpleOptionTag $tag     New option.
     *    @access public
     */
    function addTag($tag) {
        if ($tag->getTagName() == 'option') {
            $this->options[] = $tag;
        }
    }

    /**
     *    Text within the selection element is ignored.
     *    @param string $content        Ignored.
     *    @access public
     */
    function addContent($content) {
        return $this;
    }

    /**
     *    Scans options for defaults. If none, then
     *    the first option is selected.
     *    @return string        Selected field.
     *    @access public
     */
    function getDefault() {
        for ($i = 0, $count = count($this->options); $i < $count; $i++) {
            if ($this->options[$i]->getAttribute('selected') !== false) {
                return $this->options[$i]->getDefault();
            }
        }
        if ($count > 0) {
            return $this->options[0]->getDefault();
        }
        return '';
    }

    /**
     *    Can only set allowed values.
     *    @param string $value       New choice.
     *    @return boolean            True if allowed.
     *    @access public
     */
    function setValue($value) {
        for ($i = 0, $count = count($this->options); $i < $count; $i++) {
            if ($this->options[$i]->isValue($value)) {
                $this->choice = $i;
                return true;
            }
        }
        return false;
    }

    /**
     *    Accessor for current selection value.
     *    @return string      Value attribute or
     *                        content of opton.
     *    @access public
     */
    function getValue() {
        if ($this->choice === false) {
            return $this->getDefault();
        }
        return $this->options[$this->choice]->getValue();
    }
}

/**
 *    Drop down widget.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class MultipleSelectionTag extends SimpleWidget {
    private $options;
    private $values;

    /**
     *    Starts with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('select', $attributes);
        $this->options = array();
        $this->values = false;
    }

    /**
     *    Adds an option tag to a selection field.
     *    @param SimpleOptionTag $tag     New option.
     *    @access public
     */
    function addTag($tag) {
        if ($tag->getTagName() == 'option') {
            $this->options[] = &$tag;
        }
    }

    /**
     *    Text within the selection element is ignored.
     *    @param string $content        Ignored.
     *    @access public
     */
    function addContent($content) {
        return $this;
    }

    /**
     *    Scans options for defaults to populate the
     *    value array().
     *    @return array        Selected fields.
     *    @access public
     */
    function getDefault() {
        $default = array();
        for ($i = 0, $count = count($this->options); $i < $count; $i++) {
            if ($this->options[$i]->getAttribute('selected') !== false) {
                $default[] = $this->options[$i]->getDefault();
            }
        }
        return $default;
    }

    /**
     *    Can only set allowed values. Any illegal value
     *    will result in a failure, but all correct values
     *    will be set.
     *    @param array $desired      New choices.
     *    @return boolean            True if all allowed.
     *    @access public
     */
    function setValue($desired) {
        $achieved = array();
        foreach ($desired as $value) {
            $success = false;
            for ($i = 0, $count = count($this->options); $i < $count; $i++) {
                if ($this->options[$i]->isValue($value)) {
                    $achieved[] = $this->options[$i]->getValue();
                    $success = true;
                    break;
                }
            }
            if (! $success) {
                return false;
            }
        }
        $this->values = $achieved;
        return true;
    }

    /**
     *    Accessor for current selection value.
     *    @return array      List of currently set options.
     *    @access public
     */
    function getValue() {
        if ($this->values === false) {
            return $this->getDefault();
        }
        return $this->values;
    }
}

/**
 *    Option for selection field.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleOptionTag extends SimpleWidget {

    /**
     *    Stashes the attributes.
     */
    function __construct($attributes) {
        parent::__construct('option', $attributes);
    }

    /**
     *    Does nothing.
     *    @param string $value      Ignored.
     *    @return boolean           Not allowed.
     *    @access public
     */
    function setValue($value) {
        return false;
    }

    /**
     *    Test to see if a value matches the option.
     *    @param string $compare    Value to compare with.
     *    @return boolean           True if possible match.
     *    @access public
     */
    function isValue($compare) {
        $compare = trim($compare);
        if (trim($this->getValue()) == $compare) {
            return true;
        }
        return trim(strip_tags($this->getContent())) == $compare;
    }

    /**
     *    Accessor for starting value. Will be set to
     *    the option label if no value exists.
     *    @return string        Parsed value.
     *    @access public
     */
    function getDefault() {
        if ($this->getAttribute('value') === false) {
            return strip_tags($this->getContent());
        }
        return $this->getAttribute('value');
    }

    /**
     *    The content of options is not part of the page.
     *    @return boolean        True.
     *    @access public
     */
    function isPrivateContent() {
        return true;
    }
}

/**
 *    Radio button.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleRadioButtonTag extends SimpleWidget {

    /**
     *    Stashes the attributes.
     *    @param array $attributes        Hash of attributes.
     */
    function __construct($attributes) {
        parent::__construct('input', $attributes);
        if ($this->getAttribute('value') === false) {
            $this->setAttribute('value', 'on');
        }
    }

    /**
     *    Tag contains no content.
     *    @return boolean        False.
     *    @access public
     */
    function expectEndTag() {
        return false;
    }

    /**
     *    The only allowed value sn the one in the
     *    "value" attribute.
     *    @param string $value      New value.
     *    @return boolean           True if allowed.
     *    @access public
     */
    function setValue($value) {
        if ($value === false) {
            return parent::setValue($value);
        }
        if ($value != $this->getAttribute('value')) {
            return false;
        }
        return parent::setValue($value);
    }

    /**
     *    Accessor for starting value.
     *    @return string        Parsed value.
     *    @access public
     */
    function getDefault() {
        if ($this->getAttribute('checked') !== false) {
            return $this->getAttribute('value');
        }
        return false;
    }
}

/**
 *    Checkbox widget.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleCheckboxTag extends SimpleWidget {

    /**
     *    Starts with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('input', $attributes);
        if ($this->getAttribute('value') === false) {
            $this->setAttribute('value', 'on');
        }
    }

    /**
     *    Tag contains no content.
     *    @return boolean        False.
     *    @access public
     */
    function expectEndTag() {
        return false;
    }

    /**
     *    The only allowed value in the one in the
     *    "value" attribute. The default for this
     *    attribute is "on". If this widget is set to
     *    true, then the usual value will be taken.
     *    @param string $value      New value.
     *    @return boolean           True if allowed.
     *    @access public
     */
    function setValue($value) {
        if ($value === false) {
            return parent::setValue($value);
        }
        if ($value === true) {
            return parent::setValue($this->getAttribute('value'));
        }
        if ($value != $this->getAttribute('value')) {
            return false;
        }
        return parent::setValue($value);
    }

    /**
     *    Accessor for starting value. The default
     *    value is "on".
     *    @return string        Parsed value.
     *    @access public
     */
    function getDefault() {
        if ($this->getAttribute('checked') !== false) {
            return $this->getAttribute('value');
        }
        return false;
    }
}

/**
 *    A group of multiple widgets with some shared behaviour.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleTagGroup {
    private $widgets = array();

    /**
     *    Adds a tag to the group.
     *    @param SimpleWidget $widget
     *    @access public
     */
    function addWidget($widget) {
        $this->widgets[] = $widget;
    }

    /**
     *    Accessor to widget set.
     *    @return array        All widgets.
     *    @access protected
     */
    protected function &getWidgets() {
        return $this->widgets;
    }

    /**
     *    Accessor for an attribute.
     *    @param string $label    Attribute name.
     *    @return boolean         Always false.
     *    @access public
     */
    function getAttribute($label) {
        return false;
    }

    /**
     *    Fetches the name for the widget from the first
     *    member.
     *    @return string        Name of widget.
     *    @access public
     */
    function getName() {
        if (count($this->widgets) > 0) {
            return $this->widgets[0]->getName();
        }
    }

    /**
     *    Scans the widgets for one with the appropriate
     *    ID field.
     *    @param string $id        ID value to try.
     *    @return boolean          True if matched.
     *    @access public
     */
    function isId($id) {
        for ($i = 0, $count = count($this->widgets); $i < $count; $i++) {
            if ($this->widgets[$i]->isId($id)) {
                return true;
            }
        }
        return false;
    }

    /**
     *    Scans the widgets for one with the appropriate
     *    attached label.
     *    @param string $label     Attached label to try.
     *    @return boolean          True if matched.
     *    @access public
     */
    function isLabel($label) {
        for ($i = 0, $count = count($this->widgets); $i < $count; $i++) {
            if ($this->widgets[$i]->isLabel($label)) {
                return true;
            }
        }
        return false;
    }

    /**
     *    Dispatches the value into the form encoded packet.
     *    @param SimpleEncoding $encoding    Form packet.
     *    @access public
     */
    function write($encoding) {
        $encoding->add($this->getName(), $this->getValue());
    }
}

/**
 *    A group of tags with the same name within a form.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleCheckboxGroup extends SimpleTagGroup {

    /**
     *    Accessor for current selected widget or false
     *    if none.
     *    @return string/array     Widget values or false if none.
     *    @access public
     */
    function getValue() {
        $values = array();
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if ($widgets[$i]->getValue() !== false) {
                $values[] = $widgets[$i]->getValue();
            }
        }
        return $this->coerceValues($values);
    }

    /**
     *    Accessor for starting value that is active.
     *    @return string/array      Widget values or false if none.
     *    @access public
     */
    function getDefault() {
        $values = array();
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if ($widgets[$i]->getDefault() !== false) {
                $values[] = $widgets[$i]->getDefault();
            }
        }
        return $this->coerceValues($values);
    }

    /**
     *    Accessor for current set values.
     *    @param string/array/boolean $values   Either a single string, a
     *                                          hash or false for nothing set.
     *    @return boolean                       True if all values can be set.
     *    @access public
     */
    function setValue($values) {
        $values = $this->makeArray($values);
        if (! $this->valuesArePossible($values)) {
            return false;
        }
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            $possible = $widgets[$i]->getAttribute('value');
            if (in_array($widgets[$i]->getAttribute('value'), $values)) {
                $widgets[$i]->setValue($possible);
            } else {
                $widgets[$i]->setValue(false);
            }
        }
        return true;
    }

    /**
     *    Tests to see if a possible value set is legal.
     *    @param string/array/boolean $values   Either a single string, a
     *                                          hash or false for nothing set.
     *    @return boolean                       False if trying to set a
     *                                          missing value.
     *    @access private
     */
    protected function valuesArePossible($values) {
        $matches = array();
        $widgets = &$this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            $possible = $widgets[$i]->getAttribute('value');
            if (in_array($possible, $values)) {
                $matches[] = $possible;
            }
        }
        return ($values == $matches);
    }

    /**
     *    Converts the output to an appropriate format. This means
     *    that no values is false, a single value is just that
     *    value and only two or more are contained in an array.
     *    @param array $values           List of values of widgets.
     *    @return string/array/boolean   Expected format for a tag.
     *    @access private
     */
    protected function coerceValues($values) {
        if (count($values) == 0) {
            return false;
        } elseif (count($values) == 1) {
            return $values[0];
        } else {
            return $values;
        }
    }

    /**
     *    Converts false or string into array. The opposite of
     *    the coercian method.
     *    @param string/array/boolean $value  A single item is converted
     *                                        to a one item list. False
     *                                        gives an empty list.
     *    @return array                       List of values, possibly empty.
     *    @access private
     */
    protected function makeArray($value) {
        if ($value === false) {
            return array();
        }
        if (is_string($value)) {
            return array($value);
        }
        return $value;
    }
}

/**
 *    A group of tags with the same name within a form.
 *    Used for radio buttons.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleRadioGroup extends SimpleTagGroup {

    /**
     *    Each tag is tried in turn until one is
     *    successfully set. The others will be
     *    unchecked if successful.
     *    @param string $value      New value.
     *    @return boolean           True if any allowed.
     *    @access public
     */
    function setValue($value) {
        if (! $this->valueIsPossible($value)) {
            return false;
        }
        $index = false;
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if (! $widgets[$i]->setValue($value)) {
                $widgets[$i]->setValue(false);
            }
        }
        return true;
    }

    /**
     *    Tests to see if a value is allowed.
     *    @param string    Attempted value.
     *    @return boolean  True if a valid value.
     *    @access private
     */
    protected function valueIsPossible($value) {
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if ($widgets[$i]->getAttribute('value') == $value) {
                return true;
            }
        }
        return false;
    }

    /**
     *    Accessor for current selected widget or false
     *    if none.
     *    @return string/boolean   Value attribute or
     *                             content of opton.
     *    @access public
     */
    function getValue() {
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if ($widgets[$i]->getValue() !== false) {
                return $widgets[$i]->getValue();
            }
        }
        return false;
    }

    /**
     *    Accessor for starting value that is active.
     *    @return string/boolean      Value of first checked
     *                                widget or false if none.
     *    @access public
     */
    function getDefault() {
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if ($widgets[$i]->getDefault() !== false) {
                return $widgets[$i]->getDefault();
            }
        }
        return false;
    }
}

/**
 *    Tag to keep track of labels.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleLabelTag extends SimpleTag {

    /**
     *    Starts with a named tag with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('label', $attributes);
    }

    /**
     *    Access for the ID to attach the label to.
     *    @return string        For attribute.
     *    @access public
     */
    function getFor() {
        return $this->getAttribute('for');
    }
}

/**
 *    Tag to aid parsing the form.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleFormTag extends SimpleTag {

    /**
     *    Starts with a named tag with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('form', $attributes);
    }
}

/**
 *    Tag to aid parsing the frames in a page.
 *    @package SimpleTest
 *    @subpackage WebTester
 */
class SimpleFrameTag extends SimpleTag {

    /**
     *    Starts with a named tag with attributes only.
     *    @param hash $attributes    Attribute names and
     *                               string values.
     */
    function __construct($attributes) {
        parent::__construct('frame', $attributes);
    }

    /**
     *    Tag contains no content.
     *    @return boolean        False.
     *    @access public
     */
    function expectEndTag() {
        return false;
    }
}
?>