diff --git a/README.md b/README.md index e68267c..2ceab76 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ of [ReactPHP](https://reactphp.org/)'s event-driven architecture. * [EventSource::$url](#eventsourceurl) * [close()](#close) * [MessageEvent](#messageevent) + * [MessageEvent::__construct()](#messageevent__construct) * [MessageEvent::$data](#messageeventdata) * [MessageEvent::$lastEventId](#messageeventlasteventid) * [MessageEvent::$type](#messageeventtype) @@ -240,6 +241,19 @@ This will close any active connections or connection attempts and go into the The `MessageEvent` class represents an incoming EventSource message. +#### MessageEvent::__construct() + +The `new MessageEvent(string $data, string $lastEventId = '', string $type = 'message')` constructor can be used to +create a new `MessageEvent` instance. + +This is mostly used internally to represent each incoming message event +(see also [`message` event](#message-event)). Likewise, you can also use +this class in test cases to test how your application reacts to incoming +messages. + +The constructor validates and initializes all properties of this class. +It throws an `InvalidArgumentException` if any parameters are invalid. + #### MessageEvent::$data The `readonly string $data` property can be used to diff --git a/src/MessageEvent.php b/src/MessageEvent.php index 7bef4ca..08c87da 100644 --- a/src/MessageEvent.php +++ b/src/MessageEvent.php @@ -43,6 +43,7 @@ public static function parse($data, $lastEventId, &$retryTime = 0.0) $data = substr($data, 0, -1); } + /** @throws void because parameter values are validated above already */ return new self($data, $id, $type); } @@ -52,15 +53,41 @@ private static function utf8($string) return \htmlspecialchars_decode(\htmlspecialchars($string, \ENT_NOQUOTES | \ENT_SUBSTITUTE, 'utf-8')); } + /** @return bool */ + private static function isUtf8($string) + { + return $string === self::utf8($string); + } + /** - * @internal - * @param string $data - * @param string $lastEventId - * @param string $type + * Create a new `MessageEvent` instance. + * + * This is mostly used internally to represent each incoming message event + * (see also [`message` event](#message-event)). Likewise, you can also use + * this class in test cases to test how your application reacts to incoming + * messages. + * + * The constructor validates and initializes all properties of this class. + * It throws an `InvalidArgumentException` if any parameters are invalid. + * + * @param string $data message data (requires valid UTF-8 data, possibly multi-line) + * @param string $lastEventId optional last event ID (defaults to empty string, requires valid UTF-8, no null bytes, single line) + * @param string $type optional event type (defaults to "message", requires valid UTF-8, single line) + * @throws \InvalidArgumentException if any parameters are invalid */ - private function __construct($data, $lastEventId, $type) + final public function __construct($data, $lastEventId = '', $type = 'message') { - $this->data = $data; + if (!self::isUtf8($data)) { + throw new \InvalidArgumentException('Invalid $data given, must be valid UTF-8 string'); + } + if (!self::isUtf8($lastEventId) || \strpos($lastEventId, "\0") !== false || \strpos($lastEventId, "\r") !== false || \strpos($lastEventId, "\n") !== false) { + throw new \InvalidArgumentException('Invalid $lastEventId given, must be valid UTF-8 string with no null bytes or newline characters'); + } + if (!self::isUtf8($type) || $type === '' || \strpos($type, "\r") !== false || \strpos($type, "\n")) { + throw new \InvalidArgumentException('Invalid $type given, must be valid UTF-8 string with no newline characters'); + } + + $this->data = \preg_replace("/\r\n?/", "\n", $data); $this->lastEventId = $lastEventId; $this->type = $type; } @@ -72,13 +99,13 @@ private function __construct($data, $lastEventId, $type) public $data = ''; /** - * @var string + * @var string defaults to empty string * @readonly */ public $lastEventId = ''; /** - * @var string + * @var string defaults to "message" * @readonly */ public $type = 'message'; diff --git a/tests/MessageEventTest.php b/tests/MessageEventTest.php index 39f84c9..3f20bff 100644 --- a/tests/MessageEventTest.php +++ b/tests/MessageEventTest.php @@ -175,4 +175,109 @@ public function testParseRetryTime($input, $expected) $this->assertSame($expected, $retryTime); } + + public function testConstructWithDefaultLastEventIdAndType() + { + $message = new MessageEvent('hello'); + + $this->assertEquals('hello', $message->data); + $this->assertEquals('', $message->lastEventId); + $this->assertEquals('message', $message->type); + } + + public function testConstructWithEmptyDataAndId() + { + $message = new MessageEvent('', ''); + + $this->assertEquals('', $message->data); + $this->assertEquals('', $message->lastEventId); + $this->assertEquals('message', $message->type); + } + + public function testConstructWithNullBytesInDataAndType() + { + $message = new MessageEvent("h\x00llo!", '', "h\x00llo!"); + + $this->assertEquals("h\x00llo!", $message->data); + $this->assertEquals('', $message->lastEventId); + $this->assertEquals("h\x00llo!", $message->type); + } + + public function testConstructWithCarriageReturnAndLineFeedsInDataReplacedWithSimpleLineFeeds() + { + $message = new MessageEvent("hello\rworld!\r\n"); + + $this->assertEquals("hello\nworld!\n", $message->data); + } + + public function testConstructWithInvalidDataUtf8Throws() + { + $this->setExpectedException('InvalidArgumentException', 'Invalid $data given, must be valid UTF-8 string'); + new MessageEvent("h\xFFllo!"); + } + + public function testConstructWithInvalidLastEventIdUtf8Throws() + { + $this->setExpectedException('InvalidArgumentException', 'Invalid $lastEventId given, must be valid UTF-8 string with no null bytes or newline characters'); + new MessageEvent('hello', "h\xFFllo"); + } + + public function testConstructWithInvalidLastEventIdNullThrows() + { + $this->setExpectedException('InvalidArgumentException', 'Invalid $lastEventId given, must be valid UTF-8 string with no null bytes or newline characters'); + new MessageEvent('hello', "h\x00llo"); + } + + public function testConstructWithInvalidLastEventIdCarriageReturnThrows() + { + $this->setExpectedException('InvalidArgumentException', 'Invalid $lastEventId given, must be valid UTF-8 string with no null bytes or newline characters'); + new MessageEvent('hello', "hello\r"); + } + + public function testConstructWithInvalidLastEventIdLineFeedThrows() + { + $this->setExpectedException('InvalidArgumentException', 'Invalid $lastEventId given, must be valid UTF-8 string with no null bytes or newline characters'); + new MessageEvent('hello', "hello\n"); + } + + public function testConstructWithInvalidTypeUtf8Throws() + { + $this->setExpectedException('InvalidArgumentException', 'Invalid $type given, must be valid UTF-8 string with no newline characters'); + new MessageEvent('hello', '', "h\xFFllo"); + } + + public function testConstructWithInvalidTypeEmptyThrows() + { + $this->setExpectedException('InvalidArgumentException', 'Invalid $type given, must be valid UTF-8 string with no newline characters'); + new MessageEvent('hello', '', ''); + } + + public function testConstructWithInvalidTypeCarriageReturnThrows() + { + $this->setExpectedException('InvalidArgumentException', 'Invalid $type given, must be valid UTF-8 string with no newline characters'); + new MessageEvent('hello', '', "hello\r"); + } + + public function testConstructWithInvalidTypeLineFeedThrows() + { + $this->setExpectedException('InvalidArgumentException', 'Invalid $type given, must be valid UTF-8 string with no newline characters'); + new MessageEvent('hello', '', "hello\r"); + } + + public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) + { + if (method_exists($this, 'expectException')) { + // PHPUnit 5.2+ + $this->expectException($exception); + if ($exceptionMessage !== '') { + $this->expectExceptionMessage($exceptionMessage); + } + if ($exceptionCode !== null) { + $this->expectExceptionCode($exceptionCode); + } + } else { + // legacy PHPUnit < 5.2 + parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); + } + } }