Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Neuron/Net/Outputs/HTML.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ public function outputContent (Response $response)
{
if (!is_string ($response->getData ()))
{
print_r ($response->getData ());
// A data structure shown in an HTML response is a dump of
// values, not markup.
echo htmlspecialchars (print_r ($response->getData (), true), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

else
Expand Down
13 changes: 11 additions & 2 deletions src/Neuron/Net/Outputs/PrintR.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,25 @@ public function __construct ()

}

/**
* @param mixed $text
* @return string
*/
private static function escape ($text)
{
return htmlspecialchars ((string) $text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

public function outputContent (Response $response)
{
if (!is_string ($response->getData ()))
{
echo '<pre>' . print_r ($response->getData ()) . '<pre>';
echo '<pre>' . self::escape (print_r ($response->getData (), true)) . '</pre>';
}

else
{
echo $response->getData ();
echo '<pre>' . self::escape ($response->getData ()) . '</pre>';
}
}

Expand Down
23 changes: 19 additions & 4 deletions src/Neuron/Net/Outputs/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ public function __construct ()

private function printTable ($data, $var_dump = true)
{
header ('Content-type: text/html, charset=utf-8');
if (!headers_sent ()) {
header ('Content-type: text/html; charset=utf-8');
}

echo '<html>';
echo '<head>';
Expand Down Expand Up @@ -51,7 +53,7 @@ private function printTableInner ($data, $var_dump = true)
{
echo '<tr>';

echo '<th>' . $k . '</th>';
echo '<th>' . self::escape ($k) . '</th>';
echo '<td>';

if ($k === 'debug')
Expand All @@ -71,17 +73,30 @@ private function printTableInner ($data, $var_dump = true)
}
else
{
// The table is a view on data: every key and value is text and
// is escaped, whatever it contains.
if ($var_dump)
{
ob_start ();
var_dump ($data);
echo self::escape (ob_get_clean ());
}
else
{
echo $data;
echo self::escape (is_scalar ($data) || $data === null ? (string) $data : print_r ($data, true));
}
}
}

/**
* @param mixed $text
* @return string
*/
private static function escape ($text)
{
return htmlspecialchars ((string) $text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

public function outputContent (Response $response)
{
if (!is_string ($response->getData ()))
Expand All @@ -91,7 +106,7 @@ public function outputContent (Response $response)

else
{
echo $response->getData ();
echo self::escape ($response->getData ());
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/Neuron/Tools/TokenGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static function getToken ($length)
$out = '';
for ($i = 0; $i < $length; $i ++)
{
$out .= substr ($range, mt_rand (0, strlen ($range) - 1), 1);
$out .= substr ($range, random_int (0, strlen ($range) - 1), 1);
}
return $out;
}
Expand All @@ -30,7 +30,7 @@ public static function getSimplifiedToken ($length)
$out = '';
for ($i = 0; $i < $length; $i ++)
{
$out .= substr ($range, mt_rand (0, strlen ($range) - 1), 1);
$out .= substr ($range, random_int (0, strlen ($range) - 1), 1);
}
return $out;
}
Expand Down
116 changes: 116 additions & 0 deletions tests/OutputEscapingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace Neuron\Tests;

use PHPUnit\Framework\TestCase;
use Neuron\Net\Outputs\HTML;
use Neuron\Net\Outputs\PrintR;
use Neuron\Net\Outputs\Table;
use Neuron\Net\Response;
use Neuron\Tools\TokenGenerator;

/**
* The HTML data outputs show values; a value never becomes markup.
*/
class OutputEscapingTest extends TestCase
{
private function render ($output, $data)
{
$response = new Response ();
$response->setData ($data);

ob_start ();
try {
$output->outputContent ($response);
} finally {
$out = ob_get_clean ();
}

return $out;
}

public function testTableEscapesKeysAndValues ()
{
$out = $this->render (new Table (), array ('<i>key</i>' => '<b>value</b>'));

$this->assertStringNotContainsString ('<i>key</i>', $out);
$this->assertStringNotContainsString ('<b>value</b>', $out);
$this->assertStringContainsString ('&lt;i&gt;key&lt;/i&gt;', $out);
$this->assertStringContainsString ('&lt;b&gt;value&lt;/b&gt;', $out);
}

public function testTableEscapesNestedAndDebugValues ()
{
$out = $this->render (new Table (), array (
'row' => array ('name' => "it's <b>x</b>"),
'debug' => array ('log' => '<b>line</b>'),
));

$this->assertStringNotContainsString ('<b>x</b>', $out);
$this->assertStringNotContainsString ('<b>line</b>', $out);
$this->assertStringContainsString ('it&#039;s &lt;b&gt;x&lt;/b&gt;', $out);
$this->assertStringContainsString ('&lt;b&gt;line&lt;/b&gt;', $out);
}

public function testTableKeepsItsOwnMarkup ()
{
$out = $this->render (new Table (), array ('a' => 1));

$this->assertStringContainsString ('<table class="api-data-table">', $out);
$this->assertStringContainsString ('<th>a</th>', $out);
}

public function testTableEscapesStringData ()
{
$out = $this->render (new Table (), '<b>text</b>');

$this->assertSame ('&lt;b&gt;text&lt;/b&gt;', $out);
}

public function testPrintREscapesAndPrintsTheData ()
{
$out = $this->render (new PrintR (), array ('name' => '<b>value</b>'));

$this->assertStringStartsWith ('<pre>', $out);
$this->assertStringEndsWith ('</pre>', $out);
$this->assertStringContainsString ('[name] =&gt; &lt;b&gt;value&lt;/b&gt;', $out);
$this->assertStringNotContainsString ('<b>value</b>', $out);
}

public function testHtmlOutputEscapesADataStructure ()
{
$out = $this->render (new HTML (), array ('message' => '<b>value</b>'));

$this->assertStringNotContainsString ('<b>value</b>', $out);
$this->assertStringContainsString ('&lt;b&gt;value&lt;/b&gt;', $out);
}

public function testHtmlOutputLeavesABodyAlone ()
{
$response = new Response ();
$response->setBody ('<p>page</p>');

ob_start ();
(new HTML ())->outputContent ($response);
$out = ob_get_clean ();

$this->assertSame ('<p>page</p>', $out);
}

public function testTokensKeepTheirAlphabetAndLength ()
{
for ($i = 0; $i < 50; $i ++) {
$this->assertMatchesRegularExpression ('/\A[a-zA-Z0-9]{40}\z/', TokenGenerator::getToken (40));
$this->assertMatchesRegularExpression ('/\A[A-Z0-9]{12}\z/', TokenGenerator::getSimplifiedToken (12));
}
}

public function testTokenGeneratorUsesTheSecureRandomSource ()
{
$reflection = new \ReflectionClass (TokenGenerator::class);
$source = file_get_contents ($reflection->getFileName ());

$this->assertStringContainsString ('random_int', $source);
$this->assertStringNotContainsString ('mt_rand', $source);
}
}
Loading