diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..eab48cc
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending for every file
+# Indent with 4 spaces
+[php]
+end_of_line = lf
+indent_style = space
+indent_size = 4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e35df3f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+phpunit.xml
+composer.phar
+composer.lock
+vendor/
+build/
+.idea
+.DS_STORE
diff --git a/composer.json b/composer.json
index 924ee5c..286d286 100644
--- a/composer.json
+++ b/composer.json
@@ -2,7 +2,7 @@
"name": "imsglobal/lti",
"version" : "3.0.2",
"description": "LTI Tool Provider Library",
- "keywords": ["lti"],
+ "keywords": ["lti", "ims", "content-item", "edtech", "education", "lms"],
"homepage": "https://www.imsglobal.org/lti",
"type": "library",
"license": "Apache-2.0",
@@ -19,5 +19,19 @@
"psr-4": {
"IMSGlobal\\LTI\\": "src/"
}
+ },
+ "require-dev": {
+ "ext-curl": "*",
+ "phpunit/phpunit": "^5.6"
+ },
+ "autoload-dev":{
+ "psr-4": {
+ "IMSGlobal\\LTI\\Test\\": "tests/"
+ }
+ },
+ "scripts": {
+ "test": "phpunit",
+ "coverage": "phpunit --coverage-html=build/coverage",
+ "coverage-text": "phpunit --coverage-text"
}
}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..0015ede
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,21 @@
+
+
+
+
+
+ tests/
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
diff --git a/src/HTTP/Client.php b/src/HTTP/Client.php
new file mode 100644
index 0000000..b8af6d9
--- /dev/null
+++ b/src/HTTP/Client.php
@@ -0,0 +1,27 @@
+
+ * @copyright IMS Global Learning Consortium Inc
+ * @date 2016
+ * @version 3.0.0
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
+ */
+interface Client
+{
+
+ /**
+ * Send the provided HTTPMessage and then updates it with the response data.
+ *
+ * @param HTTPMessage $message The HTTP message to send
+ * @return bool If successful, returns true
+ */
+ public function send(HTTPMessage $message);
+
+}
diff --git a/src/HTTP/CurlClient.php b/src/HTTP/CurlClient.php
new file mode 100644
index 0000000..38ed0dc
--- /dev/null
+++ b/src/HTTP/CurlClient.php
@@ -0,0 +1,70 @@
+
+ * @copyright IMS Global Learning Consortium Inc
+ * @date 2016
+ * @version 3.0.0
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
+ */
+class CurlClient implements Client
+{
+
+ /**
+ * @inheritdoc
+ */
+ public function send(HTTPMessage $message)
+ {
+ $message->ok = false;
+
+ $resp = '';
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $message->url);
+ if (!empty($message->requestHeaders)) {
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $message->requestHeaders);
+ } else {
+ curl_setopt($ch, CURLOPT_HEADER, 0);
+ }
+ if ($message->method === 'POST') {
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $message->request);
+ } else if ($message->method !== 'GET') {
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $message->method);
+ if (!is_null($message->request)) {
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $message->request);
+ }
+ }
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLINFO_HEADER_OUT, true);
+ curl_setopt($ch, CURLOPT_HEADER, true);
+ curl_setopt($ch, CURLOPT_SSLVERSION, 3);
+ $chResp = curl_exec($ch);
+ $message->ok = $chResp !== false;
+ if ($message->ok) {
+ $chResp = str_replace("\r\n", "\n", $chResp);
+ $chRespSplit = explode("\n\n", $chResp, 2);
+ if ((count($chRespSplit) > 1) && (substr($chRespSplit[1], 0, 5) === 'HTTP/')) {
+ $chRespSplit = explode("\n\n", $chRespSplit[1], 2);
+ }
+ $message->responseHeaders = $chRespSplit[0];
+ $resp = $chRespSplit[1];
+ $message->status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $message->ok = $message->status < 400;
+ if (!$message->ok) {
+ $message->error = curl_error($ch);
+ }
+ }
+ $message->requestHeaders = str_replace("\r\n", "\n", curl_getinfo($ch, CURLINFO_HEADER_OUT));
+ curl_close($ch);
+ $message->response = $resp;
+
+ return $message->ok;
+ }
+
+}
diff --git a/src/HTTP/StreamClient.php b/src/HTTP/StreamClient.php
new file mode 100644
index 0000000..7bd6f8e
--- /dev/null
+++ b/src/HTTP/StreamClient.php
@@ -0,0 +1,55 @@
+
+ * @copyright IMS Global Learning Consortium Inc
+ * @date 2016
+ * @version 3.0.0
+ * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
+ */
+class StreamClient implements Client
+{
+
+ /**
+ * @inheritdoc
+ */
+ public function send(HTTPMessage $message)
+ {
+ $message->ok = false;
+
+ // Prepare options for the HTTP context.
+ $opts = array(
+ 'method' => $message->method,
+ 'content' => $message->request
+ );
+ if (!empty($message->requestHeaders)) {
+ $opts['header'] = $message->requestHeaders;
+ }
+
+ // Send the request.
+ $http_response_header = null;
+ $context = stream_context_create(['http' => $opts]);
+ $stream = @fopen($message->url, 'rb', false, $context);
+ if ($stream) {
+ $message->response = @stream_get_contents($stream);
+ fclose($stream);
+ }
+
+ // Read the headers to get the status.
+ if ($http_response_header) {
+ $message->responseHeaders = implode("\n", $http_response_header);
+ $parts = explode(' ', $message->responseHeaders, 3);
+ $message->status = $parts[1];
+ $message->ok = $message->status < 400;
+ }
+
+ return $message->ok;
+ }
+
+}
diff --git a/src/HTTPMessage.php b/src/HTTPMessage.php
index 3c043bb..a2ae09b 100644
--- a/src/HTTPMessage.php
+++ b/src/HTTPMessage.php
@@ -2,8 +2,12 @@
namespace IMSGlobal\LTI;
+use IMSGlobal\LTI\HTTP\Client as HTTPClient;
+use IMSGlobal\LTI\HTTP\CurlClient;
+use IMSGlobal\LTI\HTTP\StreamClient;
+
/**
- * Class to represent an HTTP message
+ * Class to represent an HTTP message.
*
* @author Stephen P Vickers
* @copyright IMS Global Learning Consortium Inc
@@ -13,167 +17,114 @@
*/
class HTTPMessage
{
-
-/**
- * True if message was sent successfully.
- *
- * @var boolean $ok
- */
+ /**
+ * @var HTTPClient The client used to send the request.
+ */
+ private static $httpClient;
+
+ /**
+ * @var bool True if message was sent successfully.
+ */
public $ok = false;
-/**
- * Request body.
- *
- * @var request $request
- */
+ /**
+ * @var string|null Request body.
+ */
public $request = null;
-/**
- * Request headers.
- *
- * @var request_headers $requestHeaders
- */
- public $requestHeaders = '';
+ /**
+ * @var array Request headers.
+ */
+ public $requestHeaders = [];
-/**
- * Response body.
- *
- * @var response $response
- */
+ /**
+ * @var string|null Response body.
+ */
public $response = null;
-/**
- * Response headers.
- *
- * @var response_headers $responseHeaders
- */
+ /**
+ * @var string Response headers.
+ */
public $responseHeaders = '';
-/**
- * Status of response (0 if undetermined).
- *
- * @var status $status
- */
+ /**
+ * @var int Status of response (0 if undetermined).
+ */
public $status = 0;
-/**
- * Error message
- *
- * @var error $error
- */
+ /**
+ * @var string Error message
+ */
public $error = '';
-/**
- * Request URL.
- *
- * @var url $url
- */
- private $url = null;
+ /**
+ * @var string Request URL.
+ */
+ public $url = null;
+
+ /**
+ * @var string Request method.
+ */
+ public $method = null;
+
+ /**
+ * Allows you to set a custom HTTP client.
+ *
+ * @param HTTPClient|null $httpClient The HTTP client to use for sending message.
+ */
+ public static function setHttpClient(HTTPClient $httpClient = null)
+ {
+ self::$httpClient = $httpClient;
+ }
-/**
- * Request method.
- *
- * @var method $method
- */
- private $method = null;
+ /**
+ * Retrieves the HTTP client used for sending the message. Creates a default client if one is not set.
+ *
+ * @return HTTPClient
+ */
+ public static function getHttpClient()
+ {
+ if (!self::$httpClient) {
+ // @codeCoverageIgnoreStart
+ if (function_exists('curl_init')) {
+ self::$httpClient = new CurlClient();
+ } elseif (ini_get('allow_url_fopen')) {
+ self::$httpClient = new StreamClient();
+ } else {
+ throw new \RuntimeException('Cannot create an HTTP client, because neither cURL or allow_url_fopen are enabled.');
+ }
+ // @codeCoverageIgnoreEnd
+ }
+
+ return self::$httpClient;
+ }
-/**
- * Class constructor.
- *
- * @param string $url URL to send request to
- * @param string $method Request method to use (optional, default is GET)
- * @param mixed $params Associative array of parameter values to be passed or message body (optional, default is none)
- * @param string $header Values to include in the request header (optional, default is none)
- */
+ /**
+ * Class constructor.
+ *
+ * @param string $url URL to send request to
+ * @param string $method Request method to use (optional, default is GET)
+ * @param array|string $params Associative array of parameter values to be passed or message body (optional, default is none)
+ * @param array|string $header Values to include in the request header (optional, default is none)
+ */
function __construct($url, $method = 'GET', $params = null, $header = null)
{
-
$this->url = $url;
$this->method = strtoupper($method);
- if (is_array($params)) {
- $this->request = http_build_query($params);
- } else {
- $this->request = $params;
- }
- if (!empty($header)) {
+ $this->request = is_array($params) ? http_build_query($params) : $params;
+ if ($header && !is_array($header)) {
$this->requestHeaders = explode("\n", $header);
}
-
}
-/**
- * Send the request to the target URL.
- *
- * @return boolean True if the request was successful
- */
+ /**
+ * Send the request to the target URL.
+ *
+ * @return boolean True if the request was successful
+ */
public function send()
{
-
- $this->ok = false;
-// Try using curl if available
- if (function_exists('curl_init')) {
- $resp = '';
- $ch = curl_init();
- curl_setopt($ch, CURLOPT_URL, $this->url);
- if (!empty($this->requestHeaders)) {
- curl_setopt($ch, CURLOPT_HTTPHEADER, $this->requestHeaders);
- } else {
- curl_setopt($ch, CURLOPT_HEADER, 0);
- }
- if ($this->method === 'POST') {
- curl_setopt($ch, CURLOPT_POST, true);
- curl_setopt($ch, CURLOPT_POSTFIELDS, $this->request);
- } else if ($this->method !== 'GET') {
- curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
- if (!is_null($this->request)) {
- curl_setopt($ch, CURLOPT_POSTFIELDS, $this->request);
- }
- }
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($ch, CURLINFO_HEADER_OUT, true);
- curl_setopt($ch, CURLOPT_HEADER, true);
- curl_setopt($ch, CURLOPT_SSLVERSION,3);
- $chResp = curl_exec($ch);
- $this->ok = $chResp !== false;
- if ($this->ok) {
- $chResp = str_replace("\r\n", "\n", $chResp);
- $chRespSplit = explode("\n\n", $chResp, 2);
- if ((count($chRespSplit) > 1) && (substr($chRespSplit[1], 0, 5) === 'HTTP/')) {
- $chRespSplit = explode("\n\n", $chRespSplit[1], 2);
- }
- $this->responseHeaders = $chRespSplit[0];
- $resp = $chRespSplit[1];
- $this->status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- $this->ok = $this->status < 400;
- if (!$this->ok) {
- $this->error = curl_error($ch);
- }
- }
- $this->requestHeaders = str_replace("\r\n", "\n", curl_getinfo($ch, CURLINFO_HEADER_OUT));
- curl_close($ch);
- $this->response = $resp;
- } else {
-// Try using fopen if curl was not available
- $opts = array('method' => $this->method,
- 'content' => $this->request
- );
- if (!empty($this->requestHeaders)) {
- $opts['header'] = $this->requestHeaders;
- }
- try {
- $ctx = stream_context_create(array('http' => $opts));
- $fp = @fopen($this->url, 'rb', false, $ctx);
- if ($fp) {
- $resp = @stream_get_contents($fp);
- $this->ok = $resp !== false;
- }
- } catch (\Exception $e) {
- $this->ok = false;
- }
- }
-
- return $this->ok;
-
+ return self::getHttpClient()->send($this);
}
}
diff --git a/tests/HTTP/ClientTest.php b/tests/HTTP/ClientTest.php
new file mode 100644
index 0000000..d9d644b
--- /dev/null
+++ b/tests/HTTP/ClientTest.php
@@ -0,0 +1,108 @@
+server = new TestServer();
+ $this->server->start();
+ if (!$this->server->isRunning()) {
+ $this->markTestSkipped('Test server is not running.');
+ }
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+ $this->server->stop();
+ }
+
+ public function provideClient()
+ {
+ return [[new CurlClient], [new StreamClient]];
+ }
+
+ /**
+ * @param HttpClient $client
+ * @dataProvider provideClient
+ */
+ public function testCanSendPostMessage(HttpClient $client)
+ {
+ $url = $this->server->getUrl(200, TestServer::RETURN_INPUT);
+ $message = new HTTPMessage($url, 'POST', 'hello');
+ $client->send($message);
+
+ $this->assertTrue($message->ok);
+ $this->assertEquals('hello', $message->response);
+ }
+
+ /**
+ * @param HttpClient $client
+ * @dataProvider provideClient
+ */
+ public function testCanSendUncommonMethods(HttpClient $client)
+ {
+ $url = $this->server->getUrl(200, TestServer::RETURN_INPUT);
+ $message = new HTTPMessage($url, 'PATCH', 'hello');
+ $client->send($message);
+
+ $this->assertTrue($message->ok);
+ $this->assertEquals('hello', $message->response);
+ }
+
+ /**
+ * @param HttpClient $client
+ * @dataProvider provideClient
+ */
+ public function testCanSendMessageWithHeaders(HttpClient $client)
+ {
+ $url = $this->server->getUrl(200, TestServer::RETURN_HEADER, 'Test');
+ $message = new HTTPMessage($url, 'POST', null, 'Test: foo');
+ $client->send($message);
+
+ $this->assertTrue($message->ok);
+ $this->assertEquals('foo', $message->response);
+ }
+
+ /**
+ * @param HttpClient $client
+ * @dataProvider provideClient
+ */
+ public function testCanHandleError(HttpClient $client)
+ {
+ $url = $this->server->getUrl(404);
+ $message = new HTTPMessage($url, 'GET');
+ $client->send($message);
+
+ $this->assertFalse($message->ok);
+ $this->assertEquals(404, $message->status);
+ }
+
+ public function testCanHandleResponsesWithRepeatedHeaderBlock()
+ {
+ $url = $this->server->getUrl(200, TestServer::RETURN_VALUE, "HTTP/1.1 200 OK\n\nsuccess");
+ $message = new HTTPMessage($url, 'GET');
+ $client = new CurlClient();
+ $client->send($message);
+
+ $this->assertTrue($message->ok);
+ $this->assertEquals('success', $message->response);
+ }
+}
diff --git a/tests/HTTP/TestServer.php b/tests/HTTP/TestServer.php
new file mode 100644
index 0000000..39689a6
--- /dev/null
+++ b/tests/HTTP/TestServer.php
@@ -0,0 +1,77 @@
+getPort();
+ $this->testServerPid = @exec("php -S localhost:{$port} -t {$dir} {$dir}/server.php &> /dev/null & echo $!");
+
+ // Wait a little bit for the server to come online.
+ usleep(100000);
+ }
+
+ /**
+ * Determines if the server is running.
+ *
+ * @return bool
+ */
+ public function isRunning()
+ {
+ $ping = @file_get_contents($url = $this->getUrl(200, self::RETURN_VALUE, 'ping'));
+ return $ping === 'ping';
+ }
+
+ /**
+ * Gets the URL to the test server.
+ *
+ * @return string Test server's URL
+ */
+ public function getUrl($status = 200, $type = self::RETURN_NOTHING, $value = null)
+ {
+ $port = $this->getPort();
+ $query = http_build_query(compact('status', 'type', 'value'));
+
+ return "http://localhost:{$port}/?{$query}";
+ }
+
+ /**
+ * Stop the test server, if it's running, and the PID is known.
+ */
+ public function stop()
+ {
+ if (is_numeric($this->testServerPid)) {
+ @exec("kill {$this->testServerPid}");
+ }
+ }
+
+ /**
+ * Get the test server port, as provided in phpunit.xml(.dist).
+ *
+ * @return int
+ */
+ private function getPort()
+ {
+ if (isset($_SERVER['TEST_SERVER_PORT']) && is_numeric($_SERVER['TEST_SERVER_PORT'])) {
+ return (int) $_SERVER['TEST_SERVER_PORT'];
+ } else {
+ throw new \RuntimeException('TEST_SERVER_PORT is not defined as a $_SERVER variable in your phpunit.xml');
+ }
+ }
+}
diff --git a/tests/HTTP/server.php b/tests/HTTP/server.php
new file mode 100644
index 0000000..a2c0313
--- /dev/null
+++ b/tests/HTTP/server.php
@@ -0,0 +1,18 @@
+createMock(Client::class);
+ HTTPMessage::setHttpClient($client);
+
+ $this->assertInstanceOf(Client::class, HTTPMessage::getHttpClient());
+ }
+
+ public function testUsesCurlClientIfCurlIsAvailable()
+ {
+ $client = HTTPMessage::getHttpClient();
+
+ $this->assertInstanceOf(CurlClient::class, $client);
+ }
+
+ public function testCanCreateAFormattedHttpMessage()
+ {
+ $message = new HTTPMessage(
+ 'http://example.com',
+ 'post',
+ ['a' => 1, 'b' => 2],
+ "Foo: abc\nBar: xyz"
+ );
+
+ $this->assertEquals('http://example.com', $message->url);
+ $this->assertEquals('POST', $message->method);
+ $this->assertEquals('a=1&b=2', $message->request);
+ $this->assertInternalType('array', $message->requestHeaders);
+ $this->assertCount(2, $message->requestHeaders);
+ }
+
+ public function testCanSendAnHttpMessage()
+ {
+ // Create a message to send
+ $message = new HTTPMessage('http://example.com', 'POST');
+
+ // Create a mock client and configure to be used for sending HTTP messages
+ $client = $this->createMock(Client::class);
+ $client->expects($this->once())
+ ->method('send')
+ ->with($message)
+ ->willReturnCallback(function (HTTPMessage $message) {
+ return $message->ok = true;
+ });
+ HTTPMessage::setHttpClient($client);
+
+ // Send the message
+ $result = $message->send();
+
+ // Verify success
+ $this->assertTrue($result);
+ $this->assertTrue($message->ok);
+ }
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ // Reset the HTTP client
+ HTTPMessage::setHttpClient(null);
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+
+ // Reset the HTTP client
+ HTTPMessage::setHttpClient(null);
+ }
+}