diff --git a/.gitignore b/.gitignore index c099e65..8d5f59b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,8 @@ # Generated documentation docs/ +# Generated code coverage report +coverage/ + # Local settings *.local.php diff --git a/Splunk/AmbiguousKeyException.php b/Splunk/AmbiguousKeyException.php new file mode 100644 index 0000000..e22f04f --- /dev/null +++ b/Splunk/AmbiguousKeyException.php @@ -0,0 +1,21 @@ +children(Splunk_AtomFeed::NS_S)->dict; + $listValue = $containerXml->children(Splunk_AtomFeed::NS_S)->list; + + if (Splunk_XmlUtil::elementExists($dictValue)) + { + return Splunk_AtomFeed::parseDict($dictValue); + } + else if (Splunk_XmlUtil::elementExists($listValue)) + { + return Splunk_AtomFeed::parseList($listValue); + } + else // value is scalar + { + return Splunk_XmlUtil::getTextContent($containerXml); + } + } + + private static function parseDict($dictXml) + { + $dict = array(); + foreach ($dictXml->children(Splunk_AtomFeed::NS_S)->key as $keyXml) + { + $key = Splunk_XmlUtil::getAttributeValue($keyXml, 'name'); + $value = Splunk_AtomFeed::parseValueInside($keyXml); + + $dict[$key] = $value; + } + return $dict; + } + + private static function parseList($listXml) + { + $list = array(); + foreach ($listXml->children(Splunk_AtomFeed::NS_S)->item as $itemXml) + $list[] = Splunk_AtomFeed::parseValueInside($itemXml); + return $list; + } +} \ No newline at end of file diff --git a/Splunk/Collection.php b/Splunk/Collection.php new file mode 100644 index 0000000..9156ca2 --- /dev/null +++ b/Splunk/Collection.php @@ -0,0 +1,89 @@ +service->get($this->path); + $xml = new SimpleXMLElement($response->body); + + $entries = array(); + foreach ($xml->entry as $entryData) + { + $entries[] = $this->loadEntry($entryData); + } + + $this->entries = $entries; + $this->loaded = TRUE; + } + + private function loadEntry($entryData) + { + return new Splunk_Entity( + $this->service, + "{$this->path}/" . urlencode($entryData->title), + $entryData); + } + + // === Children === + + /** + * Returns the unique entity with the specified name in this collection. + * + * @param string $name + * @return Splunk_Entity + * @throws Splunk_NoSuchKeyException + * @throws Splunk_AmbiguousKeyException + */ + public function get($name) + { + $results = array(); + foreach ($this->validate()->entries as $entry) + { + if ($entry->getName() == $name) + { + $results[] = $entry; + } + } + + if (count($results) == 0) + { + throw new Splunk_NoSuchKeyException( + "No value exists with key '{$name}'."); + } + else if (count($results) == 1) + { + return $results[0]; + } + else + { + throw new Splunk_AmbiguousKeyException( + "Multiple values exist with key '{$name}'. " . + "Specify a namespace to disambiguate."); + } + } +} diff --git a/Splunk/Context.php b/Splunk/Context.php index c554093..4b95768 100644 --- a/Splunk/Context.php +++ b/Splunk/Context.php @@ -16,7 +16,7 @@ */ /** - * This class allows clients to issue HTTP requests to a Splunk server. + * Allows clients to issue HTTP requests to a Splunk server. * * @package Splunk */ @@ -24,17 +24,19 @@ class Splunk_Context { private $username; private $password; + private $token; private $host; private $port; private $scheme; private $http; - private $token; - /** * @param array $args { * 'username' => (optional) The username to login with. Defaults to "admin". * 'password' => (optional) The password to login with. Defaults to "changeme". + * 'token' => (optional) The authentication token to use. If provided, + * the username and password are ignored and there is no + * need to call login(). In the format "Splunk SESSION_KEY". * 'host' => (optional) The hostname of the Splunk server. Defaults to "localhost". * 'port' => (optional) The port of the Splunk server. Defaults to 8089. * 'scheme' => (optional) The scheme to use: either "http" or "https". Defaults to "https". @@ -46,6 +48,7 @@ public function __construct($args) $args = array_merge(array( 'username' => 'admin', 'password' => 'changeme', + 'token' => NULL, 'host' => 'localhost', 'port' => 8089, 'scheme' => 'https', @@ -54,6 +57,7 @@ public function __construct($args) $this->username = $args['username']; $this->password = $args['password']; + $this->token = $args['token']; $this->host = $args['host']; $this->port = $args['port']; $this->scheme = $args['scheme']; @@ -72,11 +76,25 @@ public function login() 'password' => $this->password, )); - $sessionKey = Splunk_Util::getTextContentAtXpath( - new SimpleXMLElement($response['body']), + $sessionKey = Splunk_XmlUtil::getTextContentAtXpath( + new SimpleXMLElement($response->body), '/response/sessionKey'); - $this->token = 'Splunk ' . $sessionKey; + $this->token = "Splunk {$sessionKey}"; + } + + /** + * Performs an HTTP GET request to the endpoint at the specified path. + * + * @param string $path relative or absolute URL path. + * @return array + * @see Splunk_Http::get + */ + public function get($path) + { + return $this->http->get($this->url($path), array( + 'Authorization' => $this->token, + )); } // === Accessors === @@ -92,8 +110,25 @@ public function getToken() // === Utility === + /** + * @param string $path relative or absolute URL path. + * @return string absolute URL. + */ private function url($path) { - return "{$this->scheme}://{$this->host}:{$this->port}{$path}"; + return "{$this->scheme}://{$this->host}:{$this->port}{$this->abspath($path)}"; + } + + /** + * @param string $path relative or absolute URL path. + * @return string absolute URL path. + */ + private function abspath($path) + { + if ((strlen($path) >= 1) && ($path[0] == '/')) + return $path; + + // TODO: Support namespaces + return "/services/{$path}"; } } diff --git a/Splunk/Endpoint.php b/Splunk/Endpoint.php new file mode 100644 index 0000000..82d97c2 --- /dev/null +++ b/Splunk/Endpoint.php @@ -0,0 +1,55 @@ +service = $service; + $this->path = $path; + } + + // === Load === + + /** Loads this resource if not already done. Returns self. */ + protected function validate() + { + if (!$this->loaded) + { + $this->load(); + assert($this->loaded); + } + return $this; + } + + /** + * Loads this resource. + * + * Implementations must set $this->loaded to TRUE before returning. + */ + protected abstract function load(); +} \ No newline at end of file diff --git a/Splunk/Entity.php b/Splunk/Entity.php new file mode 100644 index 0000000..904a8e5 --- /dev/null +++ b/Splunk/Entity.php @@ -0,0 +1,89 @@ +data = $data; + if ($this->data != NULL) + $this->loadContentsOfData(); + } + + // === Load === + + protected function load() + { + $response = $this->service->get($this->path); + $xml = new SimpleXMLElement($response->body); + + $this->data = $xml->entry; + $this->loadContentsOfData(); + } + + private function loadContentsOfData() + { + $this->content = Splunk_AtomFeed::parseValueInside($this->data->content); + $this->loaded = TRUE; + } + + // === Accessors === + + public function getName() + { + return (string) $this->validate()->data->title; + } + + // === ArrayAccess Methods === + + public function offsetGet($key) + { + return $this->validate()->content[$key]; + } + + public function offsetSet($key, $value) + { + throw new Splunk_UnsupportedOperationException(); + } + + public function offsetUnset($key) + { + throw new Splunk_UnsupportedOperationException(); + } + + public function offsetExists($key) + { + return isset($this->validate()->content[$key]); + } +} \ No newline at end of file diff --git a/Splunk/Http.php b/Splunk/Http.php index 182c060..198aef5 100644 --- a/Splunk/Http.php +++ b/Splunk/Http.php @@ -22,28 +22,32 @@ */ class Splunk_Http { - public function get($url) + public function get($url, $request_headers=array()) { - return $this->request('get', $url); + return $this->request('get', $url, $request_headers); } public function post($url, $params=array()) { - return $this->request('post', $url, http_build_query($params)); + return $this->request('post', $url, array(), http_build_query($params)); } /** - * @param string $method HTTP request method (ex: 'get'). - * @param string $url URL to fetch. - * @param string $request_body content to send in the request. + * @param string $method HTTP request method (ex: 'get'). + * @param string $url URL to fetch. + * @param array $request_headers dictionary of header names and values. + * @param string $request_body content to send in the request. * @return object { * 'status' => HTTP status code (ex: 200). * 'reason' => HTTP reason string (ex: 'OK'). * 'headers' => Dictionary of headers. (ex: array('Content-Length' => '0')). * 'body' => Content of the response. * } + * @throws Splunk_ConnectException + * @throws Splunk_HttpException */ - private function request($method, $url, $request_body='') + private function request( + $method, $url, $request_headers=array(), $request_body='') { $opts = array( CURLOPT_HTTPGET => TRUE, @@ -55,6 +59,9 @@ private function request($method, $url, $request_body='') CURLOPT_SSL_VERIFYPEER => FALSE, ); + foreach ($request_headers as $k => $v) + $opts[CURLOPT_HTTPHEADER][] = "$k: $v"; + switch ($method) { case 'get': @@ -93,7 +100,7 @@ private function request($method, $url, $request_body='') list($http_version, $_, $reason) = explode(' ', $status_line, 3); - $response = array( + $response = (object) array( 'status' => $status, 'reason' => $reason, 'headers' => $headers, @@ -129,8 +136,7 @@ public function __construct($response) { $detail = Splunk_HttpException::parseFirstMessageFrom($response); - // FIXME: Include HTTP "reason" in message - $message = "HTTP {$response['status']} {$response['reason']}"; + $message = "HTTP {$response->status} {$response->reason}"; if ($detail != NULL) $message .= ' -- ' . $detail; @@ -140,8 +146,8 @@ public function __construct($response) private static function parseFirstMessageFrom($response) { - return Splunk_Util::getTextContentAtXpath( - new SimpleXMLElement($response['body']), + return Splunk_XmlUtil::getTextContentAtXpath( + new SimpleXMLElement($response->body), '/response/messages/msg'); } diff --git a/Splunk/NoSuchKeyException.php b/Splunk/NoSuchKeyException.php new file mode 100644 index 0000000..93a6b2b --- /dev/null +++ b/Splunk/NoSuchKeyException.php @@ -0,0 +1,21 @@ +xpath($xpathExpr); - return (count($matchingElements) == 0) - ? NULL - : Splunk_Util::getTextContentOfXmlElement($matchingElements[0]); - } - - /** - * @param SimpleXMLElement $xmlElement - * @return string - */ - private static function getTextContentOfXmlElement($xmlElement) - { - // HACK: Some versions of PHP 5 can't access the [0] element - // of a SimpleXMLElement object properly. - return (string) $xmlElement; - } -} diff --git a/Splunk/XmlUtil.php b/Splunk/XmlUtil.php new file mode 100644 index 0000000..565a5cc --- /dev/null +++ b/Splunk/XmlUtil.php @@ -0,0 +1,71 @@ +getName() != ''; + } + + /** + * @param SimpleXMLElement $xml + * @param string $attributeName + * @return string|NULL + */ + public static function getAttributeValue($xml, $attributeName) + { + return (isset($xml->attributes()->$attributeName)) + ? (string) $xml->attributes()->$attributeName + : NULL; + } + + /** + * @param SimpleXMLElement $xml + * @return string + */ + public static function getTextContent($xml) + { + // HACK: Some versions of PHP 5 can't access the [0] element + // of a SimpleXMLElement object properly. + return (string) $xml; + } + + /** + * @param SimpleXMLElement $xml + * @param string $xpathExpr + * @return string|NULL + */ + public static function getTextContentAtXpath($xml, $xpathExpr) + { + $matchingElements = $xml->xpath($xpathExpr); + return (count($matchingElements) == 0) + ? NULL + : Splunk_XmlUtil::getTextContent($matchingElements[0]); + } +} diff --git a/tests/AtomFeedTest.php b/tests/AtomFeedTest.php new file mode 100644 index 0000000..6dea115 --- /dev/null +++ b/tests/AtomFeedTest.php @@ -0,0 +1,126 @@ +1'; + $expectedValue = 1; + + $this->checkParseResult($xmlString, $expectedValue); + } + + public function testParseDict() + { + $xmlString = ' + + + v1 + v2 + + '; + $expectedValue = array( + 'k1' => 'v1', + 'k2' => 'v2', + ); + + $this->checkParseResult($xmlString, $expectedValue); + } + + public function testParseList() + { + $xmlString = ' + + + e1 + e2 + + '; + $expectedValue = array( + 'e1', + 'e2', + ); + + $this->checkParseResult($xmlString, $expectedValue); + } + + public function testParseEmpty() + { + $xmlString = ''; + $expectedValue = ''; + + $this->checkParseResult($xmlString, $expectedValue); + } + + public function testParseComplex() + { + $xmlString = ' + + + 0 + + + + 1 + + + + + * + + + + + admin + + + + + 0 + app + + + + '; + $expectedValue = array( + 'action.email' => '0', + 'action.email.sendresults' => '', + 'eai:acl' => array( + 'can_write' => '1', + 'perms' => array( + 'read' => array('*'), + 'write' => array('admin'), + ), + 'removable' => '0', + 'sharing' => 'app', + ), + ); + + $this->checkParseResult($xmlString, $expectedValue); + } + + private function checkParseResult($xmlString, $expectedValue) + { + $this->assertEquals( + $expectedValue, + Splunk_AtomFeed::parseValueInside(new SimpleXMLElement($xmlString))); + } +} \ No newline at end of file diff --git a/tests/ContextTest.php b/tests/ContextTest.php index 4d71ccf..9a59193 100644 --- a/tests/ContextTest.php +++ b/tests/ContextTest.php @@ -15,14 +15,14 @@ * under the License. */ -require_once '../Splunk.php'; +require_once 'Splunk.php'; require_once 'settings.php'; class ContextTest extends PHPUnit_Framework_TestCase { public function testLoginSuccess() { - $http_response = array( + $http_response = (object) array( 'status' => 200, 'reason' => 'OK', 'headers' => array(), @@ -39,8 +39,9 @@ public function testLoginSuccess() $context = new Splunk_Context(array( 'http' => $http, )); - $context->login(); + $this->assertEquals(NULL, $context->getToken()); + $context->login(); $this->assertEquals( 'Splunk 068b3021210eb4b67819b1a292302948', $context->getToken()); @@ -52,7 +53,7 @@ public function testLoginSuccess() */ public function testLoginFailDueToBadPassword() { - $http_response = array( + $http_response = (object) array( 'status' => 401, 'reason' => 'Unauthorized', 'headers' => array(), @@ -82,4 +83,12 @@ public function testLoginSuccessOnRealServer() $context = new Splunk_Context($Splunk_testSettings['connectArgs']); $context->login(); } + + public function testLoginWithToken() + { + $context = new Splunk_Context(array( + 'token' => 'Splunk ACEACE' + )); + $this->assertEquals('Splunk ACEACE', $context->getToken()); + } } diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 4966c8b..61e3351 100644 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -15,7 +15,7 @@ * under the License. */ -require_once '../Splunk.php'; +require_once 'Splunk.php'; class HttpTest extends PHPUnit_Framework_TestCase { @@ -24,7 +24,7 @@ public function testGet() $http = new Splunk_Http(); $response = $http->get('http://www.splunk.com/'); - $this->assertEquals(200, $response['status']); - $this->assertContains('', $response['body']); + $this->assertEquals(200, $response->status); + $this->assertContains('', $response->body); } } diff --git a/tests/Makefile b/tests/Makefile deleted file mode 100644 index 4db1caa..0000000 --- a/tests/Makefile +++ /dev/null @@ -1,2 +0,0 @@ -test: - phpunit . diff --git a/tests/README.md b/tests/README.md index 6603513..680e0b7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,13 +1,15 @@ ## Requirements * PHPUnit 3.6 +* Xdebug 2.2.0 (for code coverage) ## Running the Tests -Navigate to this directory, then run: +Navigate to the root directory containing `Splunk.php`, then run: - phpunit . + phpunit tests -Or alternatively: +To generate a code coverage report, run: - make \ No newline at end of file + phpunit --coverage-html coverage tests + open coverage/Splunk.html diff --git a/tests/SavedSearchTest.php b/tests/SavedSearchTest.php new file mode 100644 index 0000000..96fa602 --- /dev/null +++ b/tests/SavedSearchTest.php @@ -0,0 +1,88 @@ +login(); + + $response = $context->get('/servicesNS/nobody/search/saved/searches/'); + $this->assertContains( + '' . self::SAVED_SEARCH_NAME . '', + $response->body); + } + + public function testGetSavedSearchFromCollection() + { + global $Splunk_testSettings; + $service = new Splunk_Service($Splunk_testSettings['connectArgs']); + $service->login(); + + $savedSearch = $service->getSavedSearches()->get(self::SAVED_SEARCH_NAME); + return $savedSearch; + } + + /** @depends testGetSavedSearchFromCollection */ + public function testGetPropertyOfSavedSearchFromCollection($savedSearch) + { + $this->assertEquals(self::SAVED_SEARCH_QUERY, $savedSearch['search']); + } + + public function testGetSavedSearch() + { + global $Splunk_testSettings; + $service = new Splunk_Service($Splunk_testSettings['connectArgs']); + $service->login(); + + $savedSearch = $service->getSavedSearch(self::SAVED_SEARCH_NAME); + return $savedSearch; + } + + /** @depends testGetSavedSearch */ + public function testGetPropertyOfSavedSearch($savedSearch) + { + $this->assertEquals(self::SAVED_SEARCH_QUERY, $savedSearch['search']); + } + + public function testGetMissingSavedSearch() + { + global $Splunk_testSettings; + $service = new Splunk_Service($Splunk_testSettings['connectArgs']); + $service->login(); + + $savedSearch = $service->getSavedSearch('NO_SUCH_SEARCH'); + try + { + $savedSearch->getName(); // force load from server + $this->assertFail('Expected Splunk_HttpException to be thrown.'); + } + catch (Splunk_HttpException $e) + { + $this->assertEquals(404, $e->getResponse()->status); + } + } +} \ No newline at end of file