<?php
/**
 * This file contains the following classes: {@link SimpleCollector},
 * {@link SimplePatternCollector}.
 *
 * @author Travis Swicegood <development@domain51.com>
 */

/**
 * The basic collector for {@link GroupTest}
 *
 * @see collect(), GroupTest::collect()
 */
class SimpleCollector
{
    /**
     * Strips off any kind of slash at the end so as to normalise the path.
     *
     * @param string $path Path to normalise.
     *
     * @return string Path without trailing slash.
     */
    protected function removeTrailingSlash($path)
    {
        if (substr($path, -1) == DIRECTORY_SEPARATOR) {
            return substr($path, 0, -1);
        } elseif (substr($path, -1) == '/') {
            return substr($path, 0, -1);
        } else {
            return $path;
        }
    }

    /**
     * Scans the directory and adds what it can.
     *
     * @param object $test Group test with {@link GroupTest::addFile)} method.
     * @param string $path Directory to scan.
     *
     * @see _attemptToAdd()
     */
    public function collect(&$test, $path)
    {
        $path = $this->removeTrailingSlash($path);
        if ($handle = opendir($path)) {
            while (($entry = readdir($handle)) !== false) {
                if ($this->isHidden($entry)) {
                    continue;
                }
                $this->handle($test, $path . DIRECTORY_SEPARATOR . $entry);
            }
            closedir($handle);
        }
    }

    /**
     * This method determines what should be done with a given file and adds
     * it via {@link GroupTest::addFile)} if necessary.
     *
     * This method should be overriden to provide custom matching criteria,
     * such as pattern matching, recursive matching, etc.  For an example, see
     * {@link SimplePatternCollector::_handle()}.
     *
     * @param object $test     Group test with {@link GroupTest::addFile)} method.
     * @param string $filename A filename as generated by {@link collect()}
     *
     * @see collect()
     */
    protected function handle(&$test, $file)
    {
        if (is_dir($file)) {
            return;
        }
        $test->addFile($file);
    }

    /**
     * Tests for hidden files so as to skip them.
     * Currently only tests for Unix hidden files.
     *
     * @param string $filename        Plain filename.
     *
     * @return bool                True if hidden file.
     */
    protected function isHidden($filename)
    {
        return strncmp($filename, '.', 1) == 0;
    }
}

/**
 * An extension to {@link SimpleCollector} that only adds files matching a given pattern.
 *
 * See {@link http://php.net/manual/en/reference.pcre.pattern.syntax.php PHP's PCRE}
 * for documentation of valid patterns.
 *
 * @see SimpleCollector
 */
class SimplePatternCollector extends SimpleCollector
{
    private $pattern;

    /**
     * @param string $pattern Perl compatible regex to test name against
     */
    public function __construct($pattern = '/php$/i')
    {
        $this->pattern = $pattern;
    }

    /**
     * Attempts to add files that match a given pattern.
     *
     * @see SimpleCollector::_handle()
     *
     * @param object $test Group test with {@link GroupTest::addFile)} method.
     * @param string $path Directory to scan.
     */
    protected function handle(&$test, $filename)
    {
        if (preg_match($this->pattern, $filename)) {
            parent::handle($test, $filename);
        }
    }
}


<?php

require_once dirname(__FILE__) . '/scorer.php';
//require_once dirname(__FILE__) . '/arguments.php';

/**
 * Sample minimal test displayer.
 * Generates only failure messages and a pass count.
 */
class HtmlReporter extends SimpleReporter
{
    private $charset;

    /**
     * Does nothing yet.
     * The first output will be sent on the first test start.
     * For use by a web browser.
     */
    public function __construct($charset = 'utf-8')
    {
        parent::__construct();
        $this->charset = $charset;
    }

    /**
     * Paints the top of the web page setting the title to the name of the starting test.
     *
     * @param string $test_name      Name class of test.
     */
    public function paintHeader($test_name)
    {
        $this->sendNoCacheHeaders();
        print '<!DOCTYPE html>';
        print "\n\n<title>$test_name</title>\n";
        print '<meta http-equiv="Content-Type" content="text/html; charset=' . $this->charset . "\">\n";
        print "\n";
        print "\n\n";
        print "
$test_name
\n";
        flush();
    }

    /**
     * Send the headers necessary to ensure the page is reloaded on every request.
     * Otherwise you could be scratching your head over out of date test data.
     */
    public static function sendNoCacheHeaders()
    {
        if (! headers_sent()) {
            header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
            header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
            header('Cache-Control: no-store, no-cache, must-revalidate');
            header('Cache-Control: post-check=0, pre-check=0', false);
            header('Pragma: no-cache');
        }
    }

    /**
     * Paints the CSS. Add additional styles here.
     *
     * @return string            CSS code as text.
     */
    protected function getCss()
    {
        return '.fail { background-color: inherit; color: red; }' .
                '.pass { background-color: inherit; color: green; }' .
                ' pre { background-color: lightgray; color: inherit; }';
    }

    /**
     * Paints the end of the test with a summary of the passes and failures.
     *
     * @param string $test_name        Name class of test.
     */
    public function paintFooter($test_name)
    {
        $colour = ($this->getFailCount() + $this->getExceptionCount() > 0 ? 'red' : 'green');
        print '
';
        print $this->getTestCaseProgress() . '/' . $this->getTestCaseCount();
        print " test cases complete:\n";
        print '' . $this->getPassCount() . ' passes, ';
        print '' . $this->getFailCount() . ' fails and ';
        print '' . $this->getExceptionCount() . ' exceptions.';
        print "
\n";
        print "\n\n";
    }

    /**
     * Paints the test failure with a breadcrumbs trail
     * of the nesting test suites below the top level test.
     *
     * @param string $message    Failure message displayed in the context of the other tests.
     */
    public function paintFail($message)
    {
        parent::paintFail($message);
        print 'Fail: ';
        $breadcrumb = $this->getTestList();
        array_shift($breadcrumb);
        print implode(' -> ', $breadcrumb);
        print ' -> ' . $this->htmlEntities($message) . "
\n";
    }

    /**
     * Paints a PHP error.
     *
     * @param string $message        Message is ignored.
     */
    public function paintError($message)
    {
        parent::paintError($message);
        print 'Exception: ';
        $breadcrumb = $this->getTestList();
        array_shift($breadcrumb);
        print implode(' -> ', $breadcrumb);
        print ' -> ' . $this->htmlEntities($message) . "
\n";
    }

    /**
     * Paints a PHP exception.
     *
     * @param Exception $exception        Exception to display.
     */
    public function paintException($exception)
    {
        parent::paintException($exception);
        print 'Exception: ';
        $breadcrumb = $this->getTestList();
        array_shift($breadcrumb);
        print implode(' -> ', $breadcrumb);
        $message = 'Unexpected exception of type [' . get_class($exception) .
                '] with message [' . $exception->getMessage() .
                '] in [' . $exception->getFile() .
                ' line ' . $exception->getLine() . ']';
        print ' -> ' . $this->htmlEntities($message) . "
\n";
    }

    /**
     * Prints the message for skipping tests.
     *
     * @param string $message    Text of skip condition.
     */
    public function paintSkip($message)
    {
        parent::paintSkip($message);
        print 'Skipped: ';
        $breadcrumb = $this->getTestList();
        array_shift($breadcrumb);
        print implode(' -> ', $breadcrumb);
        print ' -> ' . $this->htmlEntities($message) . "
\n";
    }

    /**
     * Paints formatted text such as dumped privateiables.
     *
     * @param string $message        Text to show.
     */
    public function paintFormattedMessage($message)
    {
        print '
' . $this->htmlEntities($message) . '
';
    }

    /**
     * Character set adjusted entity conversion.
     *
     * @param string $message    Plain text or Unicode message.
     *
     * @return string            Browser readable message.
     */
    protected function htmlEntities($message)
    {
        return htmlentities($message, ENT_COMPAT, $this->charset);
    }
}

/**
 * Sample minimal test displayer.
 * Generates only failure messages and a pass count.
 * For command line use.
 *
 * Note: I've tried to make it look like JUnit,
 *       but I wanted to output the errors as they arrived, which meant dropping the dots.
 */
class TextReporter extends SimpleReporter
{
    /**
     * Does nothing yet. The first output will be sent on the first test start.
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Paints the title only.
     *
     * @param string $test_name        Name class of test.
     */
    public function paintHeader($test_name)
    {
        if (! SimpleReporter::inCli()) {
            header('Content-type: text/plain');
        }
        print "$test_name\n";
        flush();
    }

    /**
     * Paints the end of the test with a summary of the passes and failures.
     *
     * @param string $test_name        Name class of test.
     */
    public function paintFooter($test_name)
    {
        if ($this->getFailCount() + $this->getExceptionCount() === 0) {
            print "OK\n";
        } else {
            print "FAILURES!!!\n";
        }
        print 'Test cases run: ' . $this->getTestCaseProgress() .
                '/' . $this->getTestCaseCount() .
                ', Passes: ' . $this->getPassCount() .
                ', Failures: ' . $this->getFailCount() .
                ', Exceptions: ' . $this->getExceptionCount() . "\n";
    }

    /**
     * Paints the test failure as a stack trace.
     *
     * @param string $message    Failure message displayed in the context of the other tests.
     */
    public function paintFail($message)
    {
        parent::paintFail($message);
        print $this->getFailCount() . ") $message\n";
        $breadcrumb = $this->getTestList();
        array_shift($breadcrumb);
        print "\tin " . implode("\n\tin ", array_reverse($breadcrumb));
        print "\n";
    }

    /**
     * Paints a PHP error or exception.
     *
     * @param string $message        Message to be shown.
     *
     * @abstract
     */
    public function paintError($message)
    {
        parent::paintError($message);
        print 'Exception ' . $this->getExceptionCount() . "!\n$message\n";
        $breadcrumb = $this->getTestList();
        array_shift($breadcrumb);
        print "\tin " . implode("\n\tin ", array_reverse($breadcrumb));
        print "\n";
    }

    /**
     * Paints a PHP error or exception.
     *
     * @param Exception $exception      Exception to describe.
     *
     * @abstract
     */
    public function paintException($exception)
    {
        parent::paintException($exception);
        $message = 'Unexpected exception of type [' . get_class($exception) .
                '] with message [' . $exception->getMessage() .
                '] in [' . $exception->getFile() .
                ' line ' . $exception->getLine() . ']';
        print 'Exception ' . $this->getExceptionCount() . "!\n$message\n";
        $breadcrumb = $this->getTestList();
        array_shift($breadcrumb);
        print "\tin " . implode("\n\tin ", array_reverse($breadcrumb));
        print "\n";
    }

    /**
     * Prints the message for skipping tests.
     *
     * @param string $message    Text of skip condition.
     */
    public function paintSkip($message)
    {
        parent::paintSkip($message);
        print "Skip: $message\n";
    }

    /**
     * Paints formatted text such as dumped privateiables.
     *
     * @param string $message        Text to show.
     */
    public function paintFormattedMessage($message)
    {
        print "$message\n";
        flush();
    }
}

/**
 * Runs just a single test group, a single case or even a single test within that case.
 */
class SelectiveReporter extends SimpleReporterDecorator
{
    private $just_this_case = false;
    private $just_this_test = false;
    private $on;

    /**
     * Selects the test case or group to be run, and optionally a specific test.
     *
     * @param SimpleScorer $reporter    Reporter to receive events.
     * @param string $just_this_case    Only this case or group will run.
     * @param string $just_this_test    Only this test method will run.
     */
    public function __construct($reporter, $just_this_case = false, $just_this_test = false)
    {
        if (isset($just_this_case) && $just_this_case) {
            $this->just_this_case = strtolower($just_this_case);
            $this->off();
        } else {
            $this->on();
        }
        if (isset($just_this_test) && $just_this_test) {
            $this->just_this_test = strtolower($just_this_test);
        }
        parent::__construct($reporter);
    }

    /**
     * Compares criteria to actual the case/group name.
     *
     * @param string $test_case    The incoming test.
     *
     * @return bool             True if matched.
     */
    protected function matchesTestCase($test_case)
    {
        return $this->just_this_case == strtolower($test_case);
    }

    /**
     * Compares criteria to actual the test name.
     * If no name was specified at the beginning, then all tests can run.
     *
     * @param string $method       The incoming test method.
     *
     * @return bool             True if matched.
     */
    protected function shouldRunTest($test_case, $method)
    {
        if ($this->isOn() || $this->matchesTestCase($test_case)) {
            if ($this->just_this_test) {
                return $this->just_this_test == strtolower($method);
            } else {
                return true;
            }
        }

        return false;
    }

    /**
     * Switch on testing for the group or subgroup.
     */
    protected function on()
    {
        $this->on = true;
    }

    /**
     * Switch off testing for the group or subgroup.
     */
    protected function off()
    {
        $this->on = false;
    }

    /**
     * Is this group actually being tested?
     *
     * @return bool     True if the current test group is active.
     */
    protected function isOn()
    {
        return $this->on;
    }

    /**
     * Veto everything that doesn't match the method wanted.
     *
     * @param string $test_case       Name of test case.
     * @param string $method          Name of test method.
     *
     * @return bool                True if test should be run.
     */
    public function shouldInvoke($test_case, $method)
    {
        if ($this->shouldRunTest($test_case, $method)) {
            return $this->reporter->shouldInvoke($test_case, $method);
        }

        return false;
    }

    /**
     * Paints the start of a group test.
     *
     * @param string $test_case     Name of test or other label.
     * @param int $size         Number of test cases starting.
     */
    public function paintGroupStart($test_case, $size)
    {
        if ($this->just_this_case && $this->matchesTestCase($test_case)) {
            $this->on();
        }
        $this->reporter->paintGroupStart($test_case, $size);
    }

    /**
     * Paints the end of a group test.
     *
     * @param string $test_case     Name of test or other label.
     */
    public function paintGroupEnd($test_case)
    {
        $this->reporter->paintGroupEnd($test_case);
        if ($this->just_this_case && $this->matchesTestCase($test_case)) {
            $this->off();
        }
    }
}

/**
 * Suppresses skip messages.
 */
class NoSkipsReporter extends SimpleReporterDecorator
{
    /**
     * Does nothing.
     *
     * @param string $message    Text of skip condition.
     */
    public function paintSkip($message)
    {
    }
}


<?php

require_once dirname(__FILE__) . '/invoker.php';

/**
 * Can receive test events and display them.
 * Display is achieved by making display methods available
 * and visiting the incoming event.
 *
 * @abstract
 */
class SimpleScorer
{
    private $passes;
    private $fails;
    private $exceptions;
    private $is_dry_run;

    /**
     * Starts the test run with no results.
     */
    public function __construct()
    {
        $this->passes     = 0;
        $this->fails      = 0;
        $this->exceptions = 0;
        $this->is_dry_run = false;
    }

    /**
     * Signals that the next evaluation will be a dry run.
     * That is, the structure events will be recorded, but no tests will be run.
     *
     * @param bool $is_dry        Dry run if true.
     */
    public function makeDry($is_dry = true)
    {
        $this->is_dry_run = $is_dry;
    }

    /**
     * The reporter has a veto on what should be run.
     *
     * @param string $test_case_name  name of test case.
     * @param string $method          Name of test method.
     */
    public function shouldInvoke($test_case_name, $method)
    {
        return ! $this->is_dry_run;
    }

    /**
     * Can wrap the invoker in preperation for running a test.
     *
     * @param SimpleInvoker $invoker   Individual test runner.
     *
     * @return SimpleInvoker           Wrapped test runner.
     */
    public function createInvoker($invoker)
    {
        return $invoker;
    }

    /**
     * Accessor for current status.
     * Will be false if there have been any failures or exceptions.
     * Used for command line tools.
     *
     * @return bool        True if no failures.
     */
    public function getStatus()
    {
        if ($this->exceptions + $this->fails > 0) {
            return false;
        }

        return true;
    }

    /**
     * Paints the start of a group test.
     *
     * @param string $test_name     Name of test or other label.
     * @param int $size         Number of test cases starting.
     */
    public function paintGroupStart($test_name, $size)
    {
    }

    /**
     * Paints the end of a group test.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintGroupEnd($test_name)
    {
    }

    /**
     * Paints the start of a test case.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintCaseStart($test_name)
    {
    }

    /**
     * Paints the end of a test case.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintCaseEnd($test_name)
    {
    }

    /**
     * Paints the start of a test method.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintMethodStart($test_name)
    {
    }

    /**
     * Paints the end of a test method.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintMethodEnd($test_name)
    {
    }

    /**
     * Increments the pass count.
     *
     * @param string $message        Message is ignored.
     */
    public function paintPass($message)
    {
        $this->passes++;
    }

    /**
     * Increments the fail count.
     *
     * @param string $message        Message is ignored.
     */
    public function paintFail($message)
    {
        $this->fails++;
    }

    /**
     * Deals with PHP 4 throwing an error.
     *
     * @param string $message    Text of error formatted by the test case.
     */
    public function paintError($message)
    {
        $this->exceptions++;
    }

    /**
     * Deals with PHP 5 throwing an exception.
     *
     * @param Exception $exception    The actual exception thrown.
     */
    public function paintException($exception)
    {
        $this->exceptions++;
    }

    /**
     * Prints the message for skipping tests.
     *
     * @param string $message    Text of skip condition.
     */
    public function paintSkip($message)
    {
    }

    /**
     * Accessor for the number of passes so far.
     *
     * @return int       Number of passes.
     */
    public function getPassCount()
    {
        return $this->passes;
    }

    /**
     * Accessor for the number of fails so far.
     *
     * @return int       Number of fails.
     */
    public function getFailCount()
    {
        return $this->fails;
    }

    /**
     * Accessor for the number of untrapped errors so far.
     *
     * @return int       Number of exceptions.
     */
    public function getExceptionCount()
    {
        return $this->exceptions;
    }

    /**
     * Paints a simple supplementary message.
     *
     * @param string $message        Text to display.
     */
    public function paintMessage($message)
    {
    }

    /**
     * Paints a formatted ASCII message such as a privateiable dump.
     *
     * @param string $message        Text to display.
     */
    public function paintFormattedMessage($message)
    {
    }

    /**
     * By default just ignores user generated events.
     *
     * @param string $type        Event type as text.
     * @param mixed $payload      Message or object.
     */
    public function paintSignal($type, $payload)
    {
    }
}

/**
 * Recipient of generated test messages that can display page footers and headers.
 * Also keeps track of the test nesting.
 * This is the main base class on which to build the finished test (page based) displays.
 */
class SimpleReporter extends SimpleScorer
{
    private $test_stack;
    private $size;
    private $progress;

    /**
     * Starts the display with no results in.
     */
    public function __construct()
    {
        parent::__construct();
        $this->test_stack = array();
        $this->size       = null;
        $this->progress   = 0;
    }

    /**
     * Gets the formatter for small generic data items.
     *
     * @return SimpleDumper          Formatter.
     */
    public function getDumper()
    {
        return new SimpleDumper();
    }

    /**
     * Paints the start of a group test.
     * Will also paint the page header and footer if this is the first test.
     * Will stash the size if the first start.
     *
     * @param string $test_name   Name of test that is starting.
     * @param int $size       Number of test cases starting.
     */
    public function paintGroupStart($test_name, $size)
    {
        if (! isset($this->size)) {
            $this->size = $size;
        }
        if (count($this->test_stack) == 0) {
            $this->paintHeader($test_name);
        }
        $this->test_stack[] = $test_name;
    }

    /**
     * Paints the end of a group test.
     * Will paint the page footer if the stack of tests has unwound.
     *
     * @param string $test_name   Name of test that is ending.
     * @param int $progress   Number of test cases ending.
     */
    public function paintGroupEnd($test_name)
    {
        array_pop($this->test_stack);
        if (count($this->test_stack) == 0) {
            $this->paintFooter($test_name);
        }
    }

    /**
     * Paints the start of a test case.
     * Will also paint the page header and footer if this is the first test.
     * Will stash the size if the first start.
     *
     * @param string $test_name   Name of test that is starting.
     */
    public function paintCaseStart($test_name)
    {
        if (! isset($this->size)) {
            $this->size = 1;
        }
        if (count($this->test_stack) == 0) {
            $this->paintHeader($test_name);
        }
        $this->test_stack[] = $test_name;
    }

    /**
     * Paints the end of a test case.
     * Will paint the page footer if the stack of tests has unwound.
     *
     * @param string $test_name   Name of test that is ending.
     */
    public function paintCaseEnd($test_name)
    {
        $this->progress++;
        array_pop($this->test_stack);
        if (count($this->test_stack) == 0) {
            $this->paintFooter($test_name);
        }
    }

    /**
     * Paints the start of a test method.
     *
     * @param string $test_name   Name of test that is starting.
     */
    public function paintMethodStart($test_name)
    {
        $this->test_stack[] = $test_name;
    }

    /**
     * Paints the end of a test method.
     * Will paint the page footer if the stack of tests has unwound.
     *
     * @param string $test_name   Name of test that is ending.
     */
    public function paintMethodEnd($test_name)
    {
        array_pop($this->test_stack);
    }

    /**
     * Paints the test document header.
     *
     * @param string $test_name     First test top level to start.
     *
     * @abstract
     */
    public function paintHeader($test_name)
    {
    }

    /**
     * Paints the test document footer.
     *
     * @param string $test_name        The top level test.
     *
     * @abstract
     */
    public function paintFooter($test_name)
    {
    }

    /**
     * Accessor for internal test stack.
     * For subclasses that need to see the whole test history for display purposes.
     *
     * @return array     List of methods in nesting order.
     */
    public function getTestList()
    {
        return $this->test_stack;
    }

    /**
     * Accessor for total test size in number of test cases.
     * Null until the first test is started.
     *
     * @return int   Total number of cases at start.
     */
    public function getTestCaseCount()
    {
        return $this->size;
    }

    /**
     * Accessor for the number of test cases completed so far.
     *
     * @return int   Number of ended cases.
     */
    public function getTestCaseProgress()
    {
        return $this->progress;
    }

    /**
     * Static check for running in the comand line.
     *
     * @return bool        True if CLI.
     */
    public static function inCli()
    {
        return PHP_SAPI === 'cli';
    }
}

/**
 * For modifying the behaviour of the visual reporters.
 */
class SimpleReporterDecorator
{
    protected $reporter;

    /**
     * Mediates between the reporter and the test case.
     *
     * @param SimpleScorer $reporter       Reporter to receive events.
     */
    public function __construct($reporter)
    {
        $this->reporter = $reporter;
    }

    /**
     * Signals that the next evaluation will be a dry run.
     * That is, the structure events will be recorded, but no tests will be run.
     *
     * @param bool $is_dry        Dry run if true.
     */
    public function makeDry($is_dry = true)
    {
        $this->reporter->makeDry($is_dry);
    }

    /**
     * Accessor for current status.
     * Will be false if there have been any failures or exceptions.
     * Used for command line tools.
     *
     * @return bool        True if no failures.
     */
    public function getStatus()
    {
        return $this->reporter->getStatus();
    }

    /**
     * The nesting of the test cases so far. Not all reporters have this facility.
     *
     * @return array        Test list if accessible.
     */
    public function getTestList()
    {
        if (method_exists($this->reporter, 'getTestList')) {
            return $this->reporter->getTestList();
        } else {
            return array();
        }
    }

    /**
     * The reporter has a veto on what should be run.
     *
     * @param string $test_case_name  Name of test case.
     * @param string $method          Name of test method.
     *
     * @return bool                True if test should be run.
     */
    public function shouldInvoke($test_case_name, $method)
    {
        return $this->reporter->shouldInvoke($test_case_name, $method);
    }

    /**
     * Can wrap the invoker in preparation for running a test.
     *
     * @param SimpleInvoker $invoker   Individual test runner.
     *
     * @return SimpleInvoker           Wrapped test runner.
     */
    public function createInvoker($invoker)
    {
        return $this->reporter->createInvoker($invoker);
    }

    /**
     * Gets the formatter for privateiables and other small generic data items.
     *
     * @return SimpleDumper          Formatter.
     */
    public function getDumper()
    {
        return $this->reporter->getDumper();
    }

    /**
     * Paints the start of a group test.
     *
     * @param string $test_name     Name of test or other label.
     * @param int $size         Number of test cases starting.
     */
    public function paintGroupStart($test_name, $size)
    {
        $this->reporter->paintGroupStart($test_name, $size);
    }

    /**
     * Paints the end of a group test.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintGroupEnd($test_name)
    {
        $this->reporter->paintGroupEnd($test_name);
    }

    /**
     * Paints the start of a test case.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintCaseStart($test_name)
    {
        $this->reporter->paintCaseStart($test_name);
    }

    /**
     * Paints the end of a test case.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintCaseEnd($test_name)
    {
        $this->reporter->paintCaseEnd($test_name);
    }

    /**
     * Paints the start of a test method.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintMethodStart($test_name)
    {
        $this->reporter->paintMethodStart($test_name);
    }

    /**
     * Paints the end of a test method.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintMethodEnd($test_name)
    {
        $this->reporter->paintMethodEnd($test_name);
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $message        Message is ignored.
     */
    public function paintPass($message)
    {
        $this->reporter->paintPass($message);
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $message        Message is ignored.
     */
    public function paintFail($message)
    {
        $this->reporter->paintFail($message);
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $message    Text of error formatted by the test case.
     */
    public function paintError($message)
    {
        $this->reporter->paintError($message);
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param Exception $exception        Exception to show.
     */
    public function paintException($exception)
    {
        $this->reporter->paintException($exception);
    }

    /**
     * Prints the message for skipping tests.
     *
     * @param string $message    Text of skip condition.
     */
    public function paintSkip($message)
    {
        $this->reporter->paintSkip($message);
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $message        Text to display.
     */
    public function paintMessage($message)
    {
        $this->reporter->paintMessage($message);
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $message        Text to display.
     */
    public function paintFormattedMessage($message)
    {
        $this->reporter->paintFormattedMessage($message);
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $type        Event type as text.
     * @param mixed $payload      Message or object.
     *
     * @return bool     Should return false, if this type of signal should fail the test suite.
     */
    public function paintSignal($type, $payload)
    {
        $this->reporter->paintSignal($type, $payload);
    }
}

/**
 * For sending messages to multiple reporters at the same time.
 */
class MultipleReporter
{
    private $reporters = array();

    /**
     * Adds a reporter to the subscriber list.
     *
     * @param SimpleScorer $reporter     Reporter to receive events.
     */
    public function attachReporter($reporter)
    {
        $this->reporters[] = $reporter;
    }

    /**
     * Signals that the next evaluation will be a dry run.
     * That is, the structure events will be recorded, but no tests will be run.
     *
     * @param bool $is_dry        Dry run if true.
     */
    public function makeDry($is_dry = true)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->makeDry($is_dry);
        }
    }

    /**
     * Accessor for current status.
     * Will be false if there have been any failures or exceptions.
     * If any reporter reports a failure, the whole suite fails.
     *
     * @return bool        True if no failures.
     */
    public function getStatus()
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            if (! $this->reporters[$i]->getStatus()) {
                return false;
            }
        }

        return true;
    }

    /**
     * The reporter has a veto on what should be run.
     * It requires all reporters to want to run the method.
     *
     * @param string $test_case_name  name of test case.
     * @param string $method          Name of test method.
     */
    public function shouldInvoke($test_case_name, $method)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            if (! $this->reporters[$i]->shouldInvoke($test_case_name, $method)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Every reporter gets a chance to wrap the invoker.
     *
     * @param SimpleInvoker $invoker   Individual test runner.
     *
     * @return SimpleInvoker           Wrapped test runner.
     */
    public function createInvoker($invoker)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $invoker = $this->reporters[$i]->createInvoker($invoker);
        }

        return $invoker;
    }

    /**
     * Paints the start of a group test.
     *
     * @param string $test_name     Name of test or other label.
     * @param int $size         Number of test cases starting.
     */
    public function paintGroupStart($test_name, $size)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintGroupStart($test_name, $size);
        }
    }

    /**
     * Paints the end of a group test.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintGroupEnd($test_name)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintGroupEnd($test_name);
        }
    }

    /**
     * Paints the start of a test case.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintCaseStart($test_name)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintCaseStart($test_name);
        }
    }

    /**
     * Paints the end of a test case.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintCaseEnd($test_name)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintCaseEnd($test_name);
        }
    }

    /**
     * Paints the start of a test method.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintMethodStart($test_name)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintMethodStart($test_name);
        }
    }

    /**
     * Paints the end of a test method.
     *
     * @param string $test_name     Name of test or other label.
     */
    public function paintMethodEnd($test_name)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintMethodEnd($test_name);
        }
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $message        Message is ignored.
     */
    public function paintPass($message)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintPass($message);
        }
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $message        Message is ignored.
     */
    public function paintFail($message)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintFail($message);
        }
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $message    Text of error formatted by the test case.
     */
    public function paintError($message)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintError($message);
        }
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param Exception $exception    Exception to display.
     */
    public function paintException($exception)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintException($exception);
        }
    }

    /**
     * Prints the message for skipping tests.
     *
     * @param string $message    Text of skip condition.
     */
    public function paintSkip($message)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintSkip($message);
        }
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $message        Text to display.
     */
    public function paintMessage($message)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintMessage($message);
        }
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $message        Text to display.
     */
    public function paintFormattedMessage($message)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintFormattedMessage($message);
        }
    }

    /**
     * Chains to the wrapped reporter.
     *
     * @param string $type        Event type as text.
     * @param mixed $payload      Message or object.
     *
     * @return bool            Should return false if this type of signal should fail the test
     * suite.
     */
    public function paintSignal($type, $payload)
    {
        $numberOfReporters = count($this->reporters);
        for ($i = 0; $i < $numberOfReporters; $i++) {
            $this->reporters[$i]->paintSignal($type, $payload);
        }
    }

    /**
     * Gets the formatter for privateiables and other small generic data items.
     *
     * @return SimpleDumper          Formatter.
     */
    public function getDumper()
    {
        return new SimpleDumper();
    }
}


<?php
/**
 * Parses the command line arguments.
 */
class SimpleArguments
{
    private $all = array();

    /**
     * Parses the command line arguments.
     *
     * The usual formats are supported:
     * -f value
     * -f=value
     * --flag=value
     * --flag value
     * -f           (true)
     * --flag       (true)
     *
     * @param array $arguments Normally the PHP $argv.
     */
    public function __construct($arguments)
    {
        array_shift($arguments);
        while (count($arguments) > 0) {
            list($key, $value) = $this->parseArgument($arguments);
            $this->assign($key, $value);
        }
    }

    /**
     * Sets the value in the argments object. If multiple values are added under
     * the same key, the key will give an array value in the order they were
     * added.
     *
     * @param string $key The variable to assign to.
     * @param string value   The value that would norally be colected on the
     * command line.
     */
    public function assign($key, $value)
    {
        if ($this->$key === false) {
            $this->all[$key] = $value;
        } elseif (! is_array($this->$key)) {
            $this->all[$key] = array($this->$key, $value);
        } else {
            $this->all[$key][] = $value;
        }
    }

    /**
     * Extracts the next key and value from the argument list.
     *
     * @param array $arguments The remaining arguments to be parsed.
     *                         The argument list will be reduced.
     *
     * @return array Two item array of key and value.
     *               If no value can be found it will
     *               have the value true assigned instead.
     */
    private function parseArgument(&$arguments)
    {
        $argument = array_shift($arguments);
        if (preg_match('/^-(\w)=(.+)$/', $argument, $matches)) {
            return array($matches[1], $matches[2]);
        } elseif (preg_match('/^-(\w)$/', $argument, $matches)) {
            return array($matches[1], $this->nextNonFlagElseTrue($arguments));
        } elseif (preg_match('/^--(\w+)=(.+)$/', $argument, $matches)) {
            return array($matches[1], $matches[2]);
        } elseif (preg_match('/^--(\w+)$/', $argument, $matches)) {
            return array($matches[1], $this->nextNonFlagElseTrue($arguments));
        }
    }

    /**
     * Attempts to use the next argument as a value.
     * It won't use what it thinks is a flag.
     *
     * @param array $arguments Remaining arguments to be parsed. This variable
     * is modified if there is a value to be extracted.
     *
     * @return string/boolean The next value unless it's a flag.
     */
    private function nextNonFlagElseTrue(&$arguments)
    {
        return $this->valueIsNext($arguments) ? array_shift($arguments) : true;
    }

    /**
     * Test to see if the next available argument is a valid value.
     * If it starts with "-" or "--" it's a flag and doesn't count.
     *
     * @param array $arguments Remaining arguments to be parsed.
     *                         Not affected by this call.
     *                         boolean                    True if valid value.
     */
    public function valueIsNext($arguments)
    {
        return isset($arguments[0]) && ! $this->isFlag($arguments[0]);
    }

    /**
     * It's a flag if it starts with "-" or "--".
     *
     * @param string $argument Value to be tested.
     *
     * @return bool True if it's a flag.
     */
    public function isFlag($argument)
    {
        return strncmp($argument, '-', 1) == 0;
    }

    /**
     * The arguments are available as individual member variables on the object.
     *
     * @param string $key Argument name.
     *
     * @return string/array/boolean Either false for no value,
     *                              the value as a string or
     *                              a list of multiple values if
     *                              the flag had been specified more
     *                              than once.
     */
    public function __get($key)
    {
        if (isset($this->all[$key])) {
            return $this->all[$key];
        }

        return false;
    }

    /**
     * The entire argument set as a hash.
     *
     * @return hash Each argument and it's value(s).
     */
    public function all()
    {
        return $this->all;
    }
}

/**
 * Renders the help for the command line arguments.
 */
class SimpleHelp
{
    private $overview;
    private $flag_sets    = array();
    private $explanations = array();

    /**
     * Sets up the top level explanation for the program.
     *
     * @param string $overview Summary of program.
     */
    public function __construct($overview = '')
    {
        $this->overview = $overview;
    }

    /**
     * Adds the explanation for a group of flags that all
     * have the same function.
     *
     * @param string/array $flags       Flag and alternates. Don't
     *                                  worry about leading dashes
     *                                  as these are inserted automatically.
     * @param string       $explanation What that flag group does.
     */
    public function explainFlag($flags, $explanation)
    {
        $flags                = is_array($flags) ? $flags : array($flags);
        $this->flag_sets[]    = $flags;
        $this->explanations[] = $explanation;
    }

    /**
     * Generates the help text.
     *
     * @returns string      The complete formatted text.
     */
    public function render()
    {
        $tab_stop = $this->longestFlag($this->flag_sets) + 4;
        $text     = $this->overview . "\n";
        $numberOfFlags = count($this->flag_sets);
        for ($i = 0; $i < $numberOfFlags; $i++) {
            $text .= $this->renderFlagSet($this->flag_sets[$i], $this->explanations[$i], $tab_stop);
        }

        return $this->noDuplicateNewLines($text);
    }

    /**
     * Works out the longest flag for formatting purposes.
     *
     * @param array $flag_sets The internal flag set list.
     */
    private function longestFlag($flag_sets)
    {
        $longest = 0;
        foreach ($flag_sets as $flags) {
            foreach ($flags as $flag) {
                $longest = max($longest, strlen($this->renderFlag($flag)));
            }
        }

        return $longest;
    }

    /**
     * Generates the text for a single flag and it's alternate flags.
     *
     * @returns string           Help text for that flag group.
     */
    private function renderFlagSet($flags, $explanation, $tab_stop)
    {
        $flag = array_shift($flags);
        $text = str_pad($this->renderFlag($flag), $tab_stop, ' ') . $explanation . "\n";
        foreach ($flags as $flag) {
            $text .= '  ' . $this->renderFlag($flag) . "\n";
        }

        return $text;
    }

    /**
     * Generates the flag name including leading dashes.
     *
     * @param string $flag Just the name.
     * @returns                     Fag with apropriate dashes.
     */
    private function renderFlag($flag)
    {
        return (strlen($flag) == 1 ? '-' : '--') . $flag;
    }

    /**
     * Converts multiple new lines into a single new line.
     * Just there to trap accidental duplicate new lines.
     *
     * @param string $text Text to clean up.
     * @returns string          Text with no blank lines.
     */
    private function noDuplicateNewLines($text)
    {
        return preg_replace('/(\n+)/', "\n", $text);
    }
}


<?php
/**
 * Builds the page object.
 */
class SimpleTidyPageBuilder
{
    private $forms         = array();
    private $labels        = array();
    private $page;
    private $widgets_by_id = array();

    public function __destruct()
    {
        $this->free();
    }

    /**
     * Frees up any references so as to allow the PHP garbage collection from unset() to work.
     */
    private function free()
    {
        unset($this->page);
        $this->forms  = array();
        $this->labels = array();
    }

    /**
     * This builder is only available if the 'tidy' extension is loaded.
     *
     * @return bool       True if available.
     */
    public function can()
    {
        return extension_loaded('tidy');
    }

    /**
     * Reads the raw content the page using HTML Tidy.
     *
     * @param $response SimpleHttpResponse  Fetched response.
     *
     * @return SimplePage                   Newly parsed page.
     */
    public function parse($response)
    {
        $this->page = new SimplePage($response);
        $tidied     = tidy_parse_string($input = $this->insertGuards($response->getContent()),
                                    array('output-xml' => false, 'wrap' => '0', 'indent' => 'no'),
                                    'latin1');
        $this->walkTree($tidied->html());
        $this->attachLabels($this->widgets_by_id, $this->labels);
        $this->page->setForms($this->forms);
        $page = $this->page;
        $this->free();

        return $page;
    }

    /**
     * Stops HTMLTidy stripping content that we wish to preserve.
     *
     * @param string      The raw html.
     *
     * @return string     The html with guard tags inserted.
     */
    private function insertGuards($html)
    {
        return $this->insertEmptyTagGuards($this->insertTextareaSimpleWhitespaceGuards($html));
    }

    /**
     * Removes the extra content added during the parse stage in order to preserve content we don't
     * want stripped out by HTMLTidy.
     *
     * @param string      The raw html.
     *
     * @return string     The html with guard tags removed.
     */
    private function stripGuards($html)
    {
        return $this->stripTextareaWhitespaceGuards($this->stripEmptyTagGuards($html));
    }

    /**
     * HTML tidy strips out empty tags such as <option> which we need to preserve.
     * This method inserts an additional marker.
     *
     * @param string      The raw html.
     *
     * @return string     The html with guards inserted.
     */
    private function insertEmptyTagGuards($html)
    {
        return preg_replace('#<(option|textarea)([^>]*)>(\s*)</(option|textarea)>#is',
                            '<\1\2>___EMPTY___\3</\4>',
                            $html);
    }

    /**
     * HTML tidy strips out empty tags such as <option> which we need to preserve.
     * This method strips additional markers inserted by SimpleTest
     * to the tidy output used to make the tags non-empty.
     * This ensures their preservation.
     *
     * @param string      The raw html.
     *
     * @return string     The html with guards removed.
     */
    private function stripEmptyTagGuards($html)
    {
        return preg_replace('#(^|>)(\s*)___EMPTY___(\s*)(</|$)#i', '\2\3', $html);
    }

    /**
     * By parsing the XML output of tidy, we lose some whitespace information in textarea tags.
     * We temporarily recode this data ourselves so as not to lose it.
     *
     * @param string      The raw html.
     *
     * @return string     The html with guards inserted.
     */
    private function insertTextareaSimpleWhitespaceGuards($html)
    {
        return preg_replace_callback('#<textarea([^>]*)>(.*?)</textarea>#is',
                                     array($this, 'insertWhitespaceGuards'),
                                     $html);
    }

    /**
     * Callback for insertTextareaSimpleWhitespaceGuards().
     *
     * @param array $matches       Result of preg_replace_callback().
     *
     * @return string              Guard tags now replace whitespace.
     */
    private function insertWhitespaceGuards($matches)
    {
        return '<textarea' . $matches[1] . '>' .
                str_replace(array("\n", "\r", "\t", ' '),
                            array('___NEWLINE___', '___CR___', '___TAB___', '___SPACE___'),
                            $matches[2]) .
                '</textarea>';
    }

    /**
     * Removes the whitespace preserving guards we added before parsing.
     *
     * @param string      The raw html.
     *
     * @return string     The html with guards removed.
     */
    private function stripTextareaWhitespaceGuards($html)
    {
        return str_replace(array('___NEWLINE___', '___CR___', '___TAB___', '___SPACE___'),
                           array("\n", "\r", "\t", ' '),
                           $html);
    }

    /**
     * Visits the given node and all children
     *
     * @param object $node      Tidy XML node.
     */
    private function walkTree($node)
    {
        if ($node->name === 'a') {
            $this->page->addLink($this->tags()->createTag($node->name, (array) $node->attribute)
                                        ->addContent($this->innerHtml($node)));
        } elseif ($node->name === 'base' and isset($node->attribute['href'])) {
            $this->page->setBase($node->attribute['href']);
        } elseif ($node->name === 'title') {
            $this->page->setTitle($this->tags()->createTag($node->name, (array) $node->attribute)
                                         ->addContent($this->innerHtml($node)));
        } elseif ($node->name === 'frameset') {
            $this->page->setFrames($this->collectFrames($node));
        } elseif ($node->name === 'form') {
            $this->forms[] = $this->walkForm($node, $this->createEmptyForm($node));
        } elseif ($node->name === 'label') {
            $this->labels[] = $this->tags()->createTag($node->name, (array) $node->attribute)
                                           ->addContent($this->innerHtml($node));
        } else {
            $this->walkChildren($node);
        }
    }

    /**
     * Helper method for traversing the XML tree.
     *
     * @param object $node     Tidy XML node.
     */
    private function walkChildren($node)
    {
        if ($node->hasChildren()) {
            foreach ($node->child as $child) {
                $this->walkTree($child);
            }
        }
    }

    /**
     * Facade for forms containing preparsed widgets.
     *
     * @param object $node     Tidy XML node.
     *
     * @return SimpleForm      Facade for SimpleBrowser.
     */
    private function createEmptyForm($node)
    {
        return new SimpleForm($this->tags()->createTag($node->name, (array) $node->attribute), $this->page);
    }

    /**
     * Visits the given node and all children
     *
     * @param object $node      Tidy XML node.
     */
    private function walkForm($node, $form, $enclosing_label = '')
    {
        if ($node->name === 'a') {
            $this->page->addLink($this->tags()->createTag($node->name, (array) $node->attribute)
                                              ->addContent($this->innerHtml($node)));
        } elseif (in_array($node->name, array('input', 'button', 'textarea', 'select'))) {
            $this->addWidgetToForm($node, $form, $enclosing_label);
        } elseif ($node->name === 'label') {
            $this->labels[] = $this->tags()->createTag($node->name, (array) $node->attribute)
                                           ->addContent($this->innerHtml($node));
            if ($node->hasChildren()) {
                foreach ($node->child as $child) {
                    $this->walkForm($child, $form, SimplePage::normalise($this->innerHtml($node)));
                }
            }
        } elseif ($node->hasChildren()) {
            foreach ($node->child as $child) {
                $this->walkForm($child, $form);
            }
        }

        return $form;
    }

    /**
     * Tests a node for a "for" atribute. Used for attaching labels.
     *
     * @param object $node      Tidy XML node.
     *
     * @return bool          True if the "for" attribute exists.
     */
    private function hasFor($node)
    {
        return isset($node->attribute) and $node->attribute['for'];
    }

    /**
     * Adds the widget into the form container.
     *
     * @param object $node             Tidy XML node of widget.
     * @param SimpleForm $form         Form to add it to.
     * @param string $enclosing_label  The label of any label tag we might be in.
     */
    private function addWidgetToForm($node, $form, $enclosing_label)
    {
        $widget = $this->tags()->createTag($node->name, $this->attributes($node));
        if (! $widget) {
            return;
        }
        $widget->setLabel($enclosing_label)
               ->addContent($this->innerHtml($node));
        if ($node->name == 'select') {
            $widget->addTags($this->collectSelectOptions($node));
        }
        $form->addWidget($widget);
        $this->indexWidgetById($widget);
    }

    /**
     * Fills the widget cache to speed up searching.
     *
     * @param SimpleTag $widget    Parsed widget to cache.
     */
    private function indexWidgetById($widget)
    {
        $id = $widget->getAttribute('id');
        if (! $id) {
            return;
        }
        if (! isset($this->widgets_by_id[$id])) {
            $this->widgets_by_id[$id] = array();
        }
        $this->widgets_by_id[$id][] = $widget;
    }

    /**
     * Parses the options from inside an XML select node.
     *
     * @param object $node      Tidy XML node.
     *
     * @return array            List of SimpleTag options.
     */
    private function collectSelectOptions($node)
    {
        $options = array();
        if ($node->name === 'option') {
            $options[] = $this->tags()->createTag($node->name, $this->attributes($node))
                                      ->addContent($this->innerHtml($node));
        }
        if ($node->hasChildren()) {
            foreach ($node->child as $child) {
                $options = array_merge($options, $this->collectSelectOptions($child));
            }
        }

        return $options;
    }

    /**
     * Convenience method for collecting all the attributes of a tag.
     * Not sure why Tidy does not have this.
     *
     * @param object $node      Tidy XML node.
     *
     * @return array            Hash of attribute strings.
     */
    private function attributes($node)
    {
        if (! preg_match('|<[^ ]+\s(.*?)/?>|s', $node->value, $first_tag_contents)) {
            return array();
        }
        $attributes = array();
        preg_match_all('/\S+\s*=\s*\'[^\']*\'|(\S+\s*=\s*"[^"]*")|([^ =]+\s*=\s*[^ "\']+?)|[^ "\']+/', $first_tag_contents[1], $matches);
        foreach ($matches[0] as $unparsed) {
            $attributes = $this->mergeAttribute($attributes, $unparsed);
        }

        return $attributes;
    }

    /**
     * Overlay an attribute into the attributes hash.
     *
     * @param array $attributes        Current attribute list.
     * @param string $raw              Raw attribute string with both key and value.
     *
     * @return array                   New attribute hash.
     */
    private function mergeAttribute($attributes, $raw)
    {
        $parts                   = explode('=', $raw);
        list($name, $value)      = count($parts) === 1 ? array($parts[0], $parts[0]) : $parts;
        $attributes[trim($name)] = html_entity_decode($this->dequote(trim($value)), ENT_QUOTES);

        return $attributes;
    }

    /**
     * Remove start and end quotes.
     *
     * @param string $quoted    A quoted string.
     *
     * @return string           Quotes are gone.
     */
    private function dequote($quoted)
    {
        if (preg_match('/^(\'([^\']*)\'|"([^"]*)")$/', $quoted, $matches)) {
            return isset($matches[3]) ? $matches[3] : $matches[2];
        }

        return $quoted;
    }

    /**
     * Collects frame information inside a frameset tag.
     *
     * @param object $node     Tidy XML node.
     *
     * @return array           List of SimpleTag frame descriptions.
     */
    private function collectFrames($node)
    {
        $frames = array();
        if ($node->name === 'frame') {
            $frames = array($this->tags()->createTag($node->name, (array) $node->attribute));
        } elseif ($node->hasChildren()) {
            $frames = array();
            foreach ($node->child as $child) {
                $frames = array_merge($frames, $this->collectFrames($child));
            }
        }

        return $frames;
    }

    /**
     * Extracts the XML node text.
     *
     * @param object $node     Tidy XML node.
     *
     * @return string          The text only.
     */
    private function innerHtml($node)
    {
        $raw = '';
        if ($node->hasChildren()) {
            foreach ($node->child as $child) {
                $raw .= $child->value;
            }
        }

        return $this->stripGuards($raw);
    }

    /**
     * Factory for parsed content holders.
     *
     * @return SimpleTagBuilder    Factory.
     */
    private function tags()
    {
        return new SimpleTagBuilder();
    }

    /**
     * Called at the end of a parse run.
     * Attaches any non-wrapping labels to their form elements.
     *
     * @param array $widgets_by_id     Cached SimpleTag hash.
     * @param array $labels            SimpleTag label elements.
     */
    private function attachLabels($widgets_by_id, $labels)
    {
        foreach ($labels as $label) {
            $for = $label->getFor();
            if ($for and isset($widgets_by_id[$for])) {
                $text = $label->getText();
                foreach ($widgets_by_id[$for] as $widget) {
                    $widget->setLabel($text);
                }
            }
        }
    }
}


<?php

/**
 * does type matter
 */
if (! defined('TYPE_MATTERS')) {
    define('TYPE_MATTERS', true);
}

/**
 * Displays variables as text and does diffs.
 */
class SimpleDumper
{
    /**
     * Renders a variable in a shorter form than print_r().
     *
     * @param mixed $value      Variable to render as a string.
     *
     * @return string           Human readable string form.
     */
    public function describeValue($value)
    {
        $type = $this->getType($value);
        switch ($type) {
            case 'Null':
                return 'NULL';
            case 'Boolean':
                return 'Boolean: ' . ($value ? 'true' : 'false');
            case 'Array':
                return 'Array: ' . count($value) . ' items';
            case 'Object':
                return 'Object: of ' . get_class($value);
            case 'String':
                return 'String: ' . $this->clipString($value, 200);
            default:
                return $type . ': ' . $value;
        }
    }

    /**
     * Gets the string representation of a type.
     *
     * @param mixed $value    Variable to check against.
     *
     * @return string         Type.
     */
    public function getType($value)
    {
        if (! isset($value)) {
            return 'Null';
        } elseif (is_bool($value)) {
            return 'Boolean';
        } elseif (is_string($value)) {
            return 'String';
        } elseif (is_integer($value)) {
            return 'Integer';
        } elseif (is_float($value)) {
            return 'Float';
        } elseif (is_array($value)) {
            return 'Array';
        } elseif (is_resource($value)) {
            return 'Resource';
        } elseif (is_object($value)) {
            return 'Object';
        }

        return 'Unknown';
    }

    /**
     * Creates a human readable description of the difference between two variables.
     * Uses a dynamic call.
     *
     * @param mixed $first        First variable.
     * @param mixed $second       Value to compare with.
     * @param bool $identical  If true then type anomolies count.
     *
     * @return string             Description of difference.
     */
    public function describeDifference($first, $second, $identical = false)
    {
        if ($identical) {
            if (! $this->isTypeMatch($first, $second)) {
                return sprintf(
                    'with type mismatch as [%s] does not match [%s]',
                    $this->describeValue($first),
                    $this->describeValue($second)
                );
            }
        }
        $type = $this->getType($first);
        if ($type === 'Unknown') {
            return 'with unknown type';
        }
        $method = 'describe' . $type . 'Difference';

        return $this->$method($first, $second, $identical);
    }

    /**
     * Tests to see if types match.
     *
     * @param mixed $first        First variable.
     * @param mixed $second       Value to compare with.
     *
     * @return bool            True if matches.
     */
    protected function isTypeMatch($first, $second)
    {
        return ($this->getType($first) == $this->getType($second));
    }

    /**
     * Clips a string to a maximum length.
     *
     * @param string $string     String to truncate.
     * @param int $size         Minimum string size to show.
     * @param int $position     Centre of string section.
     *
     * @return string               Shortened version.
     */
    public function clipString($string, $size, $position = 0)
    {
        $utf8 = false;
        if (function_exists('mb_detect_encoding') && mb_detect_encoding($string) == 'UTF-8') {
            $utf8 = true;
        }

        $length = ($utf8 == true) ? mb_strlen($string, "UTF-8") : strlen($string);

        if ($length <= $size) {
            return $string;
        }

        $position = min($position, $length);
        $start    = ($size/2 > $position ? 0 : $position - $size/2);
        if ($start + $size > $length) {
            $start = $length - $size;
        }
        $string = ($utf8 == true) ? mb_substr($string, $start, $size) : substr($string, $start, $size);

        return ($start > 0 ? '...' : '') . $string . ($start + $size < $length ? '...' : '');
    }

    /**
     * Creates a human readable description of the difference between two variables.
     * The minimal version.
     *
     * @param null $first          First value.
     * @param mixed $second        Value to compare with.
     *
     * @return string              Human readable description.
     */
    protected function describeGenericDifference($first, $second)
    {
        return sprintf(
            'as [%s] does not match [%s]', $this->describeValue($first), $this->describeValue($second)
        );
    }

    /**
     * Creates a human readable description of the difference between a null and another variable.
     *
     * @param null $first          First null.
     * @param mixed $second        Null to compare with.
     * @param bool $identical   If true then type anomolies count.
     *
     * @return string              Human readable description.
     */
    protected function describeNullDifference($first, $second, $identical)
    {
        return $this->describeGenericDifference($first, $second);
    }

    /**
     * Creates a human readable description of the difference between a boolean and another
     * variable.
     *
     * @param bool $first       First boolean.
     * @param mixed $second        Boolean to compare with.
     * @param bool $identical   If true then type anomolies count.
     *
     * @return string              Human readable description.
     */
    protected function describeBooleanDifference($first, $second, $identical)
    {
        return $this->describeGenericDifference($first, $second);
    }

    /**
     * Creates a human readable description of the difference between
     * a string and another variable.
     *
     * @param string $first        First string.
     * @param mixed $second        String to compare with.
     * @param bool $identical   If true then type anomolies count.
     *
     * @return string              Human readable description.
     */
    protected function describeStringDifference($first, $second, $identical)
    {
        if (is_object($second) || is_array($second)) {
            return $this->describeGenericDifference($first, $second);
        }
        $position = $this->stringDiffersAt($first, $second);

        return sprintf(
            'at character %s with [%s] and [%s]',
            $position,
            $this->clipString($first, 200, $position),
            $this->clipString($second, 200, $position)
        );
    }

    /**
     * Creates a human readable description of the difference between
     * an integer and another variable.
     *
     * @param int $first       First number.
     * @param mixed $second        Number to compare with.
     * @param bool $identical   If true then type anomolies count.
     *
     * @return string              Human readable description.
     */
    protected function describeIntegerDifference($first, $second, $identical)
    {
        if (is_object($second) || is_array($second)) {
            return $this->describeGenericDifference($first, $second);
        }

        return sprintf(
            'because [%s] differs from [%s] by %s',
            $this->describeValue($first),
            $this->describeValue($second),
            abs($first - $second)
        );
    }

    /**
     * Creates a human readable description of the difference between
     * two floating point numbers.
     *
     * @param float $first         First float.
     * @param mixed $second        Float to compare with.
     * @param bool $identical   If true then type anomolies count.
     *
     * @return string              Human readable description.
     */
    protected function describeFloatDifference($first, $second, $identical)
    {
        if (is_object($second) || is_array($second)) {
            return $this->describeGenericDifference($first, $second);
        }

        return sprintf(
            'because [%s] differs from [%s] by %s',
            $this->describeValue($first),
            $this->describeValue($second),
            abs($first - $second)
        );
    }

    /**
     * Creates a human readable description of the difference between two arrays.
     *
     * @param array $first         First array.
     * @param mixed $second        Array to compare with.
     * @param bool $identical   If true then type anomolies count.
     *
     * @return string              Human readable description.
     */
    protected function describeArrayDifference($first, $second, $identical)
    {
        if (! is_array($second)) {
            return $this->describeGenericDifference($first, $second);
        }
        if (! $this->isMatchingKeys($first, $second, $identical)) {
            return sprintf(
                'as key list [%s] does not match key list [%s]',
                implode(', ', array_keys($first)),
                implode(', ', array_keys($second))
            );
        }
        foreach (array_keys($first) as $key) {
            if ($identical && ($first[$key] === $second[$key])) {
                continue;
            }
            if (! $identical && ($first[$key] == $second[$key])) {
                continue;
            }

            return sprintf(
                'with member [%s] %s',
                $key,
                $this->describeDifference($first[$key], $second[$key], $identical)
            );
        }

        return '';
    }

    /**
     * Compares two arrays to see if their key lists match.
     * For an identical match, the ordering and types of the keys is significant.
     *
     * @param array $first         First array.
     * @param array $second        Array to compare with.
     * @param bool $identical   If true then type anomolies count.
     *
     * @return bool             True if matching.
     */
    protected function isMatchingKeys($first, $second, $identical)
    {
        $first_keys  = array_keys($first);
        $second_keys = array_keys($second);
        if ($identical) {
            return ($first_keys === $second_keys);
        }
        sort($first_keys);
        sort($second_keys);

        return ($first_keys == $second_keys);
    }

    /**
     * Creates a human readable description of the difference between
     * a resource and another variable.
     *
     * @param resource $first       First resource.
     * @param mixed $second         Resource to compare with.
     * @param bool $identical    If true then type anomolies count.
     *
     * @return string              Human readable description.
     */
    protected function describeResourceDifference($first, $second, $identical)
    {
        return $this->describeGenericDifference($first, $second);
    }

    /**
     * Creates a human readable description of the difference between two objects.
     *
     * @param object $first        First object.
     * @param mixed $second        Object to compare with.
     * @param bool $identical   If true then type anomolies count.
     *
     * @return string              Human readable description.
     */
    protected function describeObjectDifference($first, $second, $identical)
    {
        if (! is_object($second)) {
            return $this->describeGenericDifference($first, $second);
        }

        return $this->describeArrayDifference(
                $this->getMembers($first),
                $this->getMembers($second),
                $identical);
    }

    /**
     * Get all members of an object including private and protected ones.
     * A safer form of casting to an array.
     *
     * @param object $object     Object to list members of, including private ones.
     *
     * @return array             Names and values in the object.
     */
    protected function getMembers($object)
    {
        $reflection = new ReflectionObject($object);
        $members    = array();
        foreach ($reflection->getProperties() as $property) {
            if (method_exists($property, 'setAccessible')) {
                $property->setAccessible(true);
            }
            try {
                $members[$property->getName()] = $property->getValue($object);
            } catch (ReflectionException $e) {
                $members[$property->getName()] =
                    $this->getPrivatePropertyNoMatterWhat($property->getName(), $object);
            }
        }

        return $members;
    }

    /**
     * Extracts a private member's value when reflection won't play ball.
     *
     * @param string $name        Property name.
     * @param object $object      Object to read.
     *
     * @return mixed              Value of property.
     */
    private function getPrivatePropertyNoMatterWhat($name, $object)
    {
        foreach ((array) $object as $mangled_name => $value) {
            if ($this->unmangle($mangled_name) == $name) {
                return $value;
            }
        }
    }

    /**
     * Removes crud from property name after it's been converted to an array.
     *
     * @param string $mangled     Name from array cast.
     *
     * @return string             Cleaned up name.
     */
    public function unmangle($mangled)
    {
        $parts = preg_split('/[^a-zA-Z0-9_\x7f-\xff]+/', $mangled);

        return array_pop($parts);
    }

    /**
     * Find the first character position that differs in two strings by binary chop.
     *
     * @param string $first        First string.
     * @param string $second       String to compare with.
     *
     * @return int             Position of first differing character.
     */
    protected function stringDiffersAt($first, $second)
    {
        if (! $first || ! $second) {
            return 0;
        }
        if (strlen($first) < strlen($second)) {
            list($first, $second) = array($second, $first);
        }
        $position = 0;
        $step     = strlen($first);
        while ($step > 1) {
            $step = (integer) (($step + 1) / 2);
            if (strncmp($first, $second, $position + $step) == 0) {
                $position += $step;
            }
        }

        return $position;
    }

    /**
     * Sends a formatted dump of a variable to a string.
     *
     * @param mixed $variable    Variable to display.
     *
     * @return string            Output from print_r().
     */
    public function dump($variable)
    {
        ob_start();
        print_r($variable);
        $formatted = ob_get_contents();
        ob_end_clean();

        return $formatted;
    }
}


<?php

require_once dirname(__FILE__) . '/http.php';
require_once dirname(__FILE__) . '/php_parser.php';
require_once dirname(__FILE__) . '/tag.php';
require_once dirname(__FILE__) . '/form.php';
require_once dirname(__FILE__) . '/selector.php';

/**
 * A wrapper for a web page.
 */
class SimplePage
{
    private $links = array();
    private $title = false;
    private $last_widget;        // TODO
    private $label;              // TODO
    private $forms  = array();
    private $frames = array();
    private $transport_error;
    private $raw;
    private $text = false;
    private $sent;
    private $headers;
    private $method;
    private $url;
    private $base = false;
    private $request_data;

    /**
     * Parses a page ready to access it's contents.
     *
     * @param SimpleHttpResponse $response     Result of HTTP fetch.
     */
    public function __construct($response = false)
    {
        if ($response) {
            $this->extractResponse($response);
        } else {
            $this->noResponse();
        }
    }

    /**
     * Extracts all of the response information.
     *
     * @param SimpleHttpResponse $response    Response being parsed.
     */
    protected function extractResponse($response)
    {
        $this->transport_error = $response->getError();
        $this->raw             = $response->getContent();
        $this->sent            = $response->getSent();
        $this->headers         = $response->getHeaders();
        $this->method          = $response->getMethod();
        $this->url             = $response->getUrl();
        $this->request_data    = $response->getRequestData();
    }

    /**
     * Sets up a missing response.
     */
    protected function noResponse()
    {
        $this->transport_error = 'No page fetched yet';
        $this->raw             = false;
        $this->sent            = false;
        $this->headers         = false;
        $this->method          = 'GET';
        $this->url             = false;
        $this->request_data    = false;
    }

    /**
     * Original request as bytes sent down the wire.
     *
     * @return mixed              Sent content.
     */
    public function getRequest()
    {
        return $this->sent;
    }

    /**
     * Accessor for raw text of page.
     *
     * @return string        Raw unparsed content.
     */
    public function getRaw()
    {
        return $this->raw;
    }

    /**
     * Accessor for plain text of page as a text browser would see it.
     *
     * @return string        Plain text of page.
     */
    public function getText()
    {
        if (! $this->text) {
            $this->text = self::normalise($this->raw);
        }

        return $this->text;
    }

    /**
     * Accessor for raw headers of page.
     *
     * @return string       Header block as text.
     */
    public function getHeaders()
    {
        if ($this->headers) {
            return $this->headers->getRaw();
        }

        return false;
    }

    /**
     * Original request method.
     *
     * @return string        GET, POST or HEAD.
     */
    public function getMethod()
    {
        return $this->method;
    }

    /**
     * Original resource name.
     *
     * @return SimpleUrl        Current url.
     */
    public function getUrl()
    {
        return $this->url;
    }

    /**
     * Base URL if set via BASE tag page url otherwise
     *
     * @return SimpleUrl        Base url.
     */
    public function getBaseUrl()
    {
        return $this->base;
    }

    /**
     * Original request data.
     *
     * @return mixed              Sent content.
     */
    public function getRequestData()
    {
        return $this->request_data;
    }

    /**
     * Accessor for last error.
     *
     * @return string        Error from last response.
     */
    public function getTransportError()
    {
        return $this->transport_error;
    }

    /**
     * Accessor for current MIME type.
     *
     * @return string    MIME type as string; e.g. 'text/html'
     */
    public function getMimeType()
    {
        if ($this->headers) {
            return $this->headers->getMimeType();
        }

        return false;
    }

    /**
     * Accessor for HTTP response code.
     *
     * @return int    HTTP response code received.
     */
    public function getResponseCode()
    {
        if ($this->headers) {
            return $this->headers->getResponseCode();
        }

        return false;
    }

    /**
     * Accessor for last Authentication type. Only valid straight after a challenge (401).
     *
     * @return string    Description of challenge type.
     */
    public function getAuthentication()
    {
        if ($this->headers) {
            return $this->headers->getAuthentication();
        }

        return false;
    }

    /**
     * Accessor for last Authentication realm. Only valid straight after a challenge (401).
     *
     * @return string    Name of security realm.
     */
    public function getRealm()
    {
        if ($this->headers) {
            return $this->headers->getRealm();
        }

        return false;
    }

    /**
     * Accessor for current frame focus. Will be false as no frames.
     *
     * @return array    Always empty.
     */
    public function getFrameFocus()
    {
        return array();
    }

    /**
     * Sets the focus by index. The integer index starts from 1.
     *
     * @param int $choice    Chosen frame.
     *
     * @return bool           Always false.
     */
    public function setFrameFocusByIndex($choice)
    {
        // TODO
        return false;
    }

    /**
     * Sets the focus by name. Always fails for a leaf page.
     *
     * @param string $name    Chosen frame.
     *
     * @return bool        False as no frames.
     */
    public function setFrameFocus($name)
    {
        // TODO
        return false;
    }

    /**
     * Clears the frame focus. Does nothing for a leaf page.
     */
    public function clearFrameFocus()
    {
    }

    /**
     * @todo docs
     */
    public function setFrames($frames)
    {
        $this->frames = $frames;
    }

    /**
     * Test to see if link is an absolute one.
     *
     * @param string $url     Url to test.
     *
     * @return bool        True if absolute.
     */
    protected function linkIsAbsolute($url)
    {
        $parsed = new SimpleUrl($url);

        return (boolean) ($parsed->getScheme() && $parsed->getHost());
    }

    /**
     * Adds a link to the page.
     *
     * @param SimpleAnchorTag $tag      Link to accept.
     */
    public function addLink($tag)
    {
        $this->links[] = $tag;
    }

    /**
     * Set the forms
     *
     * @param array $forms           An array of SimpleForm objects
     */
    public function setForms($forms)
    {
        $this->forms = $forms;
    }

    /**
     * Test for the presence of a frameset.
     *
     * @return bool        True if frameset.
     */
    public function hasFrames()
    {
        return count($this->frames) > 0;
    }

    /**
     * Accessor for frame name and source URL for every frame that will need to be loaded.
     * Immediate children only.
     *
     * @return boolean/array     False if no frameset or otherwise a hash of frame URLs.
     *                           The key is either a numerical base one index or the name attribute.
     */
    public function getFrameset()
    {
        if (! $this->hasFrames()) {
            return false;
        }
        $urls = array();
        for ($i = 0; $i < count($this->frames); $i++) {
            $name       = $this->frames[$i]->getAttribute('name');
            $url        = new SimpleUrl($this->frames[$i]->getAttribute('src'));
            $key        = $name ? $name : $i + 1;
            $urls[$key] = $this->expandUrl($url);
        }

        return $urls;
    }

    /**
     * Fetches a list of loaded frames.
     *
     * @return array/string    Just the URL for a single page.
     */
    public function getFrames()
    {
        $url = $this->expandUrl($this->getUrl());

        return $url->asString();
    }

    /**
     * Accessor for a list of all links.
     *
     * @return array   List of urls with scheme of http or https and hostname.
     */
    public function getUrls()
    {
        $all = array();
        foreach ($this->links as $link) {
            $url   = $this->getUrlFromLink($link);
            $all[] = $url->asString();
        }

        return $all;
    }

    /**
     * Accessor for URLs by the link label.
     * Label will match regardess of whitespace issues and case.
     *
     * @param string $label    Text of link.
     *
     * @return array           List of links with that label.
     */
    public function getUrlsByLabel($label)
    {
        $matches = array();
        foreach ($this->links as $link) {
            if ($link->getText() == $label) {
                $matches[] = $this->getUrlFromLink($link);
            }
        }

        return $matches;
    }

    /**
     * Accessor for a URL by the id attribute.
     *
     * @param string $id       Id attribute of link.
     *
     * @return SimpleUrl       URL with that id of false if none.
     */
    public function getUrlById($id)
    {
        foreach ($this->links as $link) {
            if ($link->getAttribute('id') === (string) $id) {
                return $this->getUrlFromLink($link);
            }
        }

        return false;
    }

    /**
     * Converts a link tag into a target URL.
     *
     * @param SimpleAnchorTag $link    Parsed link.
     *
     * @return SimpleUrl            URL with frame target if any.
     */
    protected function getUrlFromLink($link)
    {
        $url = $this->expandUrl($link->getHref());
        if ($link->getAttribute('target')) {
            $url->setTarget($link->getAttribute('target'));
        }

        return $url;
    }

    /**
     * Expands expandomatic URLs into fully qualified URLs.
     *
     * @param SimpleUrl $url        Relative URL.
     *
     * @return SimpleUrl            Absolute URL.
     */
    public function expandUrl($url)
    {
        if (! is_object($url)) {
            $url = new SimpleUrl($url);
        }
        $location = $this->getBaseUrl() ? $this->getBaseUrl() : new SimpleUrl();

        return $url->makeAbsolute($location->makeAbsolute($this->getUrl()));
    }

    /**
     * Sets the base url for the page.
     *
     * @param string $url    Base URL for page.
     */
    public function setBase($url)
    {
        $this->base = new SimpleUrl($url);
    }

    /**
     * Sets the title tag contents.
     *
     * @param SimpleTitleTag $tag    Title of page.
     */
    public function setTitle($tag)
    {
        $this->title = $tag;
    }

    /**
     * Accessor for parsed title.
     *
     * @return string|bool    Title or false if no title is present.
     */
    public function getTitle()
    {
        if ($this->title) {
            return $this->title->getText();
        }

        return false;
    }

    /**
     * Finds a held form by button label. Will only search correctly built forms.
     *
     * @param SimpleSelector $selector       Button finder.
     *
     * @return SimpleForm                    Form object containing the button.
     */
    public function getFormBySubmit($selector)
    {
        for ($i = 0; $i < count($this->forms); $i++) {
            if ($this->forms[$i]->hasSubmit($selector)) {
                return $this->forms[$i];
            }
        }

        return;
    }

    /**
     * Finds a held form by image using a selector. Will only search correctly built forms.
     *
     * @param  SelectorInterface $selector  Image finder.
     *
     * @return SimpleForm               Form object containing the image.
     */
    public function getFormByImage($selector)
    {
        for ($i = 0; $i < count($this->forms); $i++) {
            if ($this->forms[$i]->hasImage($selector)) {
                return $this->forms[$i];
            }
        }

        return;
    }

    /**
     * Finds a held form by the form ID.
     * A way of identifying a specific form when we have control of the HTML code.
     *
     * @param string $id     Form label.
     *
     * @return SimpleForm    Form object containing the matching ID.
     */
    public function getFormById($id)
    {
        for ($i = 0; $i < count($this->forms); $i++) {
            if ($this->forms[$i]->getId() == $id) {
                return $this->forms[$i];
            }
        }

        return;
    }

    /**
     * Sets a field on each form in which the field is available.
     *
     * @param SimpleSelector $selector    Field finder.
     * @param string $value               Value to set field to.
     *
     * @return bool                    True if value is valid.
     */
    public function setField($selector, $value, $position=false)
    {
        $is_set = false;
        for ($i = 0; $i < count($this->forms); $i++) {
            if ($this->forms[$i]->setField($selector, $value, $position)) {
                $is_set = true;
            }
        }

        return $is_set;
    }

    /**
     * Accessor for a form element value within a page.
     *
     * @param SimpleSelector $selector    Field finder.
     *
     * @return string/boolean             A string if the field is present, false if unchecked and
     * null if missing.
     */
    public function getField($selector)
    {
        for ($i = 0; $i < count($this->forms); $i++) {
            $value = $this->forms[$i]->getValue($selector);
            if (isset($value)) {
                return $value;
            }
        }

        return;
    }

    /**
     * Turns HTML into text browser visible text.
     * Images are converted to their alt text and tags are supressed.
     * Entities are converted to their visible representation.
     *
     * @param string $html        HTML to convert.
     *
     * @return string             Plain text.
     */
    public static function normalise($html)
    {
        $rules = array(
            '#<!--.*?-->#si',
            '#<(script|option|textarea)[^>]*>.*?</\1>#si',
            '#<img[^>]*alt\s*=\s*("([^"]*)"|\'([^\']*)\'|([a-zA-Z_]+))[^>]*>#',
            '#<[^>]*>#',
        );

        $replace = array(
            '',
            '',
            ' \2\3\4 ',
            '',
        );

        $text = preg_replace($rules, $replace, $html);
        $text = html_entity_decode($text, ENT_QUOTES);
        $text = preg_replace('#\s+#', ' ', $text);

        return trim(trim($text), "\xA0");        // @todo The \xAO is a  . Add a test for this.
    }
}


<?php

require_once dirname(__FILE__) . '/errors.php';
require_once dirname(__FILE__) . '/compatibility.php';
require_once dirname(__FILE__) . '/scorer.php';
require_once dirname(__FILE__) . '/expectation.php';
require_once dirname(__FILE__) . '/dumper.php';

// define the root constant for dependent libraries.
if (! defined('SIMPLE_TEST')) {
    define('SIMPLE_TEST', dirname(__FILE__) . '/');
}

/**
 * This is called by the class runner to run a single test method.
 * Will also run the setUp() and tearDown() methods.
 */
class SimpleInvoker
{
    private $test_case;

    /**
     * Stashes the test case for later.
     *
     * @param SimpleTestCase $test_case  Test case to run.
     */
    public function __construct($test_case)
    {
        $this->test_case = $test_case;
    }

    /**
     * Accessor for test case being run.
     *
     * @return SimpleTestCase    Test case.
     */
    public function getTestCase()
    {
        return $this->test_case;
    }

    /**
     * Runs test level set up. Used for changing the mechanics of base test cases.
     *
     * @param string $method    Test method to call.
     */
    public function before($method)
    {
        $this->test_case->before($method);
    }

    /**
     * Invokes a test method and buffered with setUp() and tearDown() calls.
     *
     * @param string $method    Test method to call.
     */
    public function invoke($method)
    {
        $this->test_case->setUp();
        $this->test_case->$method();
        $this->test_case->tearDown();
    }

    /**
     * Runs test level clean up. Used for changing the mechanics of base test cases.
     *
     * @param string $method    Test method to call.
     */
    public function after($method)
    {
        $this->test_case->after($method);
    }
}

/**
 * Do nothing decorator. Just passes the invocation straight through.
 */
class SimpleInvokerDecorator
{
    private $invoker;

    /**
     * Stores the invoker to wrap.
     *
     * @param SimpleInvoker $invoker  Test method runner.
     */
    public function __construct($invoker)
    {
        $this->invoker = $invoker;
    }

    /**
     * Accessor for test case being run.
     *
     * @return SimpleTestCase    Test case.
     */
    public function getTestCase()
    {
        return $this->invoker->getTestCase();
    }

    /**
     * Runs test level set up. Used for changing the mechanics of base test cases.
     *
     * @param string $method    Test method to call.
     */
    public function before($method)
    {
        $this->invoker->before($method);
    }

    /**
     * Invokes a test method and buffered with setUp() and tearDown() calls.
     *
     * @param string $method    Test method to call.
     */
    public function invoke($method)
    {
        $this->invoker->invoke($method);
    }

    /**
     * Runs test level clean up. Used for changing the mechanics of base test cases.
     *
     * @param string $method    Test method to call.
     */
    public function after($method)
    {
        $this->invoker->after($method);
    }
}


<?php

namespace UncleCheese\BetterButtons\Controllers;

use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\RequestHandler;
use UncleCheese\BetterButtons\Actions\BetterButtonCustomAction;

/**
 * A subcontroller that handles custom actions. The parent controller matches
 * the url_param '$Action!' and doesn't hand off any trailing params. This subcontoller
 * is aware of them
 *
 * /item/4/customaction/my-dataobject-method Invokes "my-dataobject-method" on the record
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButtonsCustomActionRequest extends RequestHandler
{
    /**
     * @var array
     */
    private static $url_handlers = array(
        '$Action!' => 'handleCustomAction'
    );

    /**
     * @var array
     */
    private static $allowed_actions = array(
        'handleCustomAction'
    );

    /**
     * The parent extension. There are actually some useful methods in the extension
     * itself, so we need access to that object
     *
     * @var GridFieldBetterButtonsItemRequest
     */
    protected $parent;

    /**
     * The parent controller
     * @var GridFieldDetailForm_ItemRequest
     */
    protected $controller;

    /**
     * The record we're editing
     * @var DataObject
     */
    protected $record;

    /**
     * The Form that is editing the record
     * @var  Form
     */
    protected $form;

    /**
     * Buidls the request
     * @param GridFieldBetterButtonsItemRequest $parent     The extension instance
     * @param GridFieldDetailForm_ItemRequest $controller The request that points to the detail form
     */
    public function __construct($parent, $controller, $form)
    {
        $this->parent = $parent;
        $this->controller = $controller;
        $this->form = $form;
        $this->record = $this->controller->record;
        parent::__construct();
    }

    /**
     * Takes the action at /customaction/my-action-name and feeds it to the DataObject.
     * Checks to see if the method is allowed to be invoked first.
     *
     * @param  HTTPRequest $r
     * @return HTTPResponse
     */
    public function handleCustomAction(HTTPRequest $r)
    {
        $action = $r->param('Action');
        if (!$this->record->isCustomActionAllowed($action)) {
            return $this->httpError(403);
        }

        $formAction = $this->record->findActionByName($action);
        if (!$formAction) {
            return $this->httpError(403, "Action $action doesn't exist");
        }

        $message = $this->record->$action($formAction, $this->controller, $r);

        Controller::curr()->getResponse()->addHeader("X-Pjax", "Content");
        Controller::curr()->getResponse()->addHeader('X-Status', $message);

        if ($formAction->getRedirectURL()) {
            return Controller::curr()->redirect($formAction->getRedirectURL());
        }

        if ($formAction->getRedirectType() == BetterButtonCustomAction::GOBACK) {
            return Controller::curr()->redirect(preg_replace('/\?.*/', '', $this->parent->getBackLink()));
        }

        return Controller::curr()->redirect(
            $this->controller->getEditLink($this->record->ID)
        );
    }
}


<?php

namespace UncleCheese\BetterButtons\Actions;

use SilverStripe\Control\Controller;
use UncleCheese\BetterButtons\Actions\BetterButtonAction;

/**
 * Defines the button that provides links to the frontend from within a gridfield detail form.
 * detail form. Only works if your DataObject has a Link() method.
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButtonFrontendLinksAction extends BetterButtonAction
{
    /**
     * Gets the link for the button
     * @return string
     */
    public function getButtonLink()
    {
        return $this->gridFieldRequest->record->hasMethod('Link') ? $this->gridFieldRequest->record->Link() : "";
    }

    /**
     * Determines if the button should display
     * @return bool
     */
    public function shouldDisplay()
    {
        return $this->gridFieldRequest->record && $this->gridFieldRequest->record->hasMethod('Link');
    }

    /**
     * Generates the HTML that represents the button
     * @return string
     */
    public function getButtonHTML()
    {
        $link = $this->getButtonLink();

        $stageLink = Controller::join_links($link, '?stage=Stage');
        $liveLink = Controller::join_links($link, '?stage=Live');

        return '
                    '
                        ._t('GridFieldBetterButtons.VIEWONDRAFTSITE', 'Draft site').
                    ' |
                    '.
                        _t('GridFieldBetterButtons.VIEWONLIVESITE', 'Live site').
                    '';
    }
}